Code Monkey home page Code Monkey logo

enchcoreapi.trprotocol's Introduction

EnchCoreApi.TrProtocol GitHub Workflow Nuget (with prereleases) License: GPL v3

An efficient terraria packet serializer and partial protocol implement.


  • This project automatically builds efficient and unsafe working code through IIncrementalGenerator.
    In addition, it provides an additional patch for OTAPI, by extracting some shared types to achieve friendly compatibility with OTAPI.
    Therefore, by using this specially designed OTAPI , the data structures provided by the protocol library can be used directly in terraria's server programs.

  • This project is based on the protocol data structures from another project by chi-rei-den. I have used their data structures as a reference and modified them according to my own needs. I would like to acknowledge and appreciate their work and contribution. You can find their original project at TrProtocol.


Installation EnchCoreApi.TrProtocol

  • You can find and install it in nuget package manager, or you can install it directly from the nuget command line
PM> NuGet\Install-Package EnchCoreApi.TrProtocol -Version 1.0.3
  • Please use version 1.0.2-beta1 or later version because 1.0.2-alpha1 or earlier version may resolves a bug that caused some fields to be serialized incorrectly due to missing conditionals.

Usage

To use EnchCoreApi.TrProtocol, you need to add a reference to the namespace EnchCoreApi.TrProtocol, EnchCoreApi.TrProtocol.NetPackets, EnchCoreApi.TrProtocol.Models .etc

using EnchCoreApi.TrProtocol;
using EnchCoreApi.TrProtocol.Models;
using EnchCoreApi.TrProtocol.NetPackets;
// create packet from given parammeters
var packet = new CombatTextInt(Vector2.Zero, Color.White, 100);
// create packet from buffer
fixed (void* ptr = buffer) {
    var ptr_current = Unsafe.Add<byte>(ptr_current, offset);
    var packet = new CombatTextInt(ref ptr_current);
}

Description of Terraria packet structure

Before we proceed, let us first understand how Terraria handles packet receiving.

  • For the server, it uses a separate thread to run the 'Neplay.ServerLoop' method, which calls the 'Neplay.UpdateConnectedClients' method to constantly read data from each client's network stream and copy it to the corresponding buffer 'MessageBuffer.readBuffer'.
  • In the main game loop, which updates at 60 frames per second, the 'Main.UpdateClientInMainThread' method will access the buffer and process all the temporary data in one go.
  • Therefore, it is possible that the buffer contains multiple packets at a time. To solve the problem of packet fragmentation, Terraria will prepend a short value to each packet to indicate its size. We can call this value the packet header. Note that the short value also includes its own two-byte length.

Example packet structure of CombatTextInt (81)

  • this structure in the partocol libarry looks like this.
public partial class CombatTextInt : NetPacket {

    public sealed override MessageID Type => MessageID.CombatTextInt;

    public Vector2 Position;
    public Color Color;
    public int Amount;
}
  • this structure in binary looks like this.
CombatTextInt (ID=81)
index 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
name packet
header
packet content (Size=16)
Type Position Color Amount
field type short MessageID Vector2 Color int32
real type short byte float float byte byte byte int32
value 18 81 X Y R G B num

Note

Serialize

  • Under normal circumstances, the protocol library does not need to know the packet header in serialization. Therefore, when using 'NetPacket.WriteContent (ref void*)', the user only needs to pass in the pointer to the binary data that represents Type. This is the pointer at index=2 in the diagram table. The protocol library then writes the content of the packet to the pointer position and adds the offset that was written back to the ref void* pointer.
  • So if you want to send a complete packet, your code should probarbly be written like this: click here

Deserialize

  • In deserialization, however, there is a notable problem. In Terraria, some packets need to be resolved based on the state of the game, such as the 'NetCreativePowersModule' packet. This packet is used to synchronize the creative powers of the players in the game. In Terraria server, it calls the function 'APerPlayerTogglePower.Deserialize_SyncEveryone', which has the following code:
// Terraria.GameContent.Creative.CreativePowers.APerPlayerTogglePower

public void Deserialize_SyncEveryone(BinaryReader reader, int userId) {
    int num = (int)Math.Ceiling((float)_perPlayerIsEnabled.Length / 8f);
    if (Main.netMode == 2 && !CreativePowersHelper.IsAvailableForPlayer(this, userId)) {
        reader.ReadBytes(num);
        return;
    }
    for (int i = 0; i < num; i++) {
        BitsByte bitsByte = reader.ReadByte();
        for (int j = 0; j < 8; j++) {
            int num2 = i * 8 + j;
            if (num2 != Main.myPlayer) {
                if (num2 >= _perPlayerIsEnabled.Length)
                    break;
                SetEnabledState(num2, bitsByte[j]);
            }
        }
    }
}
  • The condition 'CreativePowersHelper.IsAvailableForPlayer(this, userId)' shows that the resolution of this packet depends on the player who sent it. However, the protocol library that handles the packets is stateless, meaning it does not keep track of the game state or the players. Therefore, it cannot handle such data properly.

  • To solve this problem, the protocol library implements the IExtraData interface for packets that have similar properties. This interface contains an 'ExtraData:byte[]' Property where unprocessed data at the end of the packet is stored. The users of the protocol library can then handle this data themselves according to their needs.

  • Because of this, during packet deserialization, the protocol library must know the packet length in order to properly transfer the remaining data that cannot be processed to ExtraData. Therefore, the second argument of 'NetPacket.ReadNetPacket(ref void*, int restContentSize, bool isServerSide)' should be filled with packetContentSize, which is the value of the packet header minus 2.

Expamles (Unfinished)

1. Server send a CombatTextInt to player which whoAmI = 0

//socket be sended to
var socket = Netplay.Clients[0].Socket; 
//create a packet
var packet = new CombatTextInt(Vector2.Zero, Color.White, 100); 

//get a pointer to buffer index = 0
fixed (void* ptr = SendBuffer) { 
    //skip the packet header
    var ptr_current = Unsafe.Add<short>(ptr, 1); 
    //write packet
    packet.WriteContent(ref ptr_current); 
    //get the packet total size (including 2 bytes of packet header)
    var size_short = (short)((long)ptr_current - (long)ptr); 
    //write packet header value
    Unsafe.Write(ptr, size_short); 

    //send packet bytes
    socket.AsyncSend(SendBuffer, 0, size_short, delegate { }); 
}

Profermance GitHub Workflow

Take the packet WorldData (ID=7) as an example

  • Note: skip offset0 because you already know what kind of package it is.

Serialize

[Benchmark] public unsafe void Test_Unsafe() {
    fixed (void* ptr = buffer) {
        var p = Unsafe.Add<byte>(ptr, 1); // skip offset0
        worldData.ReadContent(ref p);
    }
}
[Benchmark] public void Test_BinaryWriter() {
    var bw = new BinaryWriter(new MemoryStream(buffer));
    bw.BaseStream.Position = 1; // skip offset0
    bw.Write(worldData.Time);
    //...
}
[Benchmark] public void Test_ReuseBinaryWriter() {
    bw.BaseStream.Position = 1; // skip offset0
    bw.Write(worldData.Time);
    //...
}
  • Result
Method Mean Error StdDev Rank Gen0 Allocated
Test_Unsafe 95.44 ns 0.362 ns 0.321 ns 1 0.0086 72 B
Test_BinaryWriter 339.91 ns 0.863 ns 0.720 ns 3 0.0124 104 B
Test_ReuseBinaryWriter 326.54 ns 0.950 ns 0.742 ns 2 - -

Deserialize

[Benchmark] public unsafe void Test_Unsafe() {
    fixed (void* ptr = buffer) {
        var p = Unsafe.Add<byte>(ptr, 1); // skip offset0
        worldData.ReadContent(ref p);
    }
}
[Benchmark] public void Test_BinaryReader() {
    var br = new BinaryReader(new MemoryStream(buffer));
    br.BaseStream.Position = 1; // skip offset0
    worldData.Time = br.ReadInt32();
    //...
}
[Benchmark] public void Test_ReuseBinaryReader() {
    br.BaseStream.Position = 1; // skip offset0
    worldData.Time = br.ReadInt32();
    //...
}
  • Result
Method Mean Error StdDev Rank Gen0 Allocated
Test_Unsafe 95.78 ns 0.713 ns 0.667 ns 1 0.0086 72 B
Test_BinaryReader 313.89 ns 6.125 ns 8.587 ns 3 0.0877 736 B
Test_ReuseBinaryReader 229.80 ns 0.564 ns 0.500 ns 2 0.0086 72 B

The others

  • The performance of string serialization/deserialization has been improved by about 20%. Due to space constraints, I will not discuss the details here. If you are interested, you can visit this link to see more.

RoadMap

Planned feature


  • Simplify packet construction
    • To simplify the packet construction, we plan to provide default values for the assignment parameters of the fields that are not required. However, this will change the order of the arguments, since the parameters with default values must be placed at the end of the constructor. To maintain backward compatibility with older versions of the API, we will generate a new constructor overload with the adjusted parameter order and keep the old constructor as well.

  • XML annotation for construction
    • We plan to add an XML annotation to the constructor of each packet that receives the initialization content from the pointer. This annotation will remind the user how to use it correctly. Such constructors are usually used by the protocol library, and they should not be used by the user unless they know exactly what they are doing. Otherwise, the user should use another method of reading the packet from the pointer, such as 'NetPacket.ReadNetPacket'.

enchcoreapi.trprotocol's People

Contributors

cedarycat avatar pd233 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.