Page 1 of 2 12 LastLast
Results 1 to 10 of 18
  1. #1
    Join Date
    Feb 2014
    Posts
    272

    Beyond the MMO Videos

    This post goes beyond Nelson's videos to fix the bugs left in the project and to try and finish chapter 12 without Nelson.

    Log4Net upgrades
    The first thing I noticed after the last video was that Log4Net was complaining about it still being setup for a previous factory.
    Fortunately the fixes are rather simple. They are based on the Photon LoadBalancing solution.
    If you use this, you will need to add project references to ExitGamesLogging.Log4Net which you might already have in your lib folder alongside log4net.dll. If not, copy it in there from the Photon server SDK. This applies to the Server.Master and the Server.Region projects.

    1. Edit BuzzMMO.Server.Master/Application.cs
    We are replacing the private Log field as well as moving the Logging setup out to its own method.
    I am quoting the entire file even though it will be changed later for other Photon4 fixes.

    Code:
    using System.IO;
    using BuzzMMO.Base;
    using Photon.SocketServer;
    using log4net;
    using log4net.Config;
    
    namespace MMO.Server.Master
    {
        public class Application : ApplicationBase
        {
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly MasterServerContext _application;
    
            public Application()
            {
                _application = new MasterServerContext(new SimpleSerializer());
            }
    
            protected override Photon.SocketServer.PeerBase CreatePeer(InitRequest initRequest)
            {
                return new Peer(_application, initRequest);
            }
    
            protected override void Setup()
            {
                InitLogging();
    
                Log.Info("HIGHLIGHT - Master Server Started");
            }
    
            protected virtual void InitLogging()
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = "Master";
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }
    2. Edit BuzzMMO.Server.Region/Application.cs
    Only the relevant bits are quoted below.

    Code:
    using System;
    using System.IO;
    using System.Net;
    using BuzzMMO.Base;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    using ExitGames.Concurrency.Fibers;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Region
    {
        public class Application : ApplicationBase
        {
            private const int ServerConnectTimeout = 5000;          //in mS
    
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly GameServerContext _application;
            private readonly IFiber _fiber;
    
     ................stuff left out.....................
    
            protected override void Setup()
            {
                var gameServerConfig = _application.Config.GameServer;
                using (var database = new MMODatabaseContext())
                {
                    var server = database.GameServers.Find(gameServerConfig.UniqueId);
                    if (server != null)
                    {
                        server.Name = gameServerConfig.Name;
                    }
                    else
                    {
                        server = new GameServerEntity
                        {
                            Id = gameServerConfig.UniqueId,
                            Name = gameServerConfig.Name,
                            CreatedAt = DateTime.UtcNow
                        };
    
                        database.GameServers.Add(server);
                    }
    
                    database.SaveChanges();
                }
    
                InitLogging(gameServerConfig.Name);
    
                ConnectToMasterServer();
            }
    
    ................stuff left out.....................
    
            //Log4Net setup on Region server
            protected virtual void InitLogging(string name)
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = name;
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }

    Note:
    Since we only have 2 log files to set up (BuzzMMO.Server.Game.App.Log, and BuzzMMO.Server.Master.App.Log) we don't have to edit any more logging files.
    Last edited by oldngrey; 07-23-2017 at 03:46 AM.

  2. #2
    Join Date
    Feb 2014
    Posts
    272
    Partial Upgrade to Photon 4 including some re-mapping of the Photon Ports

    Following along with am385's Photon4 upgrade (https://www.3dbuzz.com/forum//threads/204607)

    In the past we were always connecting to the master server on port 5055.
    However, since we now have to separate out the incoming connection requests we will need to have multiple ports that the master server needs to monitor for incoming connection requests.

    Here is a possible way to separate out the ports. Use this as a guide to what is connecting to stuff:
    5055 - master server listens to client on this port
    5056 - master server listens to game1 on this port
    5057 - master server listens to game2 on this port
    5058 - game1 listens on this port
    5059 - game2 listens on this port

    So now we should write our photon config files to match:

    1. Edit your PhotonControl.exe.config file in your bin_Win64 folder where Photon runs.
    Here is the entire file. You only need to find and edit the lines describing the Instances:
    Code:
    <?xml version="1.0" encoding="utf-8" ?>
    <configuration>
        
        <configSections>
            <sectionGroup name="userSettings" type="System.Configuration.UserSettingsGroup, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
                <section name="PhotonControl.PhotonControlSettings" type="System.Configuration.ClientSettingsSection, System, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" allowExeDefinition="MachineToLocalUser" requirePermission="false" />
            </sectionGroup>
        </configSections>
    
        <startup>
          <supportedRuntime version="v4.5"/>
          <supportedRuntime version="v4.0"/>
          <supportedRuntime version="v2.0.50727"/>
        </startup>
    
        <userSettings>
          <PhotonControl.PhotonControlSettings>
            <setting name="PhotonWorkingDirectory" serializeAs="String">
              <value />
            </setting>
            <setting name="LogManCreateOptions" serializeAs="String">
              <value>-si 01:00 -v mmddhhmm -cf "..\bin_tools\perfmon\logman.config.txt"</value>
            </setting>
            <setting name="LogManSetName" serializeAs="String">
              <value>photon_perf_log</value>
            </setting>
            <setting name="Compact" serializeAs="String">
              <value>False</value>
            </setting>
            <setting name="TestClientArguments" serializeAs="String">
              <value>Master</value>
            </setting>
            <setting name="TestClientPaths" serializeAs="String">
              <value>..\bin_tools\stardust.client\Photon.StarDust.Client.exe</value>
            </setting>
            <setting name="Instances" serializeAs="String">
              <value>BuzzMMOMaster,GameInstance1,GameInstance2,Default</value>
            </setting>
            <!--
            <setting name="GameServerConfigPaths" serializeAs="String">
              <value>..\Loadbalancing\GameServer\bin\Photon.LoadBalancing.dll.config</value>
            </setting>
            -->
            <setting name="ReplaceFirewallRules" serializeAs="String">
              <value>True</value>
            </setting>
            <setting name="UseCmdFileToOpenLogs" serializeAs="String">
              <value>False</value>
            </setting>
          </PhotonControl.PhotonControlSettings>
        </userSettings>
    </configuration>

    2. Now we can edit the PhotonServer.config file in your bin_Win64 folder where Photon runs.
    I mainly run the individual instances rather than ask it to run default, but that's your call.
    Code:
    <?xml version="1.0" encoding="Windows-1252"?>
    
    <Configuration>
      <!-- Multiple instances are supported. Each instance has its own node in the config file. -->
    
      
      <MasterServer
        MaxMessageSize="512000"
        MaxQueuedDataPerPeer="512000"
        PerPeerMaxReliableDataInTransit="51200"
        PerPeerTransmitRateLimitKBSec="256"
        PerPeerTransmitRatePeriodMilliseconds="200"
        MinimumTimeout="5000"
        MaximumTimeout="30000">
        
        <UDPListeners>
          <!-- listen to clients -->
          <UDPListener
            IPAddress="0.0.0.0"
            Port="5055">
          </UDPListener>
          <!-- listen to game1 -->
          <UDPListener
            IPAddress="0.0.0.0"
            Port="5056">
          </UDPListener>
          <!-- listen to game2 -->
          <UDPListener
            IPAddress="0.0.0.0"
            Port="5057">
          </UDPListener>
        </UDPListeners>
    
        <Runtime
          Assembly="PhotonHostRuntime, Culture=neutral"
          Type="PhotonHostRuntime.PhotonDomainManager"
          UnhandledExceptionPolicy="Ignore">
        </Runtime>
        
        <Applications Default="BuzzMMOMaster">
          <Application
            Name="BuzzMMOMaster"
            BaseDirectory="BuzzMMO/Master"
            Assembly="BuzzMMO.Server.Master"
            Type="BuzzMMO.Server.Master.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" 
          />
        </Applications>
      </MasterServer>   
        
     <GameInstance1
        MaxMessageSize="512000"
        MaxQueuedDataPerPeer="512000"
        PerPeerMaxReliableDataInTransit="51200"
        PerPeerTransmitRateLimitKBSec="256"
        PerPeerTransmitRatePeriodMilliseconds="200"
        MinimumTimeout="5000"
        MaximumTimeout="30000">
        
        <UDPListeners>
          <UDPListener
            IPAddress="0.0.0.0"
            Port="5058">
          </UDPListener>
        </UDPListeners>
        
        <Runtime
          Assembly="PhotonHostRuntime, Culture=neutral"
          Type="PhotonHostRuntime.PhotonDomainManager"
          UnhandledExceptionPolicy="Ignore">
        </Runtime>
        
        <Applications Default="BuzzMMOGame1">
          <Application
            Name="BuzzMMOGame1"
            BaseDirectory="BuzzMMO/Game1"
            Assembly="BuzzMMO.Server.Region"
            Type="BuzzMMO.Server.Region.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" 
          />
        </Applications>
      </GameInstance1>
      
      <GameInstance2
        MaxMessageSize="512000"
        MaxQueuedDataPerPeer="512000"
        PerPeerMaxReliableDataInTransit="51200"
        PerPeerTransmitRateLimitKBSec="256"
        PerPeerTransmitRatePeriodMilliseconds="200"
        MinimumTimeout="5000"
        MaximumTimeout="30000">
        
        <UDPListeners>
          <UDPListener
            IPAddress="0.0.0.0"
            Port="5059">
          </UDPListener>
        </UDPListeners>
        
        <Runtime
          Assembly="PhotonHostRuntime, Culture=neutral"
          Type="PhotonHostRuntime.PhotonDomainManager"
          UnhandledExceptionPolicy="Ignore">
        </Runtime>
        
        <Applications Default="BuzzMMOGame2">
          <Application
            Name="BuzzMMOGame2"
            BaseDirectory="BuzzMMO/Game2"
            Assembly="BuzzMMO.Server.Region"
            Type="BuzzMMO.Server.Region.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" 
          />
        </Applications>
      </GameInstance2>
     
      <Default
        MaxMessageSize="512000"
        MaxQueuedDataPerPeer="512000"
        PerPeerMaxReliableDataInTransit="51200"
        PerPeerTransmitRateLimitKBSec="256"
        PerPeerTransmitRatePeriodMilliseconds="200"
        MinimumTimeout="5000"
        MaximumTimeout="30000">
        
         <!--0.0.0.0 opens listeners on all available IPs. Machines with multiple IPs should define the correct one here.
         Port 5055 is Photon's default for UDP connections.-->
        <UDPListeners>
          <UDPListener
            OverrideApplication="BuzzMMOMaster"
            IPAddress="0.0.0.0"
            Port="5055">
          </UDPListener>
          <UDPListener
            OverrideApplication="BuzzMMOMaster"
            IPAddress="0.0.0.0"
            Port="5056">
          </UDPListener>
          <UDPListener
            OverrideApplication="BuzzMMOMaster"
            IPAddress="0.0.0.0"
            Port="5057">
          </UDPListener>
          <UDPListener
            OverrideApplication="BuzzMMOGame1"        
            IPAddress="0.0.0.0"
            Port="5058">
          </UDPListener>
          <UDPListener
            OverrideApplication="BuzzMMOGame2"        
            IPAddress="0.0.0.0"
            Port="5059">
          </UDPListener>
        </UDPListeners>
    
        <!--  Defines the Photon Runtime Assembly to use. -->
        <Runtime
          Assembly="PhotonHostRuntime, Culture=neutral"
          Type="PhotonHostRuntime.PhotonDomainManager"
          UnhandledExceptionPolicy="Ignore">
        </Runtime>
        
        <Applications Default="BuzzMMOMaster">
          <Application
            Name="BuzzMMOMaster"
            BaseDirectory="BuzzMMO/Master"
            Assembly="BuzzMMO.Server.Master"
            Type="BuzzMMO.Server.Master.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" />
          
          <Application
            Name="BuzzMMOGame1"
            BaseDirectory="BuzzMMO/Game1"
            Assembly="BuzzMMO.Server.Region"
            Type="BuzzMMO.Server.Region.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" />
    
          <Application
            Name="BuzzMMOGame2"
            BaseDirectory="BuzzMMO/Game2"
            Assembly="BuzzMMO.Server.Region"
            Type="BuzzMMO.Server.Region.Application"
            ForceAutoRestart="true"
            WatchFiles="dll;config"
            ExcludeFiles="log4net.config" />
        </Applications>
      </Default>  
    </Configuration>
    3. Edit BuzzMMO.Server.Region.Config/Game1.Debug.json
    Code:
    {
      "MasterServer": {
        "UdpConnection": {
          "Address": "127.0.0.1:5056",
          "ApplicationName": "BuzzMMOMaster"
        }
      },
      "GameServer": {
        "Name": "Game Server 1",
        "UdpConnection": {
          "Address": "127.0.0.1:5058",
          "ApplicationName": "BuzzMMOGame1"
        },
        "UniqueId": "156BD28C-759E-4D14-8118-6131581F009E"
      }
    }
    4. Edit BuzzMMO.Server.Region.Config/Game2.Debug.json
    Code:
    {
      "MasterServer": {
        "UdpConnection": {
          "Address": "127.0.0.1:5057",
          "ApplicationName": "MMOMaster"
        }
      },
      "GameServer": {
        "Name": "Game Server 2",
        "UdpConnection": {
          "Address": "127.0.0.1:5059",
          "ApplicationName": "MMOGame2"
        },
        "UniqueId": "6612BA17-13A8-4AD5-88BC-E5C81A260D24"
      }
    }
    5. Edit the infamous BuzzMMO.Server.Region/MasterServerPeer.cs
    Nelson was close, but he didn't realize the importance of the OnConnectionEstablished Overrride method.
    Here is the entire file:
    Code:
    using System;
    using System.Collections.Generic;
    using Autofac;
    using BuzzMMO.Base;
    using BuzzMMO.Base.Async;
    using BuzzMMO.Client.Infrastructure;
    using BuzzMMO.Client.Systems;
    using log4net;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using PhotonHostRuntimeInterfaces;
    
    namespace BuzzMMO.Server.Region
    {
        /// <summary>
        /// Halfway between a server and a client because both servers and clients need to use this. Cannot do multiple inheritence, so is kludgy
        /// 
        /// This is also part of am385's quick and dirty photon 4 migration to get us connected for now
        /// </summary>
        public class MasterServerPeer : OutboundS2SPeer, ISystemFactory, IClientTransport
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(MasterServerPeer));
    
            public event Action OnDisconnected;
    
            private readonly GameServerContext _application;
            private readonly MMO.Client.Systems.ClientSystems _clientSystems;
            private readonly ComponentMap _componentMap;
            private readonly ILifetimeScope _scope;
            private readonly CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>> _callbacks;
            private readonly Action<EventCode, Dictionary<byte, object>>[] _eventHandlers;
    
            protected readonly HashSet<IClientTransportListener> Listeners;
    
            public MasterServerPeer(Application app, GameServerContext application) : base(app)
            {
                _callbacks = new CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>>();
                _eventHandlers = new Action<EventCode, Dictionary<byte, object>>[byte.MaxValue + 1];
                Listeners = new HashSet<IClientTransportListener>();
    
                _application = application;
                _scope = application.Container.BeginLifetimeScope();
                _componentMap = new ComponentMap();
    
                var operationWriter = new SystemsOperationWriter(_application.Serializer, this);
                _clientSystems = new Client.Systems.ClientSystems(_componentMap, this, operationWriter);
                AddEventReader(new SystemsEventReader(_application.Serializer, _clientSystems, this));
    
                //Removed by am385's fix
                //SendOperation(
                //    OperationCode.InitContext,
                //    new Dictionary<byte, object>
                //    {
                //        [(byte)OperationParameter.ContextType] = ContextType.Region
                //    });
    
                application.OnDisposed += OnApplicationDisposed;
            }
    
            private void OnApplicationDisposed()
            {
                Disconnect();
            }
    
            protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
            {
            }
    
            protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
            {
                Log.Info($"MasterServerPeer Disconnect: {reasonCode} / {reasonDetail}");
                OnDisconnected?.Invoke();
                _scope.Dispose();
                _application.OnDisposed -= OnApplicationDisposed;
            }
    
            protected override void OnEvent(IEventData eventData, SendParameters sendParameters)
            {
                HandleEvent((EventCode)eventData.Code, eventData.Parameters);
            }
    
            protected override void OnOperationResponse(OperationResponse operationResponse, SendParameters sendParameters)
            {
                HandleSystemCallback((OperationCode)operationResponse.OperationCode, operationResponse.Parameters);
            }
    
            ISystemBase ISystemFactory.CreateSystem(Type interfaceType, Func<Type, object> proxyFactory, out Type concreteType)
            {
                var registeredSystem = _application.SystemTypeRegistry.GetSystemFromClientInterfaceType(interfaceType);
                var instance = _scope.Resolve(registeredSystem.ConcreteType);
                var proxy = proxyFactory(registeredSystem.ServerInterfaceType);
                ((IMasterSystemBase)instance).SetContext(_application, proxy);
    
                concreteType = registeredSystem.ConcreteType;
                return (ISystemBase)instance;
            }
    
            public void AddEventReader(IEventReaderModule eventReader)
            {
                foreach (var registration in eventReader.GetRegistration())
                {
                    if (_eventHandlers[(int)registration.Code] != null)
                    {
                        var oldHandler = _eventHandlers[(int)registration.Code];
                        var action = registration.Action;
    
                        _eventHandlers[(int)registration.Code] = (code, parameters) =>
                        {
                            oldHandler(code, parameters);
                            action(code, parameters);
                        };
                    }
                    else
                    {
                        _eventHandlers[(int)registration.Code] = registration.Action;
                    }
                }
            }
    
            public void SendOperation(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationInternal(code, parameters);
            }
    
            public void SendOperation(OperationCode code, Dictionary<byte, object> parameters, Action<OperationCode, Dictionary<byte, object>> onResponse)
            {
                parameters[(byte)OperationParameter.SystemInvokeId] = _callbacks.RegisterCallback(onResponse);
                SendOperationInternal(code, parameters);
            }
    
            public void AddListener(IClientTransportListener listener)
            {
                Listeners.Add(listener);
            }
    
            Deferred IClientTransport.Connect()
            {
                return Deferred.Success();
            }
    
            void IClientTransport.Service()
            {
            }
    
            Deferred IClientTransport.Disconnect()
            {
                return Deferred.Success();
            }
    
            protected void HandleSystemCallback(OperationCode code, Dictionary<byte, object> parameters)
            {
                if (code != OperationCode.SendSystemResponse)
                    throw new ArgumentException($"Code {code} is not valid for handling responses", nameof(code));
    
                var methodInvokeId = (byte)parameters[(byte)OperationParameter.SystemInvokeId];
                var callback = _callbacks.GetCallback(methodInvokeId);
                callback(code, parameters);
            }
    
            protected void HandleEvent(EventCode code, Dictionary<byte, object> parameters)
            {
                var handler = _eventHandlers[(byte)code];
                if (handler == null)
                    throw new InvalidOperationException($"Handler for event code {code} was not registered");
    
                handler(code, parameters);
            }
    
            protected void SendOperationInternal(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationRequest(new OperationRequest((byte)code, parameters), new SendParameters
                {
                    Unreliable = false
                });
            }
    
            protected override void OnConnectionEstablished(object responseObject)                                  //added for photon 4
            {
                // am385's code Here is where we should do that InitContext request. Code moved from the constructor
                SendOperation(
                    OperationCode.InitContext, 
                    new Dictionary<byte, object>
                    {
                        [(byte)OperationParameter.ContextType] = ContextType.Region
                    });
            }
    
            protected override void OnConnectionFailed(int errorCode, string errorMessage)                          //added for photon 4
            {
            }
        }
    }
    6. Edit BuzzMMO.Server.Region/Application.cs.
    It's only a line or 2 to change, but here is the entire file:
    Code:
    using System;
    using System.IO;
    using System.Net;
    using BuzzMMO.Base;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    using ExitGames.Concurrency.Fibers;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Region
    {
        /// <summary>
        /// Quick and dirty photon 4 migration by am385
        /// </summary>
        public class Application : ApplicationBase
        {
            private const int ServerConnectTimeout = 5000;          //in mS
    
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly GameServerContext _application;
            private readonly IFiber _fiber;
    
            public Application()
            {
                _fiber = new ThreadFiber();
                _fiber.Start();
    
                var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "Config.json")));
                _application = new GameServerContext(new SimpleSerializer(), config);
            }
    
            protected override PeerBase CreatePeer(InitRequest initRequest)
            {
                return new Peer(_application, initRequest);
            }
    
            protected override void Setup()
            {
                var gameServerConfig = _application.Config.GameServer;
                using (var database = new MMODatabaseContext())
                {
                    var server = database.GameServers.Find(gameServerConfig.UniqueId);
                    if (server != null)
                    {
                        server.Name = gameServerConfig.Name;
                    }
                    else
                    {
                        server = new GameServerEntity
                        {
                            Id = gameServerConfig.UniqueId,
                            Name = gameServerConfig.Name,
                            CreatedAt = DateTime.UtcNow
                        };
    
                        database.GameServers.Add(server);
                    }
    
                    database.SaveChanges();
                }
    
                InitLogging(gameServerConfig.Name);
    
                ConnectToMasterServer();
            }
    
            private void ConnectToMasterServer()
            {
                IPEndPoint endpoint;
                try
                {
                    var addressParts = _application.Config.MasterServer.UdpConnection.Address.Split(':');
                    endpoint = new IPEndPoint(IPAddress.Parse(addressParts[0]), int.Parse(addressParts[1]));        //assumes ip address in config.json, not dns name
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("Cannot parse Config.MasterServer.UdpConnection.Address (please specify it as <ip>:<port>", e);
                }
    
                Log.Info($"Connecting to Master Server: {_application.Config.MasterServer.UdpConnection}");
                var peer = new MasterServerPeer(this, _application); // Create a new instance of MasterServerPeer 
                if (peer.ConnectToServerUdp(endpoint, _application.Config.MasterServer.UdpConnection.ApplicationName, null, 1, null)) // If the connection succeeds 
                {
                    Log.Info($"Connected to master server {_application.Config.MasterServer.UdpConnection}"); // Log success 
                    peer.OnDisconnected += () => _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout); // attach our OnDisconnected event handler / delegate
                }
            }
    
            //removed by am385
            //protected override S2SPeerBase CreateServerPeer(InitResponse initResponse, object state)                                    //nelson's v4 photon code
            ////protected override ServerPeerBase CreateServerPeer(InitResponse initResponse, object state)                               //v3 photon code
            //{
            //    Log.Info($"Connected to master server {_application.Config.MasterServer.UdpConnection}");
            //    var peer = new MasterServerPeer(this, _application);                                                                    //nelson's v4 photon code
            //    //var peer = new MasterServerPeer(initResponse, _application);     
            //    peer.OnDisconnected += () => _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout);
            //    return peer;
            //}
    
            protected override void OnServerConnectionFailed(int errorCode, string errorMessage, object state)
            {
                Log.Error($"Could not connect to master server {_application.Config.MasterServer.UdpConnection}: {errorCode} / {errorMessage}");
                _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout);
            }
    
            //Log4Net setup on Region server
            protected virtual void InitLogging(string name)
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = name;
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }
    7. Create BuzzMMO.Server.Master/RegionPeer.cs
    This is a temporary file to connect region servers. It will be completely changed later, but for now it's almost identical to Peer.cs
    Code:
    using BuzzMMO.Base;
    using BuzzMMO.Server.Master.Systems;
    using BuzzMMO.Server.Master.Systems.Region;
    using Photon.SocketServer;
    
    namespace BuzzMMO.Server.Master
    {
        //server connection. clients use peer
        //TODO: This is not correct. Re-write to base it on MasterServerPeer in the region project
        public class RegionPeer : MMOPeerBase<ClientContext>
        {
            private readonly MasterServerContext _application;
    
            public RegionPeer(MasterServerContext application, InitRequest initRequest) : base(initRequest)
            {
                _application = application;
            }
    
            protected override ClientContext CreateContext(ContextType type, OperationRequest request)
            {
                //todo: we should do some authentication - eg get token from region server
                if (type == ContextType.Region)
                {
                    var context = new RegionServerContext(_application, this);
                    context.Systems.Create<GameInstanceSystem>();
                    return context;
                }
    
                return null;
            }
        }
    }
    8. Edit BuzzMMO.Server.Master/Application.cs. The only change is to switch the incoming request between client and region servers. We will hard-code the ports for now, but will change that in a later post.
    Code:
    using System.IO;
    using BuzzMMO.Base;
    using Photon.SocketServer;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Master
    {
        public class Application : ApplicationBase
        {
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly MasterServerContext _application;
    
            public Application()
            {
                _application = new MasterServerContext(new SimpleSerializer());
            }
    
            protected override Photon.SocketServer.PeerBase CreatePeer(InitRequest initRequest)
            {
                if (initRequest.LocalPort == 5055)
                    return new Peer(_application, initRequest);
                if (initRequest.LocalPort == 5056 || initRequest.LocalPort == 5057)
                    return new RegionPeer(_application, initRequest);
    
                Log.Error($"Unhandled incoming connection request on port {initRequest.LocalIP}");
                return null;
            }
    
            protected override void Setup()
            {
                InitLogging();
    
                Log.Info("HIGHLIGHT - Master Server Started");
            }
    
            protected virtual void InitLogging()
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = "Master";
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }

    Now the client should work as it did before the Photon 4 upgrade.
    This isn't the end of the Photon upgrades, as the RegionPeer.cs is a temporary file to get us working.
    Last edited by oldngrey; 07-23-2017 at 07:58 PM.

  3. #3
    Join Date
    Feb 2014
    Posts
    272
    Recording the User's IP address and Last Login date/time when logging in

    I decided that having the user's IP address recorded in the useringames table was both too hard and kinda useless.
    Instead the place I wanted the user's IP address recorded was in the users table. While I was at it, I decided to also include a new column recording when they last logged in to either the website or the game lobby.
    Also the Website admin Index page now shows their last login and last recorded ip address.
    Because the useringames table has an index to the id of the user, the game could easily access the user's ip address anyway, so I also dropped the ipaddress from the useringames table.

    1. First up make the changes to the user table and data/entities
    Edit BuzzMMO.Data.Entities/User.cs

    Here is the entire file, the only change is in the lines adding in LastLogin and LastIp

    Code:
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.DataAnnotations;
    using System.Security.Cryptography;
    
    namespace BuzzMMO.Data.Entities
    {
        public class User
        {
            public int Id { get; set; }
    
            [Required, MaxLength(128)]
            public string Username { get; set; }
    
            [Required, MaxLength(128)]
            public string Password { get; set; }
    
            [Required, MaxLength(128)]
            public string Email { get; set; }
            
            [MaxLength(64)]
            public string VerifyEmailToken { get; set; }
    
            [MaxLength(64)]
            public string ResetPasswordToken { get; set; }
    
            public DateTime? ResetPasswordTokenExpiresAt { get; set; }
    
            public DateTime? LastLogin { get; set; }
    
            [MaxLength(64)]
            public string LastIp { get; set; }
    
            public virtual ICollection<Role> Roles { get; set; }
    
            private const int WorkFactor = 13;
    
            public void SetPassword(string password)
            {
                Password = BCrypt.Net.BCrypt.HashPassword(password, WorkFactor);
            }
    
            public static void FakeHash()
            {
                BCrypt.Net.BCrypt.HashPassword("", WorkFactor);
            }
    
            public bool CheckPassword(string password)
            {
                return BCrypt.Net.BCrypt.Verify(password, Password);
            }
    
            public void GenerateEmailVerificationToken()
            {
                VerifyEmailToken = GenerateRandomToken();
            }
    
            public void GenerateResetPasswordToken()
            {
                ResetPasswordToken = GenerateRandomToken();
                ResetPasswordTokenExpiresAt = DateTime.UtcNow.AddMinutes(30);
            }
    
            public void ClearResetPasswordToken()
            {
                ResetPasswordToken = null;
                ResetPasswordTokenExpiresAt = null;
            }
    
            private string GenerateRandomToken()
            {
                using (var random = new RNGCryptoServiceProvider())
                {
                    var buffer = new byte[32];
                    random.GetNonZeroBytes(buffer);
                    return Convert.ToBase64String(buffer);
                }
            }
        }
    }
    You can then check that these columns are nullable after going to the PackageManagerConsole and typing in something like:
    Code:
    add-migration AddIpAndLastLoinToUser
    Then check the migration file resembles this:

    Code:
    namespace BuzzMMO.Data.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;
        
        public partial class AddIpAndLastLoinToUser : DbMigration
        {
            public override void Up()
            {
                AddColumn("dbo.Users", "LastLogin", c => c.DateTime(nullable: true, precision: 0));
                AddColumn("dbo.Users", "LastIp", c => c.String(nullable: true, maxLength: 64, storeType: "nvarchar"));
            }
            
            public override void Down()
            {
                DropColumn("Users", "LastIp");
                DropColumn("Users", "LastLogin");
            }
        }
    }
    If all went well, type
    Code:
    update-database
    When looking at the table in the database the users table should have the LastLogin and LastIp columns with the "Not Nullable" field unticked.


    2. Edit BuzzMMO.Data.Services/GameEntityService.cs to remove Nelson's dummy ip address
    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Migrations;
    using System.IO;
    using System.Linq;
    using Autofac;
    using BuzzMMO.Base;
    using BuzzMMO.Base.Async;
    using BuzzMMO.Base.Components.Systems.Master;
    using BuzzMMO.Base.Extensions;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    
    namespace BuzzMMO.Server
    {
        public abstract class ClientContext : IDisposable
        {
            private volatile bool _isConnected;
    
            public bool IsConnected => _isConnected;
            public event Action<ClientContext> Disconnected;
    
            public ServerContext Application { get; private set; }
            public ClientSystems Systems { get; private set; }
            public ILifetimeScope ClientScope { get; private set; }
            public IServerTransport Transport { get; private set; }
            public ComponentMap SystemsComponentMap { get; private set; }
    
            public UserDetails UserDetails { get; private set; }
            public bool IsAuthenticated => UserDetails != null;
    
            protected ClientContext(ServerContext application, ComponentMap systemsComponentMap, IServerTransport transport)
            {
                _isConnected = true;
    
                Application = application;
                Systems = new ClientSystems(this);
                ClientScope = application.Container.BeginLifetimeScope(this);
                SystemsComponentMap = systemsComponentMap;
                Transport = transport;
    
                transport.SendData(application.Events.SyncComponentMap(EventCode.SyncSystemsComponentMap, systemsComponentMap));
            }
    
            public void Login(User user, string requestIp)
            {
                if (IsAuthenticated)
                    throw new RpcException("Client already authenticated");
    
                // save the user's LastLogin and ip associated with the token or just an ip called debug if none
                using (var database = new MMODatabaseContext())
                {
                    var foundUser = database.Users.Find(user.Id);
                    if (foundUser == null)
                    {
                        throw new RpcException("User not found when logging in to game");
                    }
    
                    foundUser.LastLogin = DateTime.UtcNow;
                    foundUser.LastIp = requestIp;
                    database.Users.AddOrUpdate();
                    database.SaveChanges();
                }
    
                UserDetails = new UserDetails(
                    user.Id, 
                    user.Username, 
                    new HashSet<string>(user.Roles.Select(r => r.Name)));
            }
    
            public virtual void OnOperationRequest(OperationCode code, Dictionary<byte, object> parameters)
            {
                InvokeMethodOnSystem((byte)code, parameters);
            }
    
            protected void InvokeMethodOnSystem(byte serverInterfaceComponentId, Dictionary<byte, object> requestParameters)
            {
                var serverInterfaceComponent = SystemsComponentMap.Components[serverInterfaceComponentId];
                var methodId = requestParameters.Get<byte>(OperationParameter.MethodId);
                var method = serverInterfaceComponent.Methods[methodId];
                var argumentBytes = requestParameters.Get<byte[]>(OperationParameter.ArgumentBytes);
    
                object[] arguments;
                using (var ms = new MemoryStream(argumentBytes))
                using (var br = new BinaryReader(ms))
                {
                    arguments = Application.Serializer.ReadArguments(br, method.ParameterTypes);
                }
    
                var systemObject = Systems.GetByServerInterfaceId(serverInterfaceComponentId);
    
                if (method.ReturnType == MappedMethodReturnType.Void)
                {
                    method.Invoke(systemObject, arguments);
                    return;
                }
    
                Deferred deferred = null;
    
                try
                {
                    deferred = (Deferred)method.Invoke(systemObject, arguments);
    
                    if (deferred == null)
                        throw new InvalidOperationException($"Method {method.MethodInfo.Name} on component {method.Component.Type.Name} returned a null deferred");
                }
                catch (RpcException e)
                {
                    deferred = Deferred.Fail(e.Message);
                    throw;
                }
                catch
                {
                    deferred = Deferred.Fail("There was a fatal error");
                    throw;
                }
                finally
                {
                    var responseParameters = new Dictionary<byte, object>
                    {
                        [(byte)OperationParameter.SystemInvokeId] = requestParameters.Get<byte>(OperationParameter.SystemInvokeId)
                    };
    
                    deferred
                        .Save(responseParameters, Application.Serializer)
                        .OnSuccess(() => Transport.SendOperationResponse(OperationCode.SendSystemResponse, responseParameters));
                }
            }
    
            internal void OnDisconnect()
            {
                Systems.Dispose();
                ClientScope.Dispose();
                _isConnected = false;
    
                Disconnected?.Invoke(this);
            }
    
            public void Dispose()
            {
                Transport.Disconnect();
            }
        }
    }
    3. Next make the changes to the useringames table and data/entities
    Edit BuzzMMO.Data.Entities/UserInGame.cs

    Here is the entire file with the changes as the commented out lines.
    Code:
    using System.ComponentModel.DataAnnotations;
    using BuzzMMO.Base.Components;
    
    namespace BuzzMMO.Data.Entities
    {
        public class UserInGame
        {
            public virtual int Id { get; set; }
    
            [Required]
            public virtual User Player { get; set; }
    
            [Required]
            public virtual GameEntity Game { get; set; }
    
            [Required]
            public virtual bool HasAbandoned { get; set; }
    
            [Required]
            public virtual GameHero Hero { get; set; }
    
            [Required]
            public virtual GameTeam Team { get; set; }
    
            [Required, MaxLength(64)]
            public string Token { get; set; }
    
            //[Required, MaxLength(64)]
            //public string RequestIp { get; set; }
        }
    }
    Go to the PackageManagerConsole and type in something like:
    Code:
    add-migration RemoveIPFromUserInGame
    Then check the migration file resembles this:
    Code:
    namespace BuzzMMO.Data.Migrations
    {
        using System;
        using System.Data.Entity.Migrations;
        
        public partial class RemoveIPFromUserInGame : DbMigration
        {
            public override void Up()
            {
                DropColumn("dbo.UserInGames", "RequestIp");
            }
            
            public override void Down()
            {
                AddColumn("dbo.UserInGames", "RequestIp", c => c.String(nullable: false, maxLength: 64, storeType: "nvarchar"));
            }
        }
    }
    If all went well, type
    Code:
    update-database

    When looking at the table in the database the useringames table should have the ipaddress column removed.

    4. Create a file BuzzMMO.Server.Master/Config.cs
    This file is used to help us decode the json files in the region server which contain our ports for the servers.
    It's identical to the file in the region project. Here is the entire file:
    Code:
    namespace BuzzMMO.Server.Master
    {
        public class Config
        {
            public MasterServerConfig MasterServer;
            public GameServerConfig GameServer;
        }
    }
    5. Edit BuzzMMO.Server.Master/Application.cs to put int some debug information as well as get the ports from the Config.json files in the region project. Here is the entire file:
    Code:
    using System.IO;
    using BuzzMMO.Base;
    using Photon.SocketServer;
    using Newtonsoft.Json;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Master
    {
        public class Application : ApplicationBase
        {
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly int _game1MasterServerPort;
            private readonly int _game2MasterServerPort;
    
            private readonly MasterServerContext _application;
    
            public Application()
            {
                var game1Config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "../../Game1/bin/", "Config.json")));
                var game1AddressParts = game1Config.MasterServer.UdpConnection.Address.Split(':');
                _game1MasterServerPort = int.Parse(game1AddressParts[1]);
    
                var game2Config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "../../Game2/bin/", "Config.json")));
                var game2AddressParts = game2Config.MasterServer.UdpConnection.Address.Split(':');
                _game2MasterServerPort = int.Parse(game2AddressParts[1]);
    
                _application = new MasterServerContext(new SimpleSerializer());
            }
    
            protected override Photon.SocketServer.PeerBase CreatePeer(InitRequest initRequest)
            {
                if (initRequest.LocalPort == _game1MasterServerPort || initRequest.LocalPort == _game2MasterServerPort)
                {
                    Log.Debug($"Connection via port {initRequest.LocalPort} going to RegionPeer class");
                    return new RegionPeer(_application, initRequest);
                }
                else
                {
                    Log.Debug($"Connection via port {initRequest.LocalPort} going to Peer class");
                    return new Peer(_application, initRequest);
                }
            }
    
            protected override void Setup()
            {
                InitLogging();
    
                Log.Info("HIGHLIGHT - Master Server Started");
            }
    
            protected virtual void InitLogging()
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = "Master";
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }
    6. Edit BuzzMMO.Web.Areas.Admin.Views.User/Index.cshtml to add in the new lines to displayed users
    Code:
    @model BuzzMMO.Web.Areas.Admin.ViewModels.UsersIndex
    
    <h1>Users</h1>
    
    <p>
        @Html.ActionLink("Create User", "Create", new { }, new { @class = "btn btn-default btn-sm" })
    </p>
    
    
    <table class="table table-striped">
        <thead>
            <tr>
                <th>Id</th>
                <th>Username</th>
                <th>Email</th>
                <th>Last Login</th>
                <th>Last Known IP</th>
                <th>Roles</th>
                <th>Actions</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var user in Model.Users)
            {
                <tr>
                    <td>@user.Id</td>
                    <td>@user.Username</td>
                    <td>@user.Email</td>
                    <td>@user.LastLogin</td>
                    <td>@user.LastIp</td>
                    <td>@string.Join(", ", user.Roles.Select(t => t.Name))</td>
                    <td>
                        <div class="btn-group">
                            <a href="@Url.Action("Edit", new {user.Id})" class="btn btn-xs btn-primary">
                                <i class="glyphicon glyphicon-edit"></i>
                                edit
                            </a>
    
                            <a href="@Url.Action("resetpassword", new {user.Id})" class="btn btn-xs btn-default">
                                <i class="glyphicon glyphicon-lock"></i>
                                reset password
                            </a>
    
                            @if (user.Id != Auth.User.Id)
                            {
                                <a href="@Url.Action("delete", new {user.Id})" class="btn btn-xs btn-danger post" data-post="Are you sure you want to delete @user.Username?">
                                    <i class="glyphicon glyphicon-remove"></i>
                                    delete
                                </a>
                            }
                        </div>
                    </td>
                </tr>
            }
        </tbody>
    </table>
    7. Edit BuzzMMO.Server/ClientContext.cs to save the LastLogin and IP to the database
    You will also need to add the Nuget package "Entity Framework" to the BuzzMMO.Server project.
    Code:
    using System;
    using System.Collections.Generic;
    using System.Data.Entity.Migrations;
    using System.IO;
    using System.Linq;
    using Autofac;
    using BuzzMMO.Base;
    using BuzzMMO.Base.Async;
    using BuzzMMO.Base.Components.Systems.Master;
    using BuzzMMO.Base.Extensions;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    
    namespace BuzzMMO.Server
    {
        public abstract class ClientContext : IDisposable
        {
            private volatile bool _isConnected;
    
            public bool IsConnected => _isConnected;
            public event Action<ClientContext> Disconnected;
    
            public ServerContext Application { get; private set; }
            public ClientSystems Systems { get; private set; }
            public ILifetimeScope ClientScope { get; private set; }
            public IServerTransport Transport { get; private set; }
            public ComponentMap SystemsComponentMap { get; private set; }
    
            public UserDetails UserDetails { get; private set; }
            public bool IsAuthenticated => UserDetails != null;
    
            protected ClientContext(ServerContext application, ComponentMap systemsComponentMap, IServerTransport transport)
            {
                _isConnected = true;
    
                Application = application;
                Systems = new ClientSystems(this);
                ClientScope = application.Container.BeginLifetimeScope(this);
                SystemsComponentMap = systemsComponentMap;
                Transport = transport;
    
                transport.SendData(application.Events.SyncComponentMap(EventCode.SyncSystemsComponentMap, systemsComponentMap));
            }
    
            public void Login(User user, string requestIp)
            {
                if (IsAuthenticated)
                    throw new RpcException("Client already authenticated");
    
                // save the user's LastLogin and ip associated with the token or just an ip called debug if none
                using (var database = new MMODatabaseContext())
                {
                    var foundUser = database.Users.Find(user.Id);
                    if (foundUser == null)
                    {
                        throw new RpcException("User not found when logging in to game");
                    }
    
                    foundUser.LastLogin = DateTime.UtcNow;
                    foundUser.LastIp = requestIp;
                    database.Users.AddOrUpdate();
                    database.SaveChanges();
                }
    
                UserDetails = new UserDetails(
                    user.Id, 
                    user.Username, 
                    new HashSet<string>(user.Roles.Select(r => r.Name)));
            }
    
            public virtual void OnOperationRequest(OperationCode code, Dictionary<byte, object> parameters)
            {
                InvokeMethodOnSystem((byte)code, parameters);
            }
    
            protected void InvokeMethodOnSystem(byte serverInterfaceComponentId, Dictionary<byte, object> requestParameters)
            {
                var serverInterfaceComponent = SystemsComponentMap.Components[serverInterfaceComponentId];
                var methodId = requestParameters.Get<byte>(OperationParameter.MethodId);
                var method = serverInterfaceComponent.Methods[methodId];
                var argumentBytes = requestParameters.Get<byte[]>(OperationParameter.ArgumentBytes);
    
                object[] arguments;
                using (var ms = new MemoryStream(argumentBytes))
                using (var br = new BinaryReader(ms))
                {
                    arguments = Application.Serializer.ReadArguments(br, method.ParameterTypes);
                }
    
                var systemObject = Systems.GetByServerInterfaceId(serverInterfaceComponentId);
    
                if (method.ReturnType == MappedMethodReturnType.Void)
                {
                    method.Invoke(systemObject, arguments);
                    return;
                }
    
                Deferred deferred = null;
    
                try
                {
                    deferred = (Deferred)method.Invoke(systemObject, arguments);
    
                    if (deferred == null)
                        throw new InvalidOperationException($"Method {method.MethodInfo.Name} on component {method.Component.Type.Name} returned a null deferred");
                }
                catch (RpcException e)
                {
                    deferred = Deferred.Fail(e.Message);
                    throw;
                }
                catch
                {
                    deferred = Deferred.Fail("There was a fatal error");
                    throw;
                }
                finally
                {
                    var responseParameters = new Dictionary<byte, object>
                    {
                        [(byte)OperationParameter.SystemInvokeId] = requestParameters.Get<byte>(OperationParameter.SystemInvokeId)
                    };
    
                    deferred
                        .Save(responseParameters, Application.Serializer)
                        .OnSuccess(() => Transport.SendOperationResponse(OperationCode.SendSystemResponse, responseParameters));
                }
            }
    
            internal void OnDisconnect()
            {
                Systems.Dispose();
                ClientScope.Dispose();
                _isConnected = false;
    
                Disconnected?.Invoke(this);
            }
    
            public void Dispose()
            {
                Transport.Disconnect();
            }
        }
    }
    8. Edit BuzzMMO.Web.Controllers/AuthController.cs to record the login date/time when logging in to the website:
    Code:
    using System;
    using System.Data.Entity.Migrations;
    using System.Linq;
    using System.Web.Mvc;
    using BuzzMMO.Base;
    using BuzzMMO.Data;
    using BuzzMMO.Web.ViewModels;
    
    namespace BuzzMMO.Web.Controllers
    {
        public class AuthController : Controller
        {
            public ActionResult Login()
            {
                return View(new AuthLogin());
            }
    
            [HttpPost]
            public ActionResult Login(AuthLogin form, string returnUrl)
            {
                if (!ModelState.IsValid)
                    return View(form);
    
                using (var database = new MMODatabaseContext())
                {
                    var user = database.Users.SingleOrDefault(t => t.Username == form.Username);
    
    
                    if (user == null)
                        Data.Entities.User.FakeHash();
    
                    if (user == null || !user.CheckPassword(form.Password))
                    {
                        ModelState.AddModelError("Password", "Username or Password is incorrect");
                        return View(form);
                    }
    
                    user.LastLogin = DateTime.UtcNow;
                    database.Users.AddOrUpdate();
                    database.SaveChanges();
    
                    // set the cookie
                    Auth.User = user;
    
                    if (!string.IsNullOrWhiteSpace(returnUrl))
                        return Redirect(returnUrl);
    
                    return RedirectToAction("Index", "Home", new { area = "" });
                }
            }
    
            [HttpPost]
            [Authorize(Roles = Roles.Registered)]
            public ActionResult Logout()
            {
                Auth.Logout();
                return RedirectToAction("Login");
            }
        }
    }
    9. Edit BuzzMMO.Server.Master.Systems/LoginSystem.cs to add an extra argument when calling Context.Login to pass in the ip address
    Code:
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Linq;
    using BuzzMMO.Base;
    using BuzzMMO.Base.Async;
    using BuzzMMO.Base.Components.Systems.Master;
    using BuzzMMO.Data;
    using log4net;
    
    namespace BuzzMMO.Server.Master.Systems
    {
        public class LoginSystem : PlayerSystemBase<ILoginSystemServer, ILoginSystemClient>, ILoginSystemServer
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(LoginSystem));
    
            private readonly MMODatabaseContext _database;
            private readonly MainLobbyService _mainLobby;
    
            public LoginSystem(MMODatabaseContext database, MainLobbyService mainLobby)
            {
                _database = database;
                _mainLobby = mainLobby;
            }
    
            protected override void Awake()
            {
                Proxy.DebugSetAvailablePlayers(_database.Users.Include(u => u.Roles).AsEnumerable().Select(
                    user => new UserDetails(
                        user.Id, 
                        user.Username, 
                        new HashSet<string>(user.Roles.Select(r => r.Name))))
                        .ToArray());
    
                // included only in bi-directional server client communications
                //var def = Proxy.DebugSetAvailablePlayers(_database.Users.Include(u => u.Roles).AsEnumerable().Select(
                //    user => new UserDetails(user.Id, user.Username, new HashSet<string>(user.Roles.Select(r => r.Name)))).ToArray());
    
                //def.OnSuccess(res =>
                //{
                //    Log.Info($"Got {def.Result} from client");
                //});
            }
    
            // client logging in to master server with a token
            public Deferred<UserDetails> Login(string token)
            {
                Log.Info($"Logging in with token {token}");
    
                if (string.IsNullOrWhiteSpace(token))
                    return Deferred.Fail<UserDetails>("Invalid token");
    
                var tokenRecord = _database.ClientAuthenticationTokens
                     .Include(t => t.User)
                    .Include(t => t.User.Roles)
                    .FirstOrDefault(t => t.Token == token);
    
                if (tokenRecord == null)
                    return Deferred.Fail<UserDetails>("Invalid token");
    
                Context.Login(tokenRecord.User, tokenRecord.RequestIp);
                return Deferred.Success(Context.UserDetails);
            }
    
            public Deferred<UserDetails> DebugLogin(int userId)
            {
                Log.Info($"Logging in with debug login rather than a token {userId}");
    
                var user = _database.Users.Include(u => u.Roles).FirstOrDefault(u => u.Id == userId);
                if (user == null)
                    return Deferred.Fail<UserDetails>("Invalid user id");
    
                Context.Login(user, "Debug");
                return Deferred.Success(Context.UserDetails);
            }
    
            public Deferred JoinMainLobby()
            {
                Log.Info("Joining main lobby");
    
                return _mainLobby.AddPlayer(Context)
                    .OnSuccess(() => Context.Systems.Destroy<LoginSystem>());
            }
        }
    }
    10. Edit BuzzMMO.Server.Region/Peer.cs to also include the new ipaddress argument when calling Context.Login. This actually gets the ipaddress too from the MMOPeerBase when logging in with a token to the lobby
    Code:
    using Autofac;
    using BuzzMMO.Base;
    using BuzzMMO.Data.Services;
    using log4net;
    using Photon.SocketServer;
    
    namespace BuzzMMO.Server.Region
    {
        public class Peer : MMOPeerBase<ClientContext>
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(Peer));
    
            private readonly GameServerContext _application;
    
            public Peer(GameServerContext application, InitRequest initRequest) : base(initRequest)
            {
                _application = application;
            }
    
            protected override ClientContext CreateContext(ContextType type, OperationRequest request)
            {
                if (type == ContextType.Player)
                {
                    var gameId = (int)request[(byte)OperationParameter.GameId];
                    var token = (string)request[(byte)OperationParameter.Token];
    
                    Log.Info($"Player with token {token} is connecting to game {gameId}");
    
                    var gameEntityService = _application.Container.Resolve<GameEntityService>();
                    var playerInGame = gameEntityService.GetPlayerInGame(
                        _application.Config.GameServer.UniqueId,
                        gameId,
                        token);
    
                    if (playerInGame == null || playerInGame.HasAbandoned)
                    {
                        Disconnect();
                        return null;
                    }
    
                    var context = new PlayerContextGame(_application, this);
                    context.Login(playerInGame.Player, RemoteIP);           //found in MMOPeerBase
                    Log.Info($"Player from {RemoteIP} connected as {context.UserDetails.Username}");
    
                    _application.AddPlayerToGame(gameId, context)
                        .OnFail(Disconnect);
    
                    return context;
                }
    
                return null;
            }
        }
    }

    Now whenever we login we record what information we can get. (Logging in to the website doesn't save the ip address though)
    Last edited by oldngrey; 07-24-2017 at 01:15 AM.

  4. #4
    Join Date
    Feb 2014
    Posts
    272
    Remove the error when stopping a region server

    When stopping a region server you will get an error in the logs saying similar to the following:

    Could not unregister server Game Server 1 / 156bd28c-759e-4d14-8118-6131581f009e because it was not registered

    This is because in the BuzzMMO.Server.Master.Systems.Region/GameInstanceSystem.cs we call both methods UnregisterServer() and Dispose().
    We simply remove the code from the Dispose() method. Here is the entire file with the fix commented out.

    Code:
    using BuzzMMO.Base.Async;
    using BuzzMMO.Server.Components;
    using ExitGames.Concurrency.Fibers;
    using log4net;
    
    namespace BuzzMMO.Server.Master.Systems.Region
    {
        public class GameInstanceSystem : RegionServerSystemBase<IGameInstancesMaster, IGameInstancesRegion>, IGameInstancesMaster
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(GameInstanceSystem));
    
            private readonly RegionServerQueue _serverQueue;
    
            public GameInstanceSystem(RegionServerQueue serverQueue)
            {
                _serverQueue = serverQueue;
            }
    
            public Deferred RegisterServer(GameServerConfig config)
            {
                _serverQueue.RegisterServer(this, config);
                return Deferred.Success();
            }
    
            public void UnregisterServer()
            {
                _serverQueue.UnregisterServer(this);
            }
    
            public override void Dispose()
            {
                //_serverQueue.UnregisterServer(this);
            }
        }
    }

    Fix for Regex expression in ProxySyntaxAnalyzer.cs
    This one was reported by am385 in this thread:
    https://www.3dbuzz.com/forum/threads...eneration-code

    It was explained in that post, but here is the top few lines of the file again just to keep it on one place.
    Edit BuzzMMO.Tools.ProxyGenerator/ProxySyntaxAnalyzer.cs
    Code:
    ..................
       public class ProxySyntaxAnalyzer : CSharpSyntaxWalker
        {
            //fix by am385. Old code shown in commented out line
            //private static readonly Regex ReturnTypeRegex = new Regex("(.*?)Deferred(<(.+?)>$)?", RegexOptions.Compiled);
            private static readonly Regex ReturnTypeRegex = new Regex("(.*?Deferred)(<(.+?)>)?", RegexOptions.Compiled);
    
            public IEnumerable<string> UsingDirectives => _usingDirectives;
            public IEnumerable<ProxyComponent> Components => _components;
    ......
    Last edited by oldngrey; 07-24-2017 at 05:23 AM.

  5. #5
    Join Date
    Feb 2014
    Posts
    272
    Fix for TeamCity Compiler error - compiling BuildNumber.cs

    For those who are interested in the BuildNumber in a production sense, you will get a compiler error in TeamCity when it makes the production version of the BuildNumber.cs files.

    Basically, when Nelson moved the BuildNumber.cs file out of the Infrastructure folder, he forgot to update the code that updated the file.

    To fix it, edit BuzzMMO.Build.Tasks/UpdateBuildInfo.cs

    Replace the line saying:

    Code:
    var fileContents = $"[assembly: BuzzMMO.Base.Infrastructure.BuildNumber({VersionNumber}, {Timestamp}, {isDebug})]";
    with

    Code:
    var fileContents = $"[assembly: BuzzMMO.Base.BuildNumber({VersionNumber}, {Timestamp}, {isDebug})]";
    That will stop that particular compiler error. More to come......


    Fix for TeamCity compiler error - missing BuzzMMOBuild.local.proj

    The BuzzMMO.Server.Region.csproj has a line saying it wants to import BuzzMMOBuild.local.proj so that in debug mode it will copy the region files to 2 different directories.
    Unfortunately, the Import line wants to import the proj file every time it compiles whether or not it's in debug mode.

    The simple fix is to simply remove the line excluding it from source control.
    So in .gitignore remove the line:
    Code:
    /src/BuzzMMOBuild.local.proj
    Now the project will compile in TeamCity "Build And Test". No doubt more to come with all the other builds especially Photon.
    Last edited by oldngrey; 05-31-2017 at 06:55 AM.

  6. #6
    Join Date
    Feb 2014
    Posts
    272
    Getting Photon to work in Production

    When Nelson was doing the lobby and starting a game series, he had promised to do a video on upgrading the server projects to work in a release build. However, as we know, he never got to do that video.

    Getting Photon servers to work in the release version isn't terribly hard, it was a matter of figuring out what was missing rather than what was wrong.

    1. The first things to consider is the BuzzMMOBuild.proj file. Here is the bit that deals with DeployPhoton:. This will copy across the files to the correct deploy folder on the production server.

    Code:
      <!--Build photon solution-->
      <Target Name="DeployPhoton" DependsOnTargets="BuildSolution">
        <Exec Command="..\lib\msdeploy\msdeploy.exe -verb:sync -source:contentpath='$(MSBuildThisFileDirectory)BuzzMMO.Server.Master\bin\$(BuildConfig)' -dest:contentpath='$(PhotonDeployFolder)\Master\bin' -skip:directory=\\Cache$" />
        <Exec Command="..\lib\msdeploy\msdeploy.exe -verb:sync -source:contentpath='$(MSBuildThisFileDirectory)BuzzMMO.Server.Region\bin\$(BuildConfig)' -dest:contentpath='$(PhotonDeployFolder)\Game1\bin' -skip:Directory=\\Cache$ -skip:File='Config.json'" />
        <Exec Command="..\lib\msdeploy\msdeploy.exe -verb:sync -source:contentpath='$(MSBuildThisFileDirectory)BuzzMMO.Server.Region\bin\$(BuildConfig)' -dest:contentpath='$(PhotonDeployFolder)\Game2\bin' -skip:directory=\\Cache$ -skip:File='Config.json'" />
      </Target>
    2. Next we need to edit the BuzzMMO.Server.Region/BuzzMMO.Server.Region.csproj
    We need to add in an "AfterBuild" to make sure the correct Config.json file is copied into the correct deploy folder on the production server.

    I will show the last part of the file from where we copy the files to the debug location. We are just duplicating and editing the AfterBuild secton:
    Code:
     <Target Name="AfterBuild" Condition="'$(Configuration)' == 'Debug'">
        <RenderConfiguration BaseFile="$(MSBuildThisFileDirectory)Config.json" TransformFile="$(MSBuildThisFileDirectory)Config\Game1.Debug.json" DestinationFile="$(PhotonDeployFolder)\Game1\bin\Config.json" />
        <RenderConfiguration BaseFile="$(MSBuildThisFileDirectory)Config.json" TransformFile="$(MSBuildThisFileDirectory)Config\Game2.Debug.json" DestinationFile="$(PhotonDeployFolder)\Game2\bin\Config.json" />
      </Target>
      <Target Name="AfterBuild" Condition="'$(Configuration)' == 'Release'">
        <RenderConfiguration BaseFile="$(MSBuildThisFileDirectory)Config.json" TransformFile="$(MSBuildThisFileDirectory)Config\Game1.Production.json" DestinationFile="$(PhotonDeployFolder)\Game1\bin\Config.json" />
        <RenderConfiguration BaseFile="$(MSBuildThisFileDirectory)Config.json" TransformFile="$(MSBuildThisFileDirectory)Config\Game2.Production.json" DestinationFile="$(PhotonDeployFolder)\Game2\bin\Config.json" />
      </Target>
    3. We need to edit the production files in the BuzzMMO.Server.Region/Config folder

    Replace the addresses with your servers' ip address and the UniqueId with the same one in the Debug file.

    Game1.Production.json:
    Code:
    {
      "MasterServer": {
        "UdpConnection": {
          "Address": "10.0.0.4:5056",
          "ApplicationName": "BuzzMMOMaster"
        }
      },
      "GameServer": {
        "Name": "Game Server 1",
        "UdpConnection": {
          "Address": "10.0.0.4:5058",
          "ApplicationName": "BuzzMMOGame1"
        },
        "UniqueId": "156BD28C-759E-4D14-8118-6131581F009E"
      }
    }
    Game2.Production.json:
    Code:
    {
      "MasterServer": {
        "UdpConnection": {
          "Address": "10.0.0.4:5057",
          "ApplicationName": "BuzzMMOMaster"
        }
      },
      "GameServer": {
        "Name": "Game Server 2",
        "UdpConnection": {
          "Address": "10.0.0.4:5059",
          "ApplicationName": "BuzzMMOGame2"
        },
        "UniqueId": "6612BA17-13A8-4AD5-88BC-E5C81A260D24"
      }
    }

    4 Finally we need to update our app.release.config files for both the Master and Region server.

    Edit the BuzzMMO.Master/app.Release.config file to put in your database connection strings. Of course you need your own addresses, database name, account, and password..... :

    Code:
    <?xml version="1.0" encoding="utf-8" ?>
    
    <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    
      <appSettings>
        <add
          key="RegionServers"
          value="10.0.0.4:5055"
          xdt:Transform="SetAttributes"
          xdt:Locator="Match(key)"/>
      </appSettings>
      
      <connectionStrings>
        <add 
          name="MMODatabase" 
          connectionString="Server=server1.domain.com;Database=buzz_mmo;Uid=root;Pwd=root;Allow User Variables=True" 
          xdt:Transform="SetAttributes"
          xdt:Locator="Match(name)"/>
      </connectionStrings>
    </configuration>
    Edit the BuzzMMO.Region/app.Release.config file to put in your database connection strings. Of course you need your own addresses, database name, account, and password..... :
    Code:
    <?xml version="1.0" encoding="utf-8" ?>
    
    <configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
    
     <connectionStrings>
        <add 
          name="MMODatabase" 
          connectionString="Server=server1.domain.com;Database=buzz_mmo;Uid=root;Pwd=root;Allow User Variables=True"
          xdt:Transform="SetAttributes"
          xdt:Locator="Match(name)"/>     
      </connectionStrings>
    </configuration>



    If you have updated your production database to have the same tables etc as the dev build, then, after copying across the Photon 4-0-29 files to the prod server you will be able to use Team City to compile the servers and have them populate the Game1\bin and Game2\bin folders.

    When you use Photon Control to start the servers you should get an entry in the gameserverentities table for the region server/s. If so, it's working as much as we can test so far.
    Last edited by oldngrey; 07-25-2017 at 01:25 AM.

  7. #7
    Join Date
    Feb 2014
    Posts
    272
    Getting Client to work in Production

    This one didn't take a lot of work. All we have to do is set the release build location to the unity assembly the same as the debug build.

    In the solution explorer right-click the BuzzMMO.Client.Unity project and select Properties.
    On the left column click "Build"
    In the Configuration drop-down menu, select "Release'
    In the output path replace bin\release with:
    ..\..\Unity\Client\Assets\Assemblies\
    (same path as the debug build output path.)



    After this, we can login via the client, connect to the server and begin a game. This proves the login with token code works. The database correctly records the ip address of your client and it all appears to work.

    So, that concludes the changes we need to make for the production build via TeamCity. As far as I can tell, so far, everything seems to work as it does in debug - including self-updating launchers and client etc.

    As a matter of interest, I did have one problem that you probably won't have, but I will include it here as a clue if you ever get it:

    At one point my production servers would not talk to the database even though my debug build worked perfectly. The logs showed an exception trying to access MySql.Data saying that "The located assembly's manifest definition does not match the assembly reference".
    Googling that error showed that it was relatively common with NuGet packages not installing and re-configuring correctly. The fix in all instances was to do a solution-wide update to the MySql.Data package using "Manage NuGet Packages for Solution" to re-install MySql.Data across all projects.
    Last edited by oldngrey; 06-03-2017 at 09:37 PM.

  8. #8
    Join Date
    Feb 2014
    Posts
    272
    Photon 4 Upgrade - part 2

    I want to be clear that this code was provided by am385 who responded to a query about finishing off the Photon 4 upgrade that Nelson didn't finish. See: https://www.3dbuzz.com/forum/threads...ng-again/page2
    The only credit I take is to convert am385's code back to BuzzMMO and to use autofac again. As a result, this code should be a drop-in replacement for the BuzzMMO project.
    There are 12 files that need to be edited, deleted, or added.
    They are presented in the same order as they were listed in Sourcetree.

    1. First of all delete BuzzMMO.Server/MMOPeerBase.cs
    This file has been split into 3 parts and deleting it will show us the bugs which we will fix shortly.

    2. Create BuzzMMO.Server/MMOClientPeerBase.cs (yes it's almost identical to the file you just deleted.)
    This is the entire file:
    Code:
    using BuzzMMO.Base;
    using Photon.SocketServer;
    using PhotonHostRuntimeInterfaces;
    using System;
    using System.Collections.Generic;
    
    namespace BuzzMMO.Server
    {
        // incoming client connection base
        public abstract class MMOClientPeerBase<TContext> : ClientPeer, IServerTransport where TContext : ClientContext
        {
            private readonly CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>> _callbacks;
    
            protected TContext ClientContext { get; private set; }
    
            protected MMOClientPeerBase(InitRequest initRequest) : base(initRequest)
            {
                _callbacks = new CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>>();
            }
    
            protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
            {
                var operationCode = (OperationCode)operationRequest.OperationCode;
    
                if (ClientContext == null)
                {
                    if (operationCode != OperationCode.InitContext)
                        throw new ArgumentException($"Operation code {operationCode} is not supported");
    
                    var contextType = (ContextType)operationRequest.Parameters[(byte)OperationParameter.ContextType];
                    ClientContext = CreateContext(contextType, operationRequest);
    
                    if (ClientContext == null)
                        throw new ArgumentException($"Context Type {contextType} is not valid");
                }
                else if (operationCode == OperationCode.SendSystemResponse)
                {
                    var methodInvokeId = (byte)operationRequest[(byte)OperationParameter.SystemInvokeId];
                    var callback = _callbacks.GetCallback(methodInvokeId);
                    callback(operationCode, operationRequest.Parameters);
                }
                else
                {
                    ClientContext.OnOperationRequest(operationCode, operationRequest.Parameters);
                }
            }
    
            protected abstract TContext CreateContext(ContextType type, OperationRequest request);
    
            protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
            {
                ClientContext.OnDisconnect();
            }
    
            #region IServerTransport Members
            public void SendEventWithResponse(EventCode code, Dictionary<byte, object> parameters, Action<OperationCode, Dictionary<byte, object>> onResponse)
            {
                parameters[(byte)EventCodeParameter.SystemInvokeId] = _callbacks.RegisterCallback(onResponse);
                SendEvent(new EventData((byte)code, parameters), new SendParameters());
            }
    
            public virtual void SendData(Event @event)
            {
                SendEvent(@event.EventData, @event.SendParameters);
            }
    
            public virtual void SendOperationResponse(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationResponse(new OperationResponse((byte)code, parameters), Event.Reliable);
            }
            #endregion
        }
    }

    3. Create BuzzMMO.Server/MMOInboundS2SPeerBase.cs
    This is the entire file:
    Code:
    using BuzzMMO.Base;
    using BuzzMMO.Client.Infrastructure;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using PhotonHostRuntimeInterfaces;
    using System;
    using System.Collections.Generic;
    
    namespace BuzzMMO.Server
    {
        // incoming server connection base
        public abstract class MMOInboundS2SPeerBase<TContext> : InboundS2SPeer, IServerTransport
            where TContext : ClientContext
        {
            private readonly CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>> _callbacks;
            protected Action<EventCode, Dictionary<byte, object>>[] EventHandlers { get; }
    
            protected TContext ClientContext { get; private set; }
    
            protected MMOInboundS2SPeerBase(InitRequest initRequest) : base(initRequest)
            {
                _callbacks = new CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>>();
            }
    
            #region Methods
            protected abstract TContext CreateContext(ContextType type, OperationRequest request);
            protected void HandleSystemCallback(OperationCode code, Dictionary<byte, object> parameters)
            {
                if (code != OperationCode.SendSystemResponse)
                    throw new ArgumentException($"Code {code} is not valid for handling responses", nameof(code));
    
                var methodInvokeId = (byte)parameters[(byte)OperationParameter.SystemInvokeId];
                var callback = _callbacks.GetCallback(methodInvokeId);
                callback(code, parameters);
            }
    
            protected void HandleEvent(EventCode code, Dictionary<byte, object> parameters)
            {
                var handler = EventHandlers[(byte)code];
                if (handler == null)
                    throw new InvalidOperationException($"Handler for event code {code} was not registered");
    
                handler(code, parameters);
            }
    
            public void AddEventReader(IEventReaderModule eventReader)
            {
                foreach (var registration in eventReader.GetRegistration())
                {
                    if (EventHandlers[(int)registration.Code] != null)
                    {
                        var oldHandler = EventHandlers[(int)registration.Code];
                        var action = registration.Action;
    
                        EventHandlers[(int)registration.Code] = (code, parameter) =>
                        {
                            oldHandler(code, parameter);
                            action(code, parameter);
                        };
                    }
                    else
                    {
                        EventHandlers[(int)registration.Code] = registration.Action;
                    }
                }
            }
            #endregion
    
            #region Overrides
            protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
            {
                var operationCode = (OperationCode)operationRequest.OperationCode;
    
                if (ClientContext == null)
                {
                    if (operationCode != OperationCode.InitContext)
                        throw new ArgumentException($"Operation code {operationCode} is not supported");
    
                    var contextType = (ContextType)operationRequest.Parameters[(byte)OperationParameter.ContextType];
                    ClientContext = CreateContext(contextType, operationRequest);
    
                    if (ClientContext == null)
                        throw new ArgumentException($"Context Type {contextType} is not valid");
                }
                else if (operationCode == OperationCode.SendSystemResponse)
                {
                    var methodInvokeId = (byte)operationRequest[(byte)OperationParameter.SystemInvokeId];
                    var callback = _callbacks.GetCallback(methodInvokeId);
                    callback(operationCode, operationRequest.Parameters);
                }
                else
                {
                    ClientContext.OnOperationRequest(operationCode, operationRequest.Parameters);
                }
            }
    
            protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
            {
                ClientContext.OnDisconnect();
            }
    
            protected override void OnEvent(IEventData eventData, SendParameters sendParameters)
            {
                HandleEvent((EventCode)eventData.Code, eventData.Parameters);
            }
    
            protected override void OnOperationResponse(OperationResponse operationResponse, SendParameters sendParameters)
            {
                HandleSystemCallback((OperationCode)operationResponse.OperationCode, operationResponse.Parameters);
            }
            #endregion
    
            #region IServerTransport Members
            public void SendEventWithResponse(EventCode code, Dictionary<byte, object> parameters, Action<OperationCode, Dictionary<byte, object>> onRespoinse)
            {
                parameters[(byte)EventCodeParameter.SystemInvokeId] = _callbacks.RegisterCallback(onRespoinse);
                SendEvent(new EventData((byte)code, parameters), new SendParameters());
            }
    
            public virtual void SendData(Event @event)
            {
                SendEvent(@event.EventData, @event.SendParameters);
            }
    
            public virtual void SendOperationResponse(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationResponse(new OperationResponse((byte)code, parameters), Event.Reliable);
            }
            #endregion
        }
    }

    4. Create BuzzMMO.Server/MMOOutboundS2SPeerBase.cs
    This is the entire file:
    Code:
    using BuzzMMO.Base;
    using BuzzMMO.Client.Infrastructure;
    using BuzzMMO.Client.Systems;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using PhotonHostRuntimeInterfaces;
    using System;
    using System.Collections.Generic;
    using BuzzMMO.Base.Async;
    using Autofac;
    
    namespace BuzzMMO.Server
    {
        // outgoing server connection base
        public abstract class MMOOutboundS2SPeerBase<TContext> : OutboundS2SPeer, ISystemFactory, IClientTransport
            where TContext : ServerContext
        {
            public event Action OnDisconnected;
    
            protected TContext ServerContext { get; }
            protected Client.Systems.ClientSystems ClientSystems { get; }
            protected ComponentMap ComponentMap { get; }
            protected ILifetimeScope Scope { get; }
            protected CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>> Callbacks { get; }
            protected Action<EventCode, Dictionary<byte, object>>[] EventHandlers { get; }
    
            protected HashSet<IClientTransportListener> Listeners { get; }
    
            protected MMOOutboundS2SPeerBase(ApplicationBase app, TContext application) : base(app)
            {
                Callbacks = new CallbackByteMap<Action<OperationCode, Dictionary<byte, object>>>();
                EventHandlers = new Action<EventCode, Dictionary<byte, object>>[byte.MaxValue + 1];
                Listeners = new HashSet<IClientTransportListener>();
    
                ServerContext = application;
                Scope = application.Container.BeginLifetimeScope();
                ComponentMap = new ComponentMap();
    
                var operationWriter = new SystemsOperationWriter(ServerContext.Serializer, this);
                ClientSystems = new Client.Systems.ClientSystems(ComponentMap, this, operationWriter);
                AddEventReader(new SystemsEventReader(ServerContext.Serializer, ClientSystems, this));
    
                application.OnDisposed += OnApplicationDisposed;
            }
    
            #region Methods
    
            protected void HandleSystemCallback(OperationCode code, Dictionary<byte, object> parameters)
            {
                if (code != OperationCode.SendSystemResponse)
                    throw new ArgumentException($"Code {code} is not valid for handling responses", nameof(code));
    
                var methodInvokeId = (byte) parameters[(byte) OperationParameter.SystemInvokeId];
                var callback = Callbacks.GetCallback(methodInvokeId);
                callback(code, parameters);
            }
    
            protected void HandleEvent(EventCode code, Dictionary<byte, object> parameters)
            {
                var handler = EventHandlers[(byte) code];
                if (handler == null)
                    throw new InvalidOperationException($"Handler for event code {code} was not registered");
    
                handler(code, parameters);
            }
    
            protected void SendOperationInternal(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationRequest(new OperationRequest((byte) code, parameters), new SendParameters {Unreliable = false});
            }
    
            private void OnApplicationDisposed()
            {
                Disconnect();
            }
    
            #endregion
    
            #region Overrides
    
            protected override void OnConnectionEstablished(object responseObject)
            {
                SendOperation(OperationCode.InitContext, new Dictionary<byte, object> {[(byte) OperationParameter.ContextType] = ContextType.Region});
            }
    
            protected override void OnConnectionFailed(int errorCode, string errorMessage)
            {
            }
    
            protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
            {
                OnDisconnected?.Invoke();
                Scope.Dispose();
                ServerContext.OnDisposed -= OnApplicationDisposed;
            }
    
            protected override void OnOperationRequest(OperationRequest operationRequest, SendParameters sendParameters)
            {
            }
    
            protected override void OnEvent(IEventData eventData, SendParameters sendParameters)
            {
                HandleEvent((EventCode) eventData.Code, eventData.Parameters);
            }
    
            protected override void OnOperationResponse(OperationResponse operationResponse, SendParameters sendParameters)
            {
                HandleSystemCallback((OperationCode) operationResponse.OperationCode, operationResponse.Parameters);
            }
    
            #endregion
    
            #region ISystemFactory Members
    
            public abstract ISystemBase CreateSystem(Type interfaceType, Func<Type, object> proxyFactory, out Type concreteType);
    
            #endregion
    
            #region IClientTransport Members
    
            public void SendOperation(OperationCode code, Dictionary<byte, object> parameters)
            {
                SendOperationInternal(code, parameters);
            }
    
            public void SendOperation(OperationCode code, Dictionary<byte, object> parameters, Action<OperationCode, Dictionary<byte, object>> onResponse)
            {
                parameters[(byte) OperationParameter.SystemInvokeId] = Callbacks.RegisterCallback(onResponse);
                SendOperationInternal(code, parameters);
            }
    
            public void AddEventReader(IEventReaderModule eventReader)
            {
                foreach (var registration in eventReader.GetRegistration())
                {
                    if (EventHandlers[(int) registration.Code] != null)
                    {
                        var oldHandler = EventHandlers[(int) registration.Code];
                        var action = registration.Action;
    
                        EventHandlers[(int) registration.Code] = (code, parameter) =>
                        {
                            oldHandler(code, parameter);
                            action(code, parameter);
                        };
                    }
                    else
                    {
                        EventHandlers[(int) registration.Code] = registration.Action;
                    }
                }
            }
    
            public void AddListener(IClientTransportListener listener)
            {
                Listeners.Add(listener);
            }
    
            public Deferred Connect()
            {
                return Deferred.Success();
            }
    
            public void Service()
            {
            }
    
            Deferred IClientTransport.Disconnect()
            {
                return Deferred.Success();
            }
    
            #endregion
        }
    }

    5. Edit BuzzMMO.Server.Master/Application.cs
    This is the entire file:
    Code:
    using System.IO;
    using BuzzMMO.Base;
    using Photon.SocketServer;
    using Newtonsoft.Json;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Master
    {
        public class Application : ApplicationBase
        {
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly int _game1ServerPort;
            private readonly int _game2ServerPort;
    
            private readonly MasterServerContext _application;
    
            public Application()
            {
                var game1Config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "../../Game1/bin/", "Config.json")));
                var game1AddressParts = game1Config.MasterServer.UdpConnection.Address.Split(':');
                _game1ServerPort = int.Parse(game1AddressParts[1]);
    
                var game2Config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "../../Game2/bin/", "Config.json")));
                var game2AddressParts = game2Config.MasterServer.UdpConnection.Address.Split(':');
                _game2ServerPort = int.Parse(game2AddressParts[1]);
    
                _application = new MasterServerContext(new SimpleSerializer());
            }
    
            protected override Photon.SocketServer.PeerBase CreatePeer(InitRequest initRequest)
            {
                if (initRequest.LocalPort == _game1ServerPort || initRequest.LocalPort == _game2ServerPort)
                {
                    Log.Debug($"Connection via port {initRequest.LocalPort} going to RegionPeer class");
                    return new RegionPeer(_application, initRequest);
                }
                else
                {
                    Log.Debug($"Connection via port {initRequest.LocalPort} going to ClientPeer class");
                    return new ClientPeer(_application, initRequest);
                }
            }
    
            protected override void Setup()
            {
                InitLogging();
    
                Log.Info("HIGHLIGHT - Master Server Started");
            }
    
            //Log4Net setup on Master server
            protected virtual void InitLogging()
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = "Master";
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }

    6. Delete BuzzMMO.Server.Master/Peer.cs


    7. Create BuzzMMO.Server.Master/ClientPeer.cs
    This is the entire file:
    Code:
    using BuzzMMO.Base;
    using BuzzMMO.Server.Master.Systems;
    using BuzzMMO.Server.Master.Systems.Region;
    using Photon.SocketServer;
    using log4net;
    
    namespace BuzzMMO.Server.Master
    {
        //client incoming connection. servers connect via serverpeer
        public class ClientPeer : MMOClientPeerBase<ClientContext>
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(RegionServerQueue));
    
            private readonly MasterServerContext _application;
    
            public ClientPeer(MasterServerContext application, InitRequest initRequest) : base(initRequest)
            {
                _application = application;
            }
    
            protected override ClientContext CreateContext(ContextType type, OperationRequest request)
            {
                // all unity client connections
                if (type == ContextType.Player)
                {
                    var context = new PlayerContextMaster(_application, this);
                    context.Systems.Create<LoginSystem>();
                    return context;
                }
    
                // if a region server somehow connects via port 5055 udp
                if (type == ContextType.Region)
                {
                    Log.Warn($"A region server tried to connect to BuzzMMO.Server.Master/ClientPeer");
                    var context = new PlayerContextMaster(_application, this);
                    context.Systems.Create<GameInstanceSystem>();
                    return context;
                }
    
                return null;
            }
        }
    }

    8. Edit BuzzMMO.Server.Master/RegionPeer.cs
    This is the entire file:
    Code:
    using BuzzMMO.Base;
    using BuzzMMO.Server.Master.Systems;
    using MMO.Server.Master.Systems.Region;
    using Photon.SocketServer;
    
    namespace BuzzMMO.Server.Master
    {
        //inbound region server connection
        public class RegionPeer : MMOInboundS2SPeerBase<ClientContext>
        {
            private readonly MasterServerContext _application;
    
            public RegionPeer(MasterServerContext application, InitRequest initRequest) : base(initRequest)
            {
                _application = application;
            }
    
            protected override ClientContext CreateContext(ContextType type, OperationRequest request)
            {
                //todo: we should do some authentication - eg get token from region server
                if (type == ContextType.Region)
                {
                    var context = new RegionServerContext(_application, this);
                    context.Systems.Create<GameInstanceSystem>();
                    return context;
                }
    
                return null;
            }
        }
    }

    9. Edit BuzzMMO.Server.Region/Application.cs
    This is the entire file:
    Code:
    using System;
    using System.IO;
    using System.Net;
    using BuzzMMO.Base;
    using BuzzMMO.Data;
    using BuzzMMO.Data.Entities;
    using ExitGames.Concurrency.Fibers;
    using Newtonsoft.Json;
    using Photon.SocketServer;
    using log4net;
    using log4net.Config;
    
    namespace BuzzMMO.Server.Region
    {
        public class Application : ApplicationBase
        {
            private const int ServerConnectTimeout = 5000;          //in mS
    
            private static readonly ExitGames.Logging.ILogger Log = ExitGames.Logging.LogManager.GetCurrentClassLogger();
    
            private readonly GameServerContext _application;
            private readonly IFiber _fiber;
    
            public Application()
            {
                _fiber = new ThreadFiber();
                _fiber.Start();
    
                var config = JsonConvert.DeserializeObject<Config>(File.ReadAllText(Path.Combine(BinaryPath, "Config.json")));
                _application = new GameServerContext(new SimpleSerializer(), config);
            }
    
            // only for incoming requests.
            protected override PeerBase CreatePeer(InitRequest initRequest)
            {
                return new ClientPeer(_application, initRequest);
            }
    
            //called automatically when application is first run
            protected override void Setup()
            {
                var gameServerConfig = _application.Config.GameServer;
                using (var database = new MMODatabaseContext())
                {
                    var server = database.GameServers.Find(gameServerConfig.UniqueId);
                    if (server != null)
                    {
                        server.Name = gameServerConfig.Name;
                    }
                    else
                    {
                        server = new GameServerEntity
                        {
                            Id = gameServerConfig.UniqueId,
                            Name = gameServerConfig.Name,
                            CreatedAt = DateTime.UtcNow
                        };
    
                        database.GameServers.Add(server);
                    }
    
                    database.SaveChanges();
                }
    
                InitLogging(gameServerConfig.Name);
    
                ConnectToMasterServer();
            }
    
    
            private void ConnectToMasterServer()
            {
                IPEndPoint endpoint;
                try
                {
                    var addressParts = _application.Config.MasterServer.UdpConnection.Address.Split(':');
                    endpoint = new IPEndPoint(IPAddress.Parse(addressParts[0]), int.Parse(addressParts[1]));        //assumes ip address in config.json, not dns name
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("Cannot parse Config.MasterServer.UdpConnection.Address (please specify it as <ip>:<port>", e);
                }
    
                Log.Info($"Connecting to Master Server: {_application.Config.MasterServer.UdpConnection}");
    
                var peer = new MasterServerPeer(this, _application); // Create a new instance of MasterServerPeer 
                if (peer.ConnectToServerUdp(endpoint, _application.Config.MasterServer.UdpConnection.ApplicationName, null, 1, null)) // If the connection succeeds 
                {
                    Log.Info($"Connected to master server {_application.Config.MasterServer.UdpConnection}"); // Log success 
                    peer.OnDisconnected += () => _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout); // attach our OnDisconnected event handler / delegate
                }
            }
    
            protected override void OnServerConnectionFailed(int errorCode, string errorMessage, object state)
            {
                Log.Error($"Could not connect to master server {_application.Config.MasterServer.UdpConnection}: {errorCode} / {errorMessage}");
                _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout);
            }
    
            //Log4Net setup on Region server
            protected virtual void InitLogging(string name)
            {
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
                GlobalContext.Properties["ServerName"] = name;
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }

    10. Delete BuzzMMO.Server.Region/Peer.cs


    11. Create BuzzMMO.Server.Region/ClientPeer.cs
    This is the entire file:
    Code:
    using Autofac;
    using BuzzMMO.Base;
    using BuzzMMO.Data.Services;
    using Photon.SocketServer;
    using log4net;
    
    namespace BuzzMMO.Server.Region
    {
        // clients connection to the region server
        public class ClientPeer : MMOClientPeerBase<ClientContext>
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(ClientPeer));
    
            private readonly GameServerContext _application;
    
            public ClientPeer(GameServerContext application, InitRequest initRequest) : base(initRequest)
            {
                _application = application;
            }
    
            protected override ClientContext CreateContext(ContextType type, OperationRequest request)
            {
                if (type == ContextType.Player)
                {
                    var gameId = (int)request[(byte)OperationParameter.GameId];
                    var token = (string)request[(byte)OperationParameter.Token];
    
                    Log.Info($"Player with token {token} is connecting to game {gameId}");
    
                    var gameEntityService = _application.Container.Resolve<GameEntityService>();
                    var playerInGame = gameEntityService.GetPlayerInGame(
                        _application.Config.GameServer.UniqueId,
                        gameId,
                        token);
    
                    if (playerInGame == null || playerInGame.HasAbandoned)
                    {
                        Disconnect();
                        return null;
                    }
    
                    var context = new PlayerContextGame(_application, this);
                    context.Login(playerInGame.Player, RemoteIP);           //found in MMOPeerBase
    
                    Log.Info($"Player from {RemoteIP} connected as {context.UserDetails.Username}");
    
                    _application.AddPlayerToGame(gameId, context)
                        .OnFail(Disconnect);
    
                    return context;
                }
    
                return null;
            }
        }
    }

    12. Edit BuzzMMO.Server.Region/MasterServerPeer.cs
    This is the entire file:
    Code:
    using System;
    using Autofac;
    using BuzzMMO.Base;
    using PhotonHostRuntimeInterfaces;
    
    namespace BuzzMMO.Server.Region
    {
        // connecting the region server to the master server
        public class MasterServerPeer : MMOOutboundS2SPeerBase<GameServerContext>
        {
            public MasterServerPeer(Application app, GameServerContext serverContext) : base(app, serverContext)
            {
            }
    
            public override ISystemBase CreateSystem(Type interfaceType, Func<Type, object> proxyFactory, out Type concreteType)
            {
                var registeredSystem = ServerContext.SystemTypeRegistry.GetSystemFromClientInterfaceType(interfaceType);
                var instance = Scope.Resolve(registeredSystem.ConcreteType);
                var proxy = proxyFactory(registeredSystem.ServerInterfaceType);
                ((IMasterSystemBase)instance).SetContext(ServerContext, proxy);
    
                concreteType = registeredSystem.ConcreteType;
                return (ISystemBase)instance;
            }
        }
    }

    ----------------------
    Am385 says this could be further refactored to move out the replicated parts of the BuzzMMO.Server/MMO*PeerBase.cs files. However, it's time for me to move on.

    At this point we have essentially Nelson's code updated to Photon 4 and a few bugs fixed.
    Last edited by oldngrey; 07-25-2017 at 05:53 AM.

  9. #9
    Join Date
    Feb 2014
    Posts
    272
    I notice that Unity 2017.1 was released last week. This isn't a beta. The big change for us is that we can now select "Experimental .NET 4.6 Equivalent" in the build settings for players.
    I've opened the buzzmmo client project in Unity's .net4.6 mode and saw that a c#6 string interpolation line compiles. The client project is still .net3.5, but it does show promise.
    No doubt this will become stable as time goes on. More feedback when we give it a more thorough test.

    Update:
    * recompiled all projects to target .net 4.6.1
    Only NewtonSoft.Json nuget package needed to be re-installed in the base project to force it to target .net 4.6.1
    * Unity accepted the buzzmmo.base.dll, buzzmmo.base.components.dll, and buzzmmo.client.unity as .net 4.6.1 dll assemblies.
    * Unfortunately I could not find a Photon .net 4.x version of Photon3Unity3D.dll or Photon3DotNet.dll. However, the client still compiled with the .net 3.5 dll from Photon and I was able to hit play and log into the master server and create a lobby.

    Note: Many of my previous code quotes have been updated and tested with .net4.6.1 for all projects. If you used code from earlier in this post, then it might be worth comparing to your code. The main change has been to remove my master server's use of TCP and also to read in the config.json files from the region project to determine the port they use. See the BuzzMMO.Server.Master/Application.cs file.
    Last edited by oldngrey; 07-25-2017 at 06:02 AM.

  10. #10
    Join Date
    Aug 2010
    Posts
    129
    .NET is backwards compatible, but not forwards compatible. Photon .NET 3.5 will work with 4.6.1 without issue.

    I have been running 4.6.2 to test on the Unity 2017 beta and I have yet to hit an issue. I have been recently planning on how to fragment this project so that we have a MMO Framework that will handle the communication layer as a rider on top of Photon (basically all of the projects that are bases. Server, Client, Base) and then a demo project that handles game logic.

    I am going to try and on work on this soon.

Page 1 of 2 12 LastLast

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •