TShock中文插件库TShock中文插件库
插件文档
插件开发
其他文档
  • 简体中文
  • en-US
GitHub
插件文档
插件开发
其他文档
  • 简体中文
  • en-US
GitHub
  • Caiの插件教程
  • Part 0.配置开发环境​
  • Part 1.编写你的第一个插件​
  • Part 2.初步了解TShock插件模板​
  • Part 3.添加新命令​
  • Part 4.挂钩钩~
  • Part 5.玩家对象​
  • Part 6.0 数据包基础
  • Part 6.1 读取数据包 (超难理解)
  • Part 6.1发送数据包​
  • Part 6.5.1 数据包参考表格(1.4.4.9) (by @xuyuwtu & @ACaiCat)
  • Part 6.5.2 客户端与服务端的连接 (不要求学会,仅做参考)
  • Part 7.插件配置文件 (临时加更)

Part 6.1 读取数据包 (超难理解)

本章你将学到:

  • 如何解包数据包
  • 如何使用ServerApi.Hooks.NetGetData

解析数据包结构

数据包文档

你可以在Part6.5.1 数据包参考表格找到相关数据包结构(目前表格未完善)

反编译MessageBuffer.GetData

假设我们要写个插件,全服广播打开上锁箱子的玩家,并且显示箱子的坐标和物品

  1. 在随便一个方法里面加一句MessageBuffer.GetData

  2. 右键GetData,选择转到,点击前往声明或用法 (当然你可以直接按Ctrl+B)
    PixPin_2025-05-02_21-55-55

  3. 找到你需要的数据包,你可以在数据包文档找,也可以在PacketTypes里面找
    例如:我如果要处理箱子解锁相关的,我就在文档或者PacketTypes找相关的,最后发现有个数据包ChestUnlock = 52,ChestUnlock意味解锁箱子,说明52号数据包就是解锁箱子相关的。

  4. 在MessageBuffer.GetData定位相关的处理逻辑

    1. 首先我们要找到switch (packetId),然后下面有一堆的case
    2. 找到我们需要的case,例如:case 52
      PixPin_2025-05-02_22-28-29
  5. 分析代码

    Reader

    • 在分析数据包之前,简单介绍一下读取数据包的工具Reader
    • Reader意为阅读器,可以帮助我们读取数据包的每个字段。MessageBuffer.GetData中的this.reader在跑到下面这段代码时已经处理过包头的长度字段和包号字段了,也就意味着我们从这里开始读取的就是数据内容。
    • Reader会顺序读取每一个字段。例如我们在上一章提到的TileGetSection(客户端请求获取区块) [8],结构差不多是这样的:
       public struct TileGetSection
       {
         public int TileX;
         public int TileY;
       }
      我们使用下面的代码来读取:
       int x = this.reader.ReadInt32(); //先读取第一个int,即TileX
       int y = this.reader.ReadInt32(); //读取第二个int,即TileY

      每次使用reader.Read...()读取数据后,this.reader.BaseStream.Position都会移动到这个字段之后,即下一个字段的开头,你可以把this.reader.BaseStream.Position理解为reader的指针,每次读完字段指针就会移动到下一个字段的开头(前提是你读取的字段类型是正确的)

    初步分析

     case 52: 
           int number2_7 = (int) this.reader.ReadByte(); //这里读取了个byte - number2_7 啥?
           int num65 = (int) this.reader.ReadInt16(); //这里读取了个short - num65 啥啥?
           int num66 = (int) this.reader.ReadInt16(); //这里读取了个short - num66 啥啥啥?
           if (number2_7 == 1) //难以理解...
           {
             Chest.Unlock(num65, num66);
             if (Main.netMode == 2)
             {
               NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float) number2_7, number3: (float) num65, number4: (float) num66);
               NetMessage.SendTileSquare(-1, num65, num66, 2);
             }
           }
           if (number2_7 == 2)
           {
             WorldGen.UnlockDoor(num65, num66);
             if (Main.netMode == 2)
             {
               NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float) number2_7, number3: (float) num65, number4: (float) num66);
               NetMessage.SendTileSquare(-1, num65, num66, 2);
             }
           }
           if (number2_7 != 3)
             break;
           Chest.Lock(num65, num66);
           if (Main.netMode != 2)
             break;
           NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float) number2_7, number3: (float) num65, number4: (float) num66);
           NetMessage.SendTileSquare(-1, num65, num66, 2);
           break;

    ChestUnlock并不像TileGetSection那样有清晰的结构,我们不知道number2_7,num65,num66表示什么,如果你完全是入门小白,你遇到这样的反编译代码绝对会一脸懵逼,这时我们就需要NetMessage.orig_SendData辅助判断(不一定有用)

反编译NetMessage.orig_SendData

  1. 在随便一个方法里面加一句NetMessage.orig_SendData
  2. 右键GetData,选择转到,点击前往声明或用法 (当然你可以直接按Ctrl+B)
    PixPin_2025-05-02_22-35-11
  3. 找到你需要的数据包,例如我们需要的52号
  4. 在NetMessage.orig_SendData定位相关的处理逻辑
    1. 首先我们要找到switch (msgType),然后下面有一堆的case
    2. 找到我们需要的case,例如:case 52
      PixPin_2025-05-02_22-37-36
  5. 初步分析
    case 52:
         packetWriter.Write((byte) number2); //写入number2
         packetWriter.Write((short) number3); //写入number3
         packetWriter.Write((short) number4); //写入number4
         break;
    呃,貌似还是得不到有用的信息

偷看文档

这里有两个可选的文档

  1. Part6.5.1 数据包参考表格
  • 优点: 大多是最新的结构
  • 缺点: 覆盖不完整(等待填充)
    例如: 没有收录52
  1. TShock官方文档:https://tshock.readme.io/docs/multiplayer-packet-structureC
  • 优点: 覆盖全
  • 缺点: 大多过期
    例如:
    image

终极の大招

我们直接将MessageBuffer.GetData得到的代码喂给DeepSeek解析
Q: ...(代码) 重命名上述变量,并且附上每行的注释
A:

case 52:  // 处理箱子/门锁操作的数据包
    int lockActionType = (int)this.reader.ReadByte();  // 读取操作类型:1-解锁箱子,2-解锁门,3-上锁箱子
    int tileX = (int)this.reader.ReadInt16();  // 读取操作位置的X坐标
    int tileY = (int)this.reader.ReadInt16();  // 读取操作位置的Y坐标
    
    if (lockActionType == 1)  // 如果是解锁箱子操作
    {
        Chest.Unlock(tileX, tileY);  // 执行解锁箱子
        if (Main.netMode == 2)  // 如果是服务器模式
        {
            // 向所有客户端广播解锁箱子消息
            NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float)lockActionType, number3: (float)tileX, number4: (float)tileY);
            // 更新周围2格范围的方块状态
            NetMessage.SendTileSquare(-1, tileX, tileY, 2);
        }
    }
    
    if (lockActionType == 2)  // 如果是解锁门操作
    {
        WorldGen.UnlockDoor(tileX, tileY);  // 执行解锁门
        if (Main.netMode == 2)  // 如果是服务器模式
        {
            // 向所有客户端广播解锁门消息
            NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float)lockActionType, number3: (float)tileX, number4: (float)tileY);
            // 更新周围2格范围的方块状态
            NetMessage.SendTileSquare(-1, tileX, tileY, 2);
        }
    }
    
    if (lockActionType != 3)  // 如果不是上锁箱子操作则退出
        break;
        
    Chest.Lock(tileX, tileY);  // 执行上锁箱子
    if (Main.netMode != 2)  // 如果不是服务器模式则退出
        break;
        
    // 向所有客户端广播上锁箱子消息
    NetMessage.TrySendData(52, ignoreClient: this.whoAmI, number2: (float)lockActionType, number3: (float)tileX, number4: (float)tileY);
    // 更新周围2格范围的方块状态
    NetMessage.SendTileSquare(-1, tileX, tileY, 2);
    break;

注

关于Main.netMode, 你可以查看文档: 网络模式

可以看到解析结果几乎都正确,当然如果你是TMODL或者插件的老开发者不需要借助DeepSeek也能看出来这是干啥的.jpg。
一套连招之后,我们可以知道数据包的结构了 (过程是最重要的,所有数据包解包思路都是这样的)

int lockActionType = (int)this.reader.ReadByte();  // 操作类型:1-解锁箱子,2-解锁门,3-上锁箱子
int tileX = (int)this.reader.ReadInt16();  // 目标的X坐标
int tileY = (int)this.reader.ReadInt16();  // 目标的Y坐标

ServerApi.Hooks.NetGetData

注册、注销、处理三件套 (不多说了)

//插件加载时执行的代码
public override void Initialize()
{
    ServerApi.Hooks.NetGetData.Register(this, OnGetData);
}

private void OnGetData(GetDataEventArgs args)
{
}

//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        ServerApi.Hooks.NetGetData.Deregister(this, OnGetData);
    }
    base.Dispose(disposing);
}

处理方法

private void OnGetData(GetDataEventArgs args)
{
    if (args.MsgID == PacketTypes.ChestUnlock)
    {
        using BinaryReader binaryReader = new(new MemoryStream(args.Msg.readBuffer, args.Index, args.Length)); //新建一个binaryReader
        var plr = TShock.Players[args.Msg.whoAmI]; //获取玩家实例
        if (plr == null) //判断玩家是否为空 (超重要,别忘了哦)
        {
            return;
        }
        
        int lockActionType = binaryReader.ReadByte();  //操作类型:1-解锁箱子,2-解锁门,3-上锁箱子
        
        if (lockActionType != 1) //我们只处理解锁箱子
        {
            return;
        }


        int tileX = binaryReader.ReadInt16();  //目标的X坐标
        int tileY = binaryReader.ReadInt16();  //目标的Y坐标
        
        var chestId = Chest.FindChest(tileX, tileY); //获取箱子索引
        if (chestId == -1) //当找不到箱子则直接结束方法
        {
            return;
        }

        var chest = Main.chest[chestId]; //获取箱子实例
        
        var itemTags = new List<string>(); //定义一个列表存放物品Tag (eg. [i:4322])
        foreach (var i in chest.item)
        {
            if (i.netID == 0) //跳过ID为0的空格子
            {
                continue; 
            }
            itemTags.Add(TShock.Utils.ItemTag(i)); //将物品Tag加到列表
        }
        //发送广播
        TShock.Utils.Broadcast($"玩家{plr.Name}解锁了一个箱子在({tileX},{tileY}): {string.Join(',',itemTags)}",255,255,255); 
    }
}

这样我们的ServerApi.Hooks.NetGetData钩子就大功告成了!
效果:
image

处理多个数据包

我们可以使用switch-case处理

private void OnGetData(GetDataEventArgs args)
{
    switch (args.MsgID)
    {
        case PacketTypes.ChestOpen:
            ...
            break;
        case PacketTypes.ChestItem:
            ...
            break;
        case PacketTypes.ChestName:
            ...
            break;
    }
}

小结

通过以上的例子,我们需要学会如何在插件开发中去利用反编译来获取数据包的结构,这对于新人来说很难,但这对插件开发非常重要。

重要

如果你需要的数据包有GetDataHandlers,那么请使用GetDataHandlers提供的对应钩子,不要造轮子遭罪...

上一篇
Part 6.0 数据包基础
下一篇
Part 6.1发送数据包​