你的第一个插件¶
推荐:配合 EndstoneDotnet.Toolchain 使用
手工管理项目、引用路径与部署比较繁琐。推荐安装 EndstoneDotnet.Toolchain 配套应用,几步点击即可创建、构建并部署 .NET 插件。教程见 EndstoneDotnet.Toolchain 使用教程。
1. 创建项目¶
创建一个目标框架为 net10.0 的类库,并引用 Endstone.Loader.dll:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<!-- 程序集名称必须以 .Plugin 结尾;加载器匹配 *.Plugin.dll -->
<AssemblyName>My.Plugin</AssemblyName>
</PropertyGroup>
<ItemGroup>
<!-- 按 Endstone.Loader.dll 的实际路径调整 -->
<Reference Include="Endstone.Loader">
<HintPath>..\Endstone.Loader.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
</Project>
也可以像示例项目一样使用 <ProjectReference>;这样 Endstone.Loader.dll 不会复制到插件旁边。
2. 插件主类¶
using Endstone.Loader;
namespace My.Plugin;
[Plugin("my_plugin", "1.0.0",
Description = "My first .NET plugin",
Authors = ["Someone"])]
public sealed class MyPlugin : PluginBase
{
public override void OnLoad()
{
Logger.Info("Plugin loading");
}
public override void OnEnable()
{
Logger.Info("Plugin enabled");
}
public override void OnDisable()
{
Logger.Info("Plugin disabled");
}
}
说明:
- 类必须继承
PluginBase并带有[Plugin]特性 - 插件名称(
my_plugin)只能包含小写字母、数字与下划线(Endstone 要求) OnLoad在插件加载时调用(世界可能尚未就绪),OnEnable在启用时调用(世界可用),OnDisable在服务器关闭 / 插件卸载时调用Logger属性写入服务器的插件日志:Trace默认被过滤,Info及以上可见
3. 注册命令¶
public override void OnLoad()
{
Command("hello")
.Description("Say hello")
.Usage("/hello", "/hello <name>")
.Alias("hi")
.Permission("myplugin.command.hello")
.Handler(OnHelloCommand);
}
private bool OnHelloCommand(CommandSender sender, IReadOnlyList<string> args)
{
var name = args.Count > 0 ? args[0] : sender.Name;
sender.SendMessage("Hello, {0}!", name);
return true;
}
CommandSender 既可以是玩家也可以是控制台:用 sender.IsPlayer 判断,用 sender.AsPlayer() 转型。
缺少所需权限的玩家会被 Endstone 自动拒绝。
4. 注册事件¶
public override void OnEnable()
{
RegisterEvent<PlayerJoinEvent>(OnPlayerJoin);
RegisterEvent<PlayerChatEvent>(OnPlayerChat, EventPriority.High);
}
private void OnPlayerJoin(PlayerJoinEvent e)
{
var player = e.Player!;
Server.BroadcastMessage($"Welcome {player.Name}!");
}
private void OnPlayerChat(PlayerChatEvent e)
{
if (e.Message == "ping")
{
e.IsCancelled = true;
e.Player!.SendMessage("pong!");
}
}
RegisterEvent<T> 按事件类型注册;T 必须是 Endstone.Loader 命名空间内的事件类。
EventPriority 控制跨插件的处理顺序。设置 e.IsCancelled = true 可取消事件。
5. 调度器(延迟 / 周期任务)¶
// 40 tick(2 秒)后执行一次,同步(服务器主线程)
var task = Scheduler.RunTaskLater(() => Logger.Info("Sync task fired"), 40);
// 周期任务:每 20 tick 执行,首次在 10 tick 后
var timer = Scheduler.RunTaskTimer(() => Logger.Info("Once per second"), 10, 20);
// 异步任务:在独立线程上运行
var asyncTask = Scheduler.RunTaskLaterAsync(() =>
{
// 只做纯托管计算;BDS API 在这里被严格禁止
}, 40);
// 取消
timer.Cancel();
Scheduler.CancelAll(); // 插件停用时会自动执行
var pending = Scheduler.GetPendingTasks(); // 列出所有排队任务
线程安全
异步任务(RunTaskAsync、RunTaskLaterAsync、RunTaskTimerAsync)运行在原生调度器的
工作线程上。在这些回调中调用任何 BDS 侧 API(SendMessage、BroadcastMessage、世界/实体操作等)
是**严格禁止的,会导致服务器崩溃**。异步回调只应做纯托管计算;需要操作游戏世界时,
先用同步任务回到主线程。
6. 其他能力¶
- 表单:
player.ShowForm(new MessageForm()... / ActionForm / ModalForm) - boss 血条:
Server.CreateBossBar(title, color, style, flags) - 地图:
Server.CreateMap(dimension)、MapView.AddRenderer(MapRenderer)、player.SendMap(map) - 背包:
player.Inventory(读 / 加 / 删 / 护甲槽) - 物品:
ItemStack.Create("minecraft:diamond", 1) - 附魔:
Enchantment.Get("minecraft:sharpness")、item.AddEnchant(id, level, force: false) - 方块:
dimension.GetBlockAt(x, y, z)、block.CaptureState() - 实体:
dimension.SpawnActor(type, location)、player.SpawnMob(type) - 世界:
Server.Level、level.GetDimension("overworld")、dimension.GetLoadedChunks() - 消息:
Server.BroadcastMessage(...)、player.SendTitle(...)、player.SendToast(...)
完整 API 见API 参考。
7. 构建与部署¶
把 bin\Release\net10.0\ 中的 *.Plugin.dll(程序集名以 .Plugin 结尾的那个文件)复制到
服务器根目录的 plugins.net\ 文件夹,然后重启服务器。开发时也可以一并复制 .pdb 文件
以获得断点符号。
不使用工具链的话,用 ILRepack 合并依赖
加载器把 plugins.net\ 中的每个 *.Plugin.dll 文件当作一个插件,而上面的部署步骤只
复制主程序集。如果插件依赖第三方包,请用 ILRepack
把所有依赖 DLL 合并进主程序集,使单个 *.Plugin.dll 自包含。需要在独立上下文加载的
依赖(如原生绑定)可保留为独立文件。