image_1780576701141_sedvwi

原文描述
重要提示!此内容现在拆分为两个版本:特别版(1.5.x)和周年纪念版(1.6.x)。指向内存地址的ID在这两个版本间并不匹配(游戏可执行文件差异过大无法匹配,即便能匹配,对应函数内的代码本身也不一样)。说明 面向普通模组用户:从文件页面下载并安装“一体化”包。你可以使用模组管理器安装,也可以手动操作。.bin文件需要放到以下路径:Data/SKSE/Plugins/ 你无需阅读此说明的其余内容。面向SKSE DLL插件作者:这是供模组制作者使用的资源(头文件)。你可以加载一个存储偏移量的数据库,这样你的DLL插件就可以不受版本限制,无需重新编译即可运行。头文件可以从文件的可选分类中下载。针对周年纪念版,头文件名为versionlibdb.h,而非versiondb.h!如果你正在使用CommonLib,所有相关功能都已内置,你无需从这里获取任何内容。使用方法 最快上手方式:剧透:展开 #include “versiondb.h” void * MyAddress = NULL; unsigned long long MyOffset = 0; bool InitializeOffsets() { // 在栈上分配内存,这样退出该函数时资源就会被释放 // 没必要把整个数据库一直加载着白白占用内存 VersionDb db; // 加载对应当前可执行文件版本的数据库 if (!db.Load()) { _FATALERROR(“加载当前可执行文件的版本数据库失败!”); return false; } else { // 示例输出:”SkyrimSE.exe”, “1.5.97.0” _MESSAGE(“已加载 %s 版本 %s 的数据库。”, db.GetModuleName().c_str(), db.GetLoadedVersionString().c_str()); } // 这个地址已经包含模块的基地址,因此可以直接使用 MyAddress = db.FindAddressById(123); if (MyAddress == NULL) { _FATALERROR(“查找地址失败!”); return false; } // 这个偏移量不包含基地址,实际地址需要用模块基地址加上MyOffset得到 if (!db.FindOffsetById(123, MyOffset)) { _FATALERROR(“查找对应项的偏移量失败!”); return false; } // 全部操作成功 return true; } 现在你可能想知道代码里的“123”是什么值。这是一个内存地址对应的ID。不同版本的数据库中,同一个地址的ID是相同的,但指向的实际值可能不一样。如果要获取指定版本下所有ID和对应值的列表,可以执行以下操作:剧透:展开 #include “versiondb.h” bool DumpSpecificVersion() { VersionDb db; // 忽略当前运行的可执行文件版本,尝试加载1.5.62.0版本的数据库 if (!db.Load(1, 5, 62, 0)) { _FATALERROR(“加载1.5.62.0版本的数据库失败!”); return false; } // 生成名为offsets-1.5.62.0.txt的文件,每一行对应一个ID和偏移量 db.Dump(“offsets-1.5.62.0.txt”); _MESSAGE(“已导出1.5.62.0版本的偏移量”); return true; } 你可以用自己正在逆向分析、熟悉的版本号替换这里的1, 5, 62, 0。你必须先把对应版本的数据库文件放在/Data/SKSE/Plugins目录下。执行完这段代码后,你就能在《上古卷轴5:天际》的主目录下看到新生成的名为“offsets-1.5.62.0.txt”的文件,文件名是你指定的任意名称。文件格式为每一行:十进制ID十六进制偏移量 举个例子,假设你在1.5.62.0版本中有一个地址142F4DEF8(玩家角色静态指针),你想让它不受版本限制正常使用,操作步骤如下:1. 在偏移量文件中查找2F4DEF8,因为这是去掉基地址140000000之后的偏移量 2. 查到对应的ID为517014(十进制!)3. 如果你想在运行时的DLL中获取这个地址,执行以下代码:void* addressOf142F4DEF8 = db.FindAddressById(517014); 这样就完成了。VersionDb结构体包含以下成员函数:剧透:展开 bool Dump(const std::string& path); // 将当前加载的数据库导出到文件 bool Load(int major, int minor, int revision, int build); // 加载指定版本的数据库,前提是Data/SKSE/Plugins目录下存在db-主版本号-次版本号-修订号-构建号.bin文件 bool Load(); // 加载对应当前应用程序的版本数据库 void Clear(); // 清空当前加载的数据库 void GetLoadedVersion(int& major, int& minor, int& revision, int& build) const; // 获取当前已加载的数据库文件的版本号 bool GetExecutableVersion(int& major, int& minor, int& revision, int& build) const; // 获取当前正在运行的应用程序的版本号 const std::string& GetModuleName() const; // 获取当前加载的数据库对应模块的名称,正常应该显示为”SkyrimSE.exe” const std::string& GetLoadedVersionString() const; // 将当前加载的版本以字符串形式返回,例如”1.5.62.0″ const std::map& GetOffsetMap() const; // 获取ID到偏移量的映射表,如果你需要手动遍历的话使用这个 void* FindAddressById(unsigned long long id) const; // 通过ID查找地址,返回的地址已经包含模块基地址,是可直接使用的正确地址。如果查找失败会返回NULL! bool FindOffsetById(unsigned long long id, unsigned long long& result) const; // 通过ID查找偏移量,返回的偏移量不包含模块基地址 bool FindIdByAddress(void* ptr, unsigned long long& result) const; // 通过地址查找ID,会执行反向查找将地址转换为对应ID bool FindIdByOffset(unsigned long long offset, unsigned long long& result) const; // 通过偏移量查找ID,会执行反向查找将偏移量转换为对应ID 需要了解并牢记的要点:1. 你可以把任意(或所有)数据库文件和你的插件打包在一起,但这会让文件体积大幅增加(大约增加2.5MB)。目前更常见的做法是将此模组标记为你的插件的依赖项。2. 你必须始终只在游戏启动时加载一次数据库,初始化/缓存你需要的所有地址,之后就释放它。释放指的是VersionDb结构体被销毁(如果你是在栈上分配的话,函数退出就自动销毁了)。这样可以确保你在游戏运行过程中不会占用不必要的内存。游戏运行过程中完全不需要保持数据库处于加载状态。如果你使用CommonLib,这一点就无需多虑,因为CommonLib只会加载一次数据库,不会为每个DLL重复加载。3. 数据库中包含函数地址、全局变量地址、RTTI信息、虚函数表地址以及所有可能被引用的相关内容。它不包含函数中间位置的地址,也不包含全局变量中间位置的地址。如果你需要函数中间位置的地址,你应该先查到该函数的基地址,再自行加上额外的偏移量。数据库也不会存储无效内容,比如函数周围的对齐填充数据(这类数据在rdata段中被引用)、pdata段的内容会被丢弃,rdata段中部分由编译器生成的SEH信息也会被丢弃。4. 你必须始终检查操作结果,确保数据库加载成功(Load函数返回值为true),且查询到的地址确实是有效的结果(不为NULL)。如果数据库加载失败,大概率是文件缺失或者版本不对(比如试图在周年纪念版中使用特别版的头文件)。如果地址查询失败,说明该地址在对应版本的数据库中找不到。这可能是因为游戏代码改动太大,该地址在这个版本中已经完全失效,也可能是数据库本身没能检测到正确的地址。出现任意一种情况时,你都应该让插件初始化失败,告知SKSE你的插件没有正确加载,或者手动弹出错误提示信息。5. 发布你的DLL插件之前,最好先确认你用到的地址在游戏的所有版本中都存在。要实现这一点,你可以加载每一个版本的数据库文件,在每个数据库中查询同一个地址ID,确认该ID都存在:剧透:展开 bool LoadAll(std::vector& all) { static int versions[] = { 3, 16, 23, 39, 50, 53, 62, 73, 80, 97, -1 }; for (int i = 0; versions[i] >= 0; i++) { VersionDb * db = new VersionDb(); if (!db->Load(1, 5, versions[i], 0)) { delete db; return false; } all.push_back(db); } return true; } bool ExistsInAll(std::vector& all, unsigned long long id) { unsigned long long result = 0; for (auto db : all) { if (!db->FindOffsetById(id, result)) return false; } return true; } void FreeAll(std::vector& all) { for (auto db : all) delete db; all.clear(); } bool IsOk() { std::vector all; if (!LoadAll(all)) { _FATALERROR(“加载当前可执行文件对应的一个或多个版本数据库失败!”); FreeAll(all); return false; } if (!ExistsInAll(all, 517014)) { _FATALERROR(“517014这个ID并非存在于所有版本的数据库中!”); FreeAll(all); return false; } FreeAll(all); // 全部检查通过! return true; } 这样你就能确保你的DLL模组在所有版本的游戏中都能正常工作,如果某些版本中无法运行,你也可以在你的模组页面标注出来。6. 有时候你需要根据当前运行的游戏版本执行不同的逻辑。你可以用这段代码片段实现:剧透:展开 int major = 0, minor = 0, revision = 0, build = 0; if (!db.GetExecutableVersion(major, minor, revision, build)) { _FATALERROR(“操作出错!”); return false; } // 判断当前运行的游戏是1.5.x版本,且至少是1.5.39.0 if (major == 1 && minor == 5 && revision >= 39) { // 执行对应逻辑 … ? } 7. 请记住:如果你在调试模式下编译SKSE DLL,数据库的加载时间可能会长达14秒!在发布模式下这个加载时间大约是0.2秒。这是因为标准库容器在调试模式下运行速度极慢(比如std map)。权限声明 你可以随意使用本资源。

版本更新内容

最新 11 2026-02-12 20:01
版本 2 - 更新了数据库文件,这些文件之前缺失了大部分运行时类型信息。
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。