├── Format.ps1 ├── README.md ├── docs ├── resources │ ├── Events.md │ ├── Papyrus.md │ └── Plugin.md ├── setup │ ├── QuickStart.md │ ├── Script.md │ └── Setup.md └── tounknown │ ├── ASM101.md │ ├── ASM102.md │ ├── DEBUG101.md │ ├── FuncHook.md │ ├── MemPatch.md │ ├── NERR.md │ └── YCS.md └── images ├── quickstart ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 15.png ├── 16.png ├── 17.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png └── 9.png ├── resources ├── mynewplugin_project.png ├── plugin_log.png ├── plugin_main.png ├── plugin_postbuild.png ├── rebuild_pt1.png └── rebuild_pt2.png ├── setup ├── quick_add.png ├── rebuilt.png ├── vscxx.png └── win_terminal.png └── toukn ├── asm101_register_highlow.png ├── debug101_dbg_tls.png ├── nerr ├── ce_perk_filter.png ├── ce_perk_pre.png ├── dbg_2ndcaller.png ├── dbg_boolcheck.png ├── dbg_caller.png ├── dbg_doneAGAIN.png ├── dbg_hardbp.png ├── dbg_loadmain_src.png ├── dbg_nops.png ├── dbg_offset.png ├── dbg_ret.png ├── dbg_rva.png ├── re_done.png ├── re_id.png └── re_log.png └── ycs ├── ce_filter.png ├── ce_owned.png ├── dbg_1stcaller.png ├── dbg_air_loop.png ├── dbg_airloop_nop.png ├── dbg_call.png ├── dbg_cond.png ├── dbg_hardbp.png ├── dbg_hbphit.png ├── dbg_main.png ├── dbg_owned_jmp.png ├── dbg_rva.png ├── dbg_strptr.png ├── re_id.png ├── re_noair.png ├── re_pre_ce.png └── re_succeeded.png /Format.ps1: -------------------------------------------------------------------------------- 1 | $Header = '
回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
' 2 | 3 | $Docs = Get-ChildItem $PSScriptRoot -File -Filter *.md -Recurse 4 | 5 | foreach ($doc in $Docs) { 6 | $content = [IO.File]::ReadAllText($doc) 7 | 8 | $content = $content -replace '(?s)(?:(?<=\\))', $Header
9 |
10 | [IO.File]::WriteAllText($doc, $content)
11 |
12 | "Formatted $($doc.Name)"
13 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SKSE64插件开发入门
2 | for 上古卷轴5: 天际 (SKSE64) - 持续施工中
3 |
4 | 希望能够帮助到对SKSE64插件开发感兴趣的朋友.
5 |
6 | 此教程更新时游戏版本为: *SE 1.5.97*, *AE 1.6.640*, *SKSE 2.2.2*, *CommonLibSSE-NG 3.5.2*
7 |
8 | > 欢迎PR任何的更改建议, 意见或疑惑.
9 | > 联系作者? 先善用网络工具搜索, 疑难杂症可以加入QQ群, 群号`0x215249EF`.
10 |
11 | ---
12 | ## 目录导航
13 | 已完成: ✓ 施工中: ✗
14 |
15 | ### 0. 开发环境
16 | - [工具配置](/docs/setup/Setup.md) ✓
17 | - [Maxsu的快速入门](/docs/setup/QuickStart.md)
18 | - [脚本说明](/docs/setup/Script.md) ✓
19 | - [脚本Repo](https://github.com/gottyduke/SKSEPlugins)
20 |
21 | ### 1. 游戏资源
22 | - [插件基础](/docs/resources/Plugin.md) ✓
23 | - [事件响应](/docs/resources/Events.md) ✓
24 | - [Papyrus调用](/docs/resources/Papyrus.md) ✓
25 |
26 | ### 2. 探索未知
27 | - [内存补丁](/docs/tounknown/MemPatch.md) ✓
28 | - [函数Hook](/docs/tounknown/FuncHook.md) ✗
29 |
30 | ---
31 |
32 | 感谢Ryan和SKSE组让这一切成为可能.
33 |
34 | 感谢射大师(T-Avatar)的帮助.
35 |
36 | 感谢Maxsu的无数次测试和帮助.
37 |
38 | ---
39 | 作者: Dropkicker & Maxsu @ 2022 回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook事件响应
2 |
回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
14 | -------------------------------------------------------------------------------- /docs/resources/Papyrus.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本节教程演示如何在插件注册一个原生函数(`native`)到游戏中供Papyrus脚本调用. 5 | 6 | ## SKSE 7 | 8 | ### 函数原型 9 | 10 | 首先我们准备符合SKSE标准的Papyrus原生函数: 11 | ```cpp 12 | std::string GetText(RE::StaticFunctionTag*) 13 | { 14 | return "Hello Papyrus"s; 15 | } 16 | 17 | std::string GetIntText(RE::StaticFunctionTag*, std::int32_t a_int) 18 | { 19 | return fmt::format("Hello Papyrus, this is {}", a_int); 20 | } 21 | ``` 22 | 函数很简单, `GetText()`被Papyrus调用时, 返回一个内容为`Hello Papyrus`的字符串. 而`GetIntText(Int)`被调用时, 返回一个内容为`Hello Papyrus`及`Int`参数转化的字符串. Papyrus原生函数在CLib中以`RE::StaticFunctionTag*`作为第一个参数, 即使这个函数本身并不接受参数. 类似于成员函数都以`this`作为第一个参数. 23 | 24 | ### 注册Papyrus虚拟机 25 | 26 | 有了Papyrus原生函数后, 便需要把这个函数注册到游戏内部的Papyrus虚拟机中, 这样我们的Papyrus脚本就能以此调用我们SKSE插件为其注册的函数. SKSE注册Papyrus函数分为三部分, 1) 获取SKSE提供的Papyrus接口; 2) 通过Papyrus接口注册Papyrus虚拟机处理函数; 3) 通过Papyrus虚拟机处理函数注册我们的Papyrus原生函数至Papyrus虚拟机内. 27 | 以下为示例代码: 28 | ```cpp 29 | namespace 30 | { 31 | std::string GetText(RE::StaticFunctionTag*) 32 | { 33 | return "Hello Papyrus"s; 34 | } 35 | 36 | std::string GetIntText(RE::StaticFunctionTag*, std::uint32_t a_int) 37 | { 38 | return fmt::format("Hello Papyrus, this is {}", a_int); 39 | } 40 | 41 | // 3) 注册我们的Papyrus原生函数 42 | bool PapyrusVMHandler(RE::BSScript::IVirtualMachine* a_vm) 43 | { 44 | a_vm->RegisterFunction("SKSE_GetText", "MyNewPlugin_Native", GetText); 45 | a_vm->RegisterFunction("SKSE_GetIntText", "MyNewPlugin_Native", GetIntText); 46 | 47 | return true; 48 | } 49 | 50 | void MessageHandler(SKSE::MessagingInterface::Message* a_msg) noexcept 51 | { 52 | if (a_msg->type == SKSE::MessagingInterface::kDataLoaded) { 53 | // 1) 获取SKSE提供的Papyrus接口 54 | auto* papyrus = SKSE::GetPapyrusInterface(); 55 | // 2) 注册Papyrus虚拟机处理函数 56 | papyrus->Register(PapyrusVMHandler); 57 | } 58 | } 59 | } 60 | 61 | DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 62 | { 63 | #ifndef NDEBUG 64 | while (!IsDebuggerPresent()) { Sleep(100); } 65 | #endif 66 | 67 | DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string()); 68 | SKSE::Init(a_skse); 69 | INFO("{} v{} loaded", Plugin::NAME, Plugin::Version); 70 | 71 | // do stuff 72 | if (!SKSE::GetMessagingInterface()->RegisterListener(MessageHandler)) { 73 | return false; 74 | } 75 | 76 | return true; 77 | } 78 | ``` 79 | Papyrus虚拟机提供的`RegisterFunction`用于将一个C++原生函数注册到Papyrus虚拟机中, 并将其与Papyrus内对应的函数名字及类名字绑定. 80 | ```cpp 81 | // 将GetText注册为SKSE_GetText, 类名为MyNewPlugin_Native 82 | a_vm->RegisterFunction("SKSE_GetText", "MyNewPlugin_Native", GetText); 83 | // 将GetIntText注册为SKSE_GetIntText, 类名为MyNewPlugin_Native 84 | a_vm->RegisterFunction("SKSE_GetIntText", "MyNewPlugin_Native", GetIntText); 85 | ``` 86 | 87 | ### 调用Papyrus原生函数 88 | 89 | 在我们的`MyNewPlugin_Native.psc`中加入: 90 | ```papyrus 91 | Scriptname MyNewPlugin_Native 92 | 93 | String Function GetText() native 94 | String Function GetIntText(Int aiNum) native 95 | ``` 96 | 按照Papyrus脚本格式调用即可(`Scriptname XXX extends MyNewPlugin_Native`, `native global`等). 97 | 98 | ### Papyrus类型与C++类型对照 99 | 100 | 一些**常用**的类型: 101 | |Papyrus|C++| 102 | |-|-| 103 | |`Int`|`std::int32_t`| 104 | |`Float`|`float`| 105 | |`String`|`std::string`或`RE::BSFixedString`| 106 | |`Bool`|`bool`| 107 | |`Actor`|`RE::Actor*`| 108 | |`Faction`|`RE::TESFaction*`| 109 | |`Form`|`RE::TESForm*`| 110 | |`ObjectReference`|`RE::TESObjectREFR*`| 111 | |`Quest`|`RE::TESQuest*`| 112 | |`ReferenceAlias`|`RE::BGSRefAlias*`| 113 | |`Shout`|`RE::TESShout*`| 114 | |`Spell`|`RE::SpellItem*`| 115 | 116 | 当Papyrus类型为数组时, C++类型为编译期已知`std::array`或动态`std::vector`. 117 | 当Papyrus脚本传入数组参数时, C++类型为`const RE::reference_array`. 118 | 119 | --- 120 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
121 | -------------------------------------------------------------------------------- /docs/resources/Plugin.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本节教程演示如何生成一个解决方案, 编译插件并加载到游戏里获得日志反馈. 5 | 6 | ## 解决方案 7 | 8 | 按照[工具配置](/docs/setup/Setup.md)搭建好`SKSEPlugins`开发环境并完成必需的`-BOOTSTRAP`步骤后, 使用`!MakeNew MyNewPlugin`脚本生成一个名为`MyNewPlugin`的插件项目, 随后通过`!Rebuild`脚本生成解决方案并预编译`CLib`静态库, 以节省后期编译的时间. 9 | ```powershell 10 | cd .\SKSEPlugins 11 | .\!makenew MyNewPlugin 12 | .\!rebuild flatrim 13 | ``` 14 | `flatrim`指代除VR以外的所有版本. 15 |  16 |  17 | 18 | ## 项目构成 19 | 20 | 生成结束后, 打开`SKSE64_FLATRIM.sln`并定位到`Plugins\MyNewPlugin`项目,这便是我们插件项目. 21 |  22 | + `\include`: 包含插件信息和插件加载方法的头文件, 均为自动生成 23 | + `\Precompile Header File`: CMake项目自动生成, 包含预编译头(`.hxx`) 24 | + `\Source Files`: CMake项目自动生成, 包含预编译头的编译单元(`.cxx`) 25 | + `\src`: 插件项目实际源码位置, 对于插件的开发都会在此操作 26 | + `.clang-format`: 代码风格格式文件 27 | + `CMakeLists.txt`: CMake项目文件 28 | + `vcpkg.json`: `vcpkg`依赖库清单, 以及插件项目的mod安装信息 29 | 30 | 打开`main.cpp`可以看见如下结构: 31 |  32 | 如果使用的是默认的`CommonLibSSE-NG`, 则移除掉行9处的`REL::Module::reset();`. 这是一个CLib-NG库的bug的暂时修复, 但默认CLib-NG库将其放在了单元测试代码块里, 此处暂时带过. 若使用MaxSu的CLib库NG分支, 可以保持原样, 因为MaxSu的CLib库NG分支移除了单元测试限定. 33 | ```cpp 34 | DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 35 | ``` 36 | 这是SKSE插件项目的加载入口, 类似于普通dll的`DLLMAIN`, 当SKSE插件被skse_loader加载时, 会据插件名字顺序依次执行各个插件的`SKSEPlugin_Load`函数. 37 | ```cpp 38 | #ifndef NDEBUG 39 | while (!IsDebuggerPresent()) { Sleep(100); } 40 | #endif 41 | ``` 42 | 这是用于调试(debug)插件的语句, 会在插件被加载时进入等待循环, 以保证插件作者有足够的时间附加调试器到游戏进程上并加载插件项目的调试符号. 此处我们暂时将`while`语句注释掉, 具体的调试步骤后面再展开. 43 | ```cpp 44 | DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string()); 45 | SKSE::Init(a_skse); 46 | INFO("{} v{} loaded", Plugin::NAME, Plugin::Version); 47 | ``` 48 | 这部分代码用于加载logger并初始化插件内部的SKSE接口, 以确保插件可以正确的与SKSE交互. 随后打印一句标准log表示logger加载完毕. 49 | ```cpp 50 | // do stuff 51 | 52 | return true; 53 | ``` 54 | 最后这部分代码, `// do stuff`注释后则是我们实际进行插件操作的地方, 譬如注册SKSE消息回调函数, 加载配置文件, 启用内存补丁等. 当一切操作成功后, 则为SKSE返回`true`, 反之则返回`false`向SKSE汇报插件加载失败. 55 | 56 | ## 日志宏 57 | 58 | 使用`SKSEPlugins`脚本部署的插件开发环境可以使用`INFO()`, `DEBUG()`, 和`ERROR()`宏来输出log语句, 依照`std::fmt`的格式. 59 | ```cpp 60 | INFO("INFO语句, 插件名 {}, 加载成功: {}", Plugin::NAME, true); 61 | DEBUG("DEBUG语句, Release模式下未启用`DEBUG LOG`则不会输出DEBUG语句"); 62 | ERROR("致命错误语句, 会弹出当前代码部分的详细信息并中止游戏进程"); 63 | ``` 64 | 善用日志宏, 对于插件开发和纠错排bug有很大的帮助. 65 | 66 | ## 编译与部署 67 | 68 | 按下`Ctrl+B`编译插件, 在生成事件中选择复制到游戏Data(`Copy to Data`)或安装至MO2(`Copy to MO2`). 69 |  70 | 启动游戏, 加载完毕后打开SKSE log目录下的`MyNewPlugin.log` 71 |  72 | 73 | 自此一个非常基础的SKSE插件项目就完成了从生成到编译到加载的全部步骤. 74 | 75 | ## 消息回调 76 | 77 | 开发插件的过程中, 必然会遇到插件的功能不能在`SKSEPlugin_Load`处执行, 即不能在SKSE加载插件时就立刻执行, 此时很多游戏内数据并未初始化, 很多函数也并未加载, 因此需要注册一个SKSE消息回调函数, 在SKSE加载游戏的各个阶段分批次执行我们的回调函数. 78 | 79 | ### 消息处理 80 | 81 | 首先我们准备一个符合SKSE标准的消息回调函数: 82 | ```cpp 83 | // @ main.cpp 84 | void MessageHandler(SKSE::MessagingInterface::Message* a_msg) noexcept 85 | { 86 | if (a_msg->type == SKSE::MessagingInterface::kDataLoaded) { 87 | // do callback stuff 88 | INFO("This is a callback after data loaded!"); 89 | } 90 | } 91 | ``` 92 | 这是最常见的一种回调, 它的触发条件为当`SKSE`加载完所有游戏资源后(`kDataLoaded`), 对于游戏各种类和数据的调用/修改都应当于此处或之后执行. 93 | 当需要注册多种条件的回调时, 则可以将`if`语句转换为`switch (a_msg->type)`语句, 并使用SKSE提供的以下条件: 94 | ```cpp 95 | kPostLoad 96 | kPostPostLoad 97 | kPreLoadGame 98 | kPostLoadGame 99 | kSaveGame 100 | kDeleteGame 101 | kInputLoaded 102 | kNewGame 103 | kDataLoaded 104 | ``` 105 | 106 | ### 注册回调 107 | 108 | 在`SKSEPlugin_Load`函数内使用SKSE提供的消息接口来注册我们的消息回调: 109 | ```cpp 110 | // @ main.cpp @@ SKSEPlugin_Load 111 | if (!SKSE::GetMessagingInterface()->RegisterListener(MessageHandler)) { 112 | return false; 113 | } 114 | ``` 115 | 若注册失败, 则依据SKSE加载规则返回`false`跳过加载我们的插件. 116 | 117 | ### 示例 118 | ```cpp 119 | // @ main.cpp 120 | namespace 121 | { 122 | void MessageHandler(SKSE::MessagingInterface::Message* a_msg) 123 | { 124 | // 数据加载完毕后, 执行Form修改操作 125 | if (a_msg->type == SKSE::MessagingInterface::kDataLoaded) { 126 | Forms::PatchAll(); 127 | } 128 | } 129 | } 130 | 131 | 132 | DLLEXPORT bool SKSEAPI SKSEPlugin_Load(const SKSE::LoadInterface* a_skse) 133 | { 134 | #ifndef NDEBUG 135 | while (!IsDebuggerPresent()) { Sleep(100); } 136 | #endif 137 | 138 | DKUtil::Logger::Init(Plugin::NAME, REL::Module::get().version().string()); 139 | 140 | SKSE::Init(a_skse); 141 | 142 | INFO("{} v{} loaded", Plugin::NAME, Plugin::Version); 143 | 144 | // 加载配置文件 145 | Config::Load(); 146 | 147 | // 启用内存补丁 148 | if (*Config::EnableUE) { 149 | Hooks::Install(); 150 | } 151 | 152 | // 注册回调 153 | const auto* message = SKSE::GetMessagingInterface(); 154 | if (!message->RegisterListener(MessageHandler)) { 155 | return false; 156 | } 157 | 158 | return true; 159 | } 160 | ``` 161 | 162 | --- 163 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
164 | -------------------------------------------------------------------------------- /docs/setup/QuickStart.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本教程将会指引你以最简短的操作步骤编译出一个可在 _上古卷轴5 AE - 1.6.xx_ 版本的游戏中运行的**SKSE插件**. 5 | 参照此教程进行操作之前, 请先确保已按照[开发环境](/docs/setup/Setup.md)文档中的要求安装配置好所需的前置开发环境. 6 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
154 | -------------------------------------------------------------------------------- /docs/setup/Script.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本教程使用三个常用脚本辅助开发, `!Rebuild`, `!MakeNew`, 和`!Update`. 5 | 6 | --- 7 | + ### `!Rebuild回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
48 | -------------------------------------------------------------------------------- /docs/setup/Setup.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
133 | -------------------------------------------------------------------------------- /docs/tounknown/ASM101.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本节教程简单讲一下x64汇编的基础概念和后面教程会涉及到的汇编知识. 5 | 6 | ## 数据大小 7 | 8 | > 本教程使用x64 Windows环境. 9 | 10 | 对于CPU来说, 一切数据/指令都是以二进制位(bit)储存的. 下面是常见的x64数据形式: 11 | 12 | 数据 | 大小 | 含义 | x64 C++常见表达式 13 | --- | --- | --- | --- 14 | `BYTE` | 8位 | 字节 | `char` 15 | `WORD` | 16位 | 字 | `short` 16 | `DWORD` | 32位 | 双字(Double Word) | `long` 17 | `QWORD` | 64位 | 四字(Quad Word) | `long long` 18 | `REAL4` | 32位 | 单精度浮点 | `float` 19 | `REAL8` | 64位 | 双精度浮点 | `double` 20 | 21 | 当一个数据以二进制位展开时, 最左侧位为高位(High), 最右侧位为低位(Low). 以16位整数`11451`举例, 其十六进制为`0x2CBB`, 其二进制位补位后为`0010 1100 1011 1011`, 那么它的高8位为左侧的`0010 1100`, 即`0x2C`. 低8位为右侧的`1011 1011`, 即`0xBB`. 22 | 23 | ## 寄存器(Register) 24 | 25 | 寄存器是CPU用来储存二进制位的单元, 用于配合执行机器指令. 26 | 27 | 寄存器 | 含义 | 释义 28 | --- | --- | --- 29 | `AX` | Accumulator | 累加 30 | `CX` | Count | 计数 31 | `DX` | Data | 数据储存 32 | `BX` | Base | 基地址 33 | `SP` | Stack Pointer | 堆栈栈顶指针 34 | `BP` | Base Pointer | 堆栈栈底指针 35 | `SI` | Source Index | 源变址 36 | `DI` | Destination Index | 目的变址 37 | 38 | 这些寄存器被设计为储存**16位**二进制, 前缀`E`(Extended)将其拓展为**32位**, 前缀`R`(Register)将其拓展为**64位**. 后缀`L`(Low)将其限定为**低8位**, 后缀`H`(High)将其限定为**高8位**. 39 |  40 | 41 | x64架构在原有寄存器基础上添加了8个额外的通用寄存器, `R8`至`R15`. 对于这些新增的寄存器, 后缀`D`(DWORD)将其限定为**低32位**, 后缀`W`(WORD)将其限定为**低16位**, 后缀`B`(BYTE)将其限定为**低8位**. 42 | > `R8`至`R15`寄存器名字中的`R`不能去掉, 只能通过后缀访问指定的数据大小. 43 | 44 | `AX` `CX` `DX` `BX`虽然有各自的名字, 但在当今的架构下是作为通用寄存器使用的. `SP`和`BP`用于指向内存堆栈的栈顶和栈底, 对其进行加减算术运算即可创建堆栈帧(stack frame). `SI`和`DI`常被用于循环和内存分节, 但很多情况下可以当作通用寄存器使用. `R8`至`R15`也是作为通用寄存器使用. 45 | 46 | 这些寄存器的机器码(OpCode)从上往下依次为`0`至`7`, `R8`至`R15`也对应`0`至`7`. 47 | 48 | 寄存器 | 含义 | 释义 49 | --- | --- | --- 50 | `IP` | InstructionPointer | 指令指针 51 | `XMM` | ExtendedMemoryManager | SSE系列指令 52 | 53 | `IP`永远指向当前正在执行的指令地址. `XMM`为128位SSE系列指令寄存器, 从`XMMO`至`XMM7`, 被用于浮点标量相关的计算. x64架构添加了额外的`XMM8`至`XMM15`寄存器, 根据CPU支持的指令集不同还有额外的`YMM0`至`YMM15`256位以及`ZMM`512位AVX指令寄存器. `IP`寄存器可以使用通用寄存器的前/后缀, `XMM`, `YMM`, 和`ZMM`寄存器没有前/后缀. 54 | 55 | `XMM0`至`XMM7`寄存器的机器码(OpCode)从上往下依次为`0`至`7`, `XMM8`至`XMM15`也对应`0`至`7`. 56 | 57 | 寄存器名称及其前/后缀与大小写无关, 因为在实际处理时, 它们都是以二进制位表示的. 58 | 59 | ## 指令(Instruction) 60 | 61 | 当机器指令涉及二元运算时, 第一个对象为`dst`(destination)目的值, 第二个对象为`src`(source)源值. 62 | 63 | --- 64 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
65 | -------------------------------------------------------------------------------- /docs/tounknown/ASM102.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本节教程简单讲一下x64汇编中涉及函数调用的相关知识. 5 | 6 | 7 | 8 | --- 9 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
10 | -------------------------------------------------------------------------------- /docs/tounknown/DEBUG101.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 本节教程简单讲一下使用x64dbg进行调试(debug)的知识. 5 | 6 | ## 断点 7 | 8 | 断点调试是最常用的调试方式, 当设置的断点目标被击中时, x64dbg会将进程挂起(暂停). 9 | 10 | 类型 | 目标 | 击中条件 11 | --- | --- | --- 12 | 软件(Software) | 机器指令 | 指令执行至断点处时 13 | 硬件(Hardware) | 内存地址 | 读/写访问断点所在的内存地址处时 14 | 内存(Memory) | 内存分页 | 读/写访问断点所在的内存分页时 15 | 异常(Exception) | 异常类型 | 指定的异常类型被抛出时 16 | 17 | ## 调试 18 | 19 | 当断点被击中后, 我们便可以逐步调试程序, 分析每一步指令的作用, 观察寄存器和rflag的数值变化. 20 | 21 | 在汇编中, 函数体作为子程序(subroutine)存在于内存里, 通过`call`返程跳转指令调用. 调用者(caller)会先将返程地址入栈再跳转到子程序, 而被调用者(callee)执行完毕后则通过返程指令`ret`返回至栈上储存的地址并将其出栈. 通过返程地址可以找到调用者(caller). 22 | 23 | 类型 | 快捷键 | 作用 24 | --- | --- | --- 25 | 运行至选区(Run till selection) | `F4` | 执行至当前选中的地址. 26 | 步进(Step into) | `F7` | 执行下一条指令, 跟随跳转. 27 | 步过(Step over) | `F8` | 执行下一条指令, 不跟随跳转. 28 | 运行(Run) | `F9` | 恢复进程运行. 29 | 运行至返回(Execute till return) | `Ctrl + F9` | 执行至返程指令`ret`. 30 | 31 | x64dbg默认启用TLS回调函数的断点, 我们需要将它禁用掉. 32 |  33 | 34 | --- 35 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
36 | -------------------------------------------------------------------------------- /docs/tounknown/FuncHook.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | --- 5 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
6 | -------------------------------------------------------------------------------- /docs/tounknown/MemPatch.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | ## 概念 5 | 6 | 内存补丁顾名思义在游戏内存内打上我们期望的补丁. 补丁可以是一个跳转, 一个修改后的数据, 或者直接改动机器指令. 在[Address Library](https://www.nexusmods.com/skyrimspecialedition/mods/32444)的加持下, 为上古卷轴5: 天际打内存补丁格外容易. 7 | 8 | ## 需求 9 | 10 | 在涉及任何内存补丁的操作后, 代码通常不再安全, 不再稳定, 不再容易追溯bug. 11 | 在为插件项目引入内存补丁前, 问自己三个问题: 12 | 13 | 0. 这项功能是否可以不修改内存, 以其他方式做到? 14 | 1. 这项功能如果能够以其他方式达到同样效果, 修改内存相比之下有什么值得选择的优点? 15 | 2. 这项功能的内存补丁应该设置在游戏大概的哪个部分? 16 | 17 | ## 流程 18 | 19 | 0. 查找引用 20 | 1. 定位函数 21 | 2. 反汇编 22 | 3. 代码复现 23 | 24 | ## 可读资料 25 | 26 | [Dropkicker的汇编101](/docs/tounknown/ASM101.md) 27 | [Dropkicker的调试101](/docs/tounknown/DEBUG101.md) 28 | 29 | ## 实战案例 30 | 31 | ### [You Can Sleep - 解除休息/等待限制(特别版)](/docs/tounknown/YCS.md) ✓ 32 | 难度: ★ 33 | 34 | ### [No Enchantment Restriction - 解除附魔限制(年度版)](/docs/tounknown/NERR.md) ✓ 35 | 难度: ★★ 36 | 37 | --- 38 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
39 | -------------------------------------------------------------------------------- /docs/tounknown/NERR.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 难度: ★★ 5 | 练习耗时: *~15min* 6 | [源码](https://github.com/gottyduke/NoEnchantmentRestrictionRemake) 7 | 8 | ## 0. 思路 9 | 10 | 当然这里只讨论NERR的内存补丁部分: 修改一件物品允许的**最大附魔数量**. 11 | 12 | 0. *这项功能是否可以不用修改内存就能以其他方式做到?* 13 | 可以通过附加自定义perk类似于原版`ExtraEffect`修改允许的附魔数量. 14 | 1. *这项功能如果能够以其他方式就达到同样效果, 修改内存相比之下有什么值得选择的优点?* 15 | 内存补丁可以跳过调用游戏的PapyrusVM步骤, 节省时间. 16 | 2. *这项功能的内存补丁应该设置在游戏大概的哪个部分?* 17 | 应当设置在启动附魔台时. 18 | 19 | 我们的目标是找到加载附魔台的函数并为其打上内存补丁. 使用附魔台时一定会从内存中获取当前最大允许的附魔数, 数值由玩家是否拥有`ExtraEffect`perk决定. 20 | 21 | ## 1. 查找引用 22 | 23 | `是`/`否`(`true`/`false`)在内存中会用(`1`/`0`)表达. 附加Cheat Engine至游戏进程上, 并在游戏里反复添加/移除`ExtraEffect`perk, 当添加perk后, 搜索`1`. 移除perk后, 搜索`0`. 最后确定了指向当前是否有`ExtraEffect`perk的内存地址(以下简称为`perk状态值`): 24 |  25 | 26 | 在CE中打开查找地址引用窗口并附加调试器后, 在游戏内打开附魔台, 果不其然引用了这个perk状态值, 符合我们对游戏加载附魔台函数的猜想: 27 |  28 | 这里有两条不同的指令, 其中`r8 + rcx * 8 + 10`是perk状态值的内存地址. 29 | 30 | 第一条指令`7FF79BF2A6B1`: 31 | ```assembly 32 | cmp dword ptr [r8 + rcx * 8 + 10], 0 33 | ``` 34 | 这条指令将perk状态值与`0`进行比较(`cmp`, compare), C++代码为`if(*(r8+rcx*8+10) == 0)`. 35 | 36 | 第二条指令`7FF79BF2A6FA`: 37 | ```assembly 38 | mov eax, [r8 + rcx * 8 + 10] 39 | ``` 40 | 这条指令将perk状态值拷贝至`eax`寄存器(`mov`, move), 双击打开这条指令发现后面是`test eax, eax`, C++代码为`if(bool eax = *(r8+rcx*8+10);eax)`. 41 | 42 | 将这两条指令的地址记录下来后关闭CE, 下一步更细致的反编译交给x64dbg. 43 | 44 | > 教程的图里地址可能有差异, 因为每次运行时获取的地址可能不一样. 45 | > perk状态值并非内存中的perk对象. 46 | > `dword ptr`限定从内存地址`r8 + rcx * 8 + 10`读取的数据大小为`DWORD`(32位双字). 47 | 48 | ## 2.1 断点调试 49 | 50 | 附加x64dbg至游戏进程上, 转到第一条指令的地址`7FF79BF2A6B1`. 51 |  52 | 这个函数加载了第一个参数的第一个成员, 对该成员的成员(偏移量`0x288`)进行null检查并比较第二个参数是否是字符串结尾`\0`, 然后对该成员+偏移量`0x190`进行null检查并返程. 我们将它命名为`sub_check198`. 53 | 54 | 因为这个函数内并不包含堆栈帧(stack frame), 因此栈顶就是返程地址: 55 |  56 | 57 | 转到返程地址`7FF79BEA1CC5`: 58 |  59 | 在`call`指令后`test al, al`是返回值null检查. 这个函数将第一个参数的成员(偏移量`0xF0`)传递给函数`sub_check198`后对返回值进行null检查并返程. 我们将它命名为`sub_check288`. 60 | > 也可以手动步进跟随指令返程而不直接跳转到返程地址. 61 | 62 | 这个调用者函数很小, 明显不是加载附魔台的主函数, 需要返回到更上一层的调用者函数. 因为包含堆栈帧(stack frame), 所以需要加上`0x28`的栈偏移才是返程地址. 随着指令执行, 我们来到了更上一层的调用者函数. 63 |  64 | 挺长的一个函数, 截屏都没有截完整. 由于返程在这个函数主体的中间部分, 因此我们在其头部`test rdx, rdx`打上断点并进入游戏测试. 这里可以吃惊(吃惊吗?)的发现在没有打开附魔台时, 这个断点就被立刻触发了, 说明这个函数也不是加载附魔台的主函数, 仅仅是其中的一个调用. 65 | 66 | 根据CE查找到的引用信息, 附魔台打开时只调用了一次perk状态值, 而此处的函数明显是在游戏循环里反复调用(继续运行会立刻击中下一次断点), 结合我们的上一个函数`sub_check288`的返回值经常变化(说明参数经常变化), 可以合理猜想这是用于循环检测玩家是否有perk(常见于Papyrus脚本中). 我们为它命名为`sub_checkPerk`. 67 | 68 | ## 2.2. 断点调试 69 | 70 | 已经知道函数`sub_checkPerk`是处于一个循环中, 那我们就无法在设置软件断点后返回游戏打开附魔台了, 因为断点会立刻击中. 这里就需要设置一个硬件断点, 当函数`sub_check198`的参数为我们想要的`ExtraEffect`时, 挂起程序. 71 | 72 | 在x64dbg的内存视图中转到perk状态值的地址, 并为其设置硬件断点, 因为函数`sub_check198`调用这个地址时是`dword`, 因此硬件断点的条件也为读取dword(32位双字)时. 73 |  74 | 恢复游戏运行后在游戏内打开附魔台, 击中了硬件断点后再次回到x64dbg. 75 | 76 | 一直步进到函数`sub_checkPerk`, 因为明确知道这个函数只是附魔台加载函数中的一个调用, 所以我们可以执行至返程指令`ret`前. 再次步进后可以看见我们来到了一个非常大的调用者函数(假装没看见我的注释): 77 |  78 | 看见x64dbg已经把此时各个寄存器的值给解析了出来, 其中附魔相关的字符串`Enchanting`, `Choose an item to destroy ...`都证明了这就算不是附魔台加载的主函数, 也是附魔台相关的调用. 79 | 80 | 从函数`sub_checkPerk(ExtraEffect)`返程后, 我们看见了许多和浮点相关的指令, 从`movss xmm1, [rbp+588]`开始, 到`cvttss2si rax, xmm0`结束. 这一串指令用于浮点数整型转换. 结合游戏Form中perk附加的数值都是浮点数来看, 这一段指令的意义就不言而喻了: 调用函数`sub_checkPerk(ExtraEffect)`检测玩家是否有`ExtraEffect`perk, 根据返回值加载perk数值的浮点数, 将浮点数整型转换并拷贝至`rax`寄存器用于后面的调用. 81 | 82 | 这后面的指令就和perk无关了: `rax`中的整型值被拷贝至`r14`, 一个用于构建UI的字符串指针被拷贝至`rax`后再移入一个本地变量等UI相关的操作. 此时我们已经找到了内存补丁的目标: 从函数`sub_checkPerk`返回后将我们想要的值拷贝至`rax`寄存器. 83 | 84 | ## 3. 内存补丁 85 | 86 | 既然我们的目标是将想要的值拷贝至`rax`寄存器, 那么这一片浮点数整型转换的操作就不需要了. 在x64dbg中选中这一片内存, 将其以无操作指令`NOP`填充: 87 |  88 | 89 | 选中第一个`NOP`, 按下空格键输入汇编`mov eax, 5`后恢复游戏运行: 90 |  91 | 就是这样简单的一个汇编指令, 我们就改变了游戏允许的最大附魔数量 - 无论有无`ExtraEffect`perk, 无论这个perk被魔改成什么样. 92 | > 这里用`eax`而不是`rax`因为我们的值是32位常量, 所以目的寄存器也限定为32位. 93 | 94 | ## 4. SKSE 95 | 96 | 现在我们需要将这个操作复现在SKSE插件中. x64dbg中向上定位到这个函数的头部并复制它的相对偏移地址(RVA): 97 |  98 | 99 | 根据RVA在当前版本的Address Library中找到对应的ID: 100 |  101 | 102 | x64dbg中双击函数头部的地址切换为偏移量模式, 并回到我们编写内存补丁的地方获取偏移量: 103 |  104 | 其中`0x212`是补丁入口, `0x243`是补丁出口. 105 | 106 | C++代码: 107 | ```C++ 108 | #include "DKUtil/Hook.hpp" 109 | 110 | using namespace DKUtil::Alias; 111 | 112 | // 1-6-323: 0x894EE0 + 0x212 113 | constexpr std::uint64_t FuncID = 51242; 114 | constexpr std::ptrdiff_t OffsetLow = 0x212; 115 | constexpr std::ptrdiff_t OffsetHigh = 0x243; 116 | 117 | constexpr OpCode AsmSrc[]{ 118 | 0xB8, // mov eax, 119 | 0x00, 0x00, 0x00, 0x00, // Imm32 120 | }; 121 | 122 | constexpr std::ptrdiff_t ImmediateOffset = sizeof(OpCode); 123 | 124 | HookHandle _Hook_UES; 125 | 126 | 127 | void Install() 128 | { 129 | constexpr Patch AsmPatch = { 130 | std::addressof(AsmSrc), 131 | sizeof(AsmSrc) 132 | }; 133 | 134 | _Hook_UES = DKUtil::Hook::AddASMPatch回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
172 | -------------------------------------------------------------------------------- /docs/tounknown/YCS.md: -------------------------------------------------------------------------------- 1 |回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
3 | 4 | 难度: ★ 5 | 练习耗时: *~15mins* 6 | [源码](https://github.com/gottyduke/YouCanSleepRemake) 7 | 8 | ## 0. 思路 9 | 10 | YCS的功能: 允许玩家在任何情景下休息/等待. 11 | 12 | 0. *这项功能是否可以不用修改内存就能以其他方式做到?* 13 | 应该可以通过某种Papyrus脚本做到(吧?). 14 | 1. *这项功能如果能够以其他方式就达到同样效果, 修改内存相比之下有什么值得选择的优点?* 15 | 内存补丁可以跳过调用游戏的PapyrusVM步骤, 节省时间. 16 | 2. *这项功能的内存补丁应该设置在游戏大概的哪个部分?* 17 | 应当设置在检查休息/等待条件时. 18 | 19 | 我们的目标是找到判断当前是否可以休息/等待的函数并为其打上内存补丁. 休息/等待时会调用这个函数判断当前条件是否可以休息/等待, 例如床被占用, 在空中, 在战斗中等. 20 | 21 | ## 1. 查找引用 22 | 23 | 当我们试图在不能休息/等待的情景下休息/等待时, 游戏会提示相关的限制, 这是一个非常好的突破口. 因为游戏必然会先调用休息/等待函数判断条件, 如果不符合条件, 则会从内存中获取符合的字符串构建UI元素. 24 | 25 | 打开游戏后在主菜单使用`coc whiterundragonsreach`快速传送到Whiterun Dragonsreach地点, 直奔Farengar Secret-Fire的书房, 他拥有一张床可以为我们提供测试环境. 尝试在他的床上休息会提示我们: 26 |  27 | 28 | 附加Cheat Engine至游戏进程上, 并搜索字符串`You cannot sleep in an owned bed.`, 很容易就找到了这个字符串值的内存地址. 29 |  30 | 31 | 在CE中打开查找地址引用窗口并附加调试器后, 在游戏内再次试图休息, CE捕捉到了对这个字符串值的引用: 32 |  33 | 34 | 首先分析末尾的3条指令, `vmovdqu ymm`(move unaligned double quadword vector)指令是常见的字符串值的向量化优化. 再看第2条和第5条指令, 因为字符串`You cannot sleep in an owned bed.`长度为33, 所以指令2和5也是在处理字符串值. 最后再看第3条和第4条指令, `cmp byte ptr [地址], 0`指令从内存地址中读取了大小为`BYTE`的数据并将其与`0`进行比较(`cmp`, compare), 这是常见的字符串处理每一个字符(`char`, 大小为8位, 即`BYTE`)的方式, 一直读取到字符串结尾为`\0`. 35 | 36 | 第一条指令`7FF7CADB61E8`: 37 | ```assembly 38 | movsx eax, byte ptr [rdx] 39 | ``` 40 | 这条指令将字符串开头的`BYTE`数据带符号拷贝至`eax`寄存器(`movsx`, move with sign-extension). C++代码为`char* eax = (char*)(rdx)`. 41 | 42 | 将这一条指令的地址记录下来后关闭CE, 下一步更细致的反编译交给x64dbg. 43 | 44 | > 教程的图里地址可能有差异, 因为每次运行时获取的地址可能不一样. 45 | > `byte ptr`限定从寄存器`rdx`内存地址中读取的数据大小为`BYTE`(8位字节). 46 | 47 | ## 2.1 断点调试 48 | 49 | 附加x64dbg至游戏进程上, 转到第一条指令的地址`7FF7CADB61E8`, 为其设置软件断点. 50 |  51 | 这个函数从上级调用者传递的参数`rdx`中读取了一个大小为`BYTE`的数据, 并将此数据拷贝至`eax`寄存器以作他用. 这是将字符串指针所指向的字符串首字符作为内存地址传递, 类似于C++中的`&buffer[0]`. 我们将它命名为`sub_loadString`. 52 | 53 | 为函数`sub_loadString`设置断点后, 这个断点就被立刻触发了. 此时我们并未在游戏中试图在占用的床上休息, 在x64dbg中也可以注意到各个寄存器的值都是随机的, 大多为游戏内对于AI事件的调用名, 这通常来自于Papyrus脚本. 54 | 55 | ## 2.2 断点调试 56 | 57 | 既然函数`sub_loadString`被反复调用, 那我们就无法在打软件断点后返回游戏测试了, 因为断点会立刻触发. 这里就需要设置一个硬件断点, 当函数`sub_loadString`的参数为我们想要的`You cannot sleep in an owned bed.`字符串时, 触发断点. 58 | 59 | 在x64dbg的内存视图中转到字符串的地址, 并为其打上硬件断点, 因为函数`sub_loadString`调用这个地址时是`byte`, 因此硬件断点的条件也为读取byte(8位字节)时. 60 |  61 | 恢复游戏运行后在游戏内再次试图休息, 击中了硬件断点后回到x64dbg. 此时记得取消硬件断点以免调试过程中无法步过(step over). 62 | 63 |  64 | 因为知道这个函数只是用于加载字符串并对其进行字符串相关的操作(具体操作并没有分析, 但可以看见下方的`call <&toupper>`), 因此我们步进到上级调用者函数(`Ctrl+F9`). 65 | 66 | 第一个调用者函数: 67 |  68 | 可以看见这个函数依然不是我们的目标, 这是一个用于构建UI元素的函数, 这一点可以从寄存器值`UIMenuCancel`看出. 为了跳过这个UI函数的各个调用/跳转部分, 我们直接在函数体末尾的返程指令`ret`处设置软件断点并恢复游戏进程运行, 随后步进一次来到第二个调用者函数体. 69 | 70 | 第二个调用者函数(部分): 71 |  72 | 通过x64dbg解析出的各个值, 可以确定这个函数便是我们的目标. 这一部分的逻辑非常简单, 在这个函数里判断是否能够休息/等待, 如果失败, 便加载相应的UI字符串并调用下级函数发送UI信息. 游戏里如果可以休息/等待时, 会有一个UI面板询问休息/等待多长游戏时间, 而我们此次测试环境中是不能休息的(床被占用), 因此函数发送了`UIMenuCancel`来取消显示询问UI, 并发送相应的信息字符串至另一个UI函数作为提示显示给玩家, 即`You cannot sleep in an owned bed.`. 73 | 74 | 第二个调用者函数(主体): 75 |  76 | 77 | 回到目标函数的主体来分析逻辑, 我们可以发现, 各种条件判断都是这样一个逻辑: 先调用一个判断该条件的子函数, 再根据返回值, 成功便跳转到下一个条件判断, 失败则加载相应的提示信息字符串, 并跳转到此函数末尾处的UI调用相关部分以发送提示信息. 结合示例来看: 78 |  79 | 首先是调用子函数判断条件`call skyrimse.7FF6C66357C0`, 随后测试返回值`test al, al`, 成功便执行跳转开始下一个条件判断(trespassing)`je`, 失败则加载字符串`mov rcx, 字符串指针地址`, 随后跳转到函数末尾`jmp`. 80 | 81 | ## 3. 内存补丁 82 | 83 | 目标函数已经找到, 它的逻辑我们也分析过了, 每一个子条件判断中, 成功则跳转, 失败则加载UI字符串再跳转. 我们要做的就是让游戏认为我们的每一个条件都是成功的, 因此永远不会失败跳转. 这个内存补丁的实施方法太多了, 比如将调用子条件函数的指令替换为我们的函数, 比如将测试返回值的指令改为永远为`1`等等. 在这里YCS插件选择的是将跳转指令从条件跳转`je/jne`(jump if/not zero-flag)替换为无条件跳转`jmp`(unconditional jump). 84 | 85 | 根据我们分析的逻辑(`call`-`test`-`je/jne`-`字符串`-`jmp`), 很快在函数靠近末尾处(偏移量`0x3BC`)找到了此次测试所使用的条件判断"床是否被占用"(`You cannot sleep in an owned bed.`): 86 |  87 | 选中条件跳转指令`jne skyrimse.7FF6C66CD687`, 按下空格将汇编`jne`替换为`jmp`后返回游戏并再次测试: 88 |  89 | 非常简单的指令替换, 我们便解除了休息/等待其中之一的子条件`owned`. 90 | 91 | 接下来我们将此函数中的总计8个子条件跳转指令全部替换并再次进入游戏详细测试, 此时会发现一个问题: 其他的子条件补丁都生效了, 但是在空中时依然不能休息/等待. 92 |  93 | 94 | 回到条件判断函数主体, 通过设置断点和单步调试的方式可以发现此处有一个循环: 95 |  96 | 这个循环其实并不难理解, 当玩家在空中时, 每一帧滞空都属于`in air`因此会有一个循环判断. 我们要做的就是跳过这个循环, 将形成循环的跳转指令`jmp`替换为无操作指令`nop`. 97 |  98 | 再次测试发现子条件`in air`也被成功跳过了. 99 | 100 | > 函数最末尾的`You cannot sleep at this time`并不符合我们的逻辑(`call`-`test`-`je/jne`-`字符串`-`jmp`), 可以忽略. 101 | 102 | ## 4. SKSE 103 | 104 | 现在我们需要将这个操作复现在SKSE插件中. x64dbg中向上定位到这个函数的头部并复制它的相对偏移地址(RVA): 105 |  106 | 107 | 根据RVA在当前版本的Address Library中找到对应的ID: 108 |  109 | 110 | x64dbg中双击函数头部的地址切换为偏移量模式, 并记录下8个子条件内存补丁的偏移量: 111 | ``` 112 | 0x2E 113 | 0x89 114 | 0xB1 115 | 0xF6 116 | 0x11F 117 | 0x146 118 | 0x1BB 119 | 0x3BC 120 | ``` 121 | 以及子条件`in air`的循环偏移量`0xD4`. 122 | 123 | C++代码: 124 | ```C++ 125 | #include "DKUtil/Hook.hpp" 126 | 127 | using namespace DKUtil::Alias; 128 | 129 | constexpr OpCode JmpShort = 0xEB; 130 | constexpr OpCode NOP = 0x90; 131 | 132 | // 1-5-97-0 0x69D2C0 133 | constexpr std::uint64_t FuncID = 39371; 134 | constexpr std::ptrdiff_t OffsetTbl[8]{ 135 | 0x2E, // You cannot sleep in the air. 136 | 0x89, // You cannot sleep while trespassing. 137 | 0xB1, // You cannot sleep while being asked to leave. 138 | 0xF6, // You cannot sleep while guards are pursuing you. 139 | 0x11F, // You cannot sleep when enemies are nearby. 140 | 0x146, // You cannot sleep while taking health damage. 141 | 0x1BB, // This object is already in use by someone else. 142 | 0x3BC // You cannot sleep in an owned bed. 143 | }; 144 | 145 | constexpr std::ptrdiff_t InAirLoopOffset = 0xD4; 146 | 147 | OpCode InAirLoopNop[6]{ NOP, NOP, NOP, NOP, NOP, NOP }; 148 | 149 | void Install() 150 | { 151 | const auto funcAddr = DKUtil::Hook::IDToAbs(FuncID); 152 | 153 | for (auto index = 0; index < std::extent_v回到目录 | 工具配置 | 脚本说明 | 快速入门 | 插件基础 | Papyrus调用 | 事件响应 | 内存补丁 | 函数Hook
176 | -------------------------------------------------------------------------------- /images/quickstart/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/1.png -------------------------------------------------------------------------------- /images/quickstart/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/10.png -------------------------------------------------------------------------------- /images/quickstart/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/11.png -------------------------------------------------------------------------------- /images/quickstart/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/12.png -------------------------------------------------------------------------------- /images/quickstart/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/13.png -------------------------------------------------------------------------------- /images/quickstart/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/14.png -------------------------------------------------------------------------------- /images/quickstart/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/15.png -------------------------------------------------------------------------------- /images/quickstart/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/16.png -------------------------------------------------------------------------------- /images/quickstart/17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/17.png -------------------------------------------------------------------------------- /images/quickstart/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/2.png -------------------------------------------------------------------------------- /images/quickstart/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/3.png -------------------------------------------------------------------------------- /images/quickstart/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/4.png -------------------------------------------------------------------------------- /images/quickstart/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/5.png -------------------------------------------------------------------------------- /images/quickstart/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/6.png -------------------------------------------------------------------------------- /images/quickstart/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/7.png -------------------------------------------------------------------------------- /images/quickstart/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/8.png -------------------------------------------------------------------------------- /images/quickstart/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/quickstart/9.png -------------------------------------------------------------------------------- /images/resources/mynewplugin_project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/mynewplugin_project.png -------------------------------------------------------------------------------- /images/resources/plugin_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/plugin_log.png -------------------------------------------------------------------------------- /images/resources/plugin_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/plugin_main.png -------------------------------------------------------------------------------- /images/resources/plugin_postbuild.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/plugin_postbuild.png -------------------------------------------------------------------------------- /images/resources/rebuild_pt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/rebuild_pt1.png -------------------------------------------------------------------------------- /images/resources/rebuild_pt2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/resources/rebuild_pt2.png -------------------------------------------------------------------------------- /images/setup/quick_add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/setup/quick_add.png -------------------------------------------------------------------------------- /images/setup/rebuilt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/setup/rebuilt.png -------------------------------------------------------------------------------- /images/setup/vscxx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/setup/vscxx.png -------------------------------------------------------------------------------- /images/setup/win_terminal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/setup/win_terminal.png -------------------------------------------------------------------------------- /images/toukn/asm101_register_highlow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/asm101_register_highlow.png -------------------------------------------------------------------------------- /images/toukn/debug101_dbg_tls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/debug101_dbg_tls.png -------------------------------------------------------------------------------- /images/toukn/nerr/ce_perk_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/ce_perk_filter.png -------------------------------------------------------------------------------- /images/toukn/nerr/ce_perk_pre.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/ce_perk_pre.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_2ndcaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_2ndcaller.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_boolcheck.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_boolcheck.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_caller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_caller.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_doneAGAIN.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_doneAGAIN.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_hardbp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_hardbp.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_loadmain_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_loadmain_src.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_nops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_nops.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_offset.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_ret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_ret.png -------------------------------------------------------------------------------- /images/toukn/nerr/dbg_rva.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/dbg_rva.png -------------------------------------------------------------------------------- /images/toukn/nerr/re_done.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/re_done.png -------------------------------------------------------------------------------- /images/toukn/nerr/re_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/re_id.png -------------------------------------------------------------------------------- /images/toukn/nerr/re_log.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/nerr/re_log.png -------------------------------------------------------------------------------- /images/toukn/ycs/ce_filter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/ce_filter.png -------------------------------------------------------------------------------- /images/toukn/ycs/ce_owned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/ce_owned.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_1stcaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_1stcaller.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_air_loop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_air_loop.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_airloop_nop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_airloop_nop.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_call.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_call.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_cond.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_cond.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_hardbp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_hardbp.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_hbphit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_hbphit.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_main.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_owned_jmp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_owned_jmp.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_rva.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_rva.png -------------------------------------------------------------------------------- /images/toukn/ycs/dbg_strptr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/dbg_strptr.png -------------------------------------------------------------------------------- /images/toukn/ycs/re_id.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/re_id.png -------------------------------------------------------------------------------- /images/toukn/ycs/re_noair.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/re_noair.png -------------------------------------------------------------------------------- /images/toukn/ycs/re_pre_ce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/re_pre_ce.png -------------------------------------------------------------------------------- /images/toukn/ycs/re_succeeded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gottyduke/PluginTutorialCN/abff62e37009d5eccea241ccd6fbcbf4bb3ef917/images/toukn/ycs/re_succeeded.png --------------------------------------------------------------------------------