package net.sf.colossus.gui; import java.awt.Component; import java.awt.Container; import java.awt.Dimension; import java.awt.GridLayout; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.ListIterator; import java.util.logging.Level; import java.util.logging.Logger; import javax.swing.Box; import javax.swing.BoxLayout; import javax.swing.JCheckBox; import javax.swing.JComboBox; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollBar; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.ScrollPaneConstants; import javax.swing.SwingUtilities; import javax.swing.WindowConstants; import javax.swing.border.TitledBorder; import net.sf.colossus.client.Client; import net.sf.colossus.common.Constants; import net.sf.colossus.common.IOptions; import net.sf.colossus.common.Options; import net.sf.colossus.game.BattleUnit; import net.sf.colossus.game.Legion; import net.sf.colossus.game.Phase; import net.sf.colossus.game.Player; import net.sf.colossus.guiutil.KDialog; import net.sf.colossus.variant.CreatureType; /** * Event Revealing dialog. * * It collects all revealed events and displays all or * only the recent ones of them. * * * @author Clemens Katzer */ /* * @TODO: * @IMPORTANT * - Check compatibility with previous version * - History: make history events carry the "reason" tag so that reloaded * games have same event view as in running game * "TODO: investigate, why does server gives us different turn numbers * in those Split/undoSplit events?" (=> see undoEvent) * * @Nice to have: * - Player dead events * - Legion eliminated (when player dead) events * - Game over, who wins event :) * - scale change: own setting, relative to main board scale or absolut? * react more quickly when scale changed? * - checkbox "don't show Dead creatures" * - RevealedCreature could cache the panels * - combined "battle event" - one bordered panel, containing * all relevant events ( [teleport?], summon, acquire, won, lost) * plus text info what now in Engagement result window * - interface for inspector & Co: for viewMode return info revealed during * one last turn * - Engagement won (fled...), instead of "not revealed" show contents * depending on what the viewMode says * - "SolidMarker" instead of Titan for mulligan * */ final class EventViewer extends KDialog { private static final Logger LOGGER = Logger.getLogger(EventViewer.class .getName()); private final static String WINDOW_TITLE = "Event Viewer"; private IOptions options; private Client client; private boolean visible; private final List<RevealEvent> eventList = new LinkedList<RevealEvent>(); private int bookmark = 0; final private List<JPanel> displayQueue = new LinkedList<JPanel>(); private int turnNr; private Player currentPlayer; // how long back are they kept (from global settings) private int expireTurns; private String maxString; private Container eventPane; private Box settingsPane; private JScrollPane eventScrollPane; private JScrollBar eventScrollBar; // Event Viewer Filter (evf) settings (= option names): public static final String evfSplit = "Split events"; public static final String evfRecruit = "Recruit events"; public static final String evfSummon = "Summon events"; public static final String evfTeleport = "Teleport events"; public static final String evfAcquire = "Acquire events"; public static final String evfWon = "Engagement won events"; public static final String evfLoser = "Engagement lost events"; public static final String evfMulligan = "Mulligans"; public static final String evfExtraRoll = "Extra rolls"; public static final String evfMoveRoll = "Movement rolls"; public static final String evfTurnChange = "Turn change info"; public static final String evfPlayerChange = "Player change info"; public static final String evAutoScroll = "Auto-scroll to end"; public static final String evHideUndone = "Hide undone events"; public static final String evMaxTurns = "Maximum number of turns to display"; // boolean flags for them as local flags for quick access: // Index for this are the EventXXX constants in RevealEvent.java. private final boolean[] showEventType; private boolean autoScroll; private boolean hideUndoneEvents; private JComboBox<String> maxTurnsDisplayExpiringBox; // how many back are currently displayed private int maxTurns = 1; private int mulliganOldRoll = -2; private Legion attacker; private Legion defender; private RevealEvent attackerEventLegion = null; private RevealEvent defenderEventLegion = null; private RevealEvent lastAttackerEventLegion = null; private RevealEvent lastDefenderEventLegion = null; private RevealEvent winnerLegion = null; private RevealEvent loserLegion = null; /** * Inits the dialog, not necessarily displays it. * * @param frame is the parent window frame (MasterBoard) * @param options IOptions reference to the client * @param client The client, needed to ask all kind of info */ EventViewer(final JFrame frame, final IOptions options, Client client) { super(frame, WINDOW_TITLE, false); setFocusable(false); this.options = options; this.client = client; initExpireTurnsFromOptions(); showEventType = new boolean[RevealEvent.NUMBEROFEVENTS]; showEventType[RevealEvent.eventRecruit] = getBoolOption(evfRecruit, true); // Reinforce uses same checkbox as Recruit! showEventType[RevealEvent.eventReinforce] = getBoolOption(evfRecruit, true); showEventType[RevealEvent.eventSplit] = getBoolOption(evfSplit, true); showEventType[RevealEvent.eventTeleport] = getBoolOption(evfTeleport, true); showEventType[RevealEvent.eventSummon] = getBoolOption(evfSummon, true); showEventType[RevealEvent.eventAcquire] = getBoolOption(evfAcquire, true); showEventType[RevealEvent.eventWon] = getBoolOption(evfWon, true); showEventType[RevealEvent.eventLost] = getBoolOption(evfLoser, true); showEventType[RevealEvent.eventMulligan] = getBoolOption(evfMulligan, true); showEventType[RevealEvent.eventExtraRoll] = getBoolOption( evfExtraRoll, true); showEventType[RevealEvent.eventMoveRoll] = getBoolOption(evfMoveRoll, true); showEventType[RevealEvent.eventTurnChange] = getBoolOption( evfTurnChange, true); showEventType[RevealEvent.eventPlayerChange] = getBoolOption( evfPlayerChange, false); autoScroll = getBoolOption(evAutoScroll, true); hideUndoneEvents = getBoolOption(evHideUndone, false); setupGUI(); setDefaultCloseOperation(WindowConstants.HIDE_ON_CLOSE); this.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { options.setOption(Options.showEventViewer, false); } }); Point defaultLoc = getUpperRightCorner(getWidth()); defaultLoc.setLocation(defaultLoc.x, 120); useSaveWindow(options, WINDOW_TITLE, defaultLoc); setVisibleMaybe(); } // How many turns back data is kept; default to 1 if no such // option found from user options file. private void initExpireTurnsFromOptions() { int turnsToKeep = 1; String expOption = options.getStringOption(Options.eventExpiring); if (expOption != null) { if (expOption.equals(Options.eventExpiringNever)) { turnsToKeep = -1; } else { int exp; try { exp = Integer.parseInt(expOption); if (exp > 0 && exp < 9999) { turnsToKeep = exp; } else { LOGGER.log(Level.SEVERE, "Invalid value " + exp + " from option '" + Options.eventExpiring + "' - using default " + turnsToKeep); } } catch (NumberFormatException e) { LOGGER.log(Level.SEVERE, "Invalid value " + expOption + " from option '" + Options.eventExpiring + "' - using default " + turnsToKeep); } } } this.expireTurns = turnsToKeep; } private boolean getBoolOption(String name, boolean defaultVal) { boolean bVal = defaultVal; String sVal = options.getStringOption(name); if (sVal == null) { options.setOption(name, defaultVal); } else { bVal = sVal.equals("true"); } return bVal; } private void addCheckbox(final String optname, Container pane) { JCheckBox cb = new JCheckBox(optname); boolean selected = getBoolOption(optname, true); cb.setSelected(selected); cb.setAlignmentX(Component.LEFT_ALIGNMENT); cb.setAlignmentY(Component.TOP_ALIGNMENT); cb.addItemListener(new ItemListener() { public void itemStateChanged(ItemEvent e) { boolean selected = (e.getStateChange() == ItemEvent.SELECTED); options.setOption(optname, selected); if (optname.equals(evAutoScroll)) { autoScroll = selected; } else if (optname.equals(evHideUndone)) { hideUndoneEvents = selected; } else if (optname.equals(evfRecruit)) { showEventType[RevealEvent.eventRecruit] = selected; } else if (optname.equals(evfSplit)) { showEventType[RevealEvent.eventSplit] = selected; } else if (optname.equals(evfTeleport)) { showEventType[RevealEvent.eventTeleport] = selected; } else if (optname.equals(evfSummon)) { showEventType[RevealEvent.eventSummon] = selected; } else if (optname.equals(evfWon)) { showEventType[RevealEvent.eventWon] = selected; } else if (optname.equals(evfLoser)) { showEventType[RevealEvent.eventLost] = selected; } else if (optname.equals(evfAcquire)) { showEventType[RevealEvent.eventAcquire] = selected; } else if (optname.equals(evfTurnChange)) { showEventType[RevealEvent.eventTurnChange] = selected; } else if (optname.equals(evfMulligan)) { showEventType[RevealEvent.eventMulligan] = selected; } else if (optname.equals(evfMoveRoll)) { showEventType[RevealEvent.eventMoveRoll] = selected; } else if (optname.equals(evfExtraRoll)) { showEventType[RevealEvent.eventExtraRoll] = selected; } else if (optname.equals(evfPlayerChange)) { showEventType[RevealEvent.eventPlayerChange] = selected; } updatePanels(false); } }); pane.add(cb); } private void setupGUI() { // A tabbed pane, one tab the events, one tab the settings JTabbedPane tabbedPane = new JTabbedPane(); tabbedPane.setPreferredSize(new Dimension(270, 520)); // Events: Box eventTabPane = new Box(BoxLayout.Y_AXIS); tabbedPane.addTab("Events", eventTabPane); eventTabPane.setAlignmentX(Component.LEFT_ALIGNMENT); eventTabPane.setPreferredSize(new Dimension(250, 500)); eventTabPane.setAlignmentX(Component.LEFT_ALIGNMENT); eventPane = new Box(BoxLayout.Y_AXIS); eventPane.add(Box.createVerticalGlue()); eventScrollPane = new JScrollPane(eventPane, ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS, ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); eventScrollBar = eventScrollPane.getVerticalScrollBar(); eventTabPane.add(eventScrollPane); // The settings: settingsPane = new Box(BoxLayout.Y_AXIS); tabbedPane.addTab("Settings", settingsPane); JPanel checkboxPane = new JPanel(new GridLayout(0, 1)); checkboxPane.setAlignmentX(Component.LEFT_ALIGNMENT); checkboxPane.setBorder(new TitledBorder("Event Filter")); checkboxPane.setPreferredSize(new Dimension(200, 300)); checkboxPane.setMaximumSize(new Dimension(600, 300)); settingsPane.add(checkboxPane); settingsPane.add(Box.createRigidArea(new Dimension(0, 5))); addCheckbox(evfRecruit, checkboxPane); addCheckbox(evfSplit, checkboxPane); addCheckbox(evfSummon, checkboxPane); addCheckbox(evfTeleport, checkboxPane); addCheckbox(evfAcquire, checkboxPane); addCheckbox(evfWon, checkboxPane); addCheckbox(evfLoser, checkboxPane); checkboxPane.add(Box.createRigidArea(new Dimension(0, 5))); addCheckbox(evHideUndone, checkboxPane); checkboxPane.add(Box.createRigidArea(new Dimension(0, 5))); addCheckbox(evfMulligan, checkboxPane); addCheckbox(evfMoveRoll, checkboxPane); addCheckbox(evfTurnChange, checkboxPane); addCheckbox(evfPlayerChange, checkboxPane); JPanel miscPane = new JPanel(new GridLayout(0, 2)); miscPane.setAlignmentX(Component.LEFT_ALIGNMENT); miscPane.setBorder(new TitledBorder("Other Settings")); miscPane.setMinimumSize(new Dimension(200, 100)); miscPane.setPreferredSize(new Dimension(200, 100)); miscPane.setMaximumSize(new Dimension(600, 100)); settingsPane.add(miscPane); settingsPane.add(Box.createVerticalGlue()); settingsPane.add(Box.createRigidArea(new Dimension(0, 5))); addCheckbox(evAutoScroll, miscPane); miscPane.add(Box.createRigidArea(new Dimension(0, 5))); // selection for how many turns to display the data // (must be less or equal the expireTurns value set in getPlayers) int maxVal = this.expireTurns == -1 ? 1000 : this.expireTurns; List<String> alChoices = new ArrayList<String>(); for (int i = 1; i <= maxVal; i++) { // 1, 2, 3, 4, 5, if (i <= 5 || i == maxVal) { alChoices.add(String.valueOf(i)); } /* right now: no big values due to performance issues... // 10, 50, 100, 500, 1000 if applicable. else if (i==10 || i==50 || i==100 || i==500 || i==1000) */ else if (i == 10) { alChoices.add(String.valueOf(i)); } } if (this.expireTurns == -1) { alChoices.add("all"); } else { maxString = "max (=" + this.expireTurns + ")"; alChoices.add(maxString); } // read user's setting for this, but cannot exceed the Game's // general setting. String maxTurnsOptString = options.getStringOption(evMaxTurns); if (maxTurnsOptString == null) { maxTurnsOptString = "3"; } if (maxTurnsOptString.equals("all") || maxTurnsOptString.startsWith("max")) { if (this.expireTurns == -1) { maxTurns = -1; maxTurnsOptString = "all"; } else { maxTurns = this.expireTurns; maxTurnsOptString = maxString; } } else { int maxTurnsOpt = 1; try { maxTurnsOpt = Integer.parseInt(maxTurnsOptString); if (maxTurnsOpt > maxVal) { maxTurnsOpt = maxVal; maxTurnsOptString = Integer.valueOf(maxTurnsOpt) .toString(); } } catch (NumberFormatException e) { LOGGER.log(Level.SEVERE, "Illegal value '" + maxTurnsOptString + "' for option '" + evMaxTurns + "' - using default 1"); maxTurnsOptString = "1"; maxTurnsOpt = 1; } } String[] choicesArray = alChoices.toArray(new String[0]); maxTurnsDisplayExpiringBox = new JComboBox<String>(choicesArray); maxTurnsDisplayExpiringBox.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { String value = (String)maxTurnsDisplayExpiringBox .getSelectedItem(); options.setOption(evMaxTurns, value); if (value.equals("all") || value.equals(maxString)) { maxTurns = -1; } else { maxTurns = Integer.parseInt(value); } updatePanels(true); } }); maxTurnsDisplayExpiringBox.setSelectedItem(maxTurnsOptString); miscPane.add(new JLabel("Display max. (turns):")); miscPane.add(maxTurnsDisplayExpiringBox); // add all to the main contentPane Container contentPane = this.getContentPane(); contentPane.setLayout(new BoxLayout(contentPane, BoxLayout.Y_AXIS)); contentPane.add(tabbedPane); contentPane.validate(); this.pack(); } private boolean isEventTooOld(RevealEvent e) { int oldEventTurn = e.getTurn(); int oldPlayerNr = e.getPlayer().getNumber(); if (maxTurns != -1 && turnNr - oldEventTurn > maxTurns - (currentPlayer.getNumber() >= oldPlayerNr ? 1 : 0)) { // Log.debug("Not displaying event "+e.getEventTypeText()+" "+ // e.getMarkerId() + " - older than max turns value!"); return true; } return false; } private boolean isEventRelevant(RevealEvent e) { int type = e.getEventType(); boolean display = true; if (!showEventType[type]) { display = false; } else if (hideUndoneEvents && e.wasUndone()) { LOGGER.log( Level.FINEST, "Not displaying event " + e.getEventTypeText() + " " + e.getLongMarkerId() + " - was undone and hideUndoneEvents is true."); display = false; } return display; } private void queueForDisplaying(JPanel eventPanel) { synchronized (displayQueue) { displayQueue.add(eventPanel); } } /** Remove all pending events, and queue a null event to signal the * displayer to remove all from panel first before adding again. */ private void queueSignalRemoveAllForDisplaying() { synchronized (displayQueue) { displayQueue.clear(); displayQueue.add(null); } } private void displayFromQueue() { synchronized (displayQueue) { if (displayQueue.size() > 0) { Iterator<JPanel> it = displayQueue.iterator(); while (it.hasNext()) { JPanel panelForEvent = it.next(); if (panelForEvent == null) { eventPane.removeAll(); } else { eventPane.add(panelForEvent); eventPane .add(Box.createRigidArea(new Dimension(0, 5))); } } displayQueue.clear(); postAddEventActions(); } else { // ok, queue was empty, nothing to do } } } private void postAddEventActions() { getContentPane().validate(); if (autoScroll) { eventScrollBar.setValue(eventScrollBar.getMaximum()); } getContentPane().validate(); } private void addEventToEventPane(RevealEvent e) { JPanel panelForEvent = e.toPanel(); if (panelForEvent != null) { queueForDisplaying(panelForEvent); } else { LOGGER .log(Level.WARNING, "EventViewer.addEventToEventPane: event.toPanel returned null!"); } } private void addEventToList(RevealEvent e) { synchronized (eventList) { eventList.add(e); } } private void triggerDisplaying() { if (SwingUtilities.isEventDispatchThread()) { displayFromQueue(); } else { SwingUtilities.invokeLater(new Runnable() { public void run() { displayFromQueue(); } }); } } public void addEvent(RevealEvent e) { addEventToList(e); if (this.visible) { boolean display = isEventRelevant(e); if (display) { addEventToEventPane(e); triggerDisplaying(); } } } /* * Remove all, and add those again which are still in the eventList * @param forceAll: reset the bookmark, start from begin of list. * => if not set, can start searching from last * remembered position. */ private void updatePanels(boolean forceAll) { queueSignalRemoveAllForDisplaying(); synchronized (eventList) { // if never expires, we never delete, so bookmark stays ok. // But if expiring is happening (!= -1) or force is given // (e.g. when maxTurns changed) then need to start searching // from start. if (this.expireTurns != -1 || forceAll) { bookmark = 0; } if (bookmark > eventList.size()) { // sanity check... LOGGER.log(Level.SEVERE, "bookmark " + bookmark + " out of range, size=" + eventList.size()); bookmark = 0; } ListIterator<RevealEvent> it = eventList.listIterator(bookmark); while (it.hasNext()) { RevealEvent e = it.next(); if (isEventTooOld(e)) { bookmark++; } else if (isEventRelevant(e)) { addEventToEventPane(e); } } } triggerDisplaying(); this.repaint(); } // Helper methods to ask something from client: private Player getActivePlayer() { return client.getActivePlayer(); } // shortcuts: private void newRollEvent(int eventType, int roll1, int roll2) { RevealEvent e = new RevealEvent(client.getTurnNumber(), getActivePlayer(), eventType, roll1, roll2); addEvent(e); } // creature related event: private void newEvent(int eventType, Legion legion1, ArrayList<RevealedCreature> rcList, Legion legion2) { RevealEvent e = new RevealEvent(client.getTurnNumber(), getActivePlayer(), eventType, legion1, rcList, legion2); addEvent(e); } // Now come the methods with which Client can add/modify event data: public void turnOrPlayerChange(int turnNr, Player player) { setMulliganOldRoll(-2); if (turnNr != this.turnNr) { RevealEvent e = new RevealEvent(turnNr, player, RevealEvent.eventTurnChange); addEvent(e); } if (player != this.currentPlayer || turnNr != this.turnNr) { RevealEvent e = new RevealEvent(turnNr, player, RevealEvent.eventPlayerChange); addEvent(e); } this.turnNr = turnNr; this.currentPlayer = player; if (this.expireTurns != -1) { purgeOldEvents(); if (this.visible) { updatePanels(true); } } else // expire turns -1 ==> no purging. { if (this.maxTurns != -1) { // something will have got expired now // but we can use the bookmark. updatePanels(false); } // else: maxTurns -1 => stays displaying from begin on, // so no update needed at all. } } public void setMulliganOldRoll(int roll) { mulliganOldRoll = roll; } public void tellMovementRoll(int roll, String reason) { // if oldroll is -2, this is the first roll; // otherwise, player took mulligan. if (mulliganOldRoll == -2 || reason.equals(Constants.reasonNormalRoll)) { newRollEvent(RevealEvent.eventMoveRoll, roll, -1); } else if (reason.equals(Constants.reasonExtraRoll)) { newRollEvent(RevealEvent.eventExtraRoll, mulliganOldRoll, roll); } else if (reason.equals(Constants.reasonMulligan)) { newRollEvent(RevealEvent.eventMulligan, mulliganOldRoll, roll); } else { LOGGER.warning("Unrecognized reason '" + reason + "' for movement roll?"); newRollEvent(RevealEvent.eventMoveRoll, roll, -1); } mulliganOldRoll = roll; } public void tellEngagement(Legion attacker, Legion defender, int turnNumber) { this.attacker = attacker; this.defender = defender; attackerEventLegion = new RevealEvent(turnNumber, getActivePlayer(), RevealEvent.eventBattle, attacker, new ArrayList<RevealedCreature>(), null); attackerEventLegion.setEventInfo(Constants.reasonBattleStarts); attackerEventLegion.setRealPlayer(attacker.getPlayer()); defenderEventLegion = new RevealEvent(turnNumber, getActivePlayer(), RevealEvent.eventBattle, defender, new ArrayList<RevealedCreature>(), null); defenderEventLegion.setEventInfo(Constants.reasonBattleStarts); defenderEventLegion.setRealPlayer(defender.getPlayer()); } public void tellEngagementResults(Legion winner, String method, int turns) { // if those are not set, we are new version client with old // version server, who does not provide the reason argument // to some other methods; then they do not set up those // eventLegions we would need here. So, can't do anything. if (attackerEventLegion == null || defenderEventLegion == null) { LOGGER.log(Level.FINEST, "tellEngagementResultHandling, both are null"); return; } if (winner == null) { LOGGER.log(Level.FINEST, "winner is null value"); } else { LOGGER.log(Level.FINEST, "winner is '" + winner + "'"); } if (winner == null) { // null value = normal mutual // string with content "null": one legion contained titan, // titan killed, some others survived, // titan-killing-legion eliminated. // The above is for normal game. What if load from history?? LOGGER .log(Level.INFO, "tellEngagementResultHandling, winner null"); // mutual elimination // attackerEventLegion.setAllDead(); attackerEventLegion.setEventType(RevealEvent.eventLost); attackerEventLegion.setEventInfo("mutual"); addEvent(attackerEventLegion); // defenderEventLegion.setAllDead(); defenderEventLegion.setEventType(RevealEvent.eventLost); defenderEventLegion.setEventInfo("mutual"); addEvent(defenderEventLegion); } else { LOGGER.log(Level.INFO, "tellEngagementResultHandling, winner = " + winner); if (winner.equals(this.attacker)) { // attacker won winnerLegion = attackerEventLegion; loserLegion = defenderEventLegion; } else { // defender won winnerLegion = defenderEventLegion; loserLegion = attackerEventLegion; } // fled or concession there didn't come removeCreature messages, // thus make sure they are really shown dead. loserLegion.setAllDead(); loserLegion.setEventType(RevealEvent.eventLost); if (turns > 7) { method = Constants.erMethodTimeLoss; } loserLegion.setEventInfo(method); addEvent(loserLegion); int winnerHeight = winnerLegion.getHeight(); int winnerEventHeight = winnerLegion.getHeight(); if (winnerEventHeight != winnerHeight) { if (winnerEventHeight != 0) { // @TODO: is that a problem? LOGGER.log(Level.FINEST, "Winner legion " + winner + " event height mismatch: Eventheight=" + winnerLegion.getHeight() + ", actual height=" + winnerHeight); } } winnerLegion.setEventType(RevealEvent.eventWon); winnerLegion.setEventInfo(method); addEvent(winnerLegion); } lastAttackerEventLegion = attackerEventLegion; lastDefenderEventLegion = defenderEventLegion; attackerEventLegion = null; defenderEventLegion = null; winnerLegion = null; loserLegion = null; } public void newCreatureRevealEvent(int eventType, Legion legion1, CreatureType creature, Legion legion2) { RevealedCreature rc = new RevealedCreature(creature); switch (eventType) { case RevealEvent.eventSummon: rc.setWasSummoned(true); break; case RevealEvent.eventTeleport: rc.setDidTeleport(true); break; default: String markerId1 = legion1 != null ? legion1.getMarkerId() : "<null legion>"; LOGGER.log( Level.SEVERE, "Invalid event type " + RevealEvent.getEventTypeText(eventType) + " in newCreatureRevealEvent: markerId " + markerId1 + ", creature " + creature); } ArrayList<RevealedCreature> rcList = new ArrayList<RevealedCreature>(1); rcList.add(rc); newEvent(eventType, legion1, rcList, legion2); } public void newSplitEvent(int turnNr, Legion legion1, ArrayList<RevealedCreature> rcList, Legion legion2) { RevealEvent e = new RevealEvent(turnNr, getActivePlayer(), RevealEvent.eventSplit, legion1, rcList, legion2); addEvent(e); } public void revealCreatures(Legion legion, List<CreatureType> creatureTypes, String reason) { // EventViewer stuff: // looks as if right now we need this revealedInfo only for // engagements in which we are envolved. // E.g. recruit info is handled by separate didRecruit... // If this player is involved in an engagement, then server reveals // us the opponent, and our own legion we know anyway. // Thus, update the knownCreatures info in the events so that both // the attacker and defender are known in EventViewer (in THIS client) if (reason.equals(Constants.reasonEngaged)) { RevealEvent ownEvent = null; RevealEvent otherEvent = null; if (legion.equals(attacker)) { otherEvent = attackerEventLegion; ownEvent = defenderEventLegion; } else if (legion.equals(defender)) { otherEvent = defenderEventLegion; ownEvent = attackerEventLegion; } // else: Fine as well. Client just not involved in this engagement. if (otherEvent != null) { List<RevealedCreature> rcNames = new ArrayList<RevealedCreature>(); for (CreatureType type : creatureTypes) { RevealedCreature rc = new RevealedCreature(type); rcNames.add(rc); } otherEvent.updateKnownCreatures(rcNames); } if (ownEvent != null) { Legion ownLegion = ownEvent.getLegion1(); ArrayList<RevealedCreature> rcNames = new ArrayList<RevealedCreature>(); for (CreatureType creature : ownLegion.getCreatureTypes()) { RevealedCreature rc = new RevealedCreature(creature); rcNames.add(rc); } ownEvent.updateKnownCreatures(rcNames); } } } public void revealEngagedCreatures(final List<CreatureType> creatures, boolean isAttacker, String reason) { // can't do anything if (old) server or history do not provide // us the reason if (reason == null || reason.equals("<Unknown>")) { return; } if (reason.equals(Constants.reasonBattleStarts) || reason.equals(Constants.reasonFled) || reason.equals(Constants.reasonConcession)) { RevealEvent event; event = isAttacker ? attackerEventLegion : defenderEventLegion; ArrayList<RevealedCreature> rcNames = new ArrayList<RevealedCreature>(); for (CreatureType creatureType : creatures) { RevealedCreature rc = new RevealedCreature(creatureType); rcNames.add(rc); } event.updateKnownCreatures(rcNames); event.setEventInfo(reason); } else { // perhaps load from history? LOGGER.log(Level.SEVERE, "revealEngagedCreatures, unknown reason " + reason); } } public void addCreature(Legion legion, CreatureType type, String reason) { RevealEvent battleEvent = null; if (attackerEventLegion == null && defenderEventLegion == null) { // no battle events where to add creature - ok! } else if (attackerEventLegion != null && attackerEventLegion.getLongMarkerId().equals( legion.getLongMarkerId())) { battleEvent = attackerEventLegion; } else if (defenderEventLegion != null && defenderEventLegion.getLongMarkerId().equals( legion.getLongMarkerId())) { battleEvent = defenderEventLegion; } else { LOGGER.warning("No battle event found for legion " + legion.getLongMarkerId() + " where to add creature " + type + "; attacker log marker id " + (attackerEventLegion == null ? "attEvLg null" : attackerEventLegion.getLongMarkerId()) + ", defender long marker id " + (defenderEventLegion == null ? "defEvLg null" : defenderEventLegion.getLongMarkerId())); } if (battleEvent != null) { RevealedCreature rc = new RevealedCreature(type); rc.setReason(reason); battleEvent.addCreature(rc); } else { // no battle events where to add creature - fine as well. } if (reason.equals(Constants.reasonAcquire)) { // create also the separate acquire event: RevealedCreature rc = new RevealedCreature(type); rc.setWasAcquired(true); ArrayList<RevealedCreature> rcList = new ArrayList<RevealedCreature>( 1); rcList.add(rc); newEvent(RevealEvent.eventAcquire, legion, rcList, null); if (attackerEventLegion == null || defenderEventLegion == null) { // This should now never happen any more: LOGGER.log( Level.SEVERE, "no attacker and defender " + " legion event for acquiring!!" + " turn" + client.getTurnNumber() + " player " + client.getActivePlayer().getName() + " phase " + client.getPhase() + " markerid " + legion.getMarkerId() + " marker owner" + legion.getPlayer().getName() + "last engagement were" + " attacker " + lastAttackerEventLegion.getLongMarkerId() + " defender " + lastDefenderEventLegion.getLongMarkerId()); System.exit(1); } } else if (reason.equals(Constants.reasonUndoSummon)) { // addCreature adds summoned creature back to donor: int turn = client.getTurnNumber(); undoEvent(RevealEvent.eventSummon, legion, null, turn); if (!attackerEventLegion.removeSummonedCreature(turn, type.getName())) { // this should never happen... LOGGER.log(Level.WARNING, "Un-Summon " + type.getName() + " out of attacker event failed!"); } } else if (reason.equals(Constants.reasonReinforced)) { // Recruit/Reinforce event is created by didRecruit() } } public void cancelReinforcement(CreatureType creature, int turn) { defenderEventLegion.removeReinforcedCreature(turn, creature.getName()); } public void removeCreature(Legion legion, CreatureType type, String reason) { if (attackerEventLegion == null && defenderEventLegion == null) { // ok, no battle event affected return; } if (attacker != null && attackerEventLegion != null && attacker.equals(legion)) { LOGGER.log(Level.FINEST, "During battle, remove creature " + type + " from attacker legion " + legion); attackerEventLegion.setCreatureDied(type, attacker.getHeight()); } else if (defender != null && defenderEventLegion != null && defender.equals(legion)) { LOGGER.log(Level.FINEST, "During battle, remove creature " + type + " from defender legion " + legion); defenderEventLegion.setCreatureDied(type, defender.getHeight()); } else if (reason.equals(Constants.reasonSummon)) { // ok, nothing to do - this is about removing the summoned angel // from the donor legion, there is no event legion for that. } else { LOGGER.warning("No eventLegion found for legion " + legion.getLongMarkerId() + " to remove creature " + type); } } public void recruitEvent(Legion legion, CreatureType recruit, List<CreatureType> recruiters, String reason) { ArrayList<RevealedCreature> rcList = new ArrayList<RevealedCreature>(); RevealedCreature rc; for (CreatureType creature : recruiters) { rc = new RevealedCreature(creature); rc.setDidRecruit(true); rcList.add(rc); } int recruitType; rc = new RevealedCreature(recruit); if (reason.equals(Constants.reasonReinforced)) { recruitType = RevealEvent.eventReinforce; rc.setWasReinforced(true); } else { recruitType = RevealEvent.eventRecruit; rc.setWasRecruited(true); } rcList.add(rc); newEvent(recruitType, legion, rcList, null); } // for removeDeadBattleChits: public void setCreatureDead(BattleUnit battleUnit) { RevealEvent event = (battleUnit.isDefender() ? defenderEventLegion : attackerEventLegion); event.setCreatureDied(battleUnit.getType(), battleUnit.getLegion() .getHeight()); } /* * User undid one action. Event is just marked as undone, but not deleted * - information once revealed is known to the public, as in real life :) */ public void undoEvent(int type, Legion parent, Legion child, int turn) { assert parent != null : "undoEvent called for an event of type " + type + " but with null legion?"; String parentId = parent.getLongMarkerId(); String childId = child != null ? child.getLongMarkerId() : null; int found = 0; if (type == RevealEvent.eventSplit) { synchronized (eventList) { int last = eventList.size(); ListIterator<RevealEvent> it = eventList.listIterator(last); while (it.hasPrevious() && found == 0) { RevealEvent e = it.previous(); if (e.getEventType() == type && e.getTurn() == turn && e.getLongMarkerId().equals(parentId) && e.getLongMarkerId2().equals(childId) && !e.wasUndone()) { found++; e.setUndone(true); } } } // HACK: if not found, search split event also from previous round // Sometimes server/game gives us wrong (different) turn number for // the split and the unsplit event. For now, if this happens, // search also in the previous round. // TODO: investigate, why does server gives us different turn numbers // in those events? // TODO: Now newSplitEvent does not do client.getTurn() any more, // which might have been the reason for the misfit; // does this here still happen? assert found != 0 : "Mismatch for turnNr of SplitEvent still happens."; if (found == 0) { synchronized (eventList) { int last = eventList.size(); ListIterator<RevealEvent> it2 = eventList .listIterator(last); while (it2.hasPrevious() && found == 0) { RevealEvent e = it2.previous(); if (e.getEventType() == type && e.getTurn() + 1 == turn && e.getLongMarkerId().equals(parentId) && e.getLongMarkerId2().equals(childId) && !e.wasUndone()) { found++; e.setUndone(true); } } } } } else if (type == RevealEvent.eventRecruit || type == RevealEvent.eventReinforce || type == RevealEvent.eventSummon || type == RevealEvent.eventTeleport) { synchronized (eventList) { int last = eventList.size(); ListIterator<RevealEvent> it = eventList.listIterator(last); while (it.hasPrevious() && found == 0) { RevealEvent e = it.previous(); if (e.getEventType() == type && e.getTurn() == turn && e.getLongMarkerId().equals(parentId) && !e.wasUndone()) { found++; e.setUndone(true); } } } } else { LOGGER.log(Level.WARNING, "undo event for unknown type " + type + " attempted."); return; } if (found == 0) { if (type == RevealEvent.eventSplit && client.getPhase() == Phase.MOVE) { // OK. This can happen if game was saved with a split that is // now recombined by the server because the legions didn't have // valid moves. Now it was loaded in move phase, but replay // does (at least for now) NOT replay the actual event, it just // re-sends the add/remove info so that clients get the split // predict info right. Thus there is no such event here in // event viewer that could be undone. } else { LOGGER.log(Level.WARNING, "Requested '" + RevealEvent.getEventTypeText(type) + "' EVENT to undo (" + parentId + ", " + childId + ", turn " + turn + ") not found"); } } if (this.visible) { updatePanels(false); } } // Now the methods for internal data management: /* throw away all events which are expireTurns turns older * than the given turnNr/playerNr combination. */ public void purgeOldEvents() { if (this.expireTurns == -1) { LOGGER.log(Level.WARNING, "expireTurns -1 - no purging needed."); return; } // int purged = 0; synchronized (eventList) { Iterator<RevealEvent> it = eventList.iterator(); boolean done = false; while (it.hasNext() && !done) { RevealEvent e = it.next(); int oldEventTurn = e.getTurn(); int oldPlayerNr = e.getPlayer().getNumber(); if (turnNr - oldEventTurn > expireTurns - (currentPlayer.getNumber() >= oldPlayerNr ? 1 : 0)) { it.remove(); // purged++; } else { done = true; } } } } public void cleanup() { synchronized (eventList) { eventList.clear(); } synchronized (displayQueue) { displayQueue.clear(); } this.options = null; this.client = null; attackerEventLegion = null; defenderEventLegion = null; lastAttackerEventLegion = null; lastDefenderEventLegion = null; } @Override public void dispose() { cleanup(); super.dispose(); } public void setVisibleMaybe() { boolean visible = options.getOption(Options.showEventViewer); setVisible(visible); } @Override public void setVisible(boolean visible) { if (visible) { settingsPane.setMinimumSize(settingsPane.getSize()); // eventPane.setMinimumSize(eventPane.getSize()); this.visible = true; updatePanels(true); } else { this.visible = false; } super.setVisible(visible); } }