package net.sf.colossus.server;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sf.colossus.common.Constants;
import net.sf.colossus.game.Creature;
import net.sf.colossus.game.EntrySide;
import net.sf.colossus.game.Legion;
import net.sf.colossus.game.Player;
import net.sf.colossus.game.actions.AddCreatureAction;
import net.sf.colossus.game.actions.Recruitment;
import net.sf.colossus.util.Glob;
import net.sf.colossus.variant.CreatureType;
import net.sf.colossus.variant.MasterHex;
import org.jdom.Element;
/**
* Stores game history as XML.
*
* @author David Ripton
*/
public class History
{
private static final Logger LOGGER = Logger.getLogger(History.class
.getName());
/**
* History: events that happened before last commit point
*/
private final Element root;
/**
* History elements/events that happened since the last commit/"snapshot".
*/
private final List<Element> recentEvents = new LinkedList<Element>();
/**
* Set to true during the processing of {@link #fireEventsFromXML(Server)}
* to avoid triggering events we just restored again.
*/
private boolean loading = false;
/**
*
*/
private final Element loadedRedoLog;
private boolean isRedo = false;
/**
* Stores the surviving legions (this variable is not needed any more)
*
* While the history should contain all information to reproduce the game
* state, the last set of legions is currently still loaded upfront since
* they contain the battle-specific information. This collides with
* replaying the game from history...
* Now, since 08/2008, they are not stored as "survivorlegions" any more.
* Instead, they are backed up internally (done inside PlayerServerSide),
* all the history is replayed. This creates proper split prediction data
* in all clients. After that, backup data is compared with result of
* replay.
* E.g. Legion count, their content, players eliminated must be in sync.
* Then the replayed ones are discarded and the backedup ones restored
* - which have the right legion state (moved, donor, summoned, ...)
*
* TODO align the history replay more with the original gameplay so we
* don't need this anymore;
* 08/2008:==> this is now to some part done. Still replay
* events could be closer to original events (split, summon,
* acquire, teleport, ...) , not just the "result" of that
* event (reveal,add,remove effects).
*
* TODO instead: model the actual events instead of just result,
* or at least add relevant info to history elements, so that all
* replayed events carry all needed data so that they could also be
* processed by event viewer (currently EV does not process anything
* during replay).
*/
public History()
{
root = new Element("History");
// Dummy:
loadedRedoLog = new Element("LoadedRedoLog");
}
/**
* Constructor used by "LoadGame"
*/
public History(Element loadGameRoot)
{
// Get the history elements and store them to "root"
root = (Element)loadGameRoot.getChild("History").clone();
// Get the redo log content
loadedRedoLog = (Element)loadGameRoot.getChild("Redo").clone();
}
/**
* All events before last commit
*/
Element getCopy()
{
return (Element)root.clone();
}
/**
* Reached a commit point: append all recent events to the history,
* clear list of recent events; caller should do this together with creating
* the next snapshot.
*/
void flushRecentToRoot()
{
for (Element el : recentEvents)
{
el.detach();
String name = el.getName();
// TODO later, when this are proper events (not XML elements),
// ask rather from the Event whether it belongs copied to
// history or not.
// TODO At some point in future, put also those Move events
// that reveal something to history, and make either the history
// replay only send the relevant reveal messages, or make the
// Clients during replay (but not redo part) ignore the "move"
// and just process the revealing part.
// Preferrably the latter, so that proper events show up in the
// EventViewer.
if (name.equals("Move") || name.equals("UndoMove"))
{
LOGGER.finest("Flush Redo to History: skipping " + name);
}
else if (name.equals("Recruit") || name.equals("UndoRecruit"))
{
// Skipping for now, because there are also the addCreature,
// removeCreature and reveal Events still in history.
// TODO make the Recruit/UndoRecruit history events during
// replay properly, get rid of the "side effect" type of
// entries in save game.
LOGGER.finest("Flush Redo to History: skipping " + name);
}
else
{
root.addContent(el);
}
}
recentEvents.clear();
}
/**
* @return A Redo Element, containing all events since last commit
* i.e. which need to be REDOne on top of last commit point/snapshot
*/
Element getNewRedoLogElement()
{
Element redoLogElement = new Element("Redo");
for (Element el : recentEvents)
{
el.detach();
redoLogElement.addContent(el);
}
return redoLogElement;
}
/**
* TODO reconsider name
* TODO decide if we should move it all into one big handleEvent(GameEvent) method
*/
void addCreatureEvent(AddCreatureAction event, int turn, String reason)
{
if (loading)
{
return;
}
Element element = new Element("AddCreature");
element.setAttribute("markerId", event.getLegion().getMarkerId());
element.setAttribute("creatureName", event.getAddedCreatureType()
.getName());
element.setAttribute("turn", "" + turn);
element.setAttribute("reason", reason);
recentEvents.add(element);
}
void removeCreatureEvent(Legion legion, CreatureType creature, int turn,
String reason)
{
if (loading)
{
return;
}
Element event = new Element("RemoveCreature");
event.setAttribute("markerId", legion.getMarkerId());
event.setAttribute("creatureName", creature.getName());
event.setAttribute("turn", "" + turn);
event.setAttribute("reason", reason);
recentEvents.add(event);
}
void relocateLegionEvent(Legion legion)
{
Element event = new Element("RelocateLegion");
event.setAttribute("markerId", legion.getMarkerId());
event.setAttribute("destination", legion.getCurrentHex().getLabel());
recentEvents.add(event);
}
void splitEvent(Legion parent, Legion child, List<CreatureType> splitoffs,
int turn)
{
if (loading)
{
return;
}
Element event = new Element("Split");
event.setAttribute("parentId", parent.getMarkerId());
event.setAttribute("childId", child.getMarkerId());
event.setAttribute("turn", "" + turn);
Element creatures = new Element("splitoffs");
event.addContent(creatures);
for (CreatureType creatureType : splitoffs)
{
Element cr = new Element("creature");
cr.addContent(creatureType.getName());
creatures.addContent(cr);
}
recentEvents.add(event);
}
void mergeEvent(String splitoffId, String survivorId, int turn)
{
if (loading)
{
return;
}
Element event = new Element("Merge");
event.setAttribute("splitoffId", splitoffId);
event.setAttribute("survivorId", survivorId);
event.setAttribute("turn", "" + turn);
recentEvents.add(event);
}
void revealEvent(boolean allPlayers, List<Player> players, Legion legion,
List<CreatureType> creatures, int turn, String reason)
{
if (loading)
{
return;
}
if (creatures.isEmpty())
{
// this happens e.g. when in final battle (titan vs. titan)
// angel was called out of legion which was then empty,
// and in the final updateAllLegionContents there is then
// this empty legion...
// TODO if this case can happen in a regular game no warning
// should be logged
LOGGER.log(Level.WARNING, "Called revealEvent(" + allPlayers
+ ", " + (players != null ? players.toString() : "-null-")
+ ", " + legion + ", " + creatures.toString() + ", " + turn
+ ") with empty creatureNames");
return;
}
Element event = new Element("Reveal");
event.setAttribute("markerId", legion.getMarkerId());
event.setAttribute("allPlayers", "" + allPlayers);
event.setAttribute("turn", "" + turn);
event.setAttribute("reason", reason);
if (!allPlayers)
{
Element viewers = new Element("viewers");
event.addContent(viewers);
Iterator<Player> it = players.iterator();
while (it.hasNext())
{
String playerName = it.next().getName();
Element viewer = new Element("viewer");
viewer.addContent(playerName);
viewers.addContent(viewer);
}
}
Element creaturesElem = new Element("creatures");
event.addContent(creaturesElem);
for (CreatureType creatureType : creatures)
{
Element creatureElem = new Element("creature");
creatureElem.addContent(creatureType.getName());
creaturesElem.addContent(creatureElem);
}
recentEvents.add(event);
}
void playerElimEvent(Player player, Player slayer, int turn)
{
if (loading)
{
return;
}
Element event = new Element("PlayerElim");
event.setAttribute("name", player.getName());
if (slayer != null)
{
event.setAttribute("slayer", slayer.getName());
}
event.setAttribute("turn", "" + turn);
recentEvents.add(event);
}
void movementRollEvent(Player player, int roll)
{
if (loading)
{
return;
}
Element event = new Element("MovementRoll");
event.setAttribute("playerName", player.getName());
event.setAttribute("roll", "" + roll);
recentEvents.add(event);
}
void legionMoveEvent(Legion legion, MasterHex newHex, EntrySide entrySide,
boolean teleport, CreatureType lord)
{
if (loading)
{
return;
}
Element event = new Element("Move");
event.setAttribute("markerId", legion.getMarkerId());
event.setAttribute("newHex", newHex.getLabel());
event.setAttribute("entrySide", entrySide.getLabel());
event.setAttribute("teleport", "" + teleport);
String creNameOrTextNull = lord == null ? "null" : lord.getName();
event.setAttribute("revealedLord", creNameOrTextNull);
recentEvents.add(event);
}
void legionUndoMoveEvent(Legion legion)
{
if (loading)
{
return;
}
Element event = new Element("UndoMove");
event.setAttribute("markerId", legion.getMarkerId());
recentEvents.add(event);
}
void recruitEvent(Legion legion, CreatureType recruit,
CreatureType recruiter)
{
if (loading)
{
return;
}
Element event = new Element("Recruit");
event.setAttribute("markerId", legion.getMarkerId());
event.setAttribute("recruit", recruit.getName());
event.setAttribute("recruiter",
recruiter == null ? "null" : recruiter.getName());
recentEvents.add(event);
}
void undoRecruitEvent(Legion legion)
{
if (loading)
{
return;
}
Element event = new Element("UndoRecruit");
event.setAttribute("markerId", legion.getMarkerId());
recentEvents.add(event);
}
/**
* Fire all events from redoLog.
* Elements from RedoLog are processed one by one and the corresponding
* method is called on the Server object, pretty much as if a
* ClientHandler would call it when receiving such a request from Client.
* Note that in some cases overriding the processingCH is necessary
* (because technically, this all currently happens while still the
* connecting of last joining player is processed, so processingCH is
* set to his ClientHandler).
*
* Note that "loading" is not set to true, so they DO GET ADDED to the
* recentEvents list again.
*
* @param server The server on which to call all the actions to be redone
*/
void processRedoLog(Server server)
{
assert loadedRedoLog != null : "Loaded RedoLog should always "
+ "have a JDOM root element as backing store";
LOGGER.info("History: Start processing redo log");
isRedo = true;
for (Object obj : loadedRedoLog.getChildren())
{
Element el = (Element)obj;
LOGGER.info("processing redo event " + el.getName());
fireEventFromElement(server, el);
}
isRedo = false;
// TODO clear loadedRedoLog?
LOGGER.info("Completed processing redo log");
}
// unchecked conversions from JDOM
@SuppressWarnings("unchecked")
void fireEventsFromXML(Server server)
{
this.loading = true;
assert root != null : "History should always have a "
+ " JDOM root element as backing store";
List<Element> kids = root.getChildren();
Iterator<Element> it = kids.iterator();
while (it.hasNext())
{
Element el = it.next();
fireEventFromElement(server, el);
}
this.loading = false;
}
// unchecked conversions from JDOM
@SuppressWarnings("unchecked")
void fireEventFromElement(Server server, Element el)
{
GameServerSide game = server.getGame();
String eventName = el.getName();
String reasonPerhaps = el.getAttributeValue("reason");
String reason = (reasonPerhaps != null && !reasonPerhaps
.equals("null")) ? reasonPerhaps : "<undefinedReason>";
if (eventName.equals("Reveal") && isRedo
&& reason.equals(Constants.reasonRecruiter))
{
// Skip this because we redo the full recruit event
// TODO
LOGGER.finest("Skipping Reveal event (reason " + reason
+ ") during redo.");
}
else if (eventName.equals("AddCreature") && isRedo
&& reason.equals(Constants.reasonRecruited))
{
// Skip this because we redo the full recruit event
LOGGER.finest("Skipping AddCreature event (reason " + reason
+ ") during redo.");
}
else if (eventName.equals("RemoveCreature") && isRedo
&& reason.equals(Constants.reasonRecruited))
{
// Skip this because we redo the full recruit event
LOGGER.finest("Skipping RemoveCreature event (reason " + reason
+ ") during redo.");
}
else if (eventName.equals("Reveal"))
{
String allPlayers = el.getAttributeValue("allPlayers");
boolean all = allPlayers != null && allPlayers.equals("true");
String markerId = el.getAttributeValue("markerId");
List<String> playerNames = new ArrayList<String>();
Element viewEl = el.getChild("viewers");
int turn = Integer.parseInt(el.getAttributeValue("turn"));
String playerName = null;
if (viewEl != null)
{
List<Element> viewers = viewEl.getChildren();
Iterator<Element> it = viewers.iterator();
while (it.hasNext())
{
Element viewer = it.next();
playerName = viewer.getTextNormalize();
playerNames.add(playerName);
}
}
List<Element> creatureElements = el.getChild("creatures")
.getChildren();
List<CreatureType> creatures = new ArrayList<CreatureType>();
for (Element creature : creatureElements)
{
String creatureName = creature.getTextNormalize();
creatures.add(game.getVariant()
.getCreatureByName(creatureName));
}
Player player = game.getPlayerByMarkerId(markerId);
Legion legion;
if (turn == 1 && player.getLegionByMarkerId(markerId) == null)
{
// there is no create event for the startup legions,
// so we might need to create them for the reveal event
legion = new LegionServerSide(markerId, null,
player.getStartingTower(), player.getStartingTower(),
player, game, creatures.toArray(new CreatureType[creatures
.size()]));
player.addLegion(legion);
}
else
{
legion = player.getLegionByMarkerId(markerId);
}
// TODO Now we get the reason from history element - does this
// change effect/break anything?
// String reason = "<unknown>";
if (((PlayerServerSide)player).getDeadBeforeSave())
{
// Skip for players that will be dead by end of replay
}
else if (all)
{
server.allRevealCreatures(legion, creatures, reason);
}
else
{
server.oneRevealLegion(game.getPlayerByName(playerName),
legion, creatures, reason);
}
}
else if (eventName.equals("Split"))
{
String parentId = el.getAttributeValue("parentId");
String childId = el.getAttributeValue("childId");
String turnString = el.getAttributeValue("turn");
int turn = Integer.parseInt(turnString);
List<String> creatureNames = new ArrayList<String>();
List<CreatureType> creatures = new ArrayList<CreatureType>();
List<Element> splitoffs = el.getChild("splitoffs").getChildren();
Iterator<Element> it = splitoffs.iterator();
while (it.hasNext())
{
Element creature = it.next();
String creatureName = creature.getTextNormalize();
creatureNames.add(creatureName);
creatures.add(game.getVariant()
.getCreatureByName(creatureName));
}
LegionServerSide parentLegion = game.getLegionByMarkerId(parentId);
if (isRedo)
{
server.overrideProcessingCH(parentLegion.getPlayer());
server.doSplit(parentLegion, childId, creatures);
server.overrideProcessingCH(parentLegion.getPlayer());
return;
}
// LegionServerSide.split(..) doesn't like us here since the parent
// legion can't remove creatures (not there?) -- create child directly
// instead
PlayerServerSide player = parentLegion.getPlayer();
LegionServerSide childLegion;
if (player.hasLegion(childId))
{
childLegion = game.getLegionByMarkerId(childId);
LOGGER.severe("During replay of history: child legion "
+ childId + " should not " + "exist yet (turn=" + turn
+ ")!!\n" + "Exists already with: "
+ Glob.glob(",", childLegion.getCreatureTypes()) + " but "
+ "should now be created with creatures: " + creatures);
childLegion.remove();
}
childLegion = new LegionServerSide(childId, null,
parentLegion.getCurrentHex(), parentLegion.getCurrentHex(),
player, game, creatures.toArray(new CreatureType[creatures
.size()]));
player.addLegion(childLegion);
for (CreatureType creature : creatures)
{
parentLegion.removeCreature(creature, false, false);
}
// Skip for players that will be dead by end of replay
if (!player.getDeadBeforeSave())
{
server.allTellDidSplit(parentLegion, childLegion, turn, false);
}
}
else if (eventName.equals("Merge"))
{
String splitoffId = el.getAttributeValue("splitoffId");
String survivorId = el.getAttributeValue("survivorId");
String turnString = el.getAttributeValue("turn");
int turn = Integer.parseInt(turnString);
LegionServerSide splitoff = game.getLegionByMarkerId(splitoffId);
LegionServerSide survivor = game.getLegionByMarkerId(survivorId);
// Skip for players that will be dead by end of replay
if (!survivor.getPlayer().getDeadBeforeSave())
{
server.undidSplit(splitoff, survivor, false, turn);
}
// Add them back to parent:
while (splitoff.getHeight() > 0)
{
CreatureType type = splitoff.removeCreature(0, false, false);
survivor.addCreature(type, false);
}
splitoff.remove(false, false);
}
else if (eventName.equals("AddCreature"))
{
String markerId = el.getAttributeValue("markerId");
String creatureName = el.getAttributeValue("creatureName");
// TODO Now we get the reason from history element - does this
// change effect/break anything?
// String reason = "<unknown>";
LOGGER.finer("Adding creature '" + creatureName
+ "' to legion with markerId '" + markerId + "', reason '"
+ reason + "'");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
CreatureType creatureType = game.getVariant().getCreatureByName(
creatureName);
legion.addCreature(creatureType, false);
// Skip for players that will be dead by end of replay
if (!legion.getPlayer().getDeadBeforeSave())
{
boolean doHistory = (reason.equals(Constants.reasonEdit));
server.allTellAddCreature(new AddCreatureAction(legion,
creatureType), doHistory, reason);
}
LOGGER.finest("Legion '" + markerId + "' now contains "
+ legion.getCreatures());
}
else if (eventName.equals("RemoveCreature"))
{
String markerId = el.getAttributeValue("markerId");
String creatureName = el.getAttributeValue("creatureName");
// TODO Now we get the reason from history element - does this
// change effect/break anything?
// String reason = "<unknown>";
LOGGER.finer("Removing creature '" + creatureName
+ "' from legion with markerId '" + markerId + "', reason '"
+ reason + "'");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
if (legion == null)
{
LOGGER.warning("removeCreature " + creatureName
+ " from legion " + markerId + ", legion is null");
return;
}
else
{
List<? extends Creature> cres = legion.getCreatures();
List<String> crenames = new ArrayList<String>();
for (Creature c : cres)
{
crenames.add(c.getName());
}
}
// don't use disbandIfEmpty parameter since that'll fire another history event
CreatureType removedCritter = legion.removeCreature(game
.getVariant().getCreatureByName(creatureName), false, false);
// Skip for players that will be dead by end of replay
// Skip if removedCritter is null => removeCreature did not find it,
// so there is something wrong with the save game. No use to bother
// all the clients with it.
if (removedCritter != null
&& !legion.getPlayer().getDeadBeforeSave())
{
boolean doHistory = (reason.equals(Constants.reasonEdit));
server.allTellRemoveCreature(legion, removedCritter,
doHistory, reason);
}
LOGGER.finest("Legion '" + markerId + "' now contains "
+ legion.getCreatures());
if (legion.getHeight() == 0)
{
legion.remove(false, false);
LOGGER.finer("Legion '" + markerId + "' removed");
}
}
else if (eventName.equals("RelocateLegion"))
{
String markerId = el.getAttributeValue("markerId");
String hexLabel = el.getAttributeValue("destination");
// Other events come via server from client, and history replay does
// the same. This one here came direct via localServer to game, and
// thus we keep it the same.
game.editModeRelocateLegion(markerId, hexLabel);
}
else if (eventName.equals("PlayerElim"))
{
String playerName = el.getAttributeValue("name");
String slayerName = el.getAttributeValue("slayer");
Player player = game.getPlayerByName(playerName);
Player slayer = game.getPlayerByNameIgnoreNull(slayerName);
// Record the slayer and give him this player's legion markers.
if (slayer != null)
{
((PlayerServerSide)player).handleSlaying(slayer);
}
player.setDead(true);
server.allUpdatePlayerInfo("FireEvent-PlayerElim");
server.allTellPlayerElim(player, slayer, false);
}
else if (eventName.equals("MovementRoll"))
{
String playerName = el.getAttributeValue("playerName");
Player player = game.getPlayerByName(playerName);
int roll = Integer.parseInt(el.getAttributeValue("roll"));
((PlayerServerSide)player).setMovementRoll(roll);
game.movementRollEvent(player, roll);
server.allTellMovementRoll(roll, null);
}
else if (eventName.equals("Move"))
{
String markerId = el.getAttributeValue("markerId");
String lordName = el.getAttributeValue("revealedLord");
String tele = el.getAttributeValue("teleport");
String newHexLabel = el.getAttributeValue("newHex");
String entrySideName = el.getAttributeValue("entrySide");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
CreatureType revealedLord = lordName.equals("null") ? null : game
.getVariant().getCreatureByName(lordName);
MasterHex newHex = server.getGame().getVariant().getMasterBoard()
.getHexByLabel(newHexLabel);
EntrySide entrySide = EntrySide.fromLabel(entrySideName);
boolean teleport = tele != null && tele.equals("true");
LOGGER.finest("Legion Move redo event: \n" + " marker " + markerId
+ ", lordName " + revealedLord + " teleported " + teleport
+ " to hex " + newHex.getLabel() + " entrySide "
+ entrySide.toString());
server.overrideProcessingCH(legion.getPlayer());
server.doMove(legion, newHex, entrySide, teleport, revealedLord);
server.restoreProcessingCH();
}
else if (eventName.equals("UndoMove"))
{
String markerId = el.getAttributeValue("markerId");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
LOGGER.finest("Legion Undo Move redo event: \n" + " marker "
+ markerId);
server.overrideProcessingCH(legion.getPlayer());
server.undoMove(legion);
server.restoreProcessingCH();
}
else if (eventName.equals("Recruit"))
{
String markerId = el.getAttributeValue("markerId");
String recruitName = el.getAttributeValue("recruit");
String recruiterName = el.getAttributeValue("recruiter");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
CreatureType recruit = game.getVariant().getCreatureByName(
recruitName);
CreatureType recruiter = recruiterName.equals("null") ? null
: game.getVariant().getCreatureByName(recruiterName);
LOGGER.finest("Recruit redo event: \n" + " marker " + markerId
+ " recruit " + recruit + " recruiter " + recruiter);
server.overrideProcessingCH(legion.getPlayer());
server.doRecruit(new Recruitment(legion, recruit, recruiter));
server.restoreProcessingCH();
}
else if (eventName.equals("UndoRecruit"))
{
String markerId = el.getAttributeValue("markerId");
LegionServerSide legion = game.getLegionByMarkerId(markerId);
LOGGER
.finest("UndoRecruit redo event: \n" + " marker " + markerId);
server.overrideProcessingCH(legion.getPlayer());
server.undoRecruit(legion);
server.restoreProcessingCH();
}
else
{
LOGGER.warning("Unknown Redo element " + eventName);
}
}
}