sdk: add get_module_info, get_plugin_directory, d3d9 callbacks (#908)

## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
To make plugin development easier, add `get_module_info` and
`get_plugin_directory` to deal with common tasks for hooking DLLs and
reading INI files.

Also, add the ability to register for d3d9 present callbacks via
`register_d3d9`, which makes it significantly easier for plugins to draw
things on screen.

## Testing
See sample code.
This commit is contained in:
bicarus
2026-09-11 02:49:07 -07:00
committed by GitHub
parent 603fbfc69d
commit 4c77a3f148
17 changed files with 1095 additions and 15 deletions
+62 -1
View File
@@ -1,8 +1,12 @@
#include <format>
#include <chrono>
#include <thread>
#include <array>
#include <string>
#include <optional>
#include <windows.h>
#include "v0_cpp_imgui.h"
#include "sdk/include/spicesdk.h"
#include "sdk/include/spicesdk_io.h"
@@ -17,6 +21,24 @@ static spice_sdk_destroy_callback_func destroy_callback;
static std::jthread worker_thread;
static void worker_thread_main(std::stop_token stop_token);
static std::optional<std::string> utf16_to_utf8(const wchar_t *text) {
const auto bytes = WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1, nullptr, 0, nullptr, nullptr);
if (bytes == 0) {
return std::nullopt;
}
std::string result(bytes, '\0');
if (WideCharToMultiByte(
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1,
result.data(), bytes, nullptr, nullptr) != bytes) {
return std::nullopt;
}
result.pop_back();
return result;
}
// main entry point into the DLL
// spice executable will call into this shortly after the game is partially
// initialized (but not quite fully booted just yet)
@@ -42,6 +64,37 @@ spice_sdk_entry_point(
LOG_INFO("plugin loaded");
if (spice.get_plugin_directory) {
std::array<wchar_t, 32768> directory{};
uint32_t size = static_cast<uint32_t>(directory.size());
if (spice.get_plugin_directory(&spice, directory.data(), &size) ==
SPICE_SDK_STATUS_SUCCESS) {
if (const auto path = utf16_to_utf8(directory.data())) {
LOG_INFO(std::format("plugin directory: {}", *path).c_str());
}
}
}
if (spice.get_module_info) {
SPICE_SDK_MODULE_INFO info{};
info.size = sizeof(info);
if (spice.get_module_info(L"kernel32.dll", &info) == SPICE_SDK_STATUS_SUCCESS) {
LOG_INFO(std::format(
"kernel32 PE: {:x}_{:x}, base={:x}, size={:x}",
info.timestamp, info.entry_point, info.base, info.image_size).c_str());
}
}
if (spice.register_d3d9) {
status = sample_imgui::initialize(spice);
if (status != SPICE_SDK_STATUS_SUCCESS) {
LOG_INFO(std::format(
"D3D9 registration failed: {}", static_cast<int>(status)).c_str());
} else {
LOG_INFO("D3D9 renderer registered; Ctrl+Enter toggles the sample window");
}
}
// spin up a worker thread
worker_thread = std::jthread(worker_thread_main);
return 1;
@@ -77,10 +130,18 @@ static ArrowButton arrow_buttons[] = {
// worker thread for I/O
static void worker_thread_main(std::stop_token stop_token) {
bool coin_previous_state[10] = {};
bool window_toggle_previous_state = false;
while (!stop_token.stop_requested()) {
// insert coin
const bool control_pressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
const bool window_toggle_pressed = control_pressed &&
((GetAsyncKeyState(VK_RETURN) & 0x8000) != 0);
if (window_toggle_pressed && !window_toggle_previous_state) {
sample_imgui::toggle();
}
window_toggle_previous_state = window_toggle_pressed;
// insert coin
for (uint8_t amount = 0; amount < 10; amount++) {
const bool coin_pressed = control_pressed &&
((GetAsyncKeyState('0' + amount) & 0x8000) != 0);