Содержит файл заголовка и базу данных для упрощения создания версионно-независимых плагинов SKSE DLL.
ВАЖНО! Теперь разделено на две версии: Special Edition (1.5.x) и Anniversary Edition (1.6.x). Идентификаторы, указывающие на адреса, не совпадают между этими версиями (исполняемый файл игры слишком различается, и даже если бы они совпадали, код внутри этих функций всё равно отличается).
Для обычных пользователей модов: Загрузите и установите пакет "всё в одном" из раздела файлов. Вы можете использовать менеджер модов или установить вручную. Файлы .bin должны быть размещены здесь: Data/SKSE/Plugins/
Дальнейшая информация вам не потребуется.
Для разработчиков плагинов SKSE DLL:
Это ресурс для моддеров (файл заголовка). Вы можете загрузить базу данных, содержащую смещения, чтобы ваш плагин DLL был независим от версии без необходимости перекомпиляции. Файл заголовка доступен в разделе дополнительных файлов. Для Anniversary Edition файл заголовка называется versionlibdb.h вместо versiondb.h! Если вы используете CommonLib, всё это уже встроено, и вам ничего отсюда не понадобится.
Как использовать
Самый быстрый способ:
#include "versiondb.h"
void * MyAddress = NULL;
unsigned long long MyOffset = 0;
bool InitializeOffsets()
{
// Allocate on stack so it will be unloaded when we exit this function.
// No need to have the whole database loaded and using up memory for no reason.
VersionDb db;
// Load database with current executable version.
if (!db.Load())
{
_FATALERROR("Failed to load version database for current executable!");
return false;
}
else
{
// "SkyrimSE.exe", "1.5.97.0"
_MESSAGE("Loaded database for %s version %s.", db.GetModuleName().c_str(), db.GetLoadedVersionString().c_str());
}
// This address already includes the base address of module so we can use the address directly.
MyAddress = db.FindAddressById(123);
if (MyAddress == NULL)
{
_FATALERROR("Failed to find address!");
return false;
}
// This offset does not include base address. Actual address would be ModuleBase + MyOffset.
if (!db.FindOffsetById(123, MyOffset))
{
_FATALERROR("Failed to find offset for my thing!");
return false;
}
// Everything was successful.
return true;
}
Теперь вы, вероятно, задаётесь вопросом, что такое значение "123". Это идентификатор адреса. Разные версии баз данных будут иметь одинаковый идентификатор для адреса, но он может указывать на разные значения. Чтобы получить список всех пар идентификаторов и значений для определённой версии, выполните следующее:
#include "versiondb.h"
bool DumpSpecificVersion()
{
VersionDb db;
// Try to load database of version 1.5.62.0 regardless of running executable version.
if (!db.Load(1, 5, 62, 0))
{
_FATALERROR("Failed to load database for 1.5.62.0!");
return false;
}
// Write out a file called offsets-1.5.62.0.txt where each line is the ID and offset.
db.Dump("offsets-1.5.62.0.txt");
_MESSAGE("Dumped offsets for 1.5.62.0");
return true;
}
Вместо 1, 5, 62, 0 укажите версию, с которой вы работаете и которую знаете. Сначала убедитесь, что соответствующая база данных находится в папке /Data/SKSE/Plugins.
После выполнения этой команды в основной папке Skyrim появится новый файл, например, "offsets-1.5.62.0.txt" или с тем именем, которое вы указали. Он будет в формате, где каждая строка:
Десятичный идентификатор<табуляция>Шестнадцатеричное смещение<новая строка>
Например, если у вас есть адрес 142F4DEF8 (статический указатель на персонажа игрока) в версии 1.5.62.0, который вы хотите сделать независимым от версии, выполните следующее:
- Найдите 2F4DEF8 в файле смещений. Это смещение без базового адреса 140000000.
- Убедитесь, что идентификатор равен 517014 (в десятичной системе!).
- Чтобы использовать этот адрес в вашем DLL во время выполнения, сделайте следующее:
void* addressOf142F4DEF8 = db.FindAddressById(517014);
И всё готово.
Структура VersionDb имеет следующие функции:
bool Dump(const std::string& path); // Dump currently loaded database to file
bool Load(int major, int minor, int revision, int build); // Load a specific version if the db-major-minor-revision-build.bin exists in Data/SKSE/Plugins directory
bool Load(); // Load the version for current application
void Clear(); // Clear currently loaded database
void GetLoadedVersion(int& major, int& minor, int& revision, int& build) const; // Get the version of database file we have loaded right now
bool GetExecutableVersion(int& major, int& minor, int& revision, int& build) const; // Get the version of currently executing application
const std::string& GetModuleName() const; // Get the name of currently loaded database module, this should show "SkyrimSE.exe"
const std::string& GetLoadedVersionString() const; // Get the currently loaded version as string, e.g. "1.5.62.0"
const std::map& GetOffsetMap() const; // Get the map of ID to offset if you need to iterate it manually
void* FindAddressById(unsigned long long id) const; // Find address by ID, this will already include base and be correct address. It will return NULL if not found!
bool FindOffsetById(unsigned long long id, unsigned long long& result) const; // Find offset by ID, this will just be offset without base included.
bool FindIdByAddress(void* ptr, unsigned long long& result) const; // Find ID by address, this will attempt a reverse lookup to convert address to ID
bool FindIdByOffset(unsigned long long offset, unsigned long long& result) const; // Find ID by offset, this will attempt a reverse lookup to convert offset to ID
Что нужно знать и учитывать:
1. Вы можете включить любую (или все) базы данных в ваш плагин, но это может значительно увеличить размер файла (примерно на 2,5 МБ). На данный момент чаще всего этот мод указывается как зависимость.
2. Всегда загружайте базу данных только один раз при запуске, инициализируйте/кэшируйте нужные вам адреса и затем выгружайте её. Выгрузка означает, что структура VersionDb удаляется или теряется (если вы выделили её на стеке). Это гарантирует, что вы не используете лишнюю память во время игры. Нет необходимости держать базу данных загруженной во время игрового процесса. Если вы используете CommonLib, это не актуально, так как она загружается только один раз, а не для каждого DLL.
3. База данных содержит адреса функций, глобальных переменных, RTTI, vtable и всего, что может иметь ссылку. Она не включает адреса, находящиеся в середине функций или глобальных переменных. Если вам нужен адрес в середине функции, найдите базовый адрес функции и добавьте дополнительное смещение самостоятельно. Также база не содержит ненужных данных, таких как выравнивание вокруг функций (на которые ссылается rdata), раздел pdata отбрасывается, а некоторая информация SEH, сгенерированная компилятором из rdata, также исключается.
4. Всегда проверяйте результат, чтобы убедиться, что база данных загрузилась успешно (функция Load вернула true) и что запрошенные адреса возвращают действительный результат (не NULL). Если загрузка не удалась, скорее всего, файл отсутствует или используется неверная версия (например, попытка использовать заголовок SE для AE). Если запрос не удался, это значит, что адрес не найден в данной версии. Это может означать, что код игры изменился настолько, что адрес больше не действителен для этой версии, или база данных не смогла определить правильный адрес. В любом из этих случаев инициализация плагина должна завершиться неудачей, чтобы SKSE знал, что плагин не загрузился корректно, или вы должны вручную показать сообщение об ошибке.
5. Рекомендуется проверять наличие адреса во всех версиях игры перед публикацией вашего плагина DLL. Для этого загрузите базу данных каждой версии и запросите один и тот же идентификатор адреса в каждой из них, чтобы убедиться, что он существует:
bool LoadAll(std::vector<VersionDb*>& 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<VersionDb*>& 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<VersionDb*>& all)
{
for (auto db : all)
delete db;
all.clear();
}
bool IsOk()
{
std::vector<VersionDb*> all;
if (!LoadAll(all))
{
_FATALERROR("Failed to load one or more version databases for current executable!");
FreeAll(all);
return false;
}
if (!ExistsInAll(all, 517014))
{
_FATALERROR("517014 does not exist in all versions of the database!");
FreeAll(all);
return false;
}
FreeAll(all);
// Ok!
return true;
}
Таким образом, вы можете быть уверены, что ваш мод DLL будет работать во всех версиях, или, если он не работает в некоторых версиях, указать это на странице вашего мода.
6. Иногда потребуется выполнять разные действия в зависимости от версии игры. Для этого используйте следующий фрагмент кода:
int major = 0, minor = 0, revision = 0, build = 0;
if (!db.GetExecutableVersion(major, minor, revision, build))
{
_FATALERROR("Something went wrong!");
return false;
}
// Running game is 1.5.x and at least version 1.5.39.0
if (major == 1 && minor == 5 && revision >= 39)
{
// Stuff ... ?
}
7. Обратите внимание: если вы компилируете ваш SKSE DLL в режиме отладки, время загрузки базы данных может составлять около 14 секунд! В релизном режиме это примерно 0,2 секунды. Это связано с тем, что стандартные контейнеры библиотеки в режиме отладки (std map) работают очень медленно.