/**
* Copyright (C) 2002-2012 The FreeCol Team
*
* This file is part of FreeCol.
*
* FreeCol 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.
*
* FreeCol 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with FreeCol. If not, see <http://www.gnu.org/licenses/>.
*/
package net.sf.freecol.server.control;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import net.sf.freecol.common.model.Ability;
import net.sf.freecol.common.model.Feature;
import net.sf.freecol.common.model.FoundingFather;
import net.sf.freecol.common.model.FreeColGameObject;
import net.sf.freecol.common.model.FreeColObject;
import net.sf.freecol.common.model.Game;
import net.sf.freecol.common.model.GoodsType;
import net.sf.freecol.common.model.HistoryEvent;
import net.sf.freecol.common.model.LastSale;
import net.sf.freecol.common.model.Location;
import net.sf.freecol.common.model.ModelMessage;
import net.sf.freecol.common.model.Modifier;
import net.sf.freecol.common.model.Ownable;
import net.sf.freecol.common.model.Player;
import net.sf.freecol.common.model.Player.Stance;
import net.sf.freecol.common.model.Region;
import net.sf.freecol.common.model.Settlement;
import net.sf.freecol.common.model.Tile;
import net.sf.freecol.common.model.TradeRoute;
import net.sf.freecol.common.model.Unit;
import net.sf.freecol.common.model.WorkLocation;
import net.sf.freecol.common.networking.DOMMessage;
import net.sf.freecol.server.model.ServerPlayer;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
/**
* Changes to be sent to the client.
*/
public class ChangeSet {
// Convenient way to specify the relative priorities of the fixed
// change types in one place.
public static enum ChangePriority {
CHANGE_ATTRIBUTE(-1), // N/A
CHANGE_ANIMATION(0), // Do animations first
CHANGE_REMOVE(100), // Do removes last
CHANGE_STANCE(5), // Do stance before updates
CHANGE_OWNED(20), // Do owned changes after updates
CHANGE_UPDATE(10), // There are a lot of updates
// Symbolic priorities used by various non-fixed types
CHANGE_EARLY(1),
CHANGE_NORMAL(15),
CHANGE_LATE(90);
private int level;
ChangePriority(int level) {
this.level = level;
}
public int getPriority() {
return level;
}
}
private ArrayList<Change> changes;
private static Comparator<Change> changeComparator
= new Comparator<Change>() {
public int compare(final Change c1, final Change c2) {
return c1.getPriority() - c2.getPriority();
}
};
public static class See {
private static final int ALL = 1;
private static final int PERHAPS = 0;
private static final int ONLY = -1;
private ServerPlayer seeAlways;
private ServerPlayer seePerhaps;
private ServerPlayer seeNever;
private int type;
private See(int type) {
this.seeAlways = this.seePerhaps = this.seeNever = null;
this.type = type;
}
/**
* Check this visibility with respect to a player.
*
* @param player The <code>ServerPlayer</code> to consider.
* @param perhapsResult The result if the visibility is ambiguous.
* @return True if the player satisfies the visibility test.
*/
public boolean check(ServerPlayer player, boolean perhapsResult) {
return (seeNever == player) ? false
: (seeAlways == player) ? true
: (type == ONLY) ? false
: (type == ALL) ? true
: perhapsResult;
}
// Use these public constructor-like functions to define the
// visibility of changes.
/**
* Make this change visible to all players.
*
* @return a <code>See</code> value
*/
public static See all() {
return new See(ALL);
}
/**
* Make this change visible to all players, provided they can
* see the objects that are being changed.
*
* @return a <code>See</code> value
*/
public static See perhaps() {
return new See(PERHAPS);
}
/**
* Make this change visible only to the given player.
*
* @param player a <code>ServerPlayer</code> value
* @return a <code>See</code> value
*/
public static See only(ServerPlayer player) {
return new See(ONLY).always(player);
}
// Use these to modify a See visibility.
/**
* Make this change visible to the given player.
*
* @param player a <code>ServerPlayer</code> value
* @return a <code>See</code> value
*/
public See always(ServerPlayer player) {
seeAlways = player;
return this;
}
/**
* Make this change visible to the given player, provided the
* player can see the objects being changed.
*
* @param player a <code>ServerPlayer</code> value
* @return a <code>See</code> value
*/
public See perhaps(ServerPlayer player) {
seePerhaps = player;
return this;
}
/**
* Make this change invisible to the given player.
*
* @param player a <code>ServerPlayer</code> value
* @return a <code>See</code> value
*/
public See except(ServerPlayer player) {
seeNever = player;
return this;
}
/**
* Debug helper.
*/
public String toString() {
String ret = (type == ALL) ? "ALL" : (type == PERHAPS) ? "PERHAPS"
: (type == ONLY) ? "ONLY" : "BADTYPE";
if (seeAlways != null) ret += ",always(" + seeAlways.getId() + ")";
if (seePerhaps != null) ret += ",perhaps(" + seePerhaps.getId() + ")";
if (seeNever != null) ret += ",never(" + seeNever.getId() + ")";
return ret;
}
}
// Abstract template for all types of Change.
private abstract static class Change {
/**
* The visibility of the change.
*/
protected See see;
/**
* Make a new Change.
*/
Change(See see) {
this.see = see;
}
/**
* Gets the sort priority of a change, to be used by the
* changeComparator.
*/
public abstract int getPriority();
/**
* Should a player be notified of this Change?
*
* @param serverPlayer The <code>ServerPlayer</code> to consider.
* @return True if this <code>Change</code> should be sent.
*/
public boolean isNotifiable(ServerPlayer serverPlayer) {
return see.check(serverPlayer, isPerhapsNotifiable(serverPlayer));
}
/**
* Should a player be notified of a Change for which the
* visibility is delegated to the change type, allowing
* special change-specific overrides.
*
* This is false by default, subclasses should override when
* special case handling is required.
*
* @param serverPlayer The <code>ServerPlayer</code> to consider.
* @return False.
*/
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
return false;
}
/**
* Are the secondary changes consequent to this Change?
*
* @param serverPlayer The <code>ServerPlayer</code> to consider.
* @return A list of secondary <code>Change</code>s or the
* empty list if there are none, which is usually the case.
*/
public List<Change> consequences(ServerPlayer serverPlayer) {
return Collections.emptyList();
}
/**
* Can this Change be directly converted to an Element?
*
* @return True if this change can be directly converted to an Element.
*/
public boolean convertsToElement() {
return true;
}
/**
* Specialize a Change for a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code> to build the element in.
* @return An <code>Element</code> encapsulating this change.
*/
public abstract Element toElement(ServerPlayer serverPlayer,
Document doc);
/**
* Some changes can not be directly specialized, but need to be
* directly attached to an element.
*
* @param element The <code>Element</code> to attach to.
*/
public void attachToElement(Element element) {
}
/**
* Debug helper.
*/
public abstract String toString();
}
/**
* Encapsulate an attack.
*/
private static class AttackChange extends Change {
private Unit attacker;
private Unit defender;
private boolean success;
/**
* Build a new AttackChange.
*
* @param see The visibility of this change.
* @param attacker The <code>Unit</code> that is attacking.
* @param defender The <code>Unit</code> that is defending.
* @param success Did the attack succeed.
*/
AttackChange(See see, Unit attacker, Unit defender, boolean success) {
super(see);
this.attacker = attacker;
this.defender = defender;
this.success = success;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_ANIMATION".
*/
public int getPriority() {
return ChangePriority.CHANGE_ANIMATION.getPriority();
}
/**
* Should a player perhaps be notified of this attack?
* Do not use Unit.isVisibleTo because that gives a false
* negative for units in settlements, which should be animated.
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return True if the player should be notified.
*/
@Override
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
return serverPlayer == attacker.getOwner()
|| serverPlayer == defender.getOwner()
|| (attacker.getTile() != null
&& serverPlayer.canSee(attacker.getTile())
&& defender.getTile() != null
&& serverPlayer.canSee(defender.getTile()));
}
/**
* Specialize a AttackChange into an "animateAttack" element
* for a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "animateAttack" element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("animateAttack");
element.setAttribute("attacker", attacker.getId());
element.setAttribute("defender", defender.getId());
element.setAttribute("success", Boolean.toString(success));
if (!attacker.isVisibleTo(serverPlayer)) {
element.appendChild(attacker.toXMLElement(serverPlayer, doc,
false, false));
}
if (!defender.isVisibleTo(serverPlayer)) {
element.appendChild(defender.toXMLElement(serverPlayer, doc,
false, false));
}
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + attacker.getId() + " " + success
+ " " + defender.getId() + "]";
}
}
/**
* Encapsulate an attribute change.
*/
private static class AttributeChange extends Change {
private String key;
private String value;
/**
* Build a new AttributeChange.
*
* @param see The visibility of this change.
* @param key A key <code>String</code>.
* @param value The corresponding value as a <code>String</code>.
*/
AttributeChange(See see, String key, String value) {
super(see);
this.key = key;
this.value = value;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_ATTRIBUTE", attributes are special.
*/
public int getPriority() {
return ChangePriority.CHANGE_ATTRIBUTE.getPriority();
}
/**
* AttributeChanges are tacked onto the final Element, not converted
* directly.
*
* @return false.
*/
@Override
public boolean convertsToElement() {
return false;
}
/**
* We do not specialize AttributeChanges.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return Null.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
return null;
}
/**
* Tack attributes onto the element.
*
* @param element The <code>Element</code> to attach to.
*/
@Override
public void attachToElement(Element element) {
element.setAttribute(key, value);
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + key + "=" + value + "]";
}
}
/**
* Encapsulate a Message.
*/
private static class MessageChange extends Change {
private ChangePriority priority;
private DOMMessage message;
/**
* Build a new MessageChange.
*
* @param see The visibility of this change.
* @param priority The priority of the change.
* @param message The <code>Message</code> to add.
*/
MessageChange(See see, ChangePriority priority, DOMMessage message) {
super(see);
this.priority = priority;
this.message = message;
}
/**
* Gets the sort priority.
*
* @return The priority.
*/
public int getPriority() {
return priority.getPriority();
}
/**
* Specialize a MessageChange to a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = message.toXMLElement();
return (Element) doc.importNode(element, true);
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + message + "]";
}
}
/**
* Encapsulate a move.
*/
private static class MoveChange extends Change {
private Unit unit;
private Location oldLocation;
private Tile newTile;
private boolean seeOld(ServerPlayer serverPlayer) {
Tile oldTile = oldLocation.getTile();
return unit.getOwner() == serverPlayer
|| (oldTile != null && serverPlayer.canSee(oldTile)
&& oldTile.getSettlement() == null);
}
private boolean seeNew(ServerPlayer serverPlayer) {
return unit.getOwner() == serverPlayer
|| unit.isVisibleTo(serverPlayer);
}
/**
* Build a new MoveChange.
*
* @param see The visibility of this change.
* @param unit The <code>Unit</code> that is moving.
* @param oldLocation The location from which the unit is moving.
* @param newTile The <code>Tile</code> to which the unit is moving.
*/
MoveChange(See see, Unit unit, Location oldLocation,
Tile newTile) {
super(see);
this.unit = unit;
this.oldLocation = oldLocation;
this.newTile = newTile;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_ANIMATION"
*/
public int getPriority() {
return ChangePriority.CHANGE_ANIMATION.getPriority();
}
/**
* Should a player perhaps be notified of this move?
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return True if the player should be notified.
*/
@Override
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
return seeOld(serverPlayer) || seeNew(serverPlayer);
}
/**
* There are consequences to a move. If the player can not
* see the unit after the move, it should be removed.
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return A RemoveChange if the unit disappears.
*/
@Override
public List<Change> consequences(ServerPlayer serverPlayer) {
if (seeOld(serverPlayer) && !seeNew(serverPlayer)) {
List<Change> changes = new ArrayList<Change>();
List<FreeColGameObject> objects
= new ArrayList<FreeColGameObject>();
objects.add(unit);
changes.add(new RemoveChange(See.only(serverPlayer),
unit.getLocation(), objects));
return changes;
}
return Collections.emptyList();
}
/**
* Specialize a MoveChange into an "animateMove" element for a
* particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "animateMove" element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("animateMove");
element.setAttribute("unit", unit.getId());
element.setAttribute("oldTile", oldLocation.getTile().getId());
element.setAttribute("newTile", newTile.getId());
if (!seeOld(serverPlayer)) {
// We can not rely on the unit that is about to move
// being present on the client side, and it is needed
// before we can run the animation, so it is attached
// to animateMove.
element.appendChild(unit.toXMLElement(serverPlayer, doc,
false, false));
}
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + unit.getId()
+ " " + ((FreeColGameObject)oldLocation).getId()
+ " " + newTile.getId() + "]";
}
}
/**
* Encapsulate a FreeColGameObject update.
*/
private static class ObjectChange extends Change {
protected FreeColGameObject fcgo;
/**
* Build a new ObjectChange for a single object.
*
* @param see The visibility of this change.
* @param fcgo The <code>FreeColGameObject</code> to update.
*/
ObjectChange(See see, FreeColGameObject fcgo) {
super(see);
this.fcgo = fcgo;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_UPDATE"
*/
public int getPriority() {
return ChangePriority.CHANGE_UPDATE.getPriority();
}
/**
* Should a player perhaps be notified of this update?
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return True if the object update can is notifiable.
*/
@Override
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
if (fcgo instanceof Unit) {
// Units have a precise test, use that rather than
// the more general interface-based tests.
return ((Unit) fcgo).isVisibleTo(serverPlayer);
}
// If we own it, we can see it.
if (fcgo instanceof Ownable
&& ((Ownable) fcgo).getOwner() == (Player) serverPlayer) {
return true;
}
// We do not own it, so the only way we could see it is if
// it is on the map. Would like to use getTile() to
// decide that, but this will include ColonyTiles, which
// report the colony center tile, yet should never be visible.
// So just brutally disallow WorkLocations which should always
// be invisible inside colonies.
if (fcgo instanceof WorkLocation) {
return false;
}
return fcgo instanceof Location
&& ((Location) fcgo).getTile() != null
&& serverPlayer.canSee(((Location) fcgo).getTile());
}
/**
* Specialize a ObjectChange to a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "update" element, or null if the update should not
* be visible to the player.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("update");
element.appendChild(fcgo.toXMLElement(serverPlayer, doc,
false, false));
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + fcgo.getId() + "]";
}
}
/**
* Encapsulate a partial update of a FreeColGameObject.
*/
private static class PartialObjectChange extends ObjectChange {
private String[] fields;
/**
* Build a new PartialObjectChange for a single object.
*
* @param see The visibility of this change.
* @param fcgo The <code>FreeColGameObject</code> to update.
* @param fields The fields to update.
*/
PartialObjectChange(See see, FreeColGameObject fcgo,
String... fields) {
super(see, fcgo);
this.fields = fields;
}
/**
* Gets the sort priority.
*
* @return CHANGE_UPDATE. Special update, but still an update.
*/
public int getPriority() {
return ChangePriority.CHANGE_UPDATE.getPriority();
}
/**
* Should a player perhaps be notified of this update?
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return False. Revert to default from ObjectChange special case.
*/
@Override
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
return false;
}
/**
* Specialize a PartialObjectChange to a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "update" element.
*/
@Override
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("update");
element.appendChild(fcgo.toXMLElement(serverPlayer, doc,
false, false, fields));
return element;
}
/**
* Debug helper.
*/
public String toString() {
String ret = "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + fcgo.getId();
for (String f : fields) ret += " " + f;
return ret + "]";
}
}
private static class RemoveChange extends Change {
private Tile tile;
private FreeColGameObject fcgo;
private List<FreeColGameObject> contents;
/**
* Build a new RemoveChange for an object that is disposed.
*
* @param see The visibility of this change.
* @param loc The <code>Location</code> where the object was.
* @param objects The <code>FreeColGameObject</code>s to remove.
*/
RemoveChange(See see, Location loc,
List<FreeColGameObject> objects) {
super(see);
this.tile = (loc instanceof Tile) ? (Tile) loc : null;
this.fcgo = objects.remove(objects.size() - 1);
this.contents = objects;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_REMOVE"
*/
public int getPriority() {
return ChangePriority.CHANGE_REMOVE.getPriority();
}
/**
* Should a player perhaps be notified of this removal?
* They should if they can see the tile, and there is no
* other-player settlement present.
*
* @param serverPlayer The <code>ServerPlayer</code> to notify.
* @return True if the player should be notified.
*/
@Override
public boolean isPerhapsNotifiable(ServerPlayer serverPlayer) {
Settlement settlement;
return tile != null
&& serverPlayer.canSee(tile)
&& ((settlement = tile.getSettlement()) == null
|| settlement.isDisposed()
|| (ServerPlayer)settlement.getOwner() == serverPlayer);
}
/**
* Specialize a RemoveChange to a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return A "remove" element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("remove");
// The main object may be visible, but the contents are by
// only visible if the deeper ownership test succeeds.
if (fcgo instanceof Ownable
&& ((Ownable)fcgo).getOwner() == serverPlayer) {
for (FreeColGameObject o : contents) {
element.appendChild(o.toXMLElementPartial(doc));
}
element.setAttribute("divert", (tile != null) ? tile.getId()
: serverPlayer.getId());
}
element.appendChild(fcgo.toXMLElementPartial(doc));
return element;
}
/**
* Debug helper.
*/
public String toString() {
String ret = "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + ((tile == null) ? "<null>" : tile.getId());
for (FreeColGameObject f : contents) ret += " " + f.getId();
return ret + " " + fcgo.getId() + "]";
}
}
/**
* Encapsulate an owned object change.
*/
private static class OwnedChange extends Change {
private FreeColObject fco;
/**
* Build a new OwnedChange.
*
* @param see The visibility of this change.
* @param fco The <code>FreeColObject</code> to update.
*/
OwnedChange(See see, FreeColObject fco) {
super(see);
this.fco = fco;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_OWNER"
*/
public int getPriority() {
return ChangePriority.CHANGE_OWNED.getPriority();
}
/**
* Specialize a OwnedChange into an "addObject" element for a
* particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "addObject" element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("addObject");
Element child = fco.toXMLElement(serverPlayer, doc, false, false);
child.setAttribute("owner", serverPlayer.getId());
element.appendChild(child);
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName()
+ " " + see.toString()
+ " #" + getPriority()
+ " " + fco.getId() + "]";
}
}
/**
* Encapsulate a feature change.
*/
private static class FeatureChange extends Change {
private FreeColGameObject object;
private Feature feature;
private boolean add;
/**
* Build a new OwnedChange.
*
* @param see The visibility of this change.
* @param object The <code>FreeColGameObject</code> to update.
* @param feature a <code>Feature</code> value to add or remove.
* @param add a <code>boolean</code> value
*/
FeatureChange(See see, FreeColGameObject object, Feature feature, boolean add) {
super(see);
this.object = object;
this.feature = feature;
this.add = add;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_OWNER"
*/
public int getPriority() {
return ChangePriority.CHANGE_OWNED.getPriority();
}
/**
* Specialize a feature change into an element for a
* particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An "addObject" element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("featureChange");
element.setAttribute("add", Boolean.toString(add));
element.setAttribute("id", object.getId());
Element child = feature.toXMLElement(doc);
element.appendChild(child);
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName()
+ " " + see.toString()
+ " #" + getPriority()
+ " " + (add ? "add" : "remove")
+ " " + feature
+ " " + (add ? "to" : "from")
+ " " + object.getId() + "]";
}
}
private static class SpyChange extends Change {
private Tile tile;
/**
* Build a new SpyChange.
*
* @param see The visibility of this change.
* @param settlement The <code>Settlement</code> to spy on.
*/
SpyChange(See see, Settlement settlement) {
super(see);
tile = settlement.getTile();
}
/**
* Gets the sort priority.
*
* @return priority.
*/
public int getPriority() {
return ChangePriority.CHANGE_NORMAL.getPriority();
}
/**
* Specialize a SpyChange into an element with the supplied name.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("spyResult");
element.setAttribute("tile", tile.getId());
// Have to tack on two copies of the settlement tile.
// One full version, one ordinary version to restore.
element.appendChild(tile.toXMLElement(serverPlayer, doc,
true, false));
element.appendChild(tile.toXMLElement(serverPlayer, doc,
false, false));
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + tile.getId() + "]";
}
}
/**
* Encapsulate a stance change.
*/
private static class StanceChange extends Change {
private Player first;
private Stance stance;
private Player second;
/**
* Build a new StanceChange.
*
* @param see The visibility of this change.
* @param first The <code>Player</code> changing stance.
* @param stance The <code>Stance</code> to change to.
* @param second The <code>Player</code> wrt with to change.
*/
StanceChange(See see, Player first, Stance stance, Player second) {
super(see);
this.first = first;
this.stance = stance;
this.second = second;
}
/**
* Gets the sort priority.
*
* @return "CHANGE_STANCE"
*/
public int getPriority() {
return ChangePriority.CHANGE_STANCE.getPriority();
}
/**
* Specialize a StanceChange to a particular player.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return A "setStance" element.
*/
@Override
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement("setStance");
element.setAttribute("stance", stance.toString());
element.setAttribute("first", first.getId());
element.setAttribute("second", second.getId());
return element;
}
/**
* Debug helper.
*/
public String toString() {
return "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + first.getId()
+ " " + stance + " " + second.getId() + "]";
}
}
/**
* Encapsulate trivial element, which will only have attributes apart
* from its name.
*/
private static class TrivialChange extends Change {
private int priority;
private String name;
private String[] attributes;
/**
* Build a new TrivialChange.
*
* @param see The visibility of this change.
* @param name The name of the element.
* @param priority The sort priority of this change.
*/
TrivialChange(See see, String name, int priority, String[] attributes) {
super(see);
if ((attributes.length & 1) == 1) {
throw new IllegalArgumentException("Attributes must be even sized");
}
this.name = name;
this.priority = priority;
this.attributes = attributes;
}
/**
* Gets the sort priority.
*
* @return priority.
*/
public int getPriority() {
return priority;
}
/**
* Specialize a TrivialChange into an element with the supplied name.
*
* @param serverPlayer The <code>ServerPlayer</code> to update.
* @param doc The owner <code>Document</code>.
* @return An element.
*/
public Element toElement(ServerPlayer serverPlayer, Document doc) {
Element element = doc.createElement(name);
for (int i = 0; i < attributes.length; i += 2) {
element.setAttribute(attributes[i], attributes[i+1]);
}
return element;
}
/**
* Debug helper.
*/
public String toString() {
String ret = "[" + getClass().getName() + " " + see.toString()
+ " #" + getPriority()
+ " " + name;
for (String a : attributes) ret += " " + a;
return ret + "]";
}
}
/**
* Simple constructor.
*/
public ChangeSet() {
changes = new ArrayList<Change>();
}
/**
* Copying constructor.
*
* @param other The other <code>ChangeSet</code> to copy.
*/
public ChangeSet(ChangeSet other) {
changes = new ArrayList<Change>(other.changes);
}
// Helper routines that should be used to construct a change set.
/**
* Helper function to add updates for multiple objects to a ChangeSet.
*
* @param see The visibility of this change.
* @param objects The <code>FreeColGameObject</code>s that changed.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet add(See see, FreeColGameObject... objects) {
for (FreeColGameObject o : objects) {
changes.add(new ObjectChange(see, o));
}
return this;
}
/**
* Helper function to add updates for multiple objects to a ChangeSet.
*
* @param see The visibility of this change.
* @param objects The <code>FreeColGameObject</code>s that changed.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet add(See see, List<FreeColGameObject> objects) {
for (FreeColGameObject o : objects) {
changes.add(new ObjectChange(see, o));
}
return this;
}
/**
* Helper function to add a Message to a ChangeSet.
*
* @param see The visibility of this change.
* @param cp The priority of this change.
* @param message The <code>Message</code> to add.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet add(See see, ChangePriority cp, DOMMessage message) {
changes.add(new MessageChange(see, cp, message));
return this;
}
/**
* Helper function to add an attack to a ChangeSet.
*
* @param see The visibility of this change.
* @param unit The <code>Unit</code> that is attacking.
* @param defender The <code>Unit</code> that is defending.
* @param success Did the attack succeed?
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addAttack(See see, Unit unit, Unit defender,
boolean success) {
changes.add(new AttackChange(see, unit, defender, success));
return this;
}
/**
* Helper function to add an attribute setting to a ChangeSet.
*
* @param see The visibility of this change.
* @param key A key <code>String</code>.
* @param value The corresponding value as a <code>String</code>.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addAttribute(See see, String key, String value) {
changes.add(new AttributeChange(see, key, value));
return this;
}
/**
* Helper function to add a dead player event to a ChangeSet.
* Deaths are public knowledge.
*
* @param serverPlayer The <code>ServerPlayer</code> that died.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addDead(ServerPlayer serverPlayer) {
addTrivial(See.all(), "setDead", ChangePriority.CHANGE_EARLY,
"player", serverPlayer.getId());
return this;
}
/**
* Helper function to add a removal for a disposal list to a ChangeSet.
*
* @param see The visibility of this change.
* @param loc The <code>Location</code> where the object was.
* @param obj The <code>FreeColGameObject</code> to remove.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addDispose(See see, Location loc, FreeColGameObject obj) {
changes.add(new RemoveChange(see, loc, obj.disposeList()));
return this;
}
/**
* Helper function to add a removal for an object that disappears
* (that is, moves where it can not be seen) to a ChangeSet.
*
* @param owner The <code>ServerPlayer</code> that owns this object.
* @param tile The <code>Tile</code> where the object was.
* @param fcgo The <code>FreeColGameObject</code> that disappears.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addDisappear(ServerPlayer owner, Tile tile,
FreeColGameObject fcgo) {
List<FreeColGameObject> objects = new ArrayList<FreeColGameObject>();
objects.add(fcgo);
changes.add(new ObjectChange(See.perhaps().except(owner), tile));
changes.add(new RemoveChange(See.perhaps().except(owner), tile,
objects));
return this;
}
/**
* Helper function to add a founding father addition event to a ChangeSet.
* Also adds the father to the owner.
*
* @param serverPlayer The <code>ServerPlayer</code> adding the father.
* @param father The <code>FoundingFather</code> to add.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addFather(ServerPlayer serverPlayer,
FoundingFather father) {
changes.add(new OwnedChange(See.only(serverPlayer), father));
serverPlayer.addFather(father);
return this;
}
/**
* Helper function to add an Ability to a FreeColGameObject, or remove it.
*
* @param serverPlayer a <code>ServerPlayer</code> value
* @param object a <code>FreeColGameObject</code> value
* @param ability an <code>Ability</code> value
* @param add a <code>boolean</code> value
* @return a <code>ChangeSet</code> value
*/
public ChangeSet addFeatureChange(ServerPlayer serverPlayer, FreeColGameObject object,
Ability ability, boolean add) {
changes.add(new FeatureChange(See.only(serverPlayer), object, ability, add));
if (add) {
object.addAbility(ability);
} else {
object.removeAbility(ability);
}
return this;
}
/**
* Helper function to add a Modifier to a FreeColGameObject, or remove it.
*
* @param serverPlayer a <code>ServerPlayer</code> value
* @param object a <code>FreeColGameObject</code> value
* @param modifier a <code>Modifier</code> value
* @param add a <code>boolean</code> value
* @return a <code>ChangeSet</code> value
*/
public ChangeSet addFeatureChange(ServerPlayer serverPlayer, FreeColGameObject object,
Modifier modifier, boolean add) {
changes.add(new FeatureChange(See.only(serverPlayer), object, modifier, add));
if (add) {
object.addModifier(modifier);
} else {
object.removeModifier(modifier);
}
return this;
}
/**
* Helper function to add a global history event to a ChangeSet.
* Also adds the history to all the European players.
*
* @param game The <code>Game</code> to find players in.
* @param history The <code>HistoryEvent</code> to add.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addGlobalHistory(Game game, HistoryEvent history) {
changes.add(new OwnedChange(See.all(), history));
for (Player p : game.getLiveEuropeanPlayers()) {
p.addHistory(history);
}
return this;
}
/**
* Helper function to add a history event to a ChangeSet.
* Also adds the history to the owner.
*
* @param serverPlayer The <code>ServerPlayer</code> making history.
* @param history The <code>HistoryEvent</code> to add.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addHistory(ServerPlayer serverPlayer,
HistoryEvent history) {
changes.add(new OwnedChange(See.only(serverPlayer), history));
serverPlayer.addHistory(history);
return this;
}
/**
* Helper function to add a message to a ChangeSet.
*
* @param see The visibility of this change.
* @param message The <code>ModelMessage</code> to add.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addMessage(See see, ModelMessage message) {
changes.add(new OwnedChange(see, message));
return this;
}
/**
* Helper function to add a move to a ChangeSet.
*
* @param see The visibility of this change.
* @param unit The <code>Unit</code> that is moving.
* @param loc The location from which the unit is moving.
* @param tile The <code>Tile</code> to which the unit is moving.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addMove(See see, Unit unit, Location loc, Tile tile) {
changes.add(new MoveChange(see, unit, loc, tile));
return this;
}
/**
* Helper function to add a partial update change for an object to
* a ChangeSet.
*
* @param see The visibility of this change.
* @param fcgo The <code>FreeColGameObject</code> to update.
* @param fields The fields to update.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addPartial(See see, FreeColGameObject fcgo,
String... fields) {
changes.add(new PartialObjectChange(see, fcgo, fields));
return this;
}
/**
* Helper function to add a region discovery to a ChangeSet.
* Also adds the history to all Europeans.
*
* @param serverPlayer The <code>ServerPlayer</code> discovering the region.
* @param region The <code>Region</code> to discover.
* @param name The name of the region.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addRegion(ServerPlayer serverPlayer, Region region,
String name) {
Game game = serverPlayer.getGame();
HistoryEvent h = region.discover(serverPlayer, game.getTurn(), name);
changes.add(new ObjectChange(See.all(), region));
addGlobalHistory(game, h);
return this;
}
/**
* Helper function to add a sale change to a ChangeSet.
*
* @param serverPlayer The <code>ServerPlayer</code> making the sale.
* @param settlement The <code>Settlement</code> that is buying.
* @param type The <code>GoodsType</code> bought.
* @param price The per unit price.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addSale(ServerPlayer serverPlayer, Settlement settlement,
GoodsType type, int price) {
Game game = settlement.getGame();
LastSale sale = new LastSale(settlement, type, game.getTurn(), price);
changes.add(new OwnedChange(See.only(serverPlayer), sale));
serverPlayer.saveSale(sale);
return this;
}
/**
* Helper function to add a spying change to a ChangeSet.
*
* @param see The visibility of this change.
* @param settlement The <code>Settlement</code> to spy on.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addSpy(See see, Settlement settlement) {
changes.add(new SpyChange(see, settlement));
return this;
}
/**
* Helper function to add a stance change to a ChangeSet.
*
* @param see The visibility of this change.
* @param first The <code>Player</code> changing stance.
* @param stance The <code>Stance</code> to change to.
* @param second The <code>Player</code> wrt with to change.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addStance(See see, Player first, Stance stance,
Player second) {
changes.add(new StanceChange(see, first, stance, second));
return this;
}
/**
* Helper function to add a new trade route change to a ChangeSet.
* Also adds the trade route to the player.
*
* @param serverPlayer The <code>ServerPlayer</code> adding the route.
* @param tradeRoute The new <code>TradeRoute</code>.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addTradeRoute(ServerPlayer serverPlayer,
TradeRoute tradeRoute) {
changes.add(new OwnedChange(See.only(serverPlayer), tradeRoute));
serverPlayer.getTradeRoutes().add(tradeRoute);
return this;
}
/**
* Helper function to add a trivial element to a ChangeSet.
*
* @param see The visibility of this change.
* @param name The name of the element.
* @param cp The <code>ChangePriority</code> for this change.
* @param attributes Attributes to add to this trivial change.
* @return The updated <code>ChangeSet</code>.
*/
public ChangeSet addTrivial(See see, String name, ChangePriority cp,
String... attributes) {
changes.add(new TrivialChange(see, name, cp.getPriority(), attributes));
return this;
}
// Conversion of a change set to a corresponding element.
/**
* Collapse one element into another.
*
* @param head The <code>Element</code> to collapse into.
* @param tail The <code>Element</code> to extract nodes from.
*/
private static void collapseElements(Element head, Element tail) {
while (tail.hasChildNodes()) {
head.appendChild(tail.removeChild(tail.getFirstChild()));
}
}
/**
* Can two elements be collapsed?
* They need to have the same name and attributes.
*
* @param e1 The first <code>Element</code>.
* @param e2 The second <code>Element</code>.
* @return True if they can be collapsed.
*/
private static boolean collapseOK(Element e1, Element e2) {
if (e1.getTagName() != e2.getTagName()) return false;
NamedNodeMap nnm1 = e1.getAttributes();
NamedNodeMap nnm2 = e2.getAttributes();
if (nnm1.getLength() != nnm2.getLength()) return false;
for (int i = 0; i < nnm1.getLength(); i++) {
if (nnm1.item(i).getNodeType() != nnm2.item(i).getNodeType()) {
return false;
}
if (nnm1.item(i).getNodeName() != nnm2.item(i).getNodeName()) {
return false;
}
if (nnm1.item(i).getNodeValue() != nnm2.item(i).getNodeValue()) {
return false;
}
}
return true;
}
/**
* Collapse adjacent elements in a list with the same tag.
*
* @param elements The list of <code>Element</code>s to consider.
* @return A collapsed list of elements.
*/
private static List<Element> collapseElementList(List<Element> elements) {
List<Element> results = new ArrayList<Element>();
if (!elements.isEmpty()) {
Element head = elements.remove(0);
while (!elements.isEmpty()) {
Element e = elements.remove(0);
if (collapseOK(head, e)) {
collapseElements(head, e);
} else {
results.add(head);
head = e;
}
}
results.add(head);
}
return results;
}
/**
* Build a generalized update.
* Beware that removing an object does not necessarily update
* its tile correctly on the client side--- if a tile update
* is needed the tile should be supplied in the objects list.
*
* @param serverPlayer The <code>ServerPlayer</code> to send the
* update to.
* @return An element encapsulating an update of the objects to
* consider, or null if there is nothing to report.
*/
public Element build(ServerPlayer serverPlayer) {
List<Change> c = new ArrayList<Change>(changes);
List<Element> elements = new ArrayList<Element>();
List<Change> diverted = new ArrayList<Change>();
Document doc = DOMMessage.createNewDocument();
// For all sorted changes, if it is notifiable to the target
// player then convert it to an Element, or divert for later
// attachment. Then add all consequence changes to the list.
Collections.sort(c, changeComparator);
while (!c.isEmpty()) {
Change change = c.remove(0);
if (change.isNotifiable(serverPlayer)) {
if (change.convertsToElement()) {
elements.add(change.toElement(serverPlayer, doc));
} else {
diverted.add(change);
}
c.addAll(change.consequences(serverPlayer));
}
}
elements = collapseElementList(elements);
// Decide what to return. If there are several parts with
// children then return multiple, if there is one viable part,
// return that, if there is none return null unless there are
// attributes in which case they become viable as an update.
Element result;
switch (elements.size()) {
case 0:
if (diverted.isEmpty()) return null;
result = doc.createElement("update");
break;
case 1:
result = elements.get(0);
break;
default:
result = doc.createElement("multiple");
for (Element e : elements) result.appendChild(e);
break;
}
doc.appendChild(result);
for (Change change : diverted) change.attachToElement(result);
return result;
}
/**
* {@inheritDoc}
*/
public String toString() {
StringBuilder sb = new StringBuilder();
Collections.sort(changes, changeComparator);
for (Change c : changes) {
sb.append(c.toString());
sb.append("\n");
}
return sb.toString();
}
}