/*
* $Id$
*
* Copyright (c) 2000-2003 by Rodney Kinney
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License (LGPL) as published by the Free Software Foundation.
*
* This library 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
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public
* License along with this library; if not, copies are available
* at http://www.opensource.org.
*/
package VASSAL.build.module;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.BoxLayout;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import VASSAL.build.AbstractConfigurable;
import VASSAL.build.Buildable;
import VASSAL.build.Builder;
import VASSAL.build.Configurable;
import VASSAL.build.GameModule;
import VASSAL.build.module.documentation.HelpFile;
import VASSAL.command.Command;
import VASSAL.command.CommandEncoder;
import VASSAL.configure.Configurer;
import VASSAL.configure.IconConfigurer;
import VASSAL.configure.StringArrayConfigurer;
import VASSAL.configure.StringConfigurer;
import VASSAL.configure.StringEnumConfigurer;
import VASSAL.i18n.Localization;
import VASSAL.i18n.Resources;
import VASSAL.tools.DataArchive;
import VASSAL.tools.LaunchButton;
import VASSAL.tools.SequenceEncoder;
/**
* Maintains a list of players involved in the current game
*/
public class PlayerRoster extends AbstractConfigurable implements CommandEncoder, GameComponent, GameSetupStep {
public static final String BUTTON_ICON = "buttonIcon"; //$NON-NLS-1$
public static final String BUTTON_TEXT = "buttonText"; //$NON-NLS-1$
public static final String TOOL_TIP = "buttonToolTip"; //$NON-NLS-1$
public static final String SIDES = "sides"; //$NON-NLS-1$
public static final String COMMAND_PREFIX = "PLAYER\t"; //$NON-NLS-1$
public static final String OBSERVER = "<observer>"; //$NON-NLS-1$
protected List<PlayerInfo> players = new ArrayList<PlayerInfo>();
protected List<String> sides = new ArrayList<String>();
protected String[] untranslatedSides;
protected LaunchButton retireButton;
protected List<SideChangeListener> sideChangeListeners =
new ArrayList<SideChangeListener>();
protected String translatedObserver;
private boolean pickedSide = false;
public PlayerRoster() {
ActionListener al = new ActionListener() {
public void actionPerformed(ActionEvent e) {
launch();
}
};
retireButton = new LaunchButton(Resources.getString("PlayerRoster.retire"), TOOL_TIP, BUTTON_TEXT, null, BUTTON_ICON, al); //$NON-NLS-1$
retireButton.setToolTipText(Resources.getString("PlayerRoster.allow_another")); //$NON-NLS-1$
retireButton.setVisible(false);
translatedObserver = Resources.getString("PlayerRoster.observer"); //$NON-NLS-1$
}
public void removeFrom(Buildable parent) {
GameModule.getGameModule().getGameState().removeGameComponent(this);
GameModule.getGameModule().removeCommandEncoder(this);
}
public void remove(Buildable child) {
}
public void build(Element e) {
if (e != null) {
final NamedNodeMap attributes = e.getAttributes();
for (int i = 0; i < attributes.getLength(); ++i) {
final Attr att = (Attr) attributes.item(i);
// Old versions of VASSAL (pre-2.9?) wrote "Retire" as the
// icon filename for the retireButton, even when no such
// image existed in the archive. This test blocks irritating
// errors due to nonexistent "Retire" images, by ignoring
// the buttonIcon attribute when the value is "Retire" but
// no such image can be found in the archive.
if ("buttonIcon".equals(att.getName()) && //$NON-NLS-1$
"Retire".equals(att.getValue())) { //$NON-NLS-1$
try {
GameModule.getGameModule()
.getDataArchive()
.getInputStream(DataArchive.IMAGE_DIR + att.getValue());
}
catch (IOException ex) {
continue;
}
}
retireButton.setAttribute(att.getName(), att.getValue());
Localization.getInstance()
.saveTranslatableAttribute(this, att.getName(),
att.getValue());
}
final NodeList n = e.getElementsByTagName("*"); //$NON-NLS-1$
sides.clear();
for (int i = 0; i < n.getLength(); ++i) {
final Element el = (Element) n.item(i);
sides.add(Builder.getText(el));
}
Localization.getInstance()
.saveTranslatableAttribute(this, SIDES, getSidesAsString());
}
}
public String getConfigureName() {
return null;
}
public static String getConfigureTypeName() {
return Resources.getString("Editor.PlayerRoster.component_type"); //$NON-NLS-1$
}
public void add(Buildable child) {
}
public Configurable[] getConfigureComponents() {
return new Configurable[0];
}
public Element getBuildElement(Document doc) {
Element el = doc.createElement(getClass().getName());
String att = retireButton.getAttributeValueString(BUTTON_TEXT);
if (att != null)
el.setAttribute(BUTTON_TEXT, att);
att = retireButton.getAttributeValueString(BUTTON_ICON);
if (att != null)
el.setAttribute(BUTTON_ICON, att);
att = retireButton.getAttributeValueString(TOOL_TIP);
if (att != null)
el.setAttribute(TOOL_TIP, att);
for (String s : sides) {
Element sub = doc.createElement("entry"); //$NON-NLS-1$
sub.appendChild(doc.createTextNode(s));
el.appendChild(sub);
}
return el;
}
public Configurer getConfigurer() {
return new Con();
}
public void addPropertyChangeListener(PropertyChangeListener l) {
}
public static void addSideChangeListener(SideChangeListener l) {
PlayerRoster r = getInstance();
if (r != null) {
r.sideChangeListeners.add(l);
}
}
public static void removeSideChangeListener(SideChangeListener l) {
PlayerRoster r = getInstance();
if (r != null) {
r.sideChangeListeners.remove(l);
}
}
public HelpFile getHelpFile() {
return HelpFile.getReferenceManualPage("GameModule.htm", "Definition_of_Player_Sides"); //$NON-NLS-1$ //$NON-NLS-2$
}
public Class<?>[] getAllowableConfigureComponents() {
return new Class<?>[0];
}
public void addTo(Buildable b) {
GameModule.getGameModule().getGameState().addGameComponent(this);
GameModule.getGameModule().getGameState().addGameSetupStep(this);
GameModule.getGameModule().addCommandEncoder(this);
GameModule.getGameModule().getToolBar().add(retireButton);
}
protected void launch() {
final String mySide = getMySide();
if (mySide == null && allSidesAllocated()) {
return;
}
final String[] options = allSidesAllocated() ?
new String[]{
Resources.getString(Resources.YES),
Resources.getString(Resources.NO)
} :
new String[]{
Resources.getString("PlayerRoster.become_observer"), //$NON-NLS-1$
Resources.getString("PlayerRoster.join_another_side"), //$NON-NLS-1$
Resources.getString(Resources.CANCEL)
};
final int CANCEL = options.length - 1;
final int option = JOptionPane.showOptionDialog(
GameModule.getGameModule().getFrame(),
Resources.getString("PlayerRoster.give_up_position", getMyLocalizedSide()),
Resources.getString("PlayerRoster.retire"), //$NON-NLS-1$
JOptionPane.YES_NO_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
Resources.getString("PlayerRoster.become_observer") //$NON-NLS-1$
);
if (option != CANCEL) {
final String oldSide = getMySide();
String newSide;
if (option == 0) {
newSide = OBSERVER;
}
else {
newSide = promptForSide();
if (newSide == null) {
return;
}
}
remove(GameModule.getUserId());
final PlayerInfo me = new PlayerInfo(
GameModule.getUserId(),
GlobalOptions.getInstance().getPlayerId(),
newSide
);
final Add a = new Add(this, me.playerId, me.playerName, me.side);
a.execute();
GameModule.getGameModule().getServer().sendToOthers(a);
newSide = getMySide();
fireSideChange(oldSide, newSide);
}
}
protected void fireSideChange(String oldSide, String newSide) {
for (SideChangeListener l : sideChangeListeners) {
l.sideChanged(oldSide, newSide);
}
}
public static boolean isActive() {
return getInstance() != null;
}
protected static PlayerRoster getInstance() {
for (PlayerRoster pr :
GameModule.getGameModule().getComponentsOf(PlayerRoster.class)) {
return pr;
}
return null;
}
public static String getMySide() {
return getMySide(false);
}
public static String getMyLocalizedSide() {
return getMySide(true);
}
protected static String getMySide(boolean localized) {
final PlayerRoster r = getInstance();
if (r != null) {
for (PlayerInfo pi : r.getPlayers()) {
if (pi.playerId.equals(GameModule.getUserId())) {
return localized ? pi.getLocalizedSide() : pi.getSide();
}
}
}
return null;
}
public PlayerInfo[] getPlayers() {
return players.toArray(new PlayerInfo[players.size()]);
}
public void add(String playerId, String playerName, String side) {
PlayerInfo e = new PlayerInfo(playerId, playerName, side);
if (players.contains(e)) {
players.set(players.indexOf(e), e);
}
else {
players.add(e);
}
}
public void remove(String playerId) {
PlayerInfo e = new PlayerInfo(playerId, null, null);
players.remove(e);
}
public Command decode(String command) {
if (command.startsWith(COMMAND_PREFIX)) {
SequenceEncoder.Decoder st = new SequenceEncoder.Decoder(command, '\t');
st.nextToken();
return new Add(this, st.nextToken(), st.nextToken(), st.nextToken());
}
else {
return null;
}
}
public String encode(Command c) {
if (c instanceof Add) {
final Add a = (Add) c;
final SequenceEncoder se = new SequenceEncoder('\t');
se.append(a.id).append(a.name).append(a.side);
return COMMAND_PREFIX + se.getValue();
}
else {
return null;
}
}
public Command getRestoreCommand() {
Command c = null;
for (PlayerInfo entry : players) {
Command sub = new Add(this, entry.playerId, entry.playerName, entry.side);
c = c == null ? sub : c.append(sub);
}
return c;
}
public void setup(boolean gameStarting) {
if (gameStarting) {
PlayerInfo me = new PlayerInfo(GameModule.getUserId(),
GlobalOptions.getInstance().getPlayerId(), null);
if (players.contains(me)) {
PlayerInfo saved = players.get(players.indexOf(me));
saved.playerName = me.playerName;
}
}
else {
players.clear();
}
retireButton.setVisible(gameStarting && getMySide() != null);
pickedSide = false;
}
public void finish() {
final String newSide = untranslateSide(sideConfig.getValueString());
if (newSide != null) {
Add a = new Add(this, GameModule.getUserId(), GlobalOptions.getInstance().getPlayerId(), newSide);
a.execute();
GameModule.getGameModule().getServer().sendToOthers(a);
}
retireButton.setVisible(getMySide() != null);
pickedSide = true;
}
public Component getControls() {
ArrayList<String> availableSides = new ArrayList<String>(sides);
ArrayList<String> alreadyTaken = new ArrayList<String>();
for (PlayerInfo p : players) {
alreadyTaken.add(p.side);
}
availableSides.removeAll(alreadyTaken);
availableSides.add(0, translatedObserver);
sideConfig = new StringEnumConfigurer(null,
Resources.getString("PlayerRoster.join_game_as"), //$NON-NLS-1$
availableSides.toArray(new String[availableSides.size()]));
sideConfig.setValue(translatedObserver);
return sideConfig.getControls();
}
public String getStepTitle() {
return Resources.getString("PlayerRoster.choose_side"); //$NON-NLS-1$
}
// Implement GameSetupStep
public boolean isFinished() {
if (pickedSide) {
return true;
}
// Step is always finished if all sides are allocated
if (allSidesAllocated()) {
return true;
}
// If we are already recorded as a player (i.e. in Saved Game), then
// the step is only finished if we are not the Observer.
final PlayerInfo newPlayerInfo = new PlayerInfo(
GameModule.getUserId(),
GlobalOptions.getInstance().getPlayerId(), null
);
final int i = players.indexOf(newPlayerInfo);
if (i != -1) {
return !OBSERVER.equals(players.get(i).getSide());
}
// Step is not finished
return false;
}
/**
*
* @return true if all sides have been claimed by a player
*/
protected boolean allSidesAllocated() {
int allocatedSideCount = 0;
for (PlayerInfo p : players) {
if (!OBSERVER.equals(p.getSide())) {
++allocatedSideCount;
}
}
return sides.size() == allocatedSideCount;
}
protected String promptForSide() {
ArrayList<String> availableSides = new ArrayList<String>(sides);
ArrayList<String> alreadyTaken = new ArrayList<String>();
for (PlayerInfo p : players) {
alreadyTaken.add(p.side);
}
availableSides.removeAll(alreadyTaken);
availableSides.add(0, translatedObserver);
final GameModule g = GameModule.getGameModule();
String newSide = (String) JOptionPane.showInputDialog(
g.getFrame(),
Resources.getString("PlayerRoster.join_game_as"), //$NON-NLS-1$
Resources.getString("PlayerRoster.choose_side"), //$NON-NLS-1$
JOptionPane.QUESTION_MESSAGE,
null,
availableSides.toArray(new String[availableSides.size()]),
translatedObserver
);
// OBSERVER must always be stored internally in English.
if (translatedObserver.equals(newSide)) {
newSide = OBSERVER;
}
return newSide;
/*
if (newSide != null) {
final PlayerInfo me = new PlayerInfo(GameModule.getUserId(), GlobalOptions.getInstance().getPlayerId(), newSide);
final Add a = new Add(this, me.playerId, me.playerName, me.side);
a.execute();
g.getServer().sendToOthers(a);
}
*/
}
public static class PlayerInfo {
public String playerId;
public String playerName;
private String side;
public PlayerInfo(String id, String name, String side) {
if (id == null) {
throw new NullPointerException("Player id cannot be null"); //$NON-NLS-1$
}
playerId = id;
playerName = name;
this.side = side;
}
public boolean equals(Object o) {
if (o instanceof PlayerInfo && playerId != null) {
return playerId.equals(((PlayerInfo) o).playerId);
}
else {
return false;
}
}
public String getSide() {
return side;
}
public String getLocalizedSide() {
return PlayerRoster.getInstance().translateSide(side);
}
}
public static class Add extends Command {
private PlayerRoster roster;
private String id, name, side;
public Add(PlayerRoster r, String playerId,
String playerName, String side) {
roster = r;
id = playerId;
name = playerName;
this.side = side;
}
protected void executeCommand() {
roster.add(id, name, side);
}
protected Command myUndoCommand() {
return null;
}
}
private class Con extends Configurer {
private StringArrayConfigurer sidesConfig;
private IconConfigurer iconConfig;
private StringConfigurer textConfig;
private StringConfigurer tooltipConfig;
private JPanel controls;
private Con() {
super(null, null);
controls = new JPanel();
controls.setLayout(new BoxLayout(controls, BoxLayout.Y_AXIS));
sidesConfig = new StringArrayConfigurer(null, Resources.getString("Editor.PlayerRoster.sides_available"), sides.toArray(new String[sides.size()])); //$NON-NLS-1$
sidesConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
sides.clear();
sides.addAll(Arrays.asList(sidesConfig.getStringArray()));
}
});
controls.add(sidesConfig.getControls());
textConfig = new StringConfigurer(BUTTON_TEXT, Resources.getString("Editor.PlayerRoster.retire_button_text"), retireButton.getAttributeValueString(BUTTON_TEXT)); //$NON-NLS-1$
textConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
retireButton.setAttribute(BUTTON_TEXT, textConfig.getValueString());
}
});
controls.add(textConfig.getControls());
tooltipConfig = new StringConfigurer(TOOL_TIP, Resources.getString("Editor.PlayerRoster.retire_button_tooltip"), retireButton.getAttributeValueString(TOOL_TIP)); //$NON-NLS-1$
tooltipConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
retireButton.setAttribute(TOOL_TIP, tooltipConfig.getValueString());
}
});
controls.add(tooltipConfig.getControls());
iconConfig = new IconConfigurer(BUTTON_ICON, Resources.getString("Editor.PlayerRoster.retire_button_icon"), null); //$NON-NLS-1$
iconConfig.setValue(retireButton.getIcon());
iconConfig.addPropertyChangeListener(new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
retireButton.setAttribute(BUTTON_ICON, iconConfig.getValueString());
}
});
controls.add(iconConfig.getControls());
}
public String getValueString() {
return null;
}
public void setValue(String s) {
}
public Component getControls() {
return controls;
}
}
/** Call-back interface for when a player changes sides during a game */
public static interface SideChangeListener {
void sideChanged(String oldSide, String newSide);
}
protected StringEnumConfigurer sideConfig;
/**
* PlayerRoster is not a true AbstractConfigurable, it handles
* it's own configuration. Implement the rest of the AbstractConfigurable
* abstract classes for i18n.
*/
public String[] getAttributeNames() {
return new String[] {
BUTTON_TEXT,
TOOL_TIP,
SIDES
};
}
public Class<?>[] getAttributeTypes() {
return new Class<?>[] {
String.class,
String.class,
String.class
};
}
public String getAttributeValueString(String key) {
if (SIDES.equals(key)) {
return getSidesAsString();
}
return retireButton.getAttributeValueString(key);
}
/*
* Only ever called from Language.translate()
*/
public void setAttribute(String key, Object value) {
if (SIDES.equals(key)) {
untranslatedSides = sides.toArray(new String[sides.size()]);
String[] s = StringArrayConfigurer.stringToArray((String) value);
sides = new ArrayList<String>(s.length);
for (int i = 0; i < s.length; i++) {
sides.add(s[i]);
}
}
else {
retireButton.setAttribute(key, value);
}
}
protected String getSidesAsString() {
String[] s = sides.toArray(new String[sides.size()]);
return StringArrayConfigurer.arrayToString(s);
}
protected String untranslateSide(String side) {
if (translatedObserver.equals(side)) {
return OBSERVER;
}
if (untranslatedSides != null) {
for (int i = 0; i < sides.size(); i++) {
if (sides.get(i).equals(side)) {
return untranslatedSides[i];
}
}
}
return side;
}
protected String translateSide(String side) {
if (OBSERVER.equals(side)) {
return translatedObserver;
}
if (untranslatedSides != null) {
for (int i = 0; i < untranslatedSides.length; i++) {
if (untranslatedSides[i].equals(side)) {
return sides.get(i);
}
}
}
return side;
}
public String[] getAttributeDescriptions() {
return new String[] {
Resources.getString("Editor.button_text_label"),
Resources.getString("Editor.tooltip_text_label"),
Resources.getString("Editor.PlayerRoster.sides_label")
};
}
}