Revision: 437 http://skycastle.svn.sourceforge.net/skycastle/?rev=437&view=rev Author: zzorn Date: 2008-04-03 15:14:54 -0700 (Thu, 03 Apr 2008) Log Message: ----------- Moved previous 3D chat client to a 'hardcoded' pacakge, and started implementing the message based Skycastle client. Implemented the basic message handling, and wrote down open questions. Modified Paths: -------------- trunk/skycastle/modules/client/pom.xml Added Paths: ----------- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/SkycastleClient.java trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatClient.java trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatInputHandler.java trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatOutputHandler.java trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatUserHandler.java trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/Simple3DClient.java Modified: trunk/skycastle/modules/client/pom.xml =================================================================== --- trunk/skycastle/modules/client/pom.xml 2008-04-03 21:31:48 UTC (rev 436) +++ trunk/skycastle/modules/client/pom.xml 2008-04-03 22:14:54 UTC (rev 437) @@ -33,7 +33,7 @@ <archive> <manifest> - <mainClass>org.skycastle.client.Simple3DClient</mainClass> + <mainClass>org.skycastle.client.hardcoded.Simple3DClient</mainClass> </manifest> </archive> </configuration> @@ -70,7 +70,7 @@ --> <!-- <manifest> - <mainClass>org.skycastle.client.Simple3DClient</mainClass> + <mainClass>org.skycastle.client.hardcoded.Simple3DClient</mainClass> --> <!-- <packageName>org.skycastle.client</packageName> Added: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/SkycastleClient.java =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/SkycastleClient.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/SkycastleClient.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,242 @@ +package org.skycastle.client; + +import com.sun.sgs.client.ClientChannel; +import com.sun.sgs.client.ClientChannelListener; +import com.sun.sgs.client.simple.SimpleClient; +import com.sun.sgs.client.simple.SimpleClientListener; +import org.skycastle.messaging.Message; +import org.skycastle.messaging.updates.UpdateMessage; +import org.skycastle.protocol.ProtocolException; +import org.skycastle.protocol.negotiation.ClientSideProtocolNegotiator; +import org.skycastle.protocol.negotiation.ProtocolNegotiator; +import org.skycastle.protocol.registry.ProtocolRegistryImpl; + +import java.io.IOException; +import java.net.PasswordAuthentication; +import java.util.Properties; +import java.util.logging.Logger; + +/** + * The main entry point for the Skycastle client. + * + * @author Hans Häggström + */ +// IDEA: The client could be connected to multiple servers at once, or logged into multiple accounts at once. +// The GameObjects provided by different servers / accounts would need to be in different namespaces +// Possibly also connected to multiple local servers? E.g. editing some designs on a local server, +// browsing some object store server, and keeping an eye on their characters statuses on their favourite servers. +// We need a SimpleClient for each connected remote server. + +// TODO: The client could probably get the servers from a metaserver, or from user specified string, +// or it could run a server locally (not necessarily backed by SGS? Just using normal client side GameObjects?) + +// TODO: We should also initialize the client side UI. +// Each server+account can provide their own view, but the client itself needs to provide an +// overall view (maybe some menu / menu bar + keyboard binding to access it if not visible) +// When connecting to a server, the view is represented as GameObjects that get moved to the client +// They might need to keep some state and maybe calculation on the client side? Or pipe everything through the server? +// The client UI:s need to be stored locally though. +// Advantage of keeping UI:s on the server is that they are the same regardless of from which machine you connect, +// but drawback is that they need to be configured to suit you on each server and account. +// What kind of exchange format could be used? Maybe upload UI designs from the client to the server? +// --> We need the possibility of a ProxyGameObject to be of some specified subtype, that is specified by the server. +// Or maybe they could be POJO game objects.. +// They should implement some interface that has getJComponent and similar methods +// So client could do something like getAccountObject().getUiRoot().createJComponent() +public class SkycastleClient + implements SimpleClientListener +{ + + //====================================================================== + // Private Fields + + private final SimpleClient myDarkstarClient; + private final ProtocolNegotiator myProtocolNegotiator; + + //====================================================================== + // Private Constants + + /** + * The name of the host property. + */ + private static final String HOST_PROPERTY = "skycastle.host"; + + /** + * The default hostname. + */ + private static final String DEFAULT_HOST = "localhost"; + + /** + * The name of the port property. + */ + private static final String PORT_PROPERTY = "skycastle.port"; + + /** + * The default port. + */ + private static final String DEFAULT_PORT = "1139"; + + private static final String TEST_USER_NAME = "TestUser"; + + /** + * The {@link java.util.logging.Logger} for this class. + */ + private static final Logger LOGGER = + Logger.getLogger( SkycastleClient.class.getName() ); + private static final String CLIENT_TYPE = "SkycastleClient"; + private static final String CLIENT_VERSION = "0.1"; + private static final ProtocolRegistryImpl PROTOCOL_REGISTRY = new ProtocolRegistryImpl(); + + //====================================================================== + // Public Methods + + //---------------------------------------------------------------------- + // Constructors + + /** + * Creates a new {@link org.skycastle.client.SkycastleClient}. + */ + public SkycastleClient() + { + myDarkstarClient = new SimpleClient( this ); + + // DEBUG: For now, just try directly to log in. + login(); + + myProtocolNegotiator = new ClientSideProtocolNegotiator( PROTOCOL_REGISTRY, + CLIENT_TYPE, + CLIENT_VERSION ); + } + + //---------------------------------------------------------------------- + // ServerSessionListener Implementation + + public ClientChannelListener joinedChannel( final ClientChannel clientChannel ) + { + LOGGER.fine( "Joined channel '" + clientChannel + "'." ); + + return null; + } + + + public void receivedMessage( final byte[] serverMessage ) + { + if ( myProtocolNegotiator.getStatus().isFinished() ) + { + try + { + final Message message = myProtocolNegotiator.getProtocol().decode( serverMessage ); + + // TODO: Validate message to ensure a rogue server is not sending us trojans + // message.validate( ) + + // TODO: Check that the message is an update message, as the client should only get those. + final UpdateMessage updateMessage = (UpdateMessage) message; + + // Apply the update message to the client side model + // TODO: Use cient side GameContext directly instead, and specify a namespace prefix for this server and account + updateMessage.applyStateChangeToModel(); + } + catch ( ProtocolException e ) + { + // TODO: Notify user, reset server connection? + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + } + else + { + final byte[] reply = myProtocolNegotiator.handleMessage( serverMessage ); + try + { + myDarkstarClient.send( reply ); + } + catch ( IOException e ) + { + // TODO: Notify user, reset server connection? + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + } + } + + + public void reconnecting() + { + LOGGER.fine( "Reconnecting" ); + } + + + public void reconnected() + { + LOGGER.fine( "Reconnected" ); + } + + + public void disconnected( final boolean b, final String s ) + { + LOGGER.fine( "Disconnected. Graceful: '" + b + "'. Message: '" + s + "'." ); + } + + //---------------------------------------------------------------------- + // SimpleClientListener Implementation + + public PasswordAuthentication getPasswordAuthentication() + { + // TODO: Query user about password + + // DEBUG + return new PasswordAuthentication( TEST_USER_NAME, "testPassword".toCharArray() ); + } + + + public void loggedIn() + { + LOGGER.fine( "Logged In" ); + + if ( myProtocolNegotiator.startsNegotiations() ) + { + try + { + myDarkstarClient.send( myProtocolNegotiator.handleMessage( null ) ); + } + catch ( IOException e ) + { + // TODO: Notify user about failure, reset server connection? + e.printStackTrace(); //To change body of catch statement use File | Settings | File Templates. + } + } + } + + + public void loginFailed( final String s ) + { + LOGGER.fine( "Login Failed. Reason: '" + s + "'." ); + } + + //====================================================================== + // Private Methods + + /** + * Initiates asynchronous login to the SGS server specified by the host and port properties. + */ + // IDEA: This could be one action provided by a client side GameObject representing a server + private void login() + { + final String host = System.getProperty( HOST_PROPERTY, DEFAULT_HOST ); + final String port = System.getProperty( PORT_PROPERTY, DEFAULT_PORT ); + + try + { + final Properties connectProps = new Properties(); + connectProps.setProperty( "host", host ); + connectProps.setProperty( "port", port ); + myDarkstarClient.login( connectProps ); + } + catch ( Exception e ) + { + // TODO: Report error to user + e.printStackTrace(); + disconnected( false, e.getMessage() ); + } + } + +} Copied: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatClient.java (from rev 433, trunk/skycastle/modules/client/src/main/java/org/skycastle/client/ChatClient.java) =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatClient.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatClient.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,590 @@ +/* ========================================================================= + * + * This file is part of Skycastle. + * + * Skycastle is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Skycastle 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 General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Skycastle; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ========================================================================= */ + +package org.skycastle.client.hardcoded; + +import com.sun.sgs.client.ClientChannel; +import com.sun.sgs.client.ClientChannelListener; +import com.sun.sgs.client.SessionId; +import com.sun.sgs.client.simple.SimpleClient; +import com.sun.sgs.client.simple.SimpleClientListener; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.math.BigInteger; +import java.net.PasswordAuthentication; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Chat client. This client connects to the SGS Skycastle server and processes chat messages on a channel. + * Until we have implemented a real chat protocol, the client understands the following special messages. + * <p/> + * From server: + * <p/> + * JOIN [sessionId] [nickname] - User with nickname and sessionId has joined the chat. + * <p/> + * QUIT [sessionId] [nickname] - User with nickname and sessionId has left the chat. + * <p/> + * From client: + * <p/> + * NICK [newNickname] - User has changed nickname (TODO: this is very sloppy, with no protection whatsoever + * against duplicates). + * <p/> + * TODO: + * <p/> + * The channel-based chat has some issues since the communication is effectively client-to-client, even though + * the server acts as the hub for dispatching messages.. It is difficult to enforce any rules and protect + * clients from invalid messages. The state of the network may be difficult to keep consistent across clients + * (for example, when mapping sessions to nicknames. Each client has to do its own validity checking and + * enforce unique nicknames.). See also SkycastleServerListener. + */ +public class ChatClient + implements SimpleClientListener, ClientChannelListener +{ + + //====================================================================== + // Private Fields + + /** + * The random number generator for login names. + */ + private final Random random = new Random(); + + //====================================================================== + // Non-Private Fields + + /** + * The {@link SimpleClient} instance for this client. + */ + protected final SimpleClient simpleClient; + + /** + * Username. A placeholder until the system can handle actual user accounts. + */ + protected String username = ""; + + /** + * Map that associates a channel name with a {@link ClientChannel}. + */ + protected final Map<String, ClientChannel> channelsByName = + new HashMap<String, ClientChannel>(); + + /** + * Map that associates a session ID with a nickname. + */ + protected final Map<SessionId, String> nicknamesById = + new HashMap<SessionId, String>(); + + /** + * Handler for chat messages that should be displayed. + */ + protected ChatOutputHandler outputHandler = null; + + /** + * Handler for chat user events. + */ + protected ChatUserHandler userHandler; + + //====================================================================== + // Public Constants + + /** + * The name of the general chat channel: '{@value #GENERAL_CHAT}' + */ + public static final String GENERAL_CHAT = "General"; + + /** + * The name of the host property. + */ + public static final String HOST_PROPERTY = "skycastle.host"; + + /** + * The default hostname. + */ + public static final String DEFAULT_HOST = "localhost"; + + /** + * The name of the port property. + */ + public static final String PORT_PROPERTY = "skycastle.port"; + + /** + * The default port. + */ + public static final String DEFAULT_PORT = "1139"; + + /** + * The message encoding. + */ + public static final String MESSAGE_CHARSET = "UTF-8"; + + //====================================================================== + // Private Constants + + /** + * The version of the serialized form of this class. + */ + private static final long serialVersionUID = 1L; + + /** + * The {@link Logger} for this class. + */ + private static final Logger logger = + Logger.getLogger( ChatClient.class.getName() ); + + //====================================================================== + // Public Methods + + //---------------------------------------------------------------------- + // Constructors + + /** + * Create a new chat client. + */ + public ChatClient() + { + simpleClient = new SimpleClient( this ); + // TODO: Placeholder until we have something real. + setUsername( "guest-" + random.nextInt( 1000 ) ); + } + + + /** + * Create a new chat client. + * + * @param initOutputHandler Handler for chat messages to be displayed. + */ + public ChatClient( ChatOutputHandler initOutputHandler, + ChatUserHandler initUserHandler ) + { + this(); + outputHandler = initOutputHandler; + userHandler = initUserHandler; + } + + //---------------------------------------------------------------------- + // ClientChannelListener Implementation + + /** + * {@inheritDoc} + * <p/> + * This is called when a message arrives on a channel. + */ + public void receivedMessage( ClientChannel channel, + SessionId sender, + byte[] message ) + { + String msg = decodeString( message ); + logger.log( Level.INFO, "Channel message: [" + channel.getName() + + ", " + sender + "] " + msg ); + if ( outputHandler != null ) + { + if ( sender != null ) + { + // Message from client. + /* Do some quick and dirty server message handling until we + have implemented a real chat protocol. */ + String clientName = nicknamesById.get( sender ); + if ( clientName != null ) + { + /* Change nickname. + NICK <nickname> */ + /* ----- This doesn't work well yet ----- // + if (msg.startsWith("NICK")) + { + final String[] parts = msg.split(" "); + if (parts.length >= 2) + { + String newNick = parts[1]; + nicknamesById.put(sender, newNick); + getChatOutputHandler().appendChatOutput(null, + clientName + " changed nickname to " + + newNick + "."); + getChatUserHandler().removeChatUser(clientName); + getChatUserHandler().addChatUser(newNick); + } + } else + // ----- */ + getChatOutputHandler().appendChatOutput( clientName, msg ); + } + else + { + getChatOutputHandler().appendChatOutput( null, + "UNNAMED-client: " + msg ); + } + } + else + { + // Message from server. + receivedMessage( message ); + } + } + } + + + /** + * {@inheritDoc} + * <p/> + * This is called when the user leaves a channel. + */ + public void leftChannel( ClientChannel channel ) + { + channelsByName.remove( channel.getName() ); + logger.log( Level.INFO, "Left channel: " + channel.getName() ); + // TODO: Let the user know. + } + + //---------------------------------------------------------------------- + // ServerSessionListener Implementation + + /** + * {@inheritDoc} + * <p/> + * This is called when the user joins a channel. + */ + public ClientChannelListener joinedChannel( ClientChannel channel ) + { + channelsByName.put( channel.getName(), channel ); + logger.log( Level.INFO, "Joined channel: " + channel.getName() ); + // TODO: Let the user know. + return this; + } + + + /** + * {@inheritDoc} + * <p/> + * This is called when the client receives a message from the server. + */ + public void receivedMessage( byte[] message ) + { + String msg = decodeString( message ); + logger.log( Level.INFO, "Message from server: " + msg ); + /* Do some quick and dirty server message handling until we + have implemented a real chat protocol. */ + if ( msg.startsWith( "JOIN" ) ) + { + /* Let users know that a client has joined the chat. + JOIN <sessionId> <nickname> */ + final String[] parts = msg.split( " " ); + if ( parts.length >= 3 ) + { + SessionId senderId = + SessionId.fromBytes( decodeHexString( parts[ 1 ] ) ); + nicknamesById.put( senderId, parts[ 2 ] ); + getChatOutputHandler().appendChatOutput( null, parts[ 2 ] + + " joined the chat." ); + getChatUserHandler().addChatUser( parts[ 2 ] ); + } + } + else if ( msg.startsWith( "QUIT" ) ) + { + /* Let users know that a client has left the chat. + QUIT <sessionId> <nickname> */ + final String[] parts = msg.split( " " ); + if ( parts.length >= 3 ) + { + SessionId senderId = + SessionId.fromBytes( decodeHexString( parts[ 1 ] ) ); + String victim = nicknamesById.get( senderId ); + if ( victim != null ) + { + nicknamesById.remove( senderId ); + getChatOutputHandler().appendChatOutput( null, + victim + " left the chat." ); + getChatUserHandler().removeChatUser( parts[ 2 ] ); + } + } + } + else + { + getChatOutputHandler().appendChatOutput( null, + "Server: " + msg ); + } + } + + + /** + * {@inheritDoc} + * <p/> + * This is called when reconnection is attempted. + */ + public void reconnecting() + { + logger.log( Level.INFO, "Reconnecting." ); + // TODO: Let the user know. + } + + + /** + * {@inheritDoc} + * <p/> + * This is called on a successful reconnect. + */ + public void reconnected() + { + logger.log( Level.INFO, "Reconnected successfully." ); + // TODO: Let the user know. + } + + + /** + * {@inheritDoc} + * <p/> + * This is called when the user is disconnected. + */ + public void disconnected( boolean graceful, String reason ) + { + logger.log( Level.INFO, "Disconnected (" + graceful + ", " + reason + + ")." ); + // TODO: Let the user know. + } + + //---------------------------------------------------------------------- + // SimpleClientListener Implementation + + /** + * {@inheritDoc} + * <p/> + * Returns dummy credentials where user is "guest-<random>" and the password is "guest." + */ + public PasswordAuthentication getPasswordAuthentication() + { + logger.log( Level.INFO, "Logging in as " + username ); + // TODO: Let the user know. + String password = "guest"; + return new PasswordAuthentication( username, password.toCharArray() ); + } + + + /** + * {@inheritDoc} + * <p/> + * This is called on a successful login. + */ + public void loggedIn() + { + logger.log( Level.INFO, "Logged in successfully." ); + // TODO: Let the user know. + } + + + /** + * {@inheritDoc} + * <p/> + * This is called on a failed login. + */ + public void loginFailed( String reason ) + { + logger.log( Level.WARNING, "Login failed (" + reason + ")!" ); + // TODO: Let the user know. + } + + //---------------------------------------------------------------------- + // Other Public Methods + + /** + * Send a message to the general chat. + * + * @param message Message to be sent. + */ + public void send( String message ) + { + ClientChannel channel = channelsByName.get( GENERAL_CHAT ); + if ( channel != null ) + { + try + { + channel.send( encodeString( message ) ); + } + catch ( IOException e ) + { + logger.log( Level.WARNING, "Could not send message on channel: " + + GENERAL_CHAT ); + } + } + else + { + logger.log( Level.WARNING, "Client is not joined to channel: " + + GENERAL_CHAT ); + } + } + + + /** + * Set the handler for chat output. + */ + public void setChatOutputHandler( ChatOutputHandler handler ) + { + outputHandler = handler; + } + + + /** + * Get the handler for local chat output. + * + * @return Handler for local chat output. + */ + public ChatOutputHandler getChatOutputHandler() + { + return outputHandler; + } + + + /** + * Set the handler for chat user events. + */ + public void setChatUserHandler( ChatUserHandler handler ) + { + userHandler = handler; + } + + + /** + * Get the handler for chat user events. + * + * @return Handler for chat user events. + */ + public ChatUserHandler getChatUserHandler() + { + return userHandler; + } + + + /** + * Get username. + */ + public String getUsername() + { + return username; + } + + + /** + * Set username. + * + * @param newUsername Username. + */ + public void setUsername( String newUsername ) + { + username = newUsername; + } + + //====================================================================== + // Protected Methods + + /** + * Initiates asynchronous login to the SGS server specified by the host and port properties. + */ + protected void login() + { + String host = System.getProperty( HOST_PROPERTY, DEFAULT_HOST ); + String port = System.getProperty( PORT_PROPERTY, DEFAULT_PORT ); + + try + { + Properties connectProps = new Properties(); + connectProps.put( "host", host ); + connectProps.put( "port", port ); + simpleClient.login( connectProps ); + } + catch ( Exception e ) + { + e.printStackTrace(); + disconnected( false, e.getMessage() ); + } + } + + + /** + * Encodes a {@code String} into an array of bytes. + * + * @param s the string to encode + * + * @return the byte array which encodes the given string + */ + protected static byte[] encodeString( String s ) + { + try + { + return s.getBytes( MESSAGE_CHARSET ); + } + catch ( UnsupportedEncodingException e ) + { + throw new Error( "Required character set " + MESSAGE_CHARSET + + " not found", e ); + } + } + + + /** + * Decodes an array of bytes into a {@code String}. + * + * @param bytes the bytes to decode + * + * @return the decoded string + */ + protected static String decodeString( byte[] bytes ) + { + try + { + return new String( bytes, MESSAGE_CHARSET ); + } + catch ( UnsupportedEncodingException e ) + { + throw new Error( "Required character set " + MESSAGE_CHARSET + + " not found", e ); + } + } + + + /** + * Encode an array of bytes into a hexadecimal string. + * + * @param bytes The bytes to be encoded. + * + * @return The encoded string. + */ + protected static String encodeHexString( byte[] bytes ) + { + BigInteger bi = new BigInteger( bytes ); + return bi.toString( 16 ); + } + + + /** + * Decode a hexadecimal string into an array of bytes. + * + * @param source The string to be decoded. + * + * @return The decoded string. + */ + protected static byte[] decodeHexString( String source ) + { + BigInteger bi = new BigInteger( source, 16 ); + return bi.toByteArray(); + } + +} Copied: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatInputHandler.java (from rev 433, trunk/skycastle/modules/client/src/main/java/org/skycastle/client/ChatInputHandler.java) =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatInputHandler.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatInputHandler.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,34 @@ +/* ========================================================================= + * + * This file is part of Skycastle. + * + * Skycastle is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Skycastle 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with Skycastle; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ========================================================================= */ + +package org.skycastle.client.hardcoded; + +/** + * Handler for chat text entered by the local user. + */ +public interface ChatInputHandler +{ + /** + * Process a line of chat input. + * + * @param chatMessage Text of the chat message. + */ + public void processChatInput( String chatMessage ); +} Copied: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatOutputHandler.java (from rev 433, trunk/skycastle/modules/client/src/main/java/org/skycastle/client/ChatOutputHandler.java) =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatOutputHandler.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatOutputHandler.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,35 @@ +/* ========================================================================= + * + * This file is part of Skycastle. + * + * Skycastle is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Skycastle 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with Skycastle; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ========================================================================= */ + +package org.skycastle.client.hardcoded; + +/** + * Handler for chat text recieved from other users. + */ +public interface ChatOutputHandler +{ + /** + * Append a line of chat output. + * + * @param who User from which the chat message has been received. + * @param chatMessage Text of the chat message. + */ + void appendChatOutput( String who, String chatMessage ); +} Copied: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatUserHandler.java (from rev 433, trunk/skycastle/modules/client/src/main/java/org/skycastle/client/ChatUserHandler.java) =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatUserHandler.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/ChatUserHandler.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,42 @@ +/* ========================================================================= + * + * This file is part of Skycastle. + * + * Skycastle is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Skycastle 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 + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with Skycastle; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ========================================================================= */ + +package org.skycastle.client.hardcoded; + +/** + * Handler for chat user events. Classes which implement this interface handle events that pertain to users of + * the chat, such as adding a user to the chat or removing a user from the chat. + */ +public interface ChatUserHandler +{ + /** + * Add a user to the chat. + * + * @param who User to be added. + */ + void addChatUser( String who ); + + /** + * Remove a user from the chat. + * + * @param who User to be removed. + */ + void removeChatUser( String who ); +} Copied: trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/Simple3DClient.java (from rev 433, trunk/skycastle/modules/client/src/main/java/org/skycastle/client/Simple3DClient.java) =================================================================== --- trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/Simple3DClient.java (rev 0) +++ trunk/skycastle/modules/client/src/main/java/org/skycastle/client/hardcoded/Simple3DClient.java 2008-04-03 22:14:54 UTC (rev 437) @@ -0,0 +1,551 @@ +/* ========================================================================= + * + * This file is part of Skycastle. + * + * Skycastle is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * Skycastle 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 General Public License + * for more details. + * + * You should have received a copy of the GNU General Public License along + * with Skycastle; if not, write to the Free Software Foundation, + * Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * ========================================================================= */ + +package org.skycastle.client.hardcoded; + +import com.jme.app.SimpleGame; +import com.jme.bounding.BoundingBox; +import com.jme.image.Texture; +import com.jme.input.*; +import com.jme.math.Vector3f; +import com.jme.renderer.ColorRGBA; +import com.jme.renderer.Renderer; +import com.jme.scene.Controller; +import com.jme.scene.Node; +import com.jme.scene.SceneElement; +import com.jme.scene.shape.Box; +import com.jme.scene.state.LightState; +import com.jme.scene.state.TextureState; +import com.jme.util.TextureManager; +import com.jmex.awt.swingui.JMEDesktop; + +import javax.swing.*; +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Container; +import java.awt.Dimension; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Simple 3D client. A simple 3D client for Skycastle. This version displays a chat window and a rotating box + * in the background. The client is based on the SimpleGame game skeleton provided by the jME (this will + * probably change in the future). Chat messages are processed by handlers which can be set using the + * setChatInputHandler() and setChatOutputHandler() methods. setChatUserHandler() is used to specify the user + * event handler, which at this time handles adding and removing user names from the list. Right now, + * everything is handled by the Simple3DClient itself. + */ +public class Simple3DClient + extends SimpleGame + implements ChatInputHandler, ChatOutputHandler, ChatUserHandler +{ + + //====================================================================== + // Non-Private Fields + + /** + * The color to be used as background color for the desktop. This is completely transparent right now. + */ + protected static final Color DESKTOP_BACKGROUND_COLOR = new Color( + 0, 1, 0, 0.0f ); + + /** + * Keyboard look input handler. + */ + protected KeyboardLookHandler lookHandler; + /** + * Scene node for the GUI. + */ + protected Node guiNode; + /** + * JME desktop on which the GUI will be shown. + */ + protected JMEDesktop desktop; + /** + * Frame containing the chat UI. + */ + protected JInternalFrame chatFrame; + /** + * Text field used for chat input. + */ + protected JTextField chatInput; + /** + * Text pane used for chat output. + */ + protected JTextPane chatOutput; + /** + * List model for the chat user list. + */ + protected DefaultListModel chatUserListModel; + /** + * List view used for the chat user list. + */ + protected JList chatUserList; + /** + * Handler for incoming local chat messages. + */ + protected ChatInputHandler chatInputHandler; + /** + * Handler for chat messages that should be displayed. + */ + protected ChatOutputHandler chatOutputHandler; + /** + * Handler for chat user events. + */ + protected ChatUserHandler chatUserHandler; + /** + * Chat client. + */ + protected ChatClient chatClient; + + //====================================================================== + // Private Constants + + private static final Logger LOGGER = Logger.getLogger( Simple3DClient.class.getName() ); + + //====================================================================== + // Public Methods + + //---------------------------------------------------------------------- + // Main Method + + /** + * Start everything. + */ + public static void main( String[] args ) + { + LOGGER.log( Level.INFO, "Skycastle 3D Client starting up." ); + + // Do not show logging output below the WARNING level (JME outputs a lot of debugging info at INFO level). + // This way the console output is a bit more relevant. + LOGGER.setLevel( Level.WARNING ); + + Simple3DClient client = new Simple3DClient(); + client.start(); + } + + //---------------------------------------------------------------------- + // ChatInputHandler Implementation + + /** + * Process a line of input from the chat text field. This gets called in the Swing thread when the enters + * a line of chat text. Implemented from ChatInputHandler. + * + * @param chatMessage Text of the chat message. + */ + public void processChatInput( String chatMessage ) + { + // Print to the console and show in chat output window. + LOGGER.log( Level.INFO, "chat: " + chatMessage ); + getChatOutputHandler().appendChatOutput( chatClient.getUsername(), + chatMessage ); + chatClient.send( chatMessage ); + } + + //---------------------------------------------------------------------- + // ChatOutputHandler Implementation + + /** + * Append a line of chat text to the chat output area. Implemented from ChatOutputHandler. + * + * @param who User from which the chat message has been received. + * @param chatMessage Text of the chat message. + */ + public void appendChatOutput( String who, String chatMessage ) + { + // Show in chat output window. + if ( who != null ) + { + chatOutput.setText( chatOutput.getText() + "\n" + + "<" + who + "> " + chatMessage ); + } + else + { + chatOutput.setText( chatOutput.getText() + "\n" + chatMessage ); + } + } + + //---------------------------------------------------------------------- + // ChatUserHandler Implementation + + /** + * Add a user to the chat. + * + * @param who User to be added. + */ + public void addChatUser( String who ) + { + boolean found = false; + int i = 0; + while ( !found + && ( i < chatUserListModel.size() ) ) + { + if ( chatUserListModel.elementAt( i ).equals( who ) ) + { + found = true; + } + else + { + i++; + } + } + if ( found ) + { + LOGGER.log( Level.WARNING, "User '" + who + "' is already in the list!" ); + return; + } + chatUserListModel.addElement( who ); + } + + + /** + * Remove a user from the chat. + * + * @param who User to be removed. + */ + public void removeChatUser( String who ) + { + boolean found = false; + int i = 0; + while ( !found + && ( i < chatUserListModel.size() ) ) + { + if ( chatUserListModel.elementAt( i ).equals( who ) ) + { + found = true; + } + else + { + i++; + } + } + if ( found ) + { + chatUserListModel.removeElementAt( i ); + } + else + { + LOGGER.log( Level.WARNING, "User '" + who + "' is not in the list!" ); + } + } + + //---------------------------------------------------------------------- + // Other Public Methods + + /** + * Set up key bindings. + */ + public void initKeyBindings() + { + /* Remove some keybindings established by BaseSimpleGame, since they + are needed for chatting. + TODO: Find a more elegant solution. Maybe bind/rebind depending on + focus, or handling these during update. + Eventually, we may also need to implement our own base game + class. + */ + KeyBindingManager.getKeyBindingManager().remove( "toggle_pause" ); + KeyBindingManager.getKeyBindingManager().remove( "step" ); + KeyBindingManager.getKeyBindingManager().remove( "toggle_wire" ); + KeyBindingManager.getKeyBindingManager().remove( "toggle_lights" ); + KeyBindingManager.getKeyBindingManager().remove( "toggle_bounds" ); + KeyBindingManager.getKeyBindingManager().remove( "toggle_normals" ); + KeyBindingManager.getKeyBindingManager().remove( "camera_out" ); + KeyBindingManager.getKeyBindingManager().remove( "mem_report" ); + + // Bind the BaseSimpleGame functions to other keys. + KeyBindingManager.getKeyBindingManager().set( "toggle_pause", KeyInput.KEY_F4 ); + KeyBindingManager.getKeyBindingManager().set( "step", KeyInput.KEY_F5 ); + KeyBindingManager.getKeyBindingManager().set( "toggle_wire", KeyInput.KEY_F6 ); + KeyBindingManager.getKeyBindingManager().set( "toggle_lights", KeyInput.KEY_F7 ); + KeyBindingManager.getKeyBindingManager().set( "toggle_bounds", KeyInput.KEY_F8 ); + KeyBindingManager.getKeyBindingManager().set( "toggle_normals", KeyInput.KEY_F9 ); + KeyBindingManager.getKeyBindingManager().set( "camera_out", KeyInput.KEY_F10 ); + KeyBindingManager.getKeyBindingManager().set( "mem_report", KeyInput.KEY_F11 ); + } + + + /** + * Set the handlers for chat events. + * + * @param inputHandler Handler for chat input entered by the local user. + * @param outputHandler Handler for chat message sto be displayed. + * @param userHandler Handler for chat user events. + */ + public void setChatHandlers( ChatInputHandler inputHandler, + ChatOutputHandler outputHandler, ChatUserHandler userHandler ) + { + setChatInputHandler( inputHandler ); + setChatOutputHandler( outputHandler ); + setChatUserHandler( userHandler ); + } + + + /** + * Set the handler for local chat input. + */ + public void setChatInputHandler( ChatInputHandler handler ) + { + chatInputHandler = handler; + } + + + /** + * Get the handler for local chat input. + * + * @return Handler for local chat input. + */ + public ChatInputHandler getChatInputHandler() + { + return chatInputHandler; + } + + + /** + * Set the handler for chat output. + */ + public void setChatOutputHandler( ChatOutputHandler handler ) + { + chatOutputHandler = handler; + } + + + /** + * Get the handler for local chat output. + * + * @return Handler for local chat output. + */ + public ChatOutputHandler getChatOutputHandler() + { + return chatOutputHandler; + } + + + /** + * Set the handler for chat user events. + */ + public void setChatUserHandler( ChatUserHandler handler ) + { + chatUserHandler = handler; + } + + + /** + * Get the handler for chat user events. + * + * @return Handler for chat user events. + */ + public ChatUserHandler getChatUserHandler() + { + return chatUserHandler; + } + + //====================================================================== + // Protected Methods + + /** + * Create the UI. Create the elements of the Swing UI. + */ + protected void initUI() + { + /// JME part of the GUI creation. + guiNode = new Node( "gui" ); + guiNode.setRenderQueueMode( Renderer.QUEUE_ORTHO ); + + desktop = new JMEDesktop( + "desktop", + display.getWidth(), + display.getHeight(), + input + ); + guiNode.attachChild( desktop ); + desktop.getLocalTranslation().set( + display.getWidth() / 2, + display.getHeight() / 2, 0 + ); + + // AWT/Swing part of the GUI creation. + desktop.getJDesktop().setBackground( DESKTOP_BACKGROUND_COLOR ); + + chatFrame = new JInternalFrame( "Chat" ); + chatFrame.setLocation( 10, 10 ); + chatFrame.setResizable( true ); + Container contentPane = chatFrame.getContentPane(); + contentPane.setLayout( new BorderLayout() ); + + chatOutput = new JTextPane(); + chatOutput.setPreferredSize( new Dimension( 400, 300 ) ); + chatOutput.setEditable( false ); + chatOutput.setText( "Welcome to Skycastle!" ); + + final JScrollPane scrollPane = new JScrollPane( chatOutput ); + + chatUserListModel = new DefaultListModel(); + + chatUserList = new JList( chatUserListModel ); + chatUserList.setPreferredSize( new Dimension( 80, 300 ) ); + + final JSplitPane splitPane = new JSplitPane( + JSplitPane.HORIZONTAL_SPLIT, + chatUserList, + scrollPane + ); + + chatInput = new JTextField(); + chatInput.addActionListener( + new ActionListener() + { + + public void actionPerformed( ActionEvent e ) + { + getChatInputHandler().processChatInput( + chatInput.getText() ); + chatInput.setText( "" ); + } + + } + ); + + contentPane.add( splitPane, BorderLayout.CENTER ); + contentPane.add( chatInput, BorderLayout.SOUTH ); + + chatFrame.setVisible( true ); + chatFrame.pack(); + + desktop.getJDesktop().add( chatFrame ); + } + + + /** + * Create the 3D scene. In this case, this is just the rotating box. + */ + protected void create3DScene() + { + final Vector3f axis = new Vector3f( 1, 1, 0.5f ).normalizeLocal(); + final Box box = new Box( "Box", + new Vector3f( -5, -5, -5 ), + new Vector3f( 5, 5, 5 ) + ); + + box.setModelBound( new BoundingBox() ); + box.updateModelBound(); + box.setLocalTranslation( new Vector3f( 0, 0, -10 ) ); + box.setLightCombineMode( LightState.OFF ); + + TextureState ts = display.getRenderer().createTextureState(); + ts.setEnabled( true ); + ts.setTexture( TextureManager.loadTexture( + Simple3DClient.class.getClassLoader().getResource( + "org/skycastle/client/data/skycastle.png" ), + Texture.MM_LINEAR, + Texture.FM_LINEAR ) + ); + box.setRenderState( ts ); + box.addController( + // Controller which rotates the box. + new Controller() + { + + private static final long serialVersionUID = 1L; + + public void update( float time ) + { + box.getLocalRotation().fromAngleNormalAxis( + timer.getTimeInSeconds(), axis ); + } + + } + ); + + rootNode.attachChild( box ); + } + + + /** + * Initialize the chat client. + */ + protected void initChatClient() + { + /* Use this Simple3DClient for handling chat output and user events. */ + chatClient = new ChatClient( this, this ); + chatClient.login(); + } + + + /** + * Initialize game. Overridden from SimpleGame. + */ + protected void simpleInitGame() + { + display.setTitle( "Skycastle 3D Client" ); + display.getRenderer().setBackgroundColor( ColorRGBA.black ); + + // Set up the keyboard look handler. + input = new InputHandler(); + lookHandler = new KeyboardLookHandler( cam, 50, 1 ); + input.addToAttachedHandlers( lookHandler ); + // Some changes to the BaseSimpleGame keyboard bindings. + initKeyBindings(); + + // Set up GUI, static 3D scene and chat client. + initUI(); + create3DScene(); + initChatClient(); + + // Handle chat input messages as well as chat output and user events. + setChatHandlers( this, this, this ); + + // Always show the the GUI. + guiNode.setCullMode( SceneElement.CULL_NEVER ); + guiNode.setLightCombineMode( LightState.OFF ); + guiNode.updateRenderState(); + guiNode.updateGeometricState( 0, true ); + MouseInput.get().setCursorVisible( true ); + } + + + /** + * Update hook. Overridden from SimpleGame. + */ + protected void simpleUpdate() + { + // Ignore keyboard events if the chat input field has the focus. + if ( chatFrame.getFocusOwner() != chatInput ) + { + lookHandler.setEnabled( true ); + } + else + { + lookHandler.setEnabled( false ); + } + } + + + /** + * Render hook. Overridden from SimpleGame. + */ + protected void simpleRender() + { + display.getRenderer().draw( guiNode ); + } + +} This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site.