开发教程 [插件编写]TShock5插件编写入门教程

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于福建省

前言:

  • 本教程是插件入门教程而不是C#入门教程
  • C#入门相关教程请移步B站
  • 本教程的作者本身也是个菜鸡,没办法写得特别清楚.jpg
  • 本教程将使用Windows系统演示(推荐系统Windows10+)
  • 如果有那个段落听不懂或者出现错误,欢迎大家留下建议!

章节目录

Part 0.配置开发环境

本章你将学到:
  • 如何安装Microsoft Visual Studio

1.下载Microsoft Visual Studio安装程序

进入Visual Studio官方下载地址: https://visualstudio.microsoft.com/zh-hans/downloads/
选择Community版本下载(Professional、Enterprise版是付费版本)

1692965150135.png

2.打开VisualStudioSetup.exe

点击继续即可,然后会开始安装VisualStudio安装器

3.选择.NET 桌面开发并安装

1692966114824.png
注: 请不要自以为是随便勾选组件工作负载,每个工作负载都要占用相当大的储存空间修改安装位置可能导致一些玄学的安装问题,为了避免出现问题,请直接默认即可,如果你要修改部分安装设置,那么请你对你的修改有十足的把握!!!

4.等待安装

1692966713652.png

注:若提示需要重启,重启即可

5.启动VisualStudio

VisualStudio一般会出现在开始菜单下,你可以使用如下方法创建快捷方式(如果没有快捷方式)
1692967698634.png
初次启动可能会要求登录,登录完成后可以选择自定义主题(反正我喜欢黑色主题),按照自己的偏好选择即可
至此,VisualStudio安装教程结束
 

附件

  • 1692965476905.png
    1692965476905.png
    30.6 KB · 查看: 0
  • 1692965192986.png
    1692965192986.png
    366.7 KB · 查看: 0
最后编辑:

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

Part 1.编写你的第一个插件

本章你将学到:
  • 使用Visual Studio创建插件项目
  • 使用Nuget程序包管理器添加插件依赖项
  • 初步了解编写一个插件的流程

1.创建新项目

1692979710952.png
选择类库选项,然后下一步

注: 一定要看清楚是C#.NET
1692979794386.png
项目名称填写插件名,位置建议默认,具体可以参考下图配置

注:
一般来讲,项目名只由字母(A-Z,a-z),数字(0-9),下划线(_)组成,并且项目名具有实际意义

不要使用中文!不要使用中文!不要使用中文!(引自棱镜)
解决方案中可以包含多个插件项目。使用同一个解决方案存放多个插件项目可以方便管理多个插件代码,并且方便项目之间互相引用。
1692979804434.png
此处一定要选择.NET 6.0,否则可能会导致一些插件兼容问题
1692979959693.png

2.进入VS项目

我相信有C#基础的你一定对VS的界面十分熟悉,所以不做过多介绍,但请你熟记下图的代码区域
1692977890417.png
*重命名(非必要)
解决方案资源管理器找到如图.cs文件可以对其右键进行重命名
当然同样要遵循命名规范不随意使用中文
1692979970648.png

3.添加TShock插件依赖项

TShock插件依赖许多相关程序集,好消息是你可以通过NuGet程序包快速引用他们

解决方案资源管理器右键依赖项,点击管理NuGet程序包
1692979979044.png
根据下图步骤安装TShock的NuGet程序包
1692979993620.png
弹出提示点击确定、接受即可

4.开始编写代码

显然你现在并不会写,但是你可以先复制下面的TShock插件模板
C#:
//代码来源:https://github.com/chi-rei-den/PluginTemplate/blob/master/src/PluginTemplate/Program.cs
//恋恋的TShock插件模板,有改动(为了配合章节名)
//来自棱镜的插件教程

using System;
using System.Collections.Generic;
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "插件的作者";

        //插件的一句话描述
        public override string Description => "插件的描述";

        //插件的名称
        public override string Name => "插件的名字";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            //恋恋给出的模板代码中展示了如何为TShock添加一个指令
            Commands.ChatCommands.Add(new Command(
                permissions: new List<string> { "plugin.permission1", "plugin.permission2", },
                cmd: this.Cmd,
                "helloworld", "hw"));
        }
        
        //插件卸载时执行的代码

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                //移除所有由本插件添加的所有指令

                var asm = Assembly.GetExecutingAssembly();
                Commands.ChatCommands.RemoveAll(c => c.CommandDelegate.Method?.DeclaringType?.Assembly == asm);
            }
            base.Dispose(disposing);
        }

        //执行指令时对指令进行处理的方法
        private void Cmd(CommandArgs args)
        {
            args.Player.SendSuccessMessage("Hello world!");
        }
    }
}
相信你看到这串代码大概是一脸懵逼的,但是没关系,你只要知道它的功能
添加命令/helloworld,执行此命令会向执行命令的人发送一个绿色的HelloWorld
1692979826411.png
粘贴代码后,当你发现代码编辑区左下角的错误数已经变成0之后,恭喜你可以开始编译插件
1692979003458.png

注:如果有错误存在,可能是你复制漏了某些地方。

5.编译生成你的插件

点击工具栏处的绿色小三角(或者快捷键Ctrl+B)即可开始编译
1692978964620.png
开始编译后,输出区会打印编译的一些日志,最后打印编译结果
当出现"1 成功"的字样,说明插件代码已经成功编译
1692980007910.png

6.找到并安装你的插件

解决方案资源管理器右键当前项目(HelloWorld),点击在文件资源管理器中打开文件夹
1692980015118.png1692980023342.png
此时会蹦出一个文件夹,按照下图目录找到net6.0文件夹
1692979833858.png
将图示两个文件移动或复制到TShock目录中的ServerPlugins文件夹中
1692979842755.png

7.测试你的插件

启动TShock,可以发现你的插件已经被TShock成功识别并安装了

1692980077372.png
使用/help命令查看命令列表,可以发现插件添加的命令已经在命令列表中显示
执行/helloworld,控制台打印出绿色Hello world!
说明插件功能正常

1692975430672.png
 

附件

  • 1692979314302.png
    1692979314302.png
    14.4 KB · 查看: 0
  • 1692979817747.png
    1692979817747.png
    221.7 KB · 查看: 0
最后编辑:

diudiu

Lv3
LV
0
 
IP属地
上海市闵行区
2021/02/11
26
8
勋章
1
  • · 发布于上海市闵行区
 

zhl2336

Lv3
LV
0
 
IP属地
广西南宁市
2020/03/07
3
2
  • · 发布于未知
 

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

Part 2.初步了解TShock插件模板

本章你将学到:
  • TShock插件模板各部分的作用
  • TShock插件的初始化顺序

插件模板

以下是一个常用的TShock插件模板其实似乎也就是Part 1把HelloWorld删掉的样子
C#:
//代码来源:https://github.com/chi-rei-den/PluginTemplate/blob/master/src/PluginTemplate/Program.cs
//恋恋的TShock插件模板,有改动(为了配合章节名)
//来自棱镜的插件教程

using System;
using System.Collections.Generic;
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "插件的作者";

        //插件的一句话描述
        public override string Description => "插件的描述";

        //插件的名称
        public override string Name => "插件的名字";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
        }
        
        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
            }
            base.Dispose(disposing);
        }

    }
}

插件基本信息

以下代码可以设置插件的一些基本信息,插件名建议按照功能命名不要起奇怪名字(例如"炸裂")
C#:
//定义插件的作者名称
public override string Author => "插件的作者";

//插件的一句话描述
public override string Description => "插件的描述";

//插件的名称
public override string Name => "插件的名字";

//插件的版本
public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;
Author(插件作者)、Name(插件名字)、Version(插件版本)将会在插件初始化完成时显示
1693020601153.png
其中,Version可以通过new Version修改,如下:
C#:
//插件的版本
public override Version Version => new Version(1, 0, 0, 0);
//public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;
使用new Version()将会显示此方法参数为版本号(例如本例子中的"1.0.0.0")
而使用Assembly.GetExecutingAssembly().GetName().Version获取的是程序集的版本号, 程序集版本号需要在项目属性中修改

注: 如果你使用new Version()作为版本号,将会以你Version()的参数作为版本号,而不是程序集版本
1.右键解决方案资源管理器中的项目(这里是HelloWorld),然后点击属性
1693019754926.png1693019861367.png
2.在属性选项卡中依次找到包—常规—程序集版本
1693020119834.png
3.修改程序集版本即可

插件的构造器

插件的构造器,也叫构造函数构造函数中的代码会比Initialize(初始化)函数更先执行
服务端会先按照读取到插件的顺序(取决于插件的文件名)执行所有插件的构造函数,再按照Order(等下会讲)由小到大执行Initialize(初始化)函数

C#:
//插件的构造器
public Plugin(Main game) : base(game)
{
    base.Order = 1; //插件加载顺序,默认为1,数字越小越先加载
}
通常情况下,构造函数的执行会在TShock初始化之前,如果在构造函数中编写代码(例如建表),可能会导致十分玄学的问题,
所以没有特殊需要下一般插件的构造函数都会直接放空

*在构建函数中修改Order(听不懂没事)

构造函数常常会用来修改插件的初始化顺序—Order
Order是一个int类型的整数,Order可以在取int类型范围内的任何数,当然也可以是负数
Order越小插件的初始化就越优先,Order为1的插件会比Order为2的插件先初始化, TShock的Order为0,
插件的Order在默认情况下(不修改)为1,而TShock的Order为0所以TShock会比其他插件更先初始化
1.编写两个插件,在执行构造函数初始化函数执行时打印文本
1693022180741.png1693022286590.png
2.重命名并安装插件
Order为1的插件在文件夹中更靠前时(以文件名排序),Order为1的插件的构造函数将会先被执行
1693022440424.png1693022454520.png
相反,当Order为2的插件在文件夹中更靠前时(以文件名排序),Order为2的插件的构造函数将会先被执行

1693022380205.png1693022401839.png
由此可知,插件构造函数执行顺序取决于插件的文件名,与Order无关
但是无论文件名如何修改,Order为1的插件总会先初始化,如下图:
1693022488384.png
由此可知,插件初始化函数执行顺序取决于Order,与插件的文件名无关

插件的初始化函数

初始化函数是TShock插件中非常重要的部分,这个函数主要负责插件的初始化,可以用来注册钩子、添加命令、注册REST API命令、初始化配置文件、初始化数据库
C#:
//插件加载时执行的代码
public override void Initialize()
{
    ServerApi.Hooks.ServerChat.Register(this, this.OnChat); //注册钩子
    Commands.ChatCommands.Add(new Command(permissions: new List<string> {""}, cmd: this.Fish, "钓鱼排行")); //添加命令
    TShock.RestApi.Register(new SecureRestCommand("/XSB/GetMap", GetMap, "rest.xsb.admin")); //注册REST API命令
    Config.GetConfig(); //初始化配置文件
    DB.Connect(); //初始化数据库
}

插件的卸载函数

相较于初始化函数,卸载函数显得没什么存在感(你甚至可以不写卸载函数)
但是为了代码规范,还是建议你把注册过的钩子卸载,删除添加的命令
C#:
//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
     if (disposing)
     {
         ServerApi.Hooks.ServerChat.Deregister(this, this.OnChat);  //卸载钩子
         Commands.ChatCommands.RemoveAll(c => c.CommandDelegate.Method?.DeclaringType?.Assembly == Assembly.GetExecutingAssembly());
         //移除插件添加的命令
     }
     base.Dispose(disposing);
}
 

附件

  • 1693016806544.png
    1693016806544.png
    135.4 KB · 查看: 0
  • 1693016572892.png
    1693016572892.png
    115 KB · 查看: 0
  • 1693012496403.png
    1693012496403.png
    64.6 KB · 查看: 0
  • 1693012397652.png
    1693012397652.png
    14.5 KB · 查看: 0
  • 1693012391038.png
    1693012391038.png
    5.6 KB · 查看: 0
  • 1693011823357.png
    1693011823357.png
    2.4 KB · 查看: 0
  • 1693021967689.png
    1693021967689.png
    28.3 KB · 查看: 0
  • 1693021996878.png
    1693021996878.png
    28.3 KB · 查看: 0
  • 1693023458949.png
    1693023458949.png
    66.1 KB · 查看: 0
最后编辑:

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

Extra Part.一些额外的东西

本章你将学到:

  • Visual Studio常见奇怪问题解决办法​

  • 创建新的插件项目​

  • 让Visual Studio自动复制插件、重载调试服务器​


Visual Studio常见奇怪问题解决办法


解决方案资源管理器消失

1.在工具栏中选择视图
1693027480607.png
2.点击解决方案资源管理,然后就会发现解决方案资源管理又回来啦!
1693027523265.png
注: 当然输出、错误列表等没了也可以用这个办法

在中间输入代码会删除后面的代码

可能是你按到INS键切换成了OVR模式,此时代码编辑区的右下角就会显示OVR

1693027807421.png

解决办法:按下键盘上的INS键再次切换到INS编辑模式

工具栏下方出现报错横幅

如果你看到如下提示,那么恭喜你,你下载的是正版的Microsoft Visual Studio
1693028305875.jpg
这些报错会导致代码提示等出现故障

解决办法: 重新启动Visual Studio


创建新的插件项目

1.在解决方案资源管理器中右键解决方案
1693028959270.png
2.选择添加,点击新建项目然后重复和Part1一样的步骤即可
1693029096879.png
3.点击工具栏启动的白色小箭头,再点击配置启动项目
1693029614856.png
4.按照下图修改配置
1693029700993.png


生成后事件(自动复制插件/重启服务器)

1.在解决方案资源管理器中右键项目
1693102959177.png
2.点击属性
1693102963986.png
3在属性选项卡找到生成—事件—生成后事件
1693103021981.png
4.如果你只想自动复制插件在其中输入下面的命令即可
代码:
copy bin\Debug\net6.0\*.dll C:\Users\13110\Desktop\code\TShock144\ServerPlugins
copy bin\Debug\net6.0\*.pdb C:\Users\13110\Desktop\code\TShock144\ServerPlugins
注: C:\Users\13110\Desktop\code\TShock144\ServerPlugins要替换为自己调试服务器的插件目录
(可选)5.自动重启服务器
如果你是像我这样的终极懒鬼,你可以尝试一下下面的方法让你解放双手
1.先在生成后事件加上一句
代码:
taskkill /F /IM TShock.Server.exe
2.新建自启动脚本(如果你已经有了可以跳过这个步骤)
然后编辑个打开记事本编辑以下内容,并保存为编码UTF-8,后缀.bat的文件,放在和TShock.Server.exe同目录

Bash:
chcp 65001
@echo off
@echo.*自启动脚本 by Cai
set restart=1
:restart
set ip=0.0.0.0
set port=7777
set map_path=C:\Users\13110\Desktop\code\TShock144\测试.wld
set lang=7
@echo.*服务器准备启动: 语言: %lang%,IP: %ip%,端口: %port%,地图路径: %map_path%
TShock.Server.exe -lang %lang% -ip %ip% -port %port% -world %map_path%
@echo.*服务器崩溃准备重启, 目前重启次数%restart%
timeout /t 0 /NOBREAK
set/a restart=restart+1
goto restart
注: map_path要改成自己地图的路径(注意\使用/可能无法识别)port改成自己想要的端口,ip请不要更改(0.0.0.0是绑定本机所有IP的意思,请你不要自以为是的修改),lang 7是中文语言启动(你没必要修改)
用这个脚本启动服务器,然后编译插件就会自动帮你重启服务器啦!
使用效果

 

附件

  • 1693028989883.png
    1693028989883.png
    63.6 KB · 查看: 0
  • 1693029106428.png
    1693029106428.png
    75.3 KB · 查看: 0
  • 1693029113402.png
    1693029113402.png
    75.3 KB · 查看: 0
最后编辑:

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

Part 3.添加新命令

本章你将学到:
  • 如何在TShock插件中添加新命令
  • 学会处理CommandArgs参数
  • 学会卸载插件添加的命令(养成好习惯)

1.添加新的命令

在Part 1中有提到,添加命令通常是在插件的初始化函数Initialize()中添加的
添加新命令的常用格式是这样的:
C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command("mycommand", Cmd, "cmd2"));
    //Command()中第一个参数是权限名,第二个参数是回调函数,第三个参数是命令名
}
当你输入代码后由于没有相应的函数(Cmd)报错
1693031018827.png
此时我们右键我们需要创建的函数名,然后选择快速操作和重构1693031109523.png1693031137344.png
然后选择生成方法"Cmd"
1693031140633.png
此时Visual Studio就会自动帮我们创造一个回调函数
1693031155598.png
当然,此时这个函数没有任何功能,我们删去throw new NotImplementedException();就可以开始编写命令功能了

2.命令的权限

C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command("mycommand", Cmd, "cmd2"));
}


private void Cmd(CommandArgs args)
{
    args.Player.SendSuccessMessage("你使用了cmd2命令!");
}
上面的示例代码中,当玩家输入"/cmd2"命令并且拥有"mycommand"权限时,就会向玩家发送一条绿色的消息,内容为"你使用了cmd2命令!",若玩家没有相应权限则会提示玩家无权执行该命令,并且日志会记录玩家试图执行命令的行为并发送给打开接受日志的玩家1693039687698.png1693039745182.png
权限也可以设为无或者多个权限如下代码可以添加3条分别为/cmd1(无权限)、/cmd2(单权限)、/cmd3(多权限)的命令
C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command(Cmd, "cmd1")); //不设置权限
    Commands.ChatCommands.Add(new Command("mycommand", Cmd, "cmd2")); //设置一个权限
    Commands.ChatCommands.Add(new Command(new List<string> { "mycommand","mycommand2" }, Cmd, "cmd3")); //设置多个权限
}
执行上面的命令,都会调用"Cmd"函数,他们的区别在于权限,权限区别请见下表:
/cmd1无权限限制
/cmd2玩家需要拥有mycommand权限
/cmd3玩家需要拥有mycommand或者mycommand2权限
注: 像/cmd3类似的多权限命令,只要玩家拥有命令中设置的任何一个权限,玩家就可以执行对应的命令
*权限的命名
权限的命名是需要遵循一定的命名规范的并
不能像上面代码中随便命名
通常插件权限名字格式应为: 插件名.功能名(例如: si.use)
当然为了方便分类也可以使用: 插件名.功能.功能细分 (例如: uban.playerban.admin)

3.命令的别名

有时候一个命令可以由两个名字对应,例如:/off/exit他们属于同一个命令/exit就是/off的别名
我们可以通过下面的代码实现命令别名
C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command("cancanneed", Vme, "vme50", "v我50"));
}
private void Vme(CommandArgs args)
{
    args.Player.GiveItem(74, 50, 0); //给玩家50个铂金币
}
上述代码中,当玩家使用/vme50或者/v我50且有"cancanneed"权限,就会给予这个玩家50铂金币
此时"/v我50"就是"/vme50"的别名
同样即使你使用别名,你仍然可以使用那几个权限的重载,就像如下示例:
C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command(Vme, "giveme50", "送我50"));
    Commands.ChatCommands.Add(new Command("cancanneed", Vme, "vme50", "V我50"));
    Commands.ChatCommands.Add(new Command(new List<string> { "乞丐" , "beggar" } , Vme, "begme50", "乞讨50"));
}

小结: 你可以把记作回调函数(例如Vme)就像一条分界线,回调函数左侧的是权限(就像"cancanneed"),回调函数右侧的是名字(就像"vme50"、"V我50")1693035434275.png

4.命令的其他属性

TShock的命令还有4个属性,这些属性是AllowServer、DoLog、HelpDesc、HelpText,你可以使用new Command(){ 属性 = 值 }来修改他们,也可以用如下方法一次修改多个属性
C#:
public override void Initialize()
{
    Commands.ChatCommands.Add(new Command(Vme, "giveme50", "送我50")
    { AllowServer = false,DoLog=true, HelpDesc = new string[]{ "帮助文档","这是第一行"}, HelpText= "帮助文本"});
}

AllowServer

默认为true,AllowServer被设置为false时,非真实玩家(服务器后台、REST远程命令)无法使用这个命令
1693033540226.png

DoLog

默认为true,当你设置为false时,服务器日志不会记录执行该命令的具体参数,只会记录命令名(例如:使用命令/login 123456,只会记录/login)
1693034128151.png

HelpDesc和HelpText

HelpDesc的默认值为null,HelpText的默认值为No help available.(在当前设置的语言有效时会被设为对应语言的翻译)
这两者基本一样,都是命令帮助文档,使用/help 命令名可以查看
但是HelpDesc的优先级会更高,当命令的HelpDesc不为null时只会显示HelpDesc
1693034557221.png
 

附件

  • 1693036260026.png
    1693036260026.png
    69.5 KB · 查看: 0
  • 1693037084668.png
    1693037084668.png
    17.7 KB · 查看: 0
  • 1693038962650.png
    1693038962650.png
    13.7 KB · 查看: 0
最后编辑:

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

5.CommandArgs参数

当玩家执行我们添加的命令时,服务器会把一些相关的信息"打包"传给我们的回调函数,这些信息就是CommandArgs
1693039291623.png
它包含Message、Player(重要)、TPlayer、Silent、Parameters(重要)
1693039336869.png

Parameters

当你输入含有参数的命令时,TShock会把命令分割成Parameters

Parameters是命令的参数,类型为字符串列表

1693039353193.png
你可以使用switch-case结构实现子命令功能,示例如下:
C#:
//代码来自Cai的SSC管理器

private void CommandHandler(CommandArgs args)
{
    if (args.Parameters.Count == 0)
    {
        args.Player.SendErrorMessage("无效的SSC管理器子命令!有效命令如下:\n" +
                "ssc save [ID]--保存SSC\n" +
                "ssc del [ID]--删除SSC\n" +
                "ssc list --列出SSC\n" +
                "ssc restore --复原SSC");
        return;
    }
    int sscid = -1;
    switch (args.Parameters[0].ToLower())
    {
        case "保存":
        case "save":
            if (args.Parameters.Count != 2 || !int.TryParse(args.Parameters[1], out sscid))
            {
                args.Player.SendErrorMessage("用法错误!正确用法:ssc save [ID]");
                return;
            }


            if (SSCDB.InsertPlayerData(args.Player, sscid))
            {
                args.Player.SendSuccessMessage("保存成功!");
                return;
            }
            args.Player.SendErrorMessage("保存失败!");
            break;
        case "删除":
        case "del":
            if (args.Parameters.Count != 2 || !int.TryParse(args.Parameters[1], out sscid))
            {
                args.Player.SendErrorMessage("用法错误!正确用法:ssc del [ID]");
                return;
            }
            if (!SSCDB.GetPlayerData(sscid).exists)
            {
                args.Player.SendErrorMessage("你输入的SSC背包ID不存在!");
                return;
            }
            if (SSCDB.DeletePlayerData(sscid))
            {
                args.Player.SendSuccessMessage("删除成功!");
                return;
            }
            args.Player.SendErrorMessage("删除失败!");
            break;
        case "列出":
        case "list":
            args.Player.SendSuccessMessage("有效的SSC背包列表:" + string.Join(',', SSCDB.GetAllSSCId()));
            break;
        case "还原背包":
        case "restore":
            if (args.Parameters.Count != 2 || !int.TryParse(args.Parameters[1], out sscid))
            {
                args.Player.SendErrorMessage("用法错误!正确用法:ssc restore [ID]");
                return;
            }
            if (!SSCDB.GetPlayerData(sscid).exists)
            {
                args.Player.SendErrorMessage("你输入的SSC背包ID不存在!");
                return;
            }
            args.Player.PlayerData = SSCDB.GetPlayerData(sscid);
            args.Player.PlayerData.RestoreCharacter(args.Player);
            args.Player.Heal(args.Player.PlayerData.maxHealth);
            args.Player.SendSuccessMessage("背包已还原!");
            break;
        default:
            args.Player.SendErrorMessage("无效的SSC管理器子命令!有效命令如下:\n" +
                "ssc save [ID]--保存SSC\n" +
                "ssc del [ID]--删除SSC\n" +
                "ssc list --列出SSC\n" +
                "ssc restore --复原SSC");
            break;
    }
}

Player

执行命令的玩家TSPlayer对象,通常可以对其进行以下操作:
C#:
args.Player.SendData(PacketTypes.WorldInfo); //发送数据包,根据NetMessage发送数据包
args.Player.SendRawData(new byte[] { }); //发送原始数据,即手搓数据包
args.Player.SendErrorMessage("这是一个错误信息"); //发送错误信息(红色)
args.Player.SendInfoMessage("这是一个信息"); //发送信息(黄色)
args.Player.SendSuccessMessage("这是一个成功信息"); //发送成功信息(绿色)
args.Player.SendWarningMessage("这是一个警告信息"); //发送警告信息(橙色)
args.Player.Kick("这是一个踢出信息",true,true,"Cai",false);
//踢出玩家(踢出理由,强制踢出,不发送广播,踢出用户名,保存玩家的存档)
args.Player.Ban("这是一个封禁信息","Cai"); //封禁玩家(封禁理由,封禁用户名)
args.Player.SetTeam(1); //设置玩家队伍,无队伍0,红队1,绿队2,蓝队3,黄队4,粉队5
args.Player.SetBuff(1, 1); //添加玩家Buff (BuffID,Buff持续时间(单位1/60s))
args.Player.Teleport(1, 1); //传送玩家到坐标(X,Y)
args.Player.Disconnect("这是一个断开信息"); //断开玩家的连接(断开理由)(也算是踢出玩家的一种)
将会在未来的几个Part更详细地讲解TSplayer(画大饼)

TPlayer

Player对象,即Terraria原版玩家对象,过于硬核,此处不讲解

Silent

是否为静默执行
当玩家使用CommandSpecifier(默认为/)为命令起始符时Silent为false
当玩家使用CommandSilentSpecifier(默认为.)为命令起始符时Silent为true
静默执行取决于命令是否写了静默执行的逻辑,如果没有,那么静默执行和正常执行没有任何区别

Message

原消息,例如:你执行了/kick Cai 傻逼,那么Message的值就是"kick Cai 傻逼"
由于Parameters的存在,这个东西就基本上没啥卵用


5.卸载你添加的命令

使用下面的代码就可以卸载本插件添加的所有命令
C#:
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        //移除所有由本插件添加的所有指令
        var asm = Assembly.GetExecutingAssembly();
        Commands.ChatCommands.RemoveAll(c => c.CommandDelegate.Method?.DeclaringType?.Assembly == asm);
    }
    base.Dispose(disposing);
}
 
最后编辑:

TheLastPrism

Lv6
管理成员
版主
创意家
LV
0
 
IP属地
湖北省
2019/10/25
1,027
341
勋章
5
  • · 发布于湖北省
申精吧(
 

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

Part 4.挂钩钩~

本章你将学到:
  • 学习如何注册、卸载钩子
  • 学会使用常用钩子修改游戏事件

钩子可以干啥?

当你注册一个事件的钩子后,在触发这个事件时,钩子就会执行你预先设置的函数(回调函数)

举个例子:

假如你需要实现当玩家说脏话时把他踢出去,那么你就可以注册一个ServerChat钩子,并且调用OnChat函数。你需要在OnChat函数中编写代码,识别玩家是不是真的说了脏话。最后别忘了卸载你的钩子(好习惯)
C#:
//以下为示例代码,看不懂没事,我们慢慢学
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;


namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";


        //插件的一句话描述
        public override string Description => "拒绝脏话";


        //插件的名称
        public override string Name => "Fuck";


        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;


        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }


        //插件加载时执行的代码
        public override void Initialize()
        {
            ServerApi.Hooks.ServerChat.Register(this, OnChat); //注册聊天钩子
        }


        private void OnChat(ServerChatEventArgs args)
        {
            TSPlayer plr = TShock.Players[args.Who]; //获取TShock玩家对象
            if (args.Text.Contains("wdnmd")) //检测聊天是否含有脏话"wdnmd"
            {
                plr.Kick("不许说脏脏!", true); //踢出玩家
            }
        }


        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ServerApi.Hooks.ServerChat.Deregister(this, OnChat); //卸载聊天钩子
            }
            base.Dispose(disposing);
        }


    }
}

代码解析:

注册钩子语句
不同类型的钩子有不同的注册语句,
ServerApi.Hooks.ServerChat.Register(this, OnChat); 就是ServerApi钩子的注册语句,钩子要在使用前注册(一般在Initialize注册)
绿字部分是钩子的名字(一般和其功能有关)划线部分是其回调函数(触发钩子执行的函数),其他照写就好
C#:
//插件加载时执行的代码
public override void Initialize()
{
    ServerApi.Hooks.ServerChat.Register(this, OnChat); //注册聊天钩子
}
卸载钩子语句
不同类型的钩子有不同的卸载语句,
ServerApi.Hooks.ServerChat.Deregister(this, OnChat); 就是ServerApi钩子的卸载语句,钩子要在不需要使用时卸载(一般在Dispose卸载)
绿字部分是钩子的名字(一般和其功能有关)划线部分是其回调函数(和注册钩子的函数名一样),值得注意的是卸载函数是Deregister()而不是注册时的Register()
C#:
//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        ServerApi.Hooks.ServerChat.Deregister(this, OnChat); //卸载聊天钩子
    }
    base.Dispose(disposing);
}
回调函数
回调函数是触发钩子时执行的函数,创建方法类似命令的回调函数
1.右键函数名字(自己起名字)
1693893972437.png
2.点击快速操作和重构...
1693894022677.png
3.点击生成方法
1693894017431.png
4.然后就会发现Visual Studio自动生成了一个回调函数,你需要自行编写函数的逻辑
1693894123354.png

常用钩子

•ServerApi钩子
ServerApi的钩子覆盖了服务器大部分可能需要挂钩的事件,同时值得注意的是ServerApi钩子中参数的命名和Terraria原版相似
C#:
//插件加载时执行的代码
public override void Initialize()
{
    ServerApi.Hooks.ServerChat.Register(this, OnChat); //注册ServerApi钩子(玩家聊天)
}
//回调函数
private void OnChat(ServerChatEventArgs args)
{
           
}
//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        ServerApi.Hooks.ServerChat.Deregister(this, OnChat); //卸载ServerApi钩子(玩家聊天)
    }
    base.Dispose(disposing);
}
其中args参数中是不包含TSPlayer对象Player对象的,所以你需要手动获取,方法如下:
C#:
private void OnChat(ServerChatEventArgs args)
{
    //args.Who(有的地方是WhoAmI)是玩家索引的意思,也就是在玩家对象数组中的下标(索引)。Who和WhoAmI等同于TSPlayer中的Index,Who和WhoAmI也可能表示的是生物(NPC)或者弹幕(Projectile)等的索引,这要取决于你的钩子
    //TShock.Players是TShock玩家对象(TSPlayer)数组,TShock玩家对象中的绝大部分属性、变量、方法都为插件编写服务,大部分都可以在服务端使用
    //Main.player是Terraria玩家对象(Player)数组,Terraria玩家对象是Terraria客服端和服务端都使用的,所以其中的属性、变量、方法在服务器中可能不会生效
    TSPlayer plr = TShock.Players[args.Who]; //获取TShock玩家对象,说白点就是用下标获取数组元素
    Player player = Main.player[args.Who]; //获取TShock玩家对象
}
注:args.Handled可以标记这个事件是否已经处理如果将其设为true服务器可能会忽略这个事件
•TShockAPI钩子
这些钩子主要挂钩了TShock中的事件,例如:新建账号、玩家进入区域等,TShockAPI钩子分为如下几类:

类型包含钩子
AccountHooksAccountCreate(创建账号)、AccountDelete(删除账号)
PlayerHooksPlayerChat(玩家聊天)、PlayerCommand(玩家执行命令)、PlayerLogout(玩家登出)、PlayerPreLogin(玩家登录前)、PlayerPostLogin(玩家登录后)、PlayerPermission(权限检查)、PlayerHasBuildPermission(玩家建筑权限检查)、PlayerProjbanPermission(玩家服务器忽略禁用弹幕权限检查)、
PlayerTilebanPermission(玩家服务器忽略禁用图格权限检查)、
PlayerItembanPermission(玩家服务器忽略违禁物品权限检查)
GeneralHooksReloadEvent(服务器重载[使用/reload命令])
RegionHooksRegionCreated(区域创建)、RegionDeleted(区域删除)、RegionRenamed(区域重命名)、RegionEntered(玩家进入区域)、
RegionLeft(玩家离开区域)
TShockAPI钩子使用方法:
C#:
//插件加载时执行的代码
public override void Initialize()
{
    TShockAPI.Hooks.AccountHooks.AccountCreate += AccountHooks_AccountCreate;//注册TShockAPI钩子(玩家创建账号)
}

//回调函数
private void AccountHooks_AccountCreate(TShockAPI.Hooks.AccountCreateEventArgs e)
{
    TShock.Utils.Broadcast($"[欢迎新人]{e.Account.Name}注册了新账号,快举小花欢迎吧~", 0, 255, 120); //发送全服广播
}

//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        TShockAPI.Hooks.AccountHooks.AccountCreate -= AccountHooks_AccountCreate; ; //卸载TShockAPI钩子(玩家创建账号)
    }
    base.Dispose(disposing);
}

•GetDataHandlers数据包钩子
服务器会收到客户端发送的数据包,而GetDataHandlers钩子会解析这些数据包,触发对应的数据包钩子,并且打包成参数传入回调函数,但是GetDataHandlers并不包含所有数据包
C#:
//插件加载时执行的代码
public override void Initialize()
{
    GetDataHandlers.KillMe.Register(OnKillPlayer); //注册GetDataHandlers数据包钩子(玩家死亡)
}
//回调函数
private void OnKillPlayer(object? sender, GetDataHandlers.KillMeEventArgs e)
{
    TShock.Utils.Broadcast($"[死亡]{e.Player.Name}挂掉啦,快来嘲笑他吧!", 0, 255, 120); //发送全服广播
    e.Handled = false; //是否处理这个事件,默认为false,如果你标记为true,这个事件将会被忽略,会导致TShock直接忽略这个玩家死亡(不进行重生倒计时)
}

//插件卸载时执行的代码
protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        GetDataHandlers.KillMe.UnRegister(OnKillPlayer); //卸载GetDataHandlers数据包钩子(玩家死亡)
    }
    base.Dispose(disposing);
}
注:args.Handled可以标记这个事件是否已经处理如果将其设为true服务器可能会忽略这个事件
除此之外,还有On钩子和IL钩子,将在以后填坑...
 
最后编辑:

肝帝熙恩

Lv3
LV
0
 
IP属地
湖南省
2020/08/13
23
2
勋章
1
  • · 发布于湖南省
催更!
 

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港

钩子练习题

1. 使用ServerApi钩子实现,在玩家加入服务器时(ServerJoin,ServerLeave)广播"欢迎玩家XXX加入服务器!",在玩家离开服务器时广播"玩家XXX离开服务器!"
提示: 可以使用如下语句获取TShock玩家对象

C#:
TSPlayer plr = TShock.Players[args.Who]; //获取TShock玩家对象,args.Who是玩家的索引
C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            ServerApi.Hooks.ServerJoin.Register(this, OnJoin); //注册钩子
            ServerApi.Hooks.ServerLeave.Register(this, OnLeave);
        }
        private void OnJoin(JoinEventArgs args)
        {
            TSPlayer plr = TShock.Players[args.Who]; //获取TShock玩家对象,args.Who是玩家的索引
            TShock.Utils.Broadcast($"欢迎玩家{plr.Name}加入服务器!", 255, 255, 255); //发送广播Broadcast(内容,R,G,B) R,G,B是对应的RGB颜色代码
        }
        private void OnLeave(LeaveEventArgs args)
        {
            TSPlayer plr = TShock.Players[args.Who]; //获取TShock玩家对象,args.Who是玩家的索引
            TShock.Utils.Broadcast($"玩家{plr.Name}离开服务器!", 255, 255, 255); //发送广播Broadcast(内容,R,G,B) R,G,B是对应的RGB颜色代码
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ServerApi.Hooks.ServerJoin.Deregister(this, OnJoin); //注销钩子
                ServerApi.Hooks.ServerLeave.Deregister(this, OnLeave);
            }
            base.Dispose(disposing);
        }
    }
}
2. 使用ServerApi钩子实现,在服务器加载完成后(GamePostInitialize)在控制台打印"服务器已经加载完啦!"
GameInitialize和GamePostInitialize的区别:
他们都是游戏初始化钩子,但是GameInitialize是开始初始化时触发,而GamePostInitialize是完成初始化时触发(即地图加载完毕,服务器处于可加入状态)

C#:
ServerApi.Hooks.GameInitialize.Register(this,(args) => { Console.WriteLine("触发GameInitialize"); });
ServerApi.Hooks.GamePostInitialize.Register(this,(args) => { Console.WriteLine("触发GamePostInitialize"); });
1697341632062.png1697341669111.png
C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            ServerApi.Hooks.GamePostInitialize.Register(this, OnGamePostInitialize); //注册钩子
        }

        private void OnGamePostInitialize(EventArgs args)
        {
            Console.WriteLine("服务器已经加载完啦!");
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                ServerApi.Hooks.GamePostInitialize.Deregister(this, OnGamePostInitialize); //卸载钩子
            }
            base.Dispose(disposing);
        }
    }
}
3. 使用TShockAPI钩子实现,在玩家完成登录后(PlayerHooks.PlayerPostLogin) 给玩家发送"您登录的IP是:xxx.xxx.xxx.xxx"
提示: 可以使用如下语句获取玩家IP

C#:
string ip = e.Player.IP;
C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;


namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            PlayerHooks.PlayerPostLogin += PlayerHooks_PlayerPostLogin;  //注册钩子
        }

        private void PlayerHooks_PlayerPostLogin(PlayerPostLoginEventArgs e)
        {
            e.Player.SendSuccessMessage($"您的登录IP是:{e.Player.IP}");
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                PlayerHooks.PlayerPostLogin -= PlayerHooks_PlayerPostLogin;  //注销钩子
            }
            base.Dispose(disposing);
        }
    }
}
4. 使用TShockAPI钩子实现,在服务器使用/reload命令(GeneralHooks.ReloadEvent)后显示"服务器正在重载!"
重点: Reload钩子非常重要,可以让你用/reload统一重载插件

C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            GeneralHooks.ReloadEvent += GeneralHooks_ReloadEvent;
        }

        private void GeneralHooks_ReloadEvent(ReloadEventArgs e)
        {
            //这个钩子非常常用,可以让你用/reload统一重载插件
            TSPlayer.All.SendWarningMessage("服务器正在重载!!!"); //这是发送全服广播的另一种方法,TSPlayer.All对象可以代表全服所有玩家
            //上面代码也可以换为
            //TShock.Utils.Broadcast("服务器正在重载!!!",255,135,0);
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                GeneralHooks.ReloadEvent -= GeneralHooks_ReloadEvent;
            }
            base.Dispose(disposing);
        }
    }
}
 

附件

  • 1697341640028.png
    1697341640028.png
    17.8 KB · 查看: 0
最后编辑:

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港
5. 使用GetDataHandlers实现,在玩家打开箱子时(GetDataHandlers.ChestOpen)广播"玩家XXX打开了箱子XXX位于(X, Y)"
提示: 用以下代码可以获取箱子对象

C#:
int chestIndex = Chest.FindChest(e.X,e.Y); //找到箱子的索引
Chest chest = Main.chest[chestIndex]; //获取箱子对象
//箱子名称: chest.name, 箱子X坐标: chest.x, 箱子Y坐标: chest.y, 箱子物品数组: chest.item
C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            GetDataHandlers.ChestOpen.Register(ChestOpen); //注册钩子
        }

        private void ChestOpen(object? sender, GetDataHandlers.ChestOpenEventArgs e)
        {
            int chestIndex = Chest.FindChest(e.X,e.Y); //找到箱子的索引
            Chest chest = Main.chest[chestIndex]; //获取箱子对象
            TSPlayer.All.SendWarningMessage($"玩家{e.Player}打开了箱子{chest.name}位于({chest.x}, {chest.y})");
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                GetDataHandlers.ChestOpen.UnRegister(ChestOpen); //注销钩子
            }
            base.Dispose(disposing);
        }
    }
}
6. 使用GetDataHandlers实现,在玩家切换PVP模式时(GetDataHandlers.TogglePvp)发送"本服务器禁止切换PVP模式",并记录日子"玩家XXX试图开启/关闭PVP",并且阻止玩家切换
提示1: 用以下代码可以记录日志

C#:
//只写日志
TShock.Log.Info("信息Log");
TShock.Log.Warn("警告Log");
TShock.Log.Error("错误Log");
TShock.Log.Debug("调试Log,要在Config打开Debug才会生效哦");
//写日志并打印到控制台
TShock.Log.ConsoleInfo("信息Log,会打印在控制台上哦");
TShock.Log.ConsoleWarn("警告Log,会打印在控制台上哦");
TShock.Log.ConsoleError("错误Log,会打印在控制台上哦");
TShock.Log.ConsoleDebug("调试Log,会打印在控制台上哦,要在Config打开Debug才会生效哦");
提示2: 用以下代码获取玩家想要切换的PVP状态
C#:
bool pvp = e.Pvp //切换前状态为!e.pvp
提示3: 用以下代码切换PVP模式
C#:
e.Player.SetPvP(bool);

C#:
using System.Reflection;
using Terraria;
using TerrariaApi.Server;
using TShockAPI;
using TShockAPI.Hooks;

namespace Plugin
{
    [ApiVersion(2, 1)]
    public class Plugin : TerrariaPlugin
    {
        //定义插件的作者名称
        public override string Author => "Cai";

        //插件的一句话描述
        public override string Description => "钩子测试";

        //插件的名称
        public override string Name => "Hooks";

        //插件的版本
        public override Version Version => Assembly.GetExecutingAssembly().GetName().Version;

        //插件的构造器
        public Plugin(Main game) : base(game)
        {
        }

        //插件加载时执行的代码
        public override void Initialize()
        {
            GetDataHandlers.TogglePvp.Register(TogglePvp); //注册钩子
        }

        private void TogglePvp(object? sender, GetDataHandlers.TogglePvpEventArgs e)
        {
            e.Player.SendErrorMessage("本服务器禁止切换PVP模式"); //给玩家发送消息
            TShock.Log.ConsoleInfo($"玩家{e.Player.Name}试图{(e.Pvp?"打开":"关闭")}PVP"); //输出日志
            e.Player.SetPvP(!e.Pvp); //设置PVP(和e.Pvp相反)
            e.Handled = true; //标记为已处理,即忽略这个事件
        }

        //插件卸载时执行的代码
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                GetDataHandlers.TogglePvp.UnRegister(TogglePvp); //注销钩子
            }
            base.Dispose(disposing);
        }
    }
}
 

肝帝熙恩

Lv3
LV
0
 
IP属地
湖南省
2020/08/13
23
2
勋章
1
  • · 发布于湖南省

羽毛笔

Lv2
LV
0
 
IP属地
四川省
2023/10/22
3
0
  • · 发布于未知
可以把史莱姆王的仇恨改成月总吗
 

Cai233

Lv3
LV
0
 
IP属地
陕西省
2021/08/18
78
27
  • · 发布于香港
  • 标签
    插件制作 插件教程
  • * 这是一则由 Google AdSense 自动推荐的广告,与本站无关,不对其真实性与可靠性负责

    顶部