How to create a new system

Topic created ยท 1 Posts ยท 98 Views
  • You would like to contribute to Rhisis by creating new systems ? Well, you are in right place! This guide will help you find all the informations and tricks you need to develop your own system from scratch.

    You first need to read the Contribution Guidelines in order to have the sources and the repository ready.

    Next thing you need, is to know which system you want to create and add to Rhisis. Let's say you want to create the ChatSystem to be able to talk to other players around you. You will need to identify the packet header that the server receives and process it correctly in order to send information back to the clients.

    โš  The chat system is already done and used as an exemple. If you want to do it again, you can delete all the files/folders related to the chat which are the following:

    • Handlers/ChatHandler.cs
    • System/Chat/
    • Packets/ChatPacket.cs

    Now, let's get started.

    Basics

    You first need to understand the basics of the communication between a client and a server.
    Let's take a simple exemple. You enter a restaurant, the waiter welcomes you and gives you the menu. Then you choose what you want to eat/drink and you give the order to the waiter. He goes back to the kitchen and brings you your drink and food. Then you, eat, drink, pay, and say goodbye.

    In this exemple, you are the Client and the waiter is the Server. You simply ask for something, and the server answers you.

    In FlyFF, the communication between the client and the server is made using a socket and the TCP protocol. If you want more informations, Wikipedia explains it very well : TCP IP Wikipedia

    All of this process is already done in the client side and in the server side (Rhisis). You don't need to create it from scratch, but it's always good to understand what we are talking about ๐Ÿ˜‰

    So, the client sends us a request, we are going to handle and processes it and then we will send the answer to the client.

    Handle the incoming request

    When you are connected to Rhisis server, sometimes you receive this warning message in the console:

    [Warning] - Unimplemented packet CHAT(0x00FF0000)
    

    This message tells you that the server has received a packet named CHAT with the header : 0x00FF0000 and it has been ignored by the server and therefore, not handled.

    Before going further, let's explain what is a packet. A packet is "just" a message sent by the client to the server or from the server to the client. This message has an identifier that we call "header". It is used to differentiate messages. For exemple, if the server receives the CHAT header, we will know, just by looking at it that it process the chat action, in the other case if we receive the BATTLE header, it will process an action related to the battle system.

    You can visualize a packet like this image:

    packet

    We can see that the packet (in black) has an header (in blue) and some content (in red).

    Note: All packet header are located in Rhisis.Core/Network/Packets/PacketType.cs.

    To handle the chat packet, you will need to create a new file inside the Handlers folder of the Rhisis.World project. Inside this file create a static class and name it ChatHandler.

    using System;
    
    namespace Rhisis.World.Handlers
    {
        public static class ChatHandler
        {
        }
    }
    

    Next step will be to tell the world server to do an action when he receives the CHAT packet. To do that, you just need to create a static method that takes two parameters : WorldClient and INetPacketStream. We pass this parameters so we can know which client received the packet, and the INetPacketStream that contains the informations sent by the client.

    using Ether.Network.Packets;
    using Rhisis.Core.Network;
    using Rhisis.Core.Network.Packets;
    
    namespace Rhisis.World.Handlers
    {
        public static class ChatHandler
        {
            [PacketHandler(PacketType.CHAT)]
            public static void OnChat(WorldClient client, INetPacketStream packet)
            {
            }
        }
    }
    

    Thanks to the PacketHandler attribute, each time the server will receive the PacketType.CHAT packet, it will call the OnChat method and execute it. This is the entry point of the incoming packet.

    Now that our World server receives and handles the CHAT packet, we will have to read the content of the packet. To know the content of the packet, I usually go to the official files with the packet header ID (0xNNNNNN) or header name and search for what the Neuz sends to the server. For exemple, I have searched PACKETTYPE_CHAT within the official files and found what the Neuz was sending when we type a message on the chat.

    void CDPClient::SendChat( LPCSTR lpszChat )
    {
    	BEFORESENDSOLE( ar, PACKETTYPE_CHAT, DPID_UNKNOWN );
    	ar.WriteString( lpszChat );
    	SEND( ar, this, DPID_SERVERPLAYER );
    }
    

    In the BEFORESENDSOLE instruction it will just say that the PACKETTYPE_CHAT is the packet header. (Blue part in the previous image)
    What we want from this method is the content of the packet located between the BEFORESENDSOLE and SEND instruction. In our case we only have the following lines:

    ar.WriteString( lpszChat );
    

    This line will write the content of lpszChat as a string inside the packet content (Red part in the image).

    Alright, we know that the Neuz send the PACKETTYPE_CHAT header along with a single string as content. Now, let's go back to our handler and read the message from the Neuz.
    To do so, we are going to use the INetPacketStream.Read<T>() method.

    using Ether.Network.Packets;
    using Rhisis.Core.Network;
    using Rhisis.Core.Network.Packets;
    
    namespace Rhisis.World.Handlers
    {
        public static class ChatHandler
        {
            [PacketHandler(PacketType.CHAT)]
            public static void OnChat(WorldClient client, INetPacketStream packet)
            {
                string chatMessage = packet.Read<string>();
            }
        }
    }
    

    In this code, we simply had a line that reads a string from the packet variable and store it into the chatMessage. To check if it's the correct message from the client, you can add a Console.WriteLine(chatMessage); under the packet.Read<string>() line.

    Pretty simple right ? With only a few lines of code, we can handle incoming data from the client.
    From now on, that's where the fun starts, we are going to create the system that will contain the game logic.

    Create the system

    In Rhisis context, a system is where the game logic is written and executed. All systems are located in the Systems folder of the Rhisis.World project.
    Before we get started with our system, you must know that there is two types of systems:

    • Updatable : A system that is executed at every game tick on all map layers.
    • Notifiable : A system that can be called and executed by an entity.

    When you create a new system for Rhisis, you must know which type of system it is. Ask yourself this question : "How am I going to call the game logic ?" If it's related to the player, then it should be a Notifiable system. If it's related to the map, environment, time, then it should be a Updatable system.

    In our case, we want to write a system that interacts with the player. Our ChatSystem will then be : Notifiable.

    Let's get started. To create a new system, create a new folder in the Systems folder of the Rhisis.World project; let's name it "Chat".

    Then, create a new class named ChatSystem. Once you have your ChatSystem class, make it inherit from the ISystem interface.

    using Rhisis.World.Game.Core;
    using Rhisis.World.Game.Core.Systems;
    
    public class ChatSystem : ISystem
    {
        // This property indicates which type of entities can execute this system.
        // In our case, only players can chat.
        public WorldEntityType Type => WorldEntityType.Player;
    
        public void Execute(IEntity entity, SystemEventArgs e)
        {
            // TODO: Chat Game Logicc
        }
    }
    

    โ„น As you can see, you must implement the Execute method and Type property.
    The Execute method takes an IEntity as first parameters which will be the entity that is calling the system; and then takes a SystemEventArgs as second parameter.
    The SystemEventArgs is used to pass aditional arguments to the system. To create a new system arguments, you must create a new class and inherit from SystemEventArgs.

    Good, now let's mention that the ChatSystem class is a Rhisis system and give him a system type.

    using Rhisis.World.Game.Core;
    using Rhisis.World.Game.Core.Systems;
    
    [System(SystemType.Notifiable)]
    public class ChatSystem : ISystem
    {
        public WorldEntityType Type => WorldEntityType.Player;
    
        public void Execute(IEntity entity, SystemEventArgs e)
        {
            // TODO: Chat Game Logic
        }
    }
    

    Alright, we have our system ready, let's start writing the game logic. First question is, what do we need for this system ? We already have the entity that is calling the system, and we now need the chat text to send to every players around.

    As I mentioned earlier, we will need to create a new system arguments class. To do so, let's create a new class named ChatEventArgs and make it inherit from SystemEventArgs.

    This new class will need to store the chat message, so you will need to create a new readonly property and a constructor that takes one parameter : the chat message.

    using Rhisis.World.Game.Core.Systems;
    
    public sealed class ChatEventArgs : SystemEventArgs
    {
        public string Message { get; }
        
        public ChatEventArgs(string message)
        {
            this.Message = message;
        }
        
        public override bool CheckArguments() => !string.IsNullOrEmpty(this.Message);
    }
    

    With this class, we will now be able to store the chat message and pass it to the ChatSystem's Execute method.

    Let's go back to the ChatSystem class and check if the system event it is a ChatEventArgs and if his arguments are correct.

    using Rhisis.World.Game.Core;
    using Rhisis.World.Game.Core.Systems;
    
    [System(SystemType.Notifiable)]
    public class ChatSystem : ISystem
    {
        public WorldEntityType Type => WorldEntityType.Player;
    
        public void Execute(IEntity entity, SystemEventArgs e)
        {
            if (!(e is ChatEventArgs chatEvent)) // Check if e is type of ChatEventArgs
                return;  // KO: e is not a ChatEventArgs
            if (!chatEvent.CheckArguments())
                return; // KO: arguments are invalid.
    
            // OK: e is a ChatEventArgs and arguments are valid.
        }
    }
    

    Once we checked the event type and the arguments, we know that our parameters are valid ang we can now write the game logic.

    โ„น The ChatSystem can send messages to every player around the player that is calling the system, or process chat commands. For this guide, we will only handle the normal chat message.

    First of all, we are going to check if the player's message start with a /. If so, it's a chat command and will not be covered by this guide. If not, we will send the message to every player around.

    using Rhisis.World.Game.Core;
    using Rhisis.World.Game.Core.Systems;
    
    [System(SystemType.Notifiable)]
    public class ChatSystem : ISystem
    {
        public WorldEntityType Type => WorldEntityType.Player;
    
        public void Execute(IEntity entity, SystemEventArgs e)
        {
            if (!(e is ChatEventArgs chatEvent))
                return; 
            if (!chatEvent.CheckArguments())
                return; 
    
            if (chatEvent.Message.StartsWith("/"))
            {
                // It's a chat command. Not covered by the guide.
            }
            else
            {
                // TODO: send message to every player around.
            }
        }
    }
    

    Alright, let's summarize what we have done so far.

    1. Create the ChatSystem class and make it notifiable.
    2. Create the ChatEventArgs that stores the received chat message and use it in the system logic
    3. Check the system event type and argument
    4. Check if the message is a command or normal message.

    Next step is to send to every player around the system calling player the chat message. To do so, we will need to create a packet.

    โ„น All packets files are located in the Packets folder of the Rhisis.World project.

    Go to the Packets folder and create a new file named ChatPackets. This file will contain every packets related to the ChatSystem. Remove the ChatPackets class and replace it with public static partial class WorldPacketFactory.

    โ„น This will say that we are sharing the every method contained into the partial WorldPacketFactory class.

    Now, create a static method that takes the 2 parameters :

    • IEntity entity : the current entity sending the packet.
    • string message : the message to send.
    public static partial class WorldPacketFactory
    {
        public static void SendChat(IEntity entity, string message)
        {
        }
    }
    

    Just like the incoming packet, to know what the client is expected, let's make a quick search throught the official files and search for the every Chat occurence. After some minutes of search, we found the CUserMng::AddChat() method that looks like what we are searching for.

    void CUserMng::AddChat( CCtrl* pCtrl, const TCHAR* szChat )
    {
        CAr ar;
    
        ar << GETID( pCtrl ) << SNAPSHOTTYPE_CHAT;
        ar.WriteString( szChat );
        ...
    }
    

    If we take a close look, we see that the packet is storing the control id, which is in Rhisis context, the entity id. Then it stores the snapshot type, which is SNAPSHOTTYPE_CHAT.
    Finally, it writes the szChat string into the packet and then send to every player around.

    Alright, let's do this in C#.

    public static partial class WorldPacketFactory
    {
        public static void SendChat(IEntity entity, string message)
        {
            using (var packet = new FFPacket())
            {
                packet.StartNewMergedPacket(entity.Id, SnapshotType.CHAT);
                packet.Write(message);
    
                SendToVisible(packet, entity, sendToPlayer: true);
            }
        }
    }
    

    As you can see, we create a new FFPacket instance and start a new packet with the entity.Id and the SnapshotType.CHAT just like the official files. Then, we write the message into the packet and finally, we send it to every visible players around entity. Note that we also send the packet to the entity (specified with the sentToPlayer: true flag).

    Our packet seems to be correct, let's go back to the ChatSystem and call the SendChat() method we created to send the chat message.

    using Rhisis.World.Game.Core;
    using Rhisis.World.Game.Core.Systems;
    using Rhisis.World.Packets;
    
    [System(SystemType.Notifiable)]
    public class ChatSystem : ISystem
    {
        public WorldEntityType Type => WorldEntityType.Player;
    
        public void Execute(IEntity entity, SystemEventArgs e)
        {
            if (!(e is ChatEventArgs chatEvent))
                return; 
            if (!chatEvent.CheckArguments())
                return; 
    
            if (chatEvent.Message.StartsWith("/"))
            {
                // It's a chat command. Not covered by the guide.
            }
            else
            {
                WorldPacketFactory.SendChat(entity, chatEvent.Message);
            }
        }
    }
    

    Good, our system is now ready!

    Assembling Handler and System

    Now that we have our system ready, we need to call it whenever we receive a CHAT packet. Open your ChatHandler.cs file, create a new ChatEventArgs by passing it the message and call the ChatSystem.

    using Ether.Network.Packets;
    using Rhisis.Network;
    using Rhisis.Network.Packets;
    using Rhisis.Network.Packets.World;
    using Rhisis.World.Systems.Chat;
     
    namespace Rhisis.World.Handlers
    {
        public static class ChatHandler
        {
            [PacketHandler(PacketType.CHAT)]
            public static void OnChat(WorldClient client, INetPacketStream packet)
            {
                string chatMessage = packet.Read<string>();
                var chatEvent = new ChatEventArgs(chatPacket.Message);
    
                client.Player.NotifySystem<ChatSystem>(chatEvent);
            }
        }
    }
    

    And that's it. Whenever you receive the PacketType.CHAT packet, the handler will notify the ChatSystem and execute its game logic.


    Congratulations, you just created a Rhisis system! ๐ŸŽ‰

    It wasn't that hard thought! ๐Ÿ˜

    I hope you enjoyed creating a system on Rhisis, and don't hesitate to ask for help if you want to create a system an contribute to the emulator.

    All the best,
    Eastrall.