/** * $RCSfile: ,v $ * $Revision: $ * $Date: $ * * Copyright (C) 2004-2011 Jive Software. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jivesoftware.game.reversi; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Graphics; import java.awt.Image; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.util.concurrent.ConcurrentHashMap; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JSplitPane; import org.jivesoftware.smack.PacketListener; import org.jivesoftware.smack.filter.PacketIDFilter; import org.jivesoftware.smack.filter.PacketTypeFilter; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Packet; import org.jivesoftware.smack.provider.ProviderManager; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.spark.ChatManager; import org.jivesoftware.spark.SparkManager; import org.jivesoftware.spark.component.BackgroundPanel; import org.jivesoftware.spark.plugin.Plugin; import org.jivesoftware.spark.ui.ChatRoom; import org.jivesoftware.spark.ui.ChatRoomButton; import org.jivesoftware.spark.ui.ChatRoomListener; import org.jivesoftware.spark.ui.ChatRoomListenerAdapter; import org.jivesoftware.spark.ui.rooms.ChatRoomImpl; /** * Reversi plugin. Reversi is a two-player, turn-based game. See * {@link ReversiModel} for more details on how the game works. * * @author Matt Tucker * @author Bill Lynch */ public class ReversiPlugin implements Plugin { private ChatRoomListener chatRoomListener; private PacketListener gameOfferListener; private ConcurrentHashMap<String, JPanel> gameOffers; private ConcurrentHashMap<String, JPanel> gameInvitations; private JPanel inviteAlert; public void initialize() { // Offers and invitations hold all pending game offers we've sent to // other users or incoming // invitations. The map key is always the opponent's JID. The map value // is a transcript alert // UI component. gameOffers = new ConcurrentHashMap<String, JPanel>(); gameInvitations = new ConcurrentHashMap<String, JPanel>(); // Add Reversi item to chat toolbar. addToolbarButton(); // Add Smack providers. The plugin uses custom XMPP extensions to // communicate game offers // and current game state. Adding the Smack providers lets us use the // custom protocol. ProviderManager.getInstance().addIQProvider(GameOffer.ELEMENT_NAME, GameOffer.NAMESPACE, GameOffer.class); ProviderManager.getInstance().addExtensionProvider(GameMove.ELEMENT_NAME, GameMove.NAMESPACE, GameMove.class); ProviderManager.getInstance().addExtensionProvider(GameForfeit.ELEMENT_NAME, GameForfeit.NAMESPACE, GameForfeit.class); // Add IQ listener to listen for incoming game invitations. gameOfferListener = new PacketListener() { public void processPacket(Packet packet) { GameOffer invitation = (GameOffer) packet; if (invitation.getType() == IQ.Type.GET) { showInvitationAlert(invitation); } else if (invitation.getType() == IQ.Type.ERROR) { handleErrorIQ(invitation); } } }; SparkManager.getConnection().addPacketListener(gameOfferListener, new PacketTypeFilter(GameOffer.class)); } public void shutdown() { // Remove Reversi button from chat toolbar. removeToolbarButton(); // Remove IQ listener SparkManager.getConnection().removePacketListener(gameOfferListener); // // // See if there are any pending offers or invitations. If so, cancel // // them. // for (Iterator<String> i = gameOffers.keySet().iterator(); i.hasNext();i.next()) { // System.out.println("gameoffer"); // // } // System.out.println(gameInvitations.size()); // for (Iterator<String> i = gameInvitations.keySet().iterator(); i.hasNext(); i.next()) { // // } gameOffers.clear(); gameInvitations.clear(); // Remove Smack providers. ProviderManager.getInstance().removeIQProvider(GameOffer.ELEMENT_NAME, GameOffer.NAMESPACE); ProviderManager.getInstance().removeExtensionProvider(GameMove.ELEMENT_NAME, GameMove.NAMESPACE); ProviderManager.getInstance().removeExtensionProvider(GameForfeit.ELEMENT_NAME, GameForfeit.NAMESPACE); } public boolean canShutDown() { // The plugin is able to fully clean itself up, so return true. return true; } public void uninstall() { // Do nothing. Reversi has no permanent resources so shutdown can // already do a full cleanup. } /** * Display an alert that allows the user to accept or reject a game * invitation. * * @param invitation * the game invitation. */ private void showInvitationAlert(final GameOffer invitation) { // Got an offer to start a new game. So, make sure that a chat is // started with the other // user and show an invite panel. final ChatRoom room = SparkManager.getChatManager().getChatRoom(StringUtils.parseBareAddress(invitation.getFrom())); inviteAlert = new JPanel(); inviteAlert.setLayout(new BorderLayout()); JPanel invitePanel = new JPanel() { private static final long serialVersionUID = 5942001917654498678L; protected void paintComponent(Graphics g) { ImageIcon imageIcon = ReversiRes.getImageIcon(ReversiRes.REVERSI_ICON); Image image = imageIcon.getImage(); g.drawImage(image, 0, 0, null); } }; invitePanel.setPreferredSize(new Dimension(24, 24)); inviteAlert.add(invitePanel, BorderLayout.WEST); JPanel content = new JPanel(new BorderLayout()); String opponentName = invitation.getFrom(); // TODO: convert to more // readable name. content.add(new JLabel(opponentName + " is requesting a Reversi game ..."), BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); // The accept button. When clicked, accept the game offer. final JButton acceptButton = new JButton("Accept"); final JButton declineButton = new JButton("Decline"); acceptButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // Accept the game offer by sending a positive reply packet. GameOffer reply = new GameOffer(); reply.setTo(invitation.getFrom()); reply.setPacketID(invitation.getPacketID()); reply.setType(IQ.Type.RESULT); SparkManager.getConnection().sendPacket(reply); // Hide the response panel. TODO: make this work. room.getTranscriptWindow().remove(inviteAlert); inviteAlert.remove(1); inviteAlert.add(new JLabel("Starting game..."), BorderLayout.CENTER); declineButton.setEnabled(false); acceptButton.setEnabled(false); // Remove the invitation from the map. gameInvitations.remove(invitation.getFrom()); // Show the game board. showReversiBoard(invitation.getGameID(), room, !invitation.isStartingPlayer(), invitation.getFrom()); } }); buttonPanel.add(acceptButton); // The decline button. When clicked, reject the game offer. declineButton.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // Reject the game offer by sending an error packet. GameOffer reply = new GameOffer(); reply.setTo(invitation.getFrom()); reply.setPacketID(invitation.getPacketID()); reply.setType(IQ.Type.ERROR); SparkManager.getConnection().sendPacket(reply); // Hide the response panel. TODO: make this work. room.getTranscriptWindow().remove(inviteAlert); declineButton.setVisible(false); acceptButton.setVisible(false); // Remove the invitation from the map. gameInvitations.remove(invitation.getFrom()); } }); buttonPanel.add(declineButton); content.add(buttonPanel, BorderLayout.SOUTH); inviteAlert.add(content, BorderLayout.CENTER); // Add the invitation to the Map of invites. If there's a pending // invite, remove it // before adding the new one (possible if the opponent sends two invites // in a row). Object oldInvitation = gameInvitations.put(invitation.getFrom(), inviteAlert); if (oldInvitation != null) { // TODO: clean it up by removing it from the transcript window. } // Add the response panel to the transcript window. room.getTranscriptWindow().addComponent(inviteAlert); } private void handleErrorIQ(final GameOffer invitation) { // Maybe the initiator canceled the game offer, lets check that if (gameInvitations.containsKey(invitation.getFrom())) { inviteAlert.remove(1); JLabel userCanceled = new JLabel("The other player rejected the game invitation"); inviteAlert.add(userCanceled); gameInvitations.remove(invitation.getFrom()); } } /** * Adds the Reversi toolbar button. */ private void addToolbarButton() { ChatManager manager = SparkManager.getChatManager(); chatRoomListener = new ChatRoomListenerAdapter() { ImageIcon icon = ReversiRes.getImageIcon(ReversiRes.REVERSI_ICON); public void chatRoomOpened(final ChatRoom room) { if(!(room instanceof ChatRoomImpl)) { // Don't do anything if this is not a 1on1-Chat return; } ChatRoomButton button = new ChatRoomButton(icon); button.setToolTipText("Reversi"); room.getToolBar().addChatRoomButton(button); // Add a button listener that sends out a game invite on a user // click. button.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { // Show "requesting a game panel" final JPanel request = new JPanel(); request.setLayout(new BorderLayout()); JPanel requestPanel = new JPanel() { private static final long serialVersionUID = 4490592207923738251L; protected void paintComponent(Graphics g) { g.drawImage(icon.getImage(), 0, 0, null); } }; requestPanel.setPreferredSize(new Dimension(24, 24)); request.add(requestPanel, BorderLayout.WEST); String opponentJID = ((ChatRoomImpl) room).getJID(); String opponentName = "[" + opponentJID + "]"; // TODO: // convert // to // more // readable // name. final JPanel content = new JPanel(new BorderLayout()); final JLabel label = new JLabel("Requesting a Reversi game with " + opponentName + ", please wait..."); content.add(label, BorderLayout.CENTER); JPanel buttonPanel = new JPanel(); final JButton cancelButton = new JButton("Cancel"); cancelButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { GameOffer reply = new GameOffer(); reply.setTo(((ChatRoomImpl) room).getJID()); reply.setType(IQ.Type.ERROR); SparkManager.getConnection().sendPacket(reply); cancelButton.setText("Canceled"); cancelButton.setEnabled(false); } }); buttonPanel.add(cancelButton, BorderLayout.SOUTH); content.add(buttonPanel, BorderLayout.SOUTH); request.add(content, BorderLayout.CENTER); room.getTranscriptWindow().addComponent(request); final GameOffer offer = new GameOffer(); offer.setTo(opponentJID); // Add a listener for a reply to our offer. SparkManager.getConnection().addPacketListener(new PacketListener() { public void processPacket(Packet packet) { GameOffer offerReply = ((GameOffer) packet); if (offerReply.getType() == IQ.Type.RESULT) { // Remove the offer panel room.getTranscriptWindow().remove(request); content.remove(1); label.setText("Starting game..."); // Show game board (using original offer!). showReversiBoard(offer.getGameID(), room, offer.isStartingPlayer(), offerReply.getFrom()); } else if (offerReply.getType() == IQ.Type.ERROR) { cancelButton.setVisible(false); JPanel userDeclinedPanel = new JPanel(new BorderLayout()); JLabel userDeclined = new JLabel("User declined..."); userDeclinedPanel.add(userDeclined, BorderLayout.SOUTH); request.add(userDeclinedPanel, BorderLayout.SOUTH); } // TODO: Handle error case } }, new PacketIDFilter(offer.getPacketID())); SparkManager.getConnection().sendPacket(offer); } }); } public void chatRoomClosed(ChatRoom room) { super.chatRoomClosed(room); // TODO: if game is in progress, close it down. What we need is // an API that lets us see // TODO: if there's a transcript alert currently there. } }; manager.addChatRoomListener(chatRoomListener); } /** * Displays the game board. This is called after an offer has been accepted * or after accepting an invitation. * * @param gameID * the game ID. * @param room * the chat room to display the game board in. * @param startingPlayer * true if this player is the starting player (black). * @param opponentJID * the opponent's JID. */ private void showReversiBoard(int gameID, ChatRoom room, boolean startingPlayer, String opponentJID) { JSplitPane pane = room.getSplitPane(); pane.setResizeWeight(1.0); BackgroundPanel reversiBackground = new BackgroundPanel(); reversiBackground.setLayout(new BorderLayout()); reversiBackground.setOpaque(false); ReversiPanel reversi = new ReversiPanel(SparkManager.getConnection(), gameID, startingPlayer, opponentJID); reversiBackground.add(reversi, BorderLayout.CENTER); pane.setRightComponent(reversiBackground); reversi.setMinimumSize(new Dimension(ReversiPanel.TOTAL_WIDTH, ReversiPanel.TOTAL_HEIGHT)); } /** * Removes the Reversi toolbar button. */ private void removeToolbarButton() { ChatManager manager = SparkManager.getChatManager(); manager.removeChatRoomListener(chatRoomListener); // TODO: remove actual buttons from toolbar. } }