package games.strategy.engine.chat; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; import games.strategy.engine.chat.IChatController.Tag; import games.strategy.engine.message.IChannelMessenger; import games.strategy.engine.message.IRemoteMessenger; import games.strategy.engine.message.MessageContext; import games.strategy.engine.message.RemoteName; import games.strategy.net.IMessenger; import games.strategy.net.INode; import games.strategy.net.Messengers; import games.strategy.sound.ClipPlayer; import games.strategy.sound.SoundPath; import games.strategy.util.Tuple; /** * chat logic. * * <p> * A chat can be bound to multiple chat panels. * </p> */ public class Chat { private final List<IChatListener> listeners = new CopyOnWriteArrayList<>(); private final Messengers messengers; private final String chatChannelName; private final String chatName; private final SentMessagesHistory sentMessages; private volatile long chatInitVersion = -1; // mutex used for access synchronization to nodes // TODO: check if this mutex is used for something else as well private final Object mutexNodes = new Object(); private List<INode> nodes; // this queue is filled ONLY in init phase when chatInitVersion is default (-1) and nodes should not be changed // until end of // initialization // synchronizes access to queue private final Object mutexQueue = new Object(); private List<Runnable> queuedInitMessages = new ArrayList<>(); private final List<ChatMessage> chatHistory = new ArrayList<>(); private final StatusManager statusManager; private final ChatIgnoreList ignoreList = new ChatIgnoreList(); private final HashMap<INode, LinkedHashSet<String>> notesMap = new HashMap<>(); private static final String TAG_MODERATOR = "[Mod]"; private final CHAT_SOUND_PROFILE chatSoundProfile; public enum CHAT_SOUND_PROFILE { LOBBY_CHATROOM, GAME_CHATROOM, NO_SOUND } private void addToNotesMap(final INode node, final Tag tag) { if (tag == Tag.NONE) { return; } final LinkedHashSet<String> current = getTagText(tag); notesMap.put(node, current); } private static LinkedHashSet<String> getTagText(final Tag tag) { final LinkedHashSet<String> rVal = new LinkedHashSet<>(); if (tag == Tag.NONE) { return null; } if (tag == Tag.MODERATOR) { rVal.add(TAG_MODERATOR); } // add more here.... return rVal; } public String getNotesForNode(final INode node) { final LinkedHashSet<String> notes = notesMap.get(node); if (notes == null) { return null; } final StringBuilder sb = new StringBuilder(""); for (final String note : notes) { sb.append(" "); sb.append(note); } return sb.toString(); } /** Creates a new instance of Chat. */ public Chat(final String chatName, final Messengers messengers, final CHAT_SOUND_PROFILE chatSoundProfile) { this.chatSoundProfile = chatSoundProfile; this.messengers = messengers; statusManager = new StatusManager(messengers); chatChannelName = ChatController.getChatChannelName(chatName); this.chatName = chatName; sentMessages = new SentMessagesHistory(); init(); } public Chat(final IMessenger messenger, final String chatName, final IChannelMessenger channelMessenger, final IRemoteMessenger remoteMessenger, final CHAT_SOUND_PROFILE chatSoundProfile) { this(chatName, new Messengers(messenger, remoteMessenger, channelMessenger), chatSoundProfile); } public SentMessagesHistory getSentMessagesHistory() { return sentMessages; } public void addChatListener(final IChatListener listener) { listeners.add(listener); updateConnections(); } public StatusManager getStatusManager() { return statusManager; } public void removeChatListener(final IChatListener listener) { listeners.remove(listener); } public Object getMutex() { return mutexNodes; } private void init() { // the order of events is significant. // 1 register our channel listener // once the channel is registered, we are guaranteed that // when we receive the response from our init(...) message, our channel // subscriber has been added, and will see any messages broadcasted by the server // 2 call the init message on the server remote. Any add or join messages sent from the server // will queue until we receive the init return value (they queue since they see the init version is -1) // 3 when we receive the init message response, acquire the lock, and initialize our state // and run any queued messages. Queued messages may be ignored if the // server version is incorrect. // this all seems a lot more involved than it needs to be. final IChatController controller = (IChatController) messengers.getRemoteMessenger() .getRemote(ChatController.getChatControlerRemoteName(chatName)); messengers.getChannelMessenger().registerChannelSubscriber(m_chatChannelSubscribor, new RemoteName(chatChannelName, IChatChannel.class)); final Tuple<Map<INode, Tag>, Long> init = controller.joinChat(); final Map<INode, Tag> chatters = init.getFirst(); synchronized (mutexNodes) { nodes = new ArrayList<>(chatters.keySet()); } chatInitVersion = init.getSecond().longValue(); synchronized (mutexQueue) { queuedInitMessages.add(0, () -> assignNodeTags(chatters)); for (final Runnable job : queuedInitMessages) { job.run(); } queuedInitMessages = null; } updateConnections(); } /** * Call only when mutex for node is locked. * * @param chatters * map from node to tag */ private void assignNodeTags(final Map<INode, Tag> chatters) { for (final INode node : chatters.keySet()) { final Tag tag = chatters.get(node); addToNotesMap(node, tag); } } /** * Stop receiving events from the messenger. */ public void shutdown() { messengers.getChannelMessenger().unregisterChannelSubscriber(m_chatChannelSubscribor, new RemoteName(chatChannelName, IChatChannel.class)); if (messengers.getMessenger().isConnected()) { final RemoteName chatControllerName = ChatController.getChatControlerRemoteName(chatName); final IChatController controller = (IChatController) messengers.getRemoteMessenger().getRemote(chatControllerName); controller.leaveChat(); } } public void sendSlap(final String playerName) { final IChatChannel remote = (IChatChannel) messengers.getChannelMessenger() .getChannelBroadcastor(new RemoteName(chatChannelName, IChatChannel.class)); remote.slapOccured(playerName); } public void sendMessage(final String message, final boolean meMessage) { final IChatChannel remote = (IChatChannel) messengers.getChannelMessenger() .getChannelBroadcastor(new RemoteName(chatChannelName, IChatChannel.class)); if (meMessage) { remote.meMessageOccured(message); } else { remote.chatOccured(message); } sentMessages.append(message); } private void updateConnections() { synchronized (mutexNodes) { if (nodes == null) { return; } final List<INode> playerNames = new ArrayList<>(nodes); Collections.sort(playerNames); for (final IChatListener listener : listeners) { listener.updatePlayerList(playerNames); } } } public void setIgnored(final INode node, final boolean isIgnored) { if (isIgnored) { ignoreList.add(node.getName()); } else { ignoreList.remove(node.getName()); } } public boolean isIgnored(final INode node) { return ignoreList.shouldIgnore(node.getName()); } public INode getLocalNode() { return messengers.getMessenger().getLocalNode(); } public INode getServerNode() { return messengers.getMessenger().getServerNode(); } private final List<INode> m_playersThatLeft_Last10 = new ArrayList<>(); public List<INode> getPlayersThatLeft_Last10() { return new ArrayList<>(m_playersThatLeft_Last10); } public List<INode> getOnlinePlayers() { return new ArrayList<>(nodes); } private final IChatChannel m_chatChannelSubscribor = new IChatChannel() { private void assertMessageFromServer() { final INode senderNode = MessageContext.getSender(); final INode serverNode = messengers.getMessenger().getServerNode(); // this will happen if the message is queued // but to queue a message, we must first test where it came from // so it is safe in this case to return ok if (senderNode == null) { return; } if (!senderNode.equals(serverNode)) { throw new IllegalStateException("The node:" + senderNode + " sent a message as the server!"); } } @Override public void chatOccured(final String message) { final INode from = MessageContext.getSender(); if (isIgnored(from)) { return; } synchronized (mutexNodes) { chatHistory.add(new ChatMessage(message, from.getName(), false)); for (final IChatListener listener : listeners) { listener.addMessage(message, from.getName(), false); } // limit the number of messages in our history. while (chatHistory.size() > 1000) { chatHistory.remove(0); } } } @Override public void meMessageOccured(final String message) { final INode from = MessageContext.getSender(); if (isIgnored(from)) { return; } synchronized (mutexNodes) { chatHistory.add(new ChatMessage(message, from.getName(), true)); for (final IChatListener listener : listeners) { listener.addMessage(message, from.getName(), true); } } } @Override public void speakerAdded(final INode node, final Tag tag, final long version) { assertMessageFromServer(); if (chatInitVersion == -1) { synchronized (mutexQueue) { if (queuedInitMessages == null) { speakerAdded(node, tag, version); } else { queuedInitMessages.add(() -> speakerAdded(node, tag, version)); } } return; } if (version > chatInitVersion) { synchronized (mutexNodes) { nodes.add(node); addToNotesMap(node, tag); updateConnections(); } for (final IChatListener listener : listeners) { listener.addStatusMessage(node.getName() + " has joined"); if (chatSoundProfile == CHAT_SOUND_PROFILE.GAME_CHATROOM) { ClipPlayer.play(SoundPath.CLIP_CHAT_JOIN_GAME); } } } } @Override public void speakerRemoved(final INode node, final long version) { assertMessageFromServer(); if (chatInitVersion == -1) { synchronized (mutexQueue) { if (queuedInitMessages == null) { speakerRemoved(node, version); } else { queuedInitMessages.add(() -> speakerRemoved(node, version)); } } return; } if (version > chatInitVersion) { synchronized (mutexNodes) { nodes.remove(node); notesMap.remove(node); updateConnections(); } for (final IChatListener listener : listeners) { listener.addStatusMessage(node.getName() + " has left"); } m_playersThatLeft_Last10.add(node); if (m_playersThatLeft_Last10.size() > 10) { m_playersThatLeft_Last10.remove(0); } } } @Override public void speakerTagUpdated(final INode node, final Tag tag) { synchronized (mutexNodes) { notesMap.remove(node); addToNotesMap(node, tag); updateConnections(); } } @Override public void slapOccured(final String to) { final INode from = MessageContext.getSender(); if (isIgnored(from)) { return; } synchronized (mutexNodes) { if (to.equals(messengers.getChannelMessenger().getLocalNode().getName())) { handleSlap("You were slapped by " + from.getName(), from); } else if (from.equals(messengers.getChannelMessenger().getLocalNode())) { handleSlap("You just slapped " + to, from); } } } private void handleSlap(String message, INode from) { for (final IChatListener listener : listeners) { chatHistory.add(new ChatMessage(message, from.getName(), false)); listener.addMessageWithSound(message, from.getName(), false, SoundPath.CLIP_CHAT_SLAP); } } @Override public void ping() { // System.out.println("Pinged"); } }; /** * While using this, you should synchronize on getMutex(). * * @return the messages that have occured so far. */ public List<ChatMessage> getChatHistory() { return chatHistory; } }