Modding:Сетевой API

From Vintage Story Wiki
Revision as of 05:46, 21 September 2022 by Mirotworez (talk | contribs) (Created page with "Давайте начнем с создания нового файла .cs для этого мода и добавления нашего импорта и простран...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Эта страница проверялась в последний раз для версии Vintage Story 1.15.

Other languages:

Перед запуском у вас должна быть настроена среда разработки. Если у вас его еще нет, вам следует прочитать руководство Настройка среды разработки. Кроме того, мы предполагаем, что у вас есть базовые знания языка C# и объектно-ориентированного программирования. Давайте начнем!

Hint: если вам нужно отправить данные для Entities или BlockEntities, может быть более подходящим использовать для них встроенные сетевые методы. Ознакомьтесь с IServerNetworkAPI и IClientNetworkAPI для получения дополнительной информации.

Введение

В этом примере мы покажем вам, как отправлять пользовательские данные туда и обратно между сервером и клиентом. Для этого настроим сетевой канал, по которому в сетевом канале будут отправляться строковые сообщения.

Мы создадим серверную команду, которая отправит сообщение всем клиентам, и если клиент получит это, он отправит ответ обратно на сервер; после получения этого ответа сервер отобразит его вместе с именем игрока отвечающего клиента.

Подготовка

Давайте начнем с создания нового файла .cs для этого мода и добавления нашего импорта и пространства имен, в которое мы поместим все наши классы:

using System;
using ProtoBuf;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Common.Entities;
using Vintagestory.API.Config;
using Vintagestory.API.Server;

namespace Vintagestory.ServerMods
{

}

ProtoContracts

VintageStory uses the Protobuf-net library to serialize classes declared with the ProtoContract attribute.

Inside our namespace block, let's create our ProtoContract classes:

    [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
    public class NetworkApiTestMessage
    {
        public string message;
    }

    [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
    public class NetworkApiTestResponse
    {
        public string response;
    }

The argument we pass to ProtoContract makes it so all public fields will be serialized; without this, we'd declare fields for serialization by using ProtoMember attributes. If we chose this approach, we'd write our classes like so:

    [ProtoContract]
    public class NetworkApiTestMessage
    {
        [ProtoMember(1)]
        public string message;
    }

    [ProtoContract]
    public class NetworkApiTestResponse
    {
        [ProtoMember(1)]
        public string response;
    }

The argument we pass to ProtoMember is a unique unsigned integer called a tag, and it's used to identify the fields flagged for serialization. When the client receives the packet containing the serialized ProtoContract, it won't know the type of its members, and will identify them by the tag number. We use the ImplicitFields.AllPublic to flag all public fields for serialization, and tags are assigned in alphabetic order. ProtoBuf is beyond the scope of this tutorial, but you can read more about this powerful library here.

The fields in these classes hold the data we wish to send through our channel. We'll use NetworkApiTestMessage to encase the initial message from the server, while NetWorkApiTestResponse will be used by the Client to send its response.

NetworkApiTest

Now we will create our main class that inherits from the ModSystem class, which is the base class for mod systems. You can read more about this here. Moreover, we'll be separating our Client and Server sides within the class with region blocks for readability purposes.

    public class NetworkApiTest : ModSystem
    {
        #region Client
            //Client side here
        #endregion

        #region Server
            //Server side here
        #endregion 
    }

Next, we'll set up our network channel on the Server side, and register a server command to dispatch our message.

Server Setup

After declaring our Server side fields, we create an override for the StartServerSide method. This method of the ModSystem is called for all mods on the Server side by the game.

#region Server
        IServerNetworkChannel serverChannel;
        ICoreServerAPI serverApi;

        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            serverChannel =
                api.Network.RegisterChannel("networkapitest")
                .RegisterMessageType(typeof(NetworkApiTestMessage))
                .RegisterMessageType(typeof(NetworkApiTestResponse))
                .SetMessageHandler<NetworkApiTestResponse>(OnClientMessage)
            ;

            api.RegisterCommand("nwtest", "Send a test network message", "", OnNwTestCmd, Privilege.controlserver);
        }
#endregion

The Core Server API contains the Network member, which provides us with the RegisterChannel method. We pass this method a string containing the name we choose for our channel, in this case we'll call our channel "networkapitest". A channel with this exact name will be registered on the Client side.

We then register the ProtoContract classes that we'll be using to this channel with the RegisterMessageType method.

Finally, we call SetMessageHandler<T>, which takes a delegate that will be called every time a ProtoContract of type T is received from a client, in this case NetworkApiTestResponse will be handled by OnClientMessage which we'll declare later.

Now that our channel is set up on the Server side, we'll want to register a server command that will broadcast a network message to all the clients listening to our "networkapitest" channel.

#region Server
        IServerNetworkChannel serverChannel;
        ICoreServerAPI serverApi;

        public override void StartServerSide(ICoreServerAPI api)...

        private void OnNwTestCmd(IServerPlayer player, int groupId, CmdArgs args)
        {
            serverChannel.BroadcastPacket(new NetworkApiTestMessage()
            {
                message = "Hello World!",
            });
        }
#endregion

The server channel gives us the BroadcastPacket method which we'll use to send out a NetworkApiTestMessage instance to all clients listening to the "networkapitest" channel, in this case we send "Hello World!" as the message.

Client Setup

Above the Server Side region, we'll create our Client fields and an override for the StartClientSide, which is a Client side version of its Server side counterpart.

#region Client
        IClientNetworkChannel clientChannel;
        ICoreClientAPI clientApi;

        public override void StartClientSide(ICoreClientAPI api)
        {
            clientApi = api;

            clientChannel =
                api.Network.RegisterChannel("networkapitest")
                .RegisterMessageType(typeof(NetworkApiTestMessage))
                .RegisterMessageType(typeof(NetworkApiTestResponse))
                .SetMessageHandler<NetworkApiTestMessage>(OnServerMessage)
            ;
        }
#endregion

On the Client side, we set up the "networkapitest" channel much in the same way as we did on the Server side. The IClientNetworkChannel type is the client side equivalent of the IServerNetworkChannel, and shares much of the same functionality.

This time we call SetMessageHandler to listen for messages of type NetworkApiTestMessage, using a delegate named OnServerMessage.

#region Client
        IClientNetworkChannel clientChannel;
        ICoreClientAPI clientApi;

        public override void StartClientSide(ICoreClientAPI api)...

        private void OnServerMessage(NetworkApiTestMessage networkMessage)
        {
            clientApi.ShowChatMessage("Received following message from server: " + networkMessage.message);
            clientApi.ShowChatMessage("Sending response.");
            clientChannel.SendPacket(new NetworkApiTestResponse()
            {
                response = "RE: Hello World!"
            });
        }
#endregion

Here we simply show a chat message to the client with the contents of the message sent from the server.

We then let the client know that we're sending our response to the server, and call SendPacket passing it an instance of NetworkApiTestResponse.

Now let's go back to the Server side and create the handler for this response!

Handling Client Responses

In our Server side region, let's write a final delegate method:

#region Server
        IServerNetworkChannel serverChannel;
        ICoreServerAPI serverApi;

        public override void StartServerSide(ICoreServerAPI api)...

        private void OnNwTestCmd(IServerPlayer player, int groupId, CmdArgs args)...

        private void OnClientMessage(IPlayer fromPlayer, NetworkApiTestResponse networkMessage)
        {
            serverApi.SendMessageToGroup(
                GlobalConstants.GeneralChatGroup,
                "Received following response from " + fromPlayer.PlayerName + ": " + networkMessage.response,
                EnumChatType.Notification
            );
        }
#endregion

Message handlers on the server have an additional parameter compared to client handlers, which is the player whose client sent the message.

In this handler we simply broadcast a server-wide message containing the responding player's name and the content of the response.

Conclusion

If you followed the steps correctly, you should have the following code:

using System;
using ProtoBuf;
using Vintagestory.API.Client;
using Vintagestory.API.Common;
using Vintagestory.API.Common.Entities;
using Vintagestory.API.Config;
using Vintagestory.API.Server;

namespace Vintagestory.ServerMods
{
    [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
    public class NetworkApiTestMessage
    {
        public string message;
    }

    [ProtoContract(ImplicitFields = ImplicitFields.AllPublic)]
    public class NetworkApiTestResponse
    {
        public string response;
    }

    /// <summary>
    /// A basic example of client<->server networking using a custom network communication
    /// </summary>
    public class NetworkApiTest : ModSystem
    {
        #region Client
        IClientNetworkChannel clientChannel;
        ICoreClientAPI clientApi;

        public override void StartClientSide(ICoreClientAPI api)
        {
            clientApi = api;

            clientChannel =
                api.Network.RegisterChannel("networkapitest")
                .RegisterMessageType(typeof(NetworkApiTestMessage))
                .RegisterMessageType(typeof(NetworkApiTestResponse))
                .SetMessageHandler<NetworkApiTestMessage>(OnServerMessage)
            ;
        }

        private void OnServerMessage(NetworkApiTestMessage networkMessage)
        {
            clientApi.ShowChatMessage("Received following message from server: " + networkMessage.message);
            clientApi.ShowChatMessage("Sending response.");
            clientChannel.SendPacket(new NetworkApiTestResponse()
            {
                response = "RE: Hello World!"
            });
        }

        #endregion

        #region Server
        IServerNetworkChannel serverChannel;
        ICoreServerAPI serverApi;

        public override void StartServerSide(ICoreServerAPI api)
        {
            serverApi = api;

            serverChannel =
                api.Network.RegisterChannel("networkapitest")
                .RegisterMessageType(typeof(NetworkApiTestMessage))
                .RegisterMessageType(typeof(NetworkApiTestResponse))
                .SetMessageHandler<NetworkApiTestResponse>(OnClientMessage)
            ;

            api.RegisterCommand("nwtest", "Send a test network message", "", OnNwTestCmd, Privilege.controlserver);
        }

        private void OnNwTestCmd(IServerPlayer player, int groupId, CmdArgs args)
        {
            serverChannel.BroadcastPacket(new NetworkApiTestMessage()
            {
                message = "Hello World!",
            });
        }

        private void OnClientMessage(IPlayer fromPlayer, NetworkApiTestResponse networkMessage)
        {
            serverApi.SendMessageToGroup(
                GlobalConstants.GeneralChatGroup,
                "Received following response from " + fromPlayer.PlayerName + ": " + networkMessage.response,
                EnumChatType.Notification
            );
        }

        
        #endregion
    }
}

Testing

Let's run the mod now! Once you're ingame, try entering /nwtest. You should be met with an initial "Hello World!" message as well as a confirmation message that the server received the response from the Client!


Distribution

To distribute this mod, you may run the following command in the modtools cli pack <your mod id>, then copy the .zip file into your VintageStory mods folder.

Here are the official versions:

Icon Sign.png

Wondering where some links have gone?
The modding navbox is going through some changes! Check out Navigation Box Updates for more info and help finding specific pages.

Modding
Modding Introduction Getting Started Пакет тем
Content Modding Content Mods Developing a Content Mod Basic Tutorials Intermediate Tutorials Advanced Tutorials Content Mod Concepts
Code Modding Code Mods Setting up your Development Environment
Property Overview ItemEntityEntity BehaviorsBlockBlock BehaviorsBlock ClassesBlock EntitiesBlock Entity BehaviorsCollectible BehaviorsWorld properties
Workflows & Infrastructure Modding Efficiency TipsMod-engine compatibilityMod ExtensibilityVS Engine
Additional Resources Community Resources Modding API Updates Programming Languages List of server commandsList of client commandsClient startup parametersServer startup parameters
Example ModsAPI DocsGitHub Repository