Page 1 of 2 12 LastLast
Results 1 to 10 of 16
  1. #1
    Join Date
    Aug 2010
    Posts
    127

    Quick fix to get the MMO Project working again

    If you have followed Nelson's videos till the end you know that we are left in a broken state so I took 30 minutes last night to see what was going on. Let me preface this by saying it is a quick hack to get the project running again emulating the behavior of Photon 3.x from the Master Servers prospective. In version 4.x of Photon, servers should communicate with each other through the "S2SPeerBase" derived classes instead of the "ClientPeerBase" derived classes.

    In the last video we upgraded to Photon 4.x and changed the Region Server to use an implementation of the "OutboundS2SPeer" class. The correct way to fix this would be on the Master Server side to use an implementation of the "InboundS2SPeer" class and to change the Master Server "Application" class to switch on the port that the connection is made on in the "CreatePeer" method. Here is an example of what I mean.

    THIS FIRST BLOCK IS AN EXAMPLE!! YOU DO NOT NEED TO CHANGE THIS CODE BLOCK!! IT WILL STILL WORK WITH THESE CHANGES THOUGH!!
    Code:
    using log4net;
    using log4net.Config;
    using MMO.Base;
    using Photon.SocketServer;
    using System.IO;
    
    namespace MMO.Server.Master
    {
        public class Application : ApplicationBase
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(Application));
    
            private readonly MasterServerContext _application;
    
            public Application()
            {
                _application = new MasterServerContext(new SimpleSerializer());
            }
    
            protected override PeerBase CreatePeer(InitRequest initRequest)
            {
                // Check which peer is connecting here based on the incoming port.
                switch (initRequest.LocalPort)
                {
                    case 5055:
                        return new Peer(_application, initRequest);
                    case 5065: // some other port that is only accessible to region servers calling into the master server
                        //return new ClassThatInheritsFromInboundS2SPeer(_application, initRequest);                    
                    default:
                        return null; // Should log an expection for this connection or create a peer and quick disconnect them
                }
            }
    
            protected override void Setup()
            {
                GlobalContext.Properties["ServerName"] = "Master";
                XmlConfigurator.ConfigureAndWatch(new FileInfo(Path.Combine(BinaryPath, "log4net.config")));
                ExitGames.Logging.LogManager.SetLoggerFactory(ExitGames.Logging.Log4Net.Log4NetLoggerFactory.Instance);
            }
    
            protected override void TearDown()
            {
                _application.Dispose();
            }
        }
    }


    START QUICK FIX!!

    We have to open the connection in the Region Server's "Application" class like before, but this time using the OutboundS2SPeer derived class ("MasterServerPeer"). This is all handled in the last 10 lines of the "ConnectToMasterServer" method.

    Next we need to get rid of the "CreateServerPeer" method as it is no longer called. Basically we are moving this logic into the if block of our connection attempt.

    I have removed the rest of the code for brevity.
    Code:
    using log4net;
    using log4net.Config;
    using MMO.Base;
    using Newtonsoft.Json;
    using Newtonsoft.Json.Linq;
    using Photon.SocketServer;
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    using Photon.SocketServer.ServerToServer;
    using System.Net;
    using ExitGames.Concurrency.Fibers;
    using MMO.Data;
    using MMO.Data.Entities;
    
    namespace MMO.Server.Region
    {
        public class Application : ApplicationBase
        {
            ...
    
            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]));
    
                }
                catch (Exception e)
                {
                    throw new InvalidOperationException("Cannot parse Config.MasterServer.UdpConnection.Address", e);
                }
    
                //ConnectToServerUdp(endpoint, _application.Config.MasterServer.UdpConnection.ApplicationName, null, 1, null); // Old connection method
    
                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
                }
            }
    
            // THIS IS NOT NEEDED FOR THIS
            //protected override S2SPeerBase CreateServerPeer(InitResponse initResponse, object state)
            //{
            //    Log.Info($"Connected to master server {_application.Config.MasterServer.UdpConnection}");
            //    //var peer = new MasterServerPeer(this, _application);
            //    //peer.OnDisconnected += () => _fiber.Schedule(ConnectToMasterServer, ServerConnectTimeout);
    
            //    return peer;
            //}
    
            ...
        }
    }



    Now that we are connecting at a difference point we need to have the Region Server's "MasterServerPeer" class initiate the "InitContext " request at a different point.

    I have removed the rest of the code for brevity.
    Code:
    using Autofac;
    using log4net;
    using MMO.Base;
    using MMO.Base.Async;
    using MMO.Client.Infrastructure;
    using MMO.Client.Systems;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using PhotonHostRuntimeInterfaces;
    using System;
    using System.Collections.Generic;
    
    namespace MMO.Server.Region
    {
        public class MasterServerPeer : OutboundS2SPeer, ISystemFactory, IClientTransport
        {
            ...
    
            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));
    
                // Move the InitContext request to the OnConnectionEstablished method
                //SendOperation(OperationCode.InitContext, new Dictionary<byte, object> { [(byte)OperationParameter.ContextType] = ContextType.Region });
    
                application.OnDiposed += OnApplicationDisposed;
            }
    
            ...
    
            protected override void OnConnectionEstablished(object responseObject)
            {
                // Here is where we should do that InitContext request
                SendOperation(OperationCode.InitContext, new Dictionary<byte, object> { [(byte)OperationParameter.ContextType] = ContextType.Region });
            }
    
            protected override void OnConnectionFailed(int errorCode, string errorMessage)
            {
            }
        }
    }

  2. #2
    Join Date
    Feb 2015
    Location
    Turkiye
    Posts
    12
    Thanks, i've completed the MMO series and i was wondering how am i achieve upgrading photon version 4. As soon as possible, i'll apply the changes you share. If i find any other improvement, i'll share it too. The Project (even the state of "not completed") seems promising. But it needs some refactoring, changing some techniques, changing some code. I've learnt a lot from it and am considering continuing as much as possible.

  3. #3
    Join Date
    Feb 2014
    Posts
    246
    Ah thanks. At a glance only 2 files need to change from the last Nelson video! Thanks for caring enough to help us
    I will go ahead and complete the vids to the end - I had stopped at the end of the downloadable zips he released.

    After that I will try to get the release version working and see how the newer version of TeamCity copes.

    As an aside I am interested in exactly what Nelson is delivering as his final deliverable. We just don't know how close it will be to the playtest session or if it's just source code of what we already have.

  4. #4
    Join Date
    Feb 2015
    Location
    Turkiye
    Posts
    12
    For those who pursuing personal MMO, i've found very similar free content (photon, unity) : https://www.youtube.com/user/cjrgaming.

  5. #5
    Join Date
    Aug 2010
    Posts
    127
    CJR (Christian Richards) was actually a member here who was displeased with the speed of the MMO class years ago. He went off to build his own. He had some life troubles that caused his project to freeze for a long time too. Haven't checked back on him in a while.

  6. #6
    Join Date
    Feb 2014
    Posts
    246
    Am385, the Region server changes seem fine to me. How would you improve them - you said it was a quick fix? (I did use TCP for the conenction though - see below)

    The Photon.SocketServer api's are not that easy for me to follow. I don't know how you find your way around their documentation! The chm file doesn't search well. It wasn't clear to me that CreateServerPeer wasn't called any more. I had to put a Log statement in there to prove you were right.
    Do you use dotpeek like Nelson to find methods and comments inside Photon.SocketServer as your main source of documentation or do you use the chm files included in the photon sdk, or do you browse the loadbalancing demo project? Maybe all 3...

    I notice in the Photon loadbalancing demo they use the port to decide what's connecting just as you do. However since all connections to the server are on port 5055, your code seems to not work - for me at least. (Edit: I note the loadbalancing demo uses tcp for incoming server connections on 4520.... when I tried that, it did work. So I now have clients connecting via udp on port 5055 and servers connect via tcp on port 4520. It kinda seems like a cheap trick though).

    However, the Master server application.cs is making me think of bigger issues.
    Creating a new class alongside Peer to handle the region server inbound connection is making me wonder if it's going to be getting unwieldy. I have only put in your fixes so far and updated the logging factory. So how would you go about sorting out the Server.Master/Application.cs class in a way that satisfies you? I hope my comment on the server connection was useful.
    Last edited by oldngrey; 05-22-2017 at 12:45 AM.

  7. #7
    Join Date
    Feb 2014
    Posts
    246
    Hmm maybe am385 is a bit absent at the moment... darn it.

  8. #8
    Join Date
    Aug 2010
    Posts
    127
    Hey,
    Sorry for my delay in response. I have been super busy at work and home. We had our feature complete deadline recently at work so there were tons of build breaks and people trying to party on the development branch. On top of that I have a 10 month old at home. Basically I only get an hour or two a week if I am lucky to work on projects, play games, and things like that.


    I think what I would do to re-architect these issues with the Peer classes and the ClientContext classes is create new MMOPeerBase classes.
    First off we have some terminology issues. Server/Client can be confusing because the region server is both a client and a server. It is a client to the MasterServer and a server to the "Game" clients. This means we need to thing about a better naming convention to use to better differentiate them. I won't touch this now though.

    As far as the classes I mentioned. I would probably do the following.

    New MMOPeerBase classes (for now these are copy/paste of the MMOPeerBase classes with tiny changes. I havent changed logic with the exception of the OutboundS2S constructor needs an ApplicationBase. Other logic will change, but just as a jumping off point)
    Code:
        // Old MMOPeerBase 
        public abstract class MMOPeerBase<TContext> : ClientPeer, IServerTransport
            where TContext : ClientContext
    
        // Change MMOPeerBase to this to handle Clients
        public abstract class MMOClientPeerBase<TContext> : ClientPeer, IServerTransport
            where TContext : ClientContext
    
        // New Class to handle Inbound Server 2 Server Peers
        public abstract class MMOInboundS2SPeerBase<TContext> : InboundS2SPeer, IServerTransport
            where TContext : ClientContext
    
        // New Class to handle Outbound Server 2 Server Peers
        public abstract class MMOOutboundS2SPeerBase<TContext> : OutboundS2SPeer, IServerTransport
            where TContext : ClientContext
    Then I would replace any place where the MMOPeerBase is referenced with its appropriate version. Refactor the MMO.Server.Region.MasterServerPeer to inherit from the new MMOOutboundS2SPeerBase

    Now we can then move our logic on what is connecting at the application level instead of just the context level. We can also keep some safe checking logic in the context level too.

  9. #9
    Join Date
    Feb 2014
    Posts
    246
    Nice start. Thanks.

    I haven't got the Region/MasterServerPeer to successfully inherit from MMOOutboundS2SPeerBase yet, but the rest of the small changes do work, mainly because they were trivial

    I had already renamed my Master/Peer.cs to ClientPeer.cs and added a RegionPeer.cs class. Names weren't too hard to come up with. There is a peer.cs in the Region project that might best be renamed too - maybe another ClientPeer.cs.

    Hopefully you might have some time in the near future to spend more than a few minutes looking at the extensive changes required for MasterServerPeer and MMOOutboundS2SPeerBase and even the various Context files. Do we actually need MMOOutboundS2SPeerBase?

    I've raised kids. Know what you are going through. Good fun on the whole!

  10. #10
    Join Date
    Aug 2010
    Posts
    127
    I would think an MMOOutboundS2SPeerBase would be a great idea as it creates the basis for multiple server types that are communicating to each other. This just pulls what appears to be common code out of the MMO.Server.Region.MasterServerPeer class and moves it one level down to where it can be re-used with multiple server types.

    I haven't compiled or tested this code, just a quick copy paste refactor. But this is the direction I would start heading in.

    Code:
    using MMO.Base;
    using MMO.Client.Infrastructure;
    using MMO.Client.Systems;
    using Photon.SocketServer;
    using Photon.SocketServer.ServerToServer;
    using PhotonHostRuntimeInterfaces;
    using System;
    using System.Collections.Generic;
    using MMO.Base.Async;
    using Microsoft.Extensions.DependencyInjection;
    
    namespace MMO.Server
    {
        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 IServiceScope _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.CreateScope();
                _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.OnDiposed += OnApplicationDisposed;
            }
    
            #region Methods
            protected void HandleSystemCallbak(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.OnDiposed -= 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)
            {
                HandleSystemCallbak((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
        }
    }
    Code:
    using log4net;
    using MMO.Base;
    using PhotonHostRuntimeInterfaces;
    using System;
    
    namespace MMO.Server.Region
    {
        public class MasterServerPeer : MMOOutboundS2SPeerBase<GameServerContext>
        {
            private static readonly ILog Log = LogManager.GetLogger(typeof(MasterServerPeer));
            
            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.ServiceProvider.GetService(registeredSystem.ConcreteType);
                var proxy = proxyFactory(registeredSystem.ServerInterfaceType);
                ((IMasterSystemBase)instance).SetContext(_serverContext, proxy);
    
                concreteType = registeredSystem.ConcreteType;
                return (ISystemBase)instance;
            }
    
            protected override void OnDisconnect(DisconnectReason reasonCode, string reasonDetail)
            {
                Log.Info($"MasterServerPeer Disconnect - {reasonCode}: {reasonDetail}");
                base.OnDisconnect(reasonCode, reasonDetail);
            }
        }
    }

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
  •