前言
为什么是从一开始呢? 因为这个教程面向的是至少了解最基础的C#语法的玩家编写的, 想要入门C#的hxd可以去 【插件编写】TSHOCK服务器插件编写入门 康康棱镜的教学贴. 另外我只是课余时间学一学编程, 有些地方可能写得不到位或者存在纰漏, 如果可以的话也希望你能帮我指出来, 谢谢.
距 Journeys End 旅途的终点发售已经过去半年了, Terraria圈在短暂的热潮之后也逐渐沉寂下来. 随着游戏版本逐渐稳定, 越来越同质化的服务器玩法也要求服主们创作出独特的游戏机制. 如果你想要制作出独一无二的服务器插件, 发挥自己的创意, 那么就请静下心来, 从这里开始TShock插件开发之旅.
至于不会编程能不能写? 在这里我引用裙子序言中的一段话
当然能,编程语言只是表达自己思维方式的一种途径而已,就像学习英语一样。就算我对C#这门语言比较熟悉了,每次遇到不常用的特性的时候还是得去查文档。不过这不重要,重要的是你的思想,对于一个特定的问题,你需要告诉编程语言如何去解决它。一旦你有了思路,就可以通过查文档,百度等方式写出代码。
有人问我,做MOD到底难不难,我的看法是,只要你想做,愿意付出时间,没有什么是做不成的。其实人与人之间的智商并没有相差很多,那么为什么有人能做成有人做不成呢,我认为差距就在遇到困难以后,能不能鼓起勇气再试一次。许多科学家在谈到自己成功的原因时,都一再强调自己对学习有浓厚的兴趣。当你对一件事情有兴趣的时候,它会驱使着你达到目标,即使遇到困难也不会退缩,因为你乐在其中。
解决问题
当遇到无法解决的困难时, 首先应该查询Google, 必应等搜索引擎, 而在搜索过后依然找不到解决方法的话你或许可以加入TS官方群 816771079 向大家提问或者直接留下评论. 不过请注意提问的方式, 过于基础或者常见的问题可能不会在群里得到解答.
可用资源
以下文本来自鱼鱼Axeel
TS的文档和说明在 https://tshock.readme.io 上,但是都非常简陋,主要可以参考的有:
- 权限说明: https://tshock.readme.io/docs/permissions
- 设置文件说明:https://tshock.readme.io/docs/config-settings
- tr网络包结构(tr网络协议):https://tshock.readme.io/docs/multiplayer-packet-structure
ID等数据信息可以从tr的wiki上获取,wiki的id表附带图片,比较方便:
- 物品ID列表:https://terraria.gamepedia.com/Item_IDs
- 弹幕ID列表:https://terraria.gamepedia.com/Projectile_IDs
- NPCID列表:https://terraria.gamepedia.com/List_of_NPCs
- 物块ID列表:https://terraria.gamepedia.com/Tile_IDs
tml的wiki提供了很多信息,可以去查阅 ,主要可以看的有:
- tr常用的字段,但是很多字段更改只能在服务端生效,没有对应的网络包传递的话是没用的:https://github.com/tModLoader/tModLoader/wiki/Useful-Vanilla-Fields
- tr常用的方法:https://github.com/tModLoader/tModLoader/wiki/Useful-Vanilla-Methods
TShock项目
初阶编写
插件基础格式
如图, 这是一个默认 .NET Framework 模板
我们就从这里开始编写自己的第一个插件.引用
既然要为Terraria服务器编写插件, 那我们首先当然要引用的库. 以下是必备的几个程序集, 他们都可以在TShock根目录下找到:
- TerrariaServer.exe – 这是Terraria服务器本身, 基于OTAPI制作,提供了插件系统, 可以区分优先级以及更加细致的Hook, 同时在网络方面进行了部分修改, 更加方便.
- OTAPI.dll – 全称Open Terraria API, 即开放的泰拉瑞亚API, 开源项目, 提供公开的原版Terraria内部函数, 能够简单的对tr进行魔改, 同时被作为TShock的底层.
- TShockAPI.dll(非必须) – 位于根目录下 /ServerPlugins , TShock项目本身, 提供用户管理, 用户组,云存档, 基础的反作弊等功能, 同时包含了一系列更加细致的网络Hook.工具类供开发者调用.其本身也相当于一个Tr插件, 如果你要做的功能与其无关则无需引用此库.
可以看出来这几者之间的关系: OTAPI提供底层构架, TerrariaServer在其基础上增加插件系统, 而TShock就是一个功能完善的插件.
格式
引用完成后我们开始编写TShock插件基本格式. 首先先放一个完成后的代码:
using System; using Terraria; using TerrariaApi.Server; namespace ExamplePlugin { [ApiVersion(2, 1)] public class ExampleClass : TerrariaPlugin { public ExampleClass(Main game) : base(game) { } public override string Name => "ExamplePlugin"; public override Version Version => new Version(1, 0); public override string Author => "Megghy"; public override string Description => "示例插件"; public override void Initialize() { } protected override void Dispose(bool disposing) { base.Dispose(disposing); } } }
对于初学者来说只要知道按照这个格式即可.
至此, 一个最基础的插件已经完成了. 虽然没有任何功能
钩子(Hook)
那么问题来了. 假设你想要在玩家进入时向他问声好, 应该怎么做呢?
不过要知道怎么让服务器在事件发生时通知插件, 我们需要首先了解下编写插件时可以使用的几种Hook类型.
OTAPI提供的Hook
位于OTAPI.Hooks
OTAPI提供的大多为本地处理的钩子, 同时基本都提供Pre(执行前)和Post(处理后)两种, 通过对Pre进行魔改后返回HookResult.Cancel
来取消tr原版处理能够较为方便的对游戏进行修改.
所有钩子都在OTAPI.Hooks
下, 通过VS的自动补全可以查看
其实OTAPI的钩子在这个阶段并不怎么用得到, TShockHook和TerrariaServerHook已经将大多常用钩子包含进去了.
让我们看看怎么使用OTAPIHooks来完成玩家进入时欢迎
public override void Initialize() { Hooks.Player.PreGreet += PrePlayerJoin; //向PreGreet事件进行注册 } public HookResult PrePlayerJoin(ref int playerId) { TShock.Players[playerId].SendInfoMessage("Hi!"); //向玩家问个好 return HookResult.Continue; } protected override void Dispose(bool disposing) { if (disposing) { Hooks.Player.PreGreet -= PrePlayerJoin; //卸载钩子 } base.Dispose(disposing); }
HookResult
.而Post在处理完成后触发, 所以类型为void
. 这两者不能混淆, 否则会产生错误. 如图所示在这种情况下我们需要将PostPlayerJoin的类型改为void
, 并删除掉 return
的部分.TerrariaServer提供的Hook
位于TerrariaServer.ServerApi.Hooks
与OTAPIHook不同的是, TerrariaServerHook所需求的委托类型都是void, 仅需求对应不同事件的各种形如JoinEventArgs
的参数
示例:
public override void Initialize() { ServerApi.Hooks.ServerJoin.Register(this, OnPlayerJoin, -1); //向PostGreet事件进行注册 } public void OnPlayerJoin(JoinEventArgs args) { TShock.Players[args.Who].SendInfoMessage("Hi!"); //向玩家问个好 } protected override void Dispose(bool disposing) { if (disposing) { ServerApi.Hooks.ServerJoin.Deregister(this, OnPlayerJoin); //卸载钩子 } base.Dispose(disposing); }
很容易看出来的是, TerrariaServerHook并不是使用 +=
这样的写法, 而是使用了Register
, 在编写代码的时候需要注意这点.
另外传入的三个参数分别是注册钩子的对象, 处理函数和优先度. 注册者一般都是插件自己, 大多数情况下可以直接填this. 而优先度数字越小优先度越高, 填-1基本上就是第一个执行的了.
如果想要阻止注册这个钩子的后续其他函数进行处理, 那么可以将 args.Handled
设为 true
(默认为false). 其类似前面的 HookResult
. 另外因为args是一个个处理函数往后传递的, 所以你修改过其中的东西之后其他代码里接收到的也是你修改后的数据.
TShock提供的Hook
TShock包含了两套钩子, 一套是自身功能如用户组, 玩家注册登陆之类的, 位于TShockAPI.Hooks
. 而另一套是重新封装了一些常用的网络包, 位于TShockAPI.GetDataHandlers
.
TShockAPI.Hooks
的例子public override void Initialize() { TShockAPI.Hooks.PlayerHooks.PlayerChat += OnChat; } public void OnChat(TShockAPI.Hooks.PlayerChatEventArgs args) { args.Player.SendInfoMessage("闭嘴!"); //不准玩家说话 另外发送命令也算聊天 args.Handled = true; } protected override void Dispose(bool disposing) { if (disposing) { TShockAPI.Hooks.PlayerHooks.PlayerChat -= OnChat; } base.Dispose(disposing); }
TShockAPI.GetDataHandlers
的例子public override void Initialize() { //也支持 GetDataHandlers.Emoji += OnEmoji 这样的写法, 不修改优先度的话直接 += 就行了 GetDataHandlers.Emoji.Register(OnEmoji, HandlerPriority.High); } public void OnEmoji(object sender, GetDataHandlers.EmojiEventArgs args) { if (args.EmojiID == 88) { args.Player.SendInfoMessage("不准亲亲!"); args.Handled = true; } } protected override void Dispose(bool disposing) { if (disposing) { GetDataHandlers.Emoji.UnRegister(OnEmoji); } base.Dispose(disposing); }
没错, 这套钩子也支持优先度, 留空的话默认是HandlerPriority.Normal
.
另外在这套钩子中所有注册的函数都需要一个类型为object
的参数, 不过大多数情况都是null, 在这里并没有什么用处, 只需记住必须得带上这个就行了.
编译及调试
编写中…
催更φ( ̄∇ ̄o)
最近被爹妈拖去帮忙, 感觉得下半年才有空了
加油呀,催更
催更
大佬写的很好,希望大佬有时间能继续
This is my first time pay a quick visit at here and i am really happy to read everthing at one place