Wednesday, June 22, 2011

Foray into Photon - Part 15 - LoginOperation client side

This time around we are going to create our first real operation. I am going to start off in the AegisBornCommon project as we need to add some data there first. First we need to pull up the OperationCode.cs file. We are going to add a new operation code. In projects past, I tend to lump messages into groups. It makes it easier to know what type of message you are working with if you have your codes in groups. For this instance we are going to take numbers 100-110 for any operations before we are fully in the game. Exit Games uses some numbers for their own special operations and parameters. They have stated that operations 100 and up and Parameters 61 and up are open so for now just add this after Nil,

        /// <summary>
        /// Login to the server
        /// </summary>
        Login = 100,

Next we need to add our parameter codes after DebugMessage in ParameterCode.cs:

        /// <summary>
        /// The player's username
        /// </summary>
        UserName = 70,

        /// <summary>
        /// The player's password
        /// </summary>
        Password = 71,

Now that we have our operation code and parameter code, we can recompile the Common project and make sure it is in the same directory as our AegisBorn3dPhoton project so that Unity3d can use it. Now if you read through the SmartFoxServer blog posts, you'll know I hate building stuff manually, meaning if I'm going to build a message to send to a server, I build it in a class/function so that I don't accidentally have 15 versions floating around in code. Exit Games did this nicely with a static class called Operations. However, I feel lumping them all into the same class defeats the purpose of a class. So I'm going to build a file called LoginOperations and I'm going to put it in a directory called _Operations under _Scripts:

using System.Collections;
using AegisBornCommon;

public static class LoginOperations
{
    public static void Login(Game game, string username, string password)
    {
        var vartable = new Hashtable
                           {
                               {(byte) ParameterCode.UserName, username},
                               {(byte) ParameterCode.Password, password}
                           };

        game.SendOp(OperationCode.Login, vartable, true, 0, true);
    }
}

And that will now send the login operation to the server. Next we are going to grab our old GUI code from the SmartFoxServer code and put it into the Login.cs OnGUI with some minor modifications:

    private Game _engine;


    void Start()
    {
        _engine = MMOEngine.Engine;
        _engine.afterKeysExchanged += AfterKeysExchanged;
    }


    private string _username = "";
    private string _password = "";


    void OnGUI()
    {


        GUI.Label(new Rect(10, 116, 100, 100), "Userame: ");
        _username = GUI.TextField(new Rect(100, 116, 200, 20), _username, 25);
        GUI.Label(new Rect(10, 141, 100, 100), "Password: ");
        _password = GUI.PasswordField(new Rect(100, 141, 200, 20), _password, '*', 25);


        GUI.Label(new Rect(10, 225, 400, 100), _engine.State.ToString());


        if (GUI.Button(new Rect(100, 165, 100, 25), "Login") || (Event.current.type == EventType.keyDown && Event.current.character == '\n'))
        {
            var peer = new PhotonPeer(_engine, false);


            _engine.Initialize(peer, "localhost:5055", "AegisBorn");
        }
        if (GUI.Button(new Rect(100, 195, 100, 25), "Logout"))
        {
            _engine.SetDisconnected(0);
        }
    }


    public void AfterKeysExchanged()
    {
        LoginOperations.Login(_engine, _username, _password);
    }

With encryption in place, we are now sending the server our username and password encrypted. The server will only send a response stating that it got the message. 


If you were to run the program at this point, you will see that we have our login screen, when you click Login you will see the state transfer from Disconnected to WaitingForConnect to Connected. The log should have a message stating connected and then Keys Exchanged and then an unknown response to our Login operation.


Next time we will look into modifying the server with NHibernate to begin looking up our data and checking our login information.


As an aside, I've joined Empire Avenue. {EAV_BLOG_VER:caf99dc9611a09b5}

Foray into Photon - Part 14 - Migrating to Operation and Event Handlers

So before we get into creating and sending the Login operation we are going to modify our GameStates to have a dictionary of OperationHandlers and EventHandlers. But first we need a set of objects to base our handlers off of. I began by opening Unity3d and creating a new folder called _Handlers. Inside that folder I created a folder called Operations and another one called Events. In Operations I created a new class called IOperationHandler and it looks like this:


using System.Collections;
using AegisBornCommon;


public abstract class IOperationHandler
{
    public delegate void BeforeMessageRecieved();
    public BeforeMessageRecieved beforeMessageRecieved;


    public delegate void AfterMessageRecieved();
    public AfterMessageRecieved afterMessageRecieved;


    public void HandleMessage(Game gameLogic, OperationCode operationCode, int returnCode, Hashtable returnValues)
    {
        if (beforeMessageRecieved != null)
        {
            beforeMessageRecieved();
        }
        OnHandleMessage(gameLogic, operationCode, returnCode, returnValues);
        if (afterMessageRecieved != null)
        {
            afterMessageRecieved();
        }
    }


    public abstract void OnHandleMessage(Game gameLogic, OperationCode operationCode, int returnCode, Hashtable returnValues);
}


If you notice, this class is exactly like our IMessageHandler from the SmartFoxProject. The only difference is the HandleMessage and OnHandleMessage where we pass along the game, the op code, the return code and the table of values.


Then we need to wire in the handlers to the IGameState:


    Dictionary OperationHandlers { get; }

This means we need to change our Disconnected and WaitingForConnect to include the following:

    public Dictionary OperationHandlers
    {
        get { throw new NotImplementedException(); }
    }

So we will get an error if Disconnected or WaitingForConnect tries to load our handlers. Next we need Connected to be modified. Here we will create a real dictionary and set it up:

    private readonly Dictionary _handlers;
    public Dictionary OperationHandlers
    {
        get { return _handlers; }
    }

    public Connected()
    {
        _handlers = new Dictionary();
        // Add handlers here
        var keyHandler = new ExchangeKeysHandler();
        _handlers.Add(OperationCode.ExchangeKeysForEncryption, keyHandler);
    }

So here we have created our handler dictionary and added a new class that we haven't created yet called ExchangeKeysHandler. The last thing we are going to do before we create this new class is make the modifications to look through our list of handlers and attempt to find one to handle this method:

    public void OnOperationReturn(Game gameLogic, OperationCode operationCode, int returnCode, Hashtable returnValues)
    {
        IOperationHandler handler;

        if (_handlers.TryGetValue(operationCode, out handler))
        {
            handler.HandleMessage(gameLogic, operationCode, returnCode, returnValues);
        }
        else
        {
            gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
        }
    }

This code does like it does in the SmartFox code, it attempts to find a handler and if one isn't found, we get an unexpected return which prints a message in our debug stating that we couldn't handle it.

So now we need to build our ExchangeKeysHandler class which we will create in the Operations folder under _Handlers:

using System.Collections;
using AegisBornCommon;

public class ExchangeKeysHandler : IOperationHandler
{
    public override void OnHandleMessage(Game gameLogic, OperationCode operationCode, int returnCode, Hashtable returnValues)
    {
        gameLogic.Peer.DeriveSharedKey((byte[])returnValues[(byte)ParameterCode.ServerKey]);
        gameLogic.NotifyKeysExchanged();
    }
}

This contains the code that was originally in the switch statement with the exception of the handled boolean to tell us we handled it.

Now we can handle messages in any of our IGameStates by creating a new class of type IOperationHandler and adding it to our list of handlers in our game state. Again, if you are interested, this is already checked into the github repository, which has now been split between the smartfox and photon versions. Links are in the right hand column.

Foray into Photon - Part 13 - Exchanging keys for encryption

So before we create our login, we need to take the time to handle encryption. There are several file modifications we must perform to pass along when we want to encrypt an operation. We begin with IGameState. The last function is SendOperation, this now changes to:


    void SendOperation(Game gameLogic, OperationCode operationCode, Hashtable parameter, bool sendReliable, byte channelId, bool encrypt);

Because of this change we have to modify Disconnected, WaitingForConnect, and Connected. Modify each of those classes so they also have bool encrypt at the end. Lastly we need to modify the code inside Connected.SendOperation to be the following:

    public void SendOperation(Game gameLogic, AegisBornCommon.OperationCode operationCode, System.Collections.Hashtable parameter, bool sendReliable, byte channelId, bool encrypt)
    {
        gameLogic.Peer.OpCustom((byte)operationCode, parameter, sendReliable, channelId, encrypt);
    }

Disconnected and WaitingForConnect only need the first change because they have no code in send operation.

With this change in place we need to modify Game.SendOp to pass along an encrypt:

    public void SendOp(OperationCode operationCode, Hashtable parameter, bool sendReliable, byte channelId, bool encrypt)
    {
        _stateStrategy.SendOperation(this, operationCode, parameter, sendReliable, channelId, encrypt);
    }

Now that we are set up to pass along encrypted messages, we need to exchange keys with the server. The best place for this would be in Game.SetConnected because it ensures we are connected to the server and we can handle the response in Connected's handling code. Here is the new function:

    public void SetConnected()
    {
        _stateStrategy = Connected.Instance;
        _listener.OnConnect(this);
        _peer.OpExchangeKeysForEncryption();
    }

    public delegate void AfterKeysExchanged();
    public AfterKeysExchanged afterKeysExchanged;

    public void NotifyKeysExchanged()
    {
        if (afterKeysExchanged != null)
        {
            afterKeysExchanged();
        }
        _listener.LogInfo(this, "Keys successfully exchanged");
    }

In Connected.cs we need to handle the response as it comes back from the server in OnOperationResponse:

        var handled = false;

        switch(operationCode)
        {
            case OperationCode.ExchangeKeysForEncryption:
                gameLogic.Peer.DeriveSharedKey((byte[])returnValues[(byte)ParameterCode.ServerKey]);
                gameLogic.NotifyKeysExchanged();
                handled = true;
                break;
        }

        if (!handled)
        {
            gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
        }

At this point you will notice we need to add a few things to AegisBornCommon. Lets do that now. In OperationCode.cs we need to add the ExchangeKeysForEncryption code:

        ///
        /// Code for exchanging keys using PhotonPeer.OpExchangeKeysForEncryption
        ///
        ExchangeKeysForEncryption = 95,

Then we need to add a couple values to the ParameterCode.cs file:

        ///
        /// Client key parameter used to establish secure communication.
        ///
        ClientKey = 16,

        ///
        /// Server key parameter used to establish secure communication.
        ///
        ServerKey = 17,

Once you recompile and ensure the new dll is in the Plugins directory in your Unity3d project the compile errors will disappear and you will be ready to implement the server portion of our key exchange.

First we need to create an operation to handle the exchange. I created a folder in AegisBorn called Operations and inside I created the EstablishSecureCommunicationOperation:

using AegisBornCommon;
using Photon.SocketServer;
using Photon.SocketServer.Rpc;

public class EstablishSecureCommunicationOperation : Operation
{
    public EstablishSecureCommunicationOperation(OperationRequest operationRequest)
        : base(operationRequest)
    {
    }

    ///
    /// Gets or sets the clients public key.
    ///
    [RequestParameter(Code = (short)ParameterCode.ClientKey, IsOptional = false)]
    public byte[] ClientKey { get; set; }

    ///
    /// Gets or sets the servers public key.
    ///
    [ResponseParameter(Code = (short)ParameterCode.ServerKey, IsOptional = false)]
    public byte[] ServerKey { get; set; }
}

This class' job is to take the information from the client, create a key, and send back a key to the client that the client will use when sending encrypted messages. Next we need to add our code to the dispatcher AegisBornPeer:

        [Operation(OperationCode = (byte)OperationCode.ExchangeKeysForEncryption)]
        public OperationResponse OperationKeyExchange(Peer peer, OperationRequest request)
        {
            var operation = new EstablishSecureCommunicationOperation(request);
            if (operation.IsValid == false)
            {
                return new OperationResponse(request, (int)ErrorCode.InvalidOperationParameter, operation.GetErrorMessage());
            }

            // initialize the peer to support encrytion
            operation.ServerKey = peer.PhotonPeer.InitializeEncryption(operation.ClientKey);

            // publish the servers public key to the client
            return operation.GetOperationResponse(0, "OK");
        }

Thanks to the work we did earlier, this operation is automatically handled. It initializes the encryption and returns an OK response to the client. At this time I also went ahead and added the following code to the ErrorCode.cs file which comes directly from the Exit Games Demo:

        ///
        /// The parameter out of range.
        ///
        ParameterOutOfRange = 51,

        ///
        /// The operation not supported.
        ///
        OperationNotSupported,

        ///
        /// The invalid operation parameter.
        ///
        InvalidOperationParameter,

        ///
        /// The invalid operation.
        ///
        InvalidOperation, 

With all this in place, when you click connect, you will get a message says Keys successfully exchanged. At this point we are ready to send an encrypted message to the server. You will notice that I used a switch statement in Connected.OnOperationResponse. We will be removing this and adding Operation Handlers in the near future. If you pull down the latest GitHub code, I already have them in place as well as what we will cover in the next blog, which is sending a login operation.

Tuesday, June 21, 2011

Foray into Photon - Part 12 - Seperating the Engine from the GUI

So today I hope to walk through the code necessary to log in. We are going to start with the client code as that is probably the easiest to work with. The first thing I want to do is separate the the IGameListener from the GUI login code. This is because we don't want to keep creating listeners that contain the same code. So lets go ahead and create a new script called MMOEngine and keep the Login script. Here is the MMOEngine code - it is basically all the code from login except the OnGUI code, the Start function is now Awake and the Game variable is now static:


using System;
using ExitGames.Client.Photon;
using UnityEngine;


public class MMOEngine : MonoBehaviour, IGameListener
{

    private static Game _engine;


    public static Game Engine
    {
        get { return _engine; }   
    }


    // Use this for initialization
    void Awake()
    {
        Application.runInBackground = true;
        _engine = new Game(this);
    }


    // Update is called once per frame
    void Update()
    {
        try
        {
            _engine.Update();
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }
    }


    ///


    /// The on application quit.
    ///

    public void OnApplicationQuit()
    {
        try
        {
            _engine.Disconnect();
        }
        catch (Exception e)
        {
            Debug.Log(e);
        }
    }


    #region Inherited Interfaces


    #region IGameListener
    public bool IsDebugLogEnabled
    {
        get { return true; }
    }


    public void LogDebug(Game game, string message)
    {
        Debug.Log(message);
    }


    public void LogError(Game game, string message)
    {
        Debug.Log(message);
    }


    public void LogError(Game game, Exception exception)
    {
        Debug.Log(exception.ToString());
    }


    public void LogInfo(Game game, string message)
    {
        Debug.Log(message);
    }


    public void OnConnect(Game game)
    {
        Debug.Log("connected");
    }


    public void OnDisconnect(Game game, StatusCode returnCode)
    {
        Debug.Log("disconnected");
    }


    #endregion


    #endregion
}

The new code for Login.cs is this:


using System.Collections;
using UnityEngine;
using ExitGames.Client.Photon;
using AegisBornCommon;


public class Login : MonoBehaviour
{

    private Game _engine;


    public void Start()
    {
        _engine = MMOEngine.Engine;
    }


    public void OnGUI()
    {
        if (GUI.Button(new Rect(100, 60, 100, 30), "Connect"))
        {
            var peer = new PhotonPeer(_engine, false);


            _engine.Initialize(peer, "localhost:5055", "AegisBorn");
        }
        if (GUI.Button(new Rect(200, 60, 100, 30), "Send Operation"))
        {
            _engine.SendOp((OperationCode)100, new Hashtable(), false, 0);
        }
        GUI.Label(new Rect(100, 100, 300, 300), _engine.State.ToString());
    }
}

Now that we have them separated, we want to create a new game object called MMOEngine and attach our script to it. This now makes it like our SmartFoxServer code where we had the connection handler get a connection to the SmartFoxConnection class.

Nothing too dramatic this time. Next time we will be pulling over our GUI code from our other project and sending an operation that asks to log in. Enjoy!

Saturday, June 18, 2011

Foray into Photon - Part 11 - WaitingForConnect and Connected

In our last post, the Disconnected state really didn't do anything more than report errors. It isn't very helpful in terms of doing anything for us. This time we will be creating our Waiting for Connect which again doesn't do anything different from Disconnected except call Service in the OnUpdate and let us know when we are connected. So lets take a look at it:


using AegisBornCommon;
using ExitGames.Client.Photon;


public class WaitingForConnect : IGameState
{


    public static readonly IGameState Instance = new WaitingForConnect();


    public GameState State
    {
        get { return GameState.WaitingForConnect; }
    }


    public void OnEventReceive(Game gameLogic, AegisBornCommon.EventCode eventCode, System.Collections.Hashtable eventData)
    {
        gameLogic.OnUnexpectedEventReceive(eventCode, eventData);
    }


    public void OnOperationReturn(Game gameLogic, AegisBornCommon.OperationCode operationCode, int returnCode, System.Collections.Hashtable returnValues)
    {
        gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
    }


    public void OnPeerStatusCallback(Game gameLogic, ExitGames.Client.Photon.StatusCode returnCode)
    {
        switch (returnCode)
        {
            case StatusCode.Connect:
                {
                    gameLogic.SetConnected();
                    break;
                }


            case StatusCode.Disconnect:
            case StatusCode.DisconnectByServer:
            case StatusCode.DisconnectByServerLogic:
            case StatusCode.DisconnectByServerUserLimit:
            case StatusCode.TimeoutDisconnect:
                {
                    gameLogic.SetDisconnected(returnCode);
                    break;
                }


            default:
                {
                    gameLogic.OnUnexpectedPhotonReturn((int)returnCode, OperationCode.Nil, null);
                    break;
                }
        }
    }


    public void OnUpdate(Game gameLogic)
    {
        gameLogic.Peer.Service();
    }


    public void SendOperation(Game gameLogic, AegisBornCommon.OperationCode operationCode, System.Collections.Hashtable parameter, bool sendReliable, byte channelId)
    {
    }
}

It uses 2 new functions we need to add to Game, SetConnected and SetDisconnected. These functions set the state of Game to Connected and Disconnected, respectively. Open up Game.cs and add them real quick:

    public void SetConnected()
    {
        _stateStrategy = Connected.Instance;
    }

    public void SetDisconnected(StatusCode returnCode)
    {
        _stateStrategy = Disconnected.Instance;
    }

Next we are going to add the last piece to begin our connect phase. To do this, go into Game.Initialize and add the following line before the Peer.Connect:

        _stateStrategy = WaitingForConnect.Instance;

Once this is done, Initialize will connect to the server. When the callback gets called because our status changes, it sets the strategy to Connected. The reason we are going with connected is because we need a state that is between waiting for a connection and being logged in. Here is the code for Connected:


using AegisBornCommon;
using ExitGames.Client.Photon;


public class Connected : IGameState
{


    public static readonly IGameState Instance = new Connected();


    public GameState State
    {
        get { return GameState.Connected; }
    }


    public void OnEventReceive(Game gameLogic, AegisBornCommon.EventCode eventCode, System.Collections.Hashtable eventData)
    {
        gameLogic.OnUnexpectedEventReceive(eventCode, eventData);
    }


    public void OnOperationReturn(Game gameLogic, AegisBornCommon.OperationCode operationCode, int returnCode, System.Collections.Hashtable returnValues)
    {
        gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
    }


    public void OnPeerStatusCallback(Game gameLogic, ExitGames.Client.Photon.StatusCode returnCode)
    {
        switch (returnCode)
        {
            case StatusCode.Disconnect:
            case StatusCode.DisconnectByServer:
            case StatusCode.DisconnectByServerLogic:
            case StatusCode.DisconnectByServerUserLimit:
            case StatusCode.TimeoutDisconnect:
                {
                    gameLogic.SetDisconnected(returnCode);
                    break;
                }


            default:
                {
                    gameLogic.OnUnexpectedPhotonReturn((int)returnCode, OperationCode.Nil, null);
                    break;
                }
        }
    }


    public void OnUpdate(Game gameLogic)
    {
        gameLogic.Peer.Service();
    }


    public void SendOperation(Game gameLogic, AegisBornCommon.OperationCode operationCode, System.Collections.Hashtable parameter, bool sendReliable, byte channelId)
    {
        gameLogic.Peer.OpCustom((byte)operationCode, parameter, sendReliable, channelId);
    }
}


I made a couple minor modifications to my code in Game.cs and Login.cs. I removed the status and added a new property that adds the state. I added this code to Game.cs:


    public GameState State
    {
        get
        {
            return _stateStrategy.State;
        }
    }

Then I went into Login.cs and modified the OnGUI line for the label and made it:

        GUI.Label(new Rect(100, 100, 300, 300), _engine.State.ToString());

So now it uses the engine's state to display if we are disconnected, waiting for connection, or connected. If you press the operation button it sends operation 100 like we did before and we get an unexpected return because we didn't implement that portion into the connect class.

With those changes, we are caught up to my check in. This covers the majority of the functionality of the demo in terms of patterns and design. From here on out we are going to modify how things are done to rebuild what we built in SmartFoxServer by making the server look up our user data and allow logins and follow through character select. Next time we will look at modifying Connected to pass our login data and begin getting the operation on the server side. See you then!

Friday, June 17, 2011

Foray into Photon - Part 10 - Creating our Disconnected state and wiring it into the game

The first thing we need to do is set up our Game.cs calls to our state so the state can handle them, then we will create our disconnected state and tell Game.cs to use it going forward. I have updated all the IPhotonPeerListener functions. They look exactly like the ones Exit Games uses only without the debug logging:


    public void DebugReturn(DebugLevel level, string message)
    {
        if (this.listener.IsDebugLogEnabled)
        {
            this.listener.LogDebug(this, string.Concat(this.avatar.Id, ": ", debug));
        }
    }


    public void EventAction(byte eventCode, Hashtable photonEvent)
    {
        stateStrategy.OnEventReceive(this, (EventCode)eventCode, photonEvent);
    }


    public void OperationResult(byte operationCode, int returnCode, Hashtable returnValues, short invocId)
    {
        try
        {
            stateStrategy.OnOperationReturn(this, (OperationCode)operationCode, returnCode, returnValues);
        }
        catch (Exception e)
        {
            listener.LogError(this, e);
        }
    }


    public void PeerStatusCallback(StatusCode statusCode)
    {
        try
        {
            stateStrategy.OnPeerStatusCallback(this, returnCode);
        }
        catch (Exception e)
        {
            listener.LogError(this, e);
        }
    }


    public void Update()
    {
        stateStrategy.OnUpdate(this);
    }


    public void SendOp(OperationCode operationCode, Hashtable parameter, bool sendReliable, byte channelId)
    {
        stateStrategy.SendOperation(this, operationCode, parameter, sendReliable, channelId);
    }


As you can see, each place we normally would have done something because we received a message through the peer, we now pass on to the state. The state will then determine if we need to process the message and if so, how.

Now to create our Disconnect state. It is a very simple state, it does nothing when asked to send an operation or to update. The other functions all call the error logs we created earlier.


using AegisBornCommon;


public class Disconnected : IGameState
{


    public static readonly IGameState Instance = new Disconnected();


    public GameState State
    {
        get { return GameState.Disconnected; }
    }


    public void OnEventReceive(Game gameLogic, AegisBornCommon.EventCode eventCode, System.Collections.Hashtable eventData)
    {
        gameLogic.OnUnexpectedEventReceive(eventCode, eventData);
    }


    public void OnOperationReturn(Game gameLogic, AegisBornCommon.OperationCode operationCode, int returnCode, System.Collections.Hashtable returnValues)
    {
        gameLogic.OnUnexpectedPhotonReturn(returnCode, operationCode, returnValues);
    }


    public void OnPeerStatusCallback(Game gameLogic, ExitGames.Client.Photon.StatusCode returnCode)
    {
        gameLogic.OnUnexpectedPhotonReturn((int)returnCode, OperationCode.Nil, null);
    }


    public void OnUpdate(Game gameLogic)
    {
        // Do nothing on updates because we are disconnected.
    }


    public void SendOperation(Game gameLogic, AegisBornCommon.OperationCode operationCode, System.Collections.Hashtable parameter, bool sendReliable, byte channelId)
    {
        // Do not send operations because we are disconnected.
    }
}


So now we have our disconnected state and we are almost back to where we were. It feels like a ton of work just to be right back where we started, but this is a very good setup. The time we take setting up this project the first time, means less time will be spent when we need to add new states or messages.

Next up we will get WaitingForConnect and CharacterSelect states up and running!