回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
本节教程演示如何生成一个解决方案, 编译插件并加载到游戏里获得日志反馈.
按照工具配置搭建好SKSEPlugins
开发环境并完成必需的-BOOTSTRAP
步骤后, 使用!MakeNew MyNewPlugin
脚本生成一个名为MyNewPlugin
的插件项目, 随后通过!Rebuild
脚本生成解决方案并预编译CLib
静态库, 以节省后期编译的时间.
cd .\SKSEPlugins
.\!makenew MyNewPlugin
.\!rebuild flatrim
生成结束后, 打开SKSE64_FLATRIM.sln
并定位到Plugins\MyNewPlugin
项目,这便是我们插件项目.
\include
: 包含插件信息和插件加载方法的头文件, 均为自动生成\Precompile Header File
: CMake项目自动生成, 包含预编译头(.hxx
)\Source Files
: CMake项目自动生成, 包含预编译头的编译单元(.cxx
)\src
: 插件项目实际源码位置, 对于插件的开发都会在此操作.clang-format
: 代码风格格式文件CMakeLists.txt
: CMake项目文件vcpkg.json
:vcpkg
依赖库清单, 以及插件项目的mod安装信息
打开main.cpp
可以看见如下结构:
如果使用的是默认的CommonLibSSE-NG
, 则移除掉行9处的REL::Module::reset();
. 这是一个CLib-NG库的bug的暂时修复, 但默认CLib-NG库将其放在了单元测试代码块里, 此处暂时带过. 若使用MaxSu的CLib库NG分支, 可以保持原样, 因为MaxSu的CLib库NG分支移除了单元测试限定.
DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse)
这是SKSE插件项目的加载入口, 类似于普通dll的DLLMAIN
, 当SKSE插件被skse_loader加载时, 会据插件名字顺序依次执行各个插件的SKSEPlugin_Load
函数.
#ifndef NDEBUG
while (!IsDebuggerPresent()) { Sleep(100); }
#endif
这是用于调试(debug)插件的语句, 会在插件被加载时进入等待循环, 以保证插件作者有足够的时间附加调试器到游戏进程上并加载插件项目的调试符号. 此处我们暂时将while
语句注释掉, 具体的调试步骤后面再展开.
DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string());
SKSE::Init(a_skse);
INFO("{} v{} loaded", Plugin::NAME, Plugin::Version);
这部分代码用于加载logger并初始化插件内部的SKSE接口, 以确保插件可以正确的与SKSE交互. 随后打印一句标准log表示logger加载完毕.
// do stuff
return true;
最后这部分代码, // do stuff
注释后则是我们实际进行插件操作的地方, 譬如注册SKSE消息回调函数, 加载配置文件, 启用内存补丁等. 当一切操作成功后, 则为SKSE返回true
, 反之则返回false
向SKSE汇报插件加载失败.
使用SKSEPlugins
脚本部署的插件开发环境可以使用INFO()
, DEBUG()
, 和ERROR()
宏来输出log语句, 依照std::fmt
的格式.
INFO("INFO语句, 插件名 {}, 加载成功: {}", Plugin::NAME, true);
DEBUG("DEBUG语句, Release模式下未启用`DEBUG LOG`则不会输出DEBUG语句");
ERROR("致命错误语句, 会弹出当前代码部分的详细信息并中止游戏进程");
善用日志宏, 对于插件开发和纠错排bug有很大的帮助.
按下Ctrl+B
编译插件, 在生成事件中选择复制到游戏Data(Copy to Data
)或安装至MO2(Copy to MO2
).
启动游戏, 加载完毕后打开SKSE log目录下的MyNewPlugin.log
自此一个非常基础的SKSE插件项目就完成了从生成到编译到加载的全部步骤.
开发插件的过程中, 必然会遇到插件的功能不能在SKSEPlugin_Load
处执行, 即不能在SKSE加载插件时就立刻执行, 此时很多游戏内数据并未初始化, 很多函数也并未加载, 因此需要注册一个SKSE消息回调函数, 在SKSE加载游戏的各个阶段分批次执行我们的回调函数.
首先我们准备一个符合SKSE标准的消息回调函数:
// @ main.cpp
void MessageHandler(SKSE::MessagingInterface::Message* a_msg) noexcept
{
if (a_msg->type == SKSE::MessagingInterface::kDataLoaded) {
// do callback stuff
INFO("This is a callback after data loaded!");
}
}
这是最常见的一种回调, 它的触发条件为当SKSE
加载完所有游戏资源后(kDataLoaded
), 对于游戏各种类和数据的调用/修改都应当于此处或之后执行.
当需要注册多种条件的回调时, 则可以将if
语句转换为switch (a_msg->type)
语句, 并使用SKSE提供的以下条件:
kPostLoad
kPostPostLoad
kPreLoadGame
kPostLoadGame
kSaveGame
kDeleteGame
kInputLoaded
kNewGame
kDataLoaded
在SKSEPlugin_Load
函数内使用SKSE提供的消息接口来注册我们的消息回调:
// @ main.cpp @@ SKSEPlugin_Load
if (!SKSE::GetMessagingInterface()->RegisterListener(MessageHandler)) {
return false;
}
若注册失败, 则依据SKSE加载规则返回false
跳过加载我们的插件.
// @ main.cpp
namespace
{
void MessageHandler(SKSE::MessagingInterface::Message* a_msg)
{
// 数据加载完毕后, 执行Form修改操作
if (a_msg->type == SKSE::MessagingInterface::kDataLoaded) {
Forms::PatchAll();
}
}
}
DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse)
{
#ifndef NDEBUG
while (!IsDebuggerPresent()) { Sleep(100); }
#endif
DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string());
SKSE::Init(a_skse);
INFO("{} v{} loaded", Plugin::NAME, Plugin::Version);
// 加载配置文件
Config::Load();
// 启用内存补丁
if (*Config::EnableUE) {
Hooks::Install();
}
// 注册回调
const auto* message = SKSE::GetMessagingInterface();
if (!message->RegisterListener(MessageHandler)) {
return false;
}
return true;
}
回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook