Compare commits

...

12 Commits

Author SHA1 Message Date
bicarus 4c77a3f148 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.
2026-09-11 02:49:07 -07:00
drmext 603fbfc69d network: heap-allocate NIC spoof tunnel UDP queue (#913)
## Link to GitHub Issue or related Pull Request, if one exists
#912

## Description of change
The nicspoof tunnel kept a static UDP ring of 32 sockets x 64 slots x
8KiB payloads (~16MiB) inside the binary’s data/BSS. Now we store only
metadata + a pointer per slot. Payloads are malloc'd on enqueue and
free'd on dequeue (or when the socket is destroyed / a failed enqueue is
discarded). Empty datagrams skip malloc. MSG_PEEK copies under the
socket lock so a concurrent pop can't free the buffer out from under the
peek.

## Testing
DDR
2026-09-11 02:20:21 -07:00
drmext 5e4afecd90 network: NIC spoof matching tunnel (#912)
## Link to GitHub Issue or related Pull Request, if one exists
#911

## Description of change
Add tunnelhost/tunnelclient modes to -nicspoof so two cabs can do
LAN-style BPL matching over a real UDP path (including Wine), while
keeping the fake overlay NIC. Offline spoof remains available as before.

## Testing
DDR
2026-09-11 00:37:45 -07:00
drmext 531e7a54b3 network: NIC spoof (#911)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Spoof a valid NIC, so games can use a local server completely offline.
Previously this was impossible on macOS wine, or it required km-test
loopback adapter on Windows.

## Testing
Tested DDR (32bit/64bit) and SDVX on Windows 10 and macOS Tahoe.
Enabling -nicspoof and -icmphook connects to airgapped 127.0.0.1.
2026-09-10 13:00:07 -07:00
CHO-HSUN-TE f11b9d0189 scard: Add SCARD_ATR_PROTOCOL_ISO15693_PART4 support (#910)
## Link to GitHub Issue or related Pull Request, if one exists
None

## Description of change
Add ISO15693_PART4 support for old e-Amusement Pass.

Changes Proposed:
Added SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C to the scard_atr_protocol
enum to natively support SONY's vendor implementation.
Added an else if branch in the protocol check to handle 0x0C. 
When 0x0C is detected, the code now properly flags shouldReverseUid =
true.
Sinces old e-Amusement Pass is E00401 format, not enable the is_felica
flag.

## Testing
Tested with real HW on Sony RC-S300 with Windows WUDF drivers.

Verified that compiling with this change correctly identifies SONY
PaSoRi readers.
Verified that the UID is successfully swapped back to the correct
E00401XXXXXXXXXX format and logs into the game normally.

Signed-off-by: SugiuraAyano <ousi4706@gmail.com>
2026-09-08 14:20:15 -07:00
Will 112b1aecb0 Give options a designated initializer to make crossreferencing their usage easier (#905)
## Description of change
This always bothered me, you have a commandline arg and it's a massive
pain in the ass to work out where it's used because the flag never
really matches the enum name.

Array designated initializers are a C99 extension that clang+gcc both
support. I suspect msvc still blows up, but are people still using that?
I can change it to a std::invoke lambda instead.

Chose to drop the (unused?) option sorting func because, well, unused.

## Testing
Builds in CI, didn't do much more intense testing.
2026-09-07 01:13:37 -07:00
Will de466db29f CI: add MSVC build and fix building with MSVC (#907)
Yess yessss let's continue supporting the worst compiler toolchain
😈

Fell out of the changes in #905

CI changes were claude because I cannot be arsed with yaml.

Can explain the individual code fixes if ya want any more context.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-06 21:52:18 -07:00
bicarus 08e99dea4a misc: fix minor bugs (#906)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change

Fix hang when a DLL using the SDK calls `log_fatal` (deadlock)

Fix spicecfg crashing if API server fails to bind (make websocket
teardown conditional)

## Testing
2026-09-06 15:43:37 -07:00
bicarus 00e904539e Update C writing guidelines in CONTRIBUTING.md
Clarified guidelines for writing in C, emphasizing RAII patterns.
2026-09-05 17:20:02 -07:00
bicarus eaab4698eb api: log JSON parse error (#904) 2026-09-05 05:32:50 -07:00
bicarus a48eccab93 sdk: insert coin (#903)
## Link to GitHub Issue or related Pull Request, if one exists
developer request

## Description of change
Expose coin insert API over the SDK

## Testing
see sample sdk code
2026-09-05 04:26:44 -07:00
bicarus 81c913726d danevo: add 2p card reader (#902)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #901

## Description of change
This adds 2p card reader support to DanEvo. The cab only has one keypad
but this is not a configuration spice supports today so spicecfg will
show two keypads.

## Testing
2p card was recognized in test mode. No other testing performed.
2026-09-05 00:00:41 -07:00
36 changed files with 4890 additions and 883 deletions
+30 -1
View File
@@ -29,4 +29,33 @@ jobs:
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
if-no-files-found: error
if-no-files-found: error
msvc-ci:
name: Build (MSVC ${{ matrix.arch }}-bit)
# windows-latest ships Visual Studio 2026; pin to the VS 2022 image, which is
# the toolset spice2x is actually developed against
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- arch: 32
platform: Win32
target: spicetools_spice spicetools_cfg
- arch: 64
platform: x64
target: spicetools_spice64
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
- name: Configure
run: >
cmake -S . -B cmake-build-msvc-${{ matrix.arch }}
-A ${{ matrix.platform }}
- name: Compile
run: >
cmake --build cmake-build-msvc-${{ matrix.arch }}
--config Release --target ${{ matrix.target }}
+1 -1
View File
@@ -105,5 +105,5 @@ if (conditional)
* We don't use GSL.
* Don't throw exceptions.
* Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`.
* Writing in C is also completely acceptable.
* Writing in C is also completely acceptable but prefer to use RAII patterns.
* Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying.
+13 -1
View File
@@ -616,6 +616,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/nicspoof.cpp
hooks/nicspoof_tunnel.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
@@ -717,6 +719,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/crypt.cpp
# sdk
sdk/d3d9.cpp
sdk/modules.cpp
sdk/sdk.cpp
# stubs
@@ -1045,8 +1049,16 @@ if(NOT MSVC)
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
set(SOURCE_FILES
sdk/sample/v0/cpp/v0_cpp.cpp
sdk/sample/v0/cpp/v0_cpp_imgui.cpp
external/imgui/imgui.cpp
external/imgui/imgui_draw.cpp
external/imgui/imgui_tables.cpp
external/imgui/imgui_widgets.cpp
external/imgui/backends/imgui_impl_dx9.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
target_link_libraries(spicetools_sdk_sample_v0_cpp_64 PRIVATE imm32)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
+8 -1
View File
@@ -8,6 +8,7 @@
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/error/en.h"
#include "util/crypt.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -92,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// bind socket to address
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -102,6 +104,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// set socket to listen
if (listen(this->server, server_backlog) == -1) {
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -312,6 +315,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
// check for parse error
if (document.HasParseError()) {
log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)",
GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size);
// return empty response and close connection
out->push_back(0);
@@ -443,7 +448,9 @@ void Controller::free_socket() {
this->server = INVALID_SOCKET;
}
this->websocket->free_socket();
if (this->websocket) {
this->websocket->free_socket();
}
for (auto &s : this->serial) {
s->free_port();
+3 -3
View File
@@ -41,21 +41,21 @@ namespace api {
bool pretty;
// server
WebSocketController *websocket;
WebSocketController *websocket = nullptr;
std::vector<SerialController *> serial;
std::vector<std::thread> server_workers;
std::vector<std::thread> server_handlers;
std::mutex server_handlers_m;
std::vector<api::ClientState *> client_states;
std::mutex client_states_m;
SOCKET server;
SOCKET server = INVALID_SOCKET;
void server_worker();
void connection_handler(ClientState client_state);
public:
// state
bool server_running;
bool server_running = false;
// constructor / destructor
Controller(unsigned short port, std::string password, bool pretty);
+1 -1
View File
@@ -61,7 +61,7 @@ namespace api::modules {
return error_type(res, "amount", "int");
// add to coin stock
eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt()));
eamuse_coin_add(std::max(0, req.params[0].GetInt()));
}
/*
+7 -3
View File
@@ -50,6 +50,7 @@ static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u };
enum scard_atr_protocol {
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C,
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
SCARD_ATR_PROTOCOL_FELICA_424K = 0x12,
};
@@ -121,11 +122,14 @@ void scard_update(SCARDCONTEXT hContext, LPCTSTR readerName, uint8_t unit_no) {
BYTE cardProtocol = atr[12];
BOOL shouldReverseUid = false;
bool is_felica = false;
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
log_info("scard", "card protocol: ISO14443_PART3");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
log_info("scard", "card protocol: ISO15693_PART3");
shouldReverseUid = true;
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
log_info("scard", "card protocol: ISO14443_PART3");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART4) {
log_info("scard", "card protocol: ISO15693_PART4");
shouldReverseUid = true;
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
log_info("scard", "card protocol: FELICA_212K");
is_felica = true;
+9 -10
View File
@@ -2,6 +2,7 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <d3d9.h>
#include "mf_wrappers.h"
#include "avs/game.h"
@@ -46,9 +47,7 @@ struct PredefinedHook {
uintptr_t hook_afp_texture_offset;
};
PredefinedHook g_predefinedHooks[] = {};
const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks);
std::array<PredefinedHook, 0> g_predefinedHooks = {};
namespace games::iidx {
@@ -110,14 +109,14 @@ namespace games::iidx {
auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);
for (DWORD i = 0; i < g_predefinedHooksLength; i++) {
if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) {
for (auto &hook : g_predefinedHooks) {
if (pe.compare(hook.pe_identifier) == 0) {
log_misc("iidx:camhook", "Found predefined addresses");
addr_hook_a = g_predefinedHooks[i].hook_a;
addr_textures = g_predefinedHooks[i].hook_textures;
addr_camera_manager = g_predefinedHooks[i].hook_camera_manager;
addr_device_offset = g_predefinedHooks[i].hook_device_offset;
addr_afp_texture_offset = g_predefinedHooks[i].hook_afp_texture_offset;
addr_hook_a = hook.hook_a;
addr_textures = hook.hook_textures;
addr_camera_manager = hook.hook_camera_manager;
addr_device_offset = hook.hook_device_offset;
addr_afp_texture_offset = hook.hook_afp_texture_offset;
return TRUE;
}
}
@@ -29,6 +29,10 @@ DEFINE_GUID(IID_IAudioClient,
0x1cb9ad4c, 0xdbfa, 0x4c32,
0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2);
DEFINE_GUID(IID_IAudioClient3,
0x7ed4ee07, 0x8e67, 0x4cd4,
0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42);
DEFINE_GUID(IID_IAudioClock,
0xcd63314f, 0x3fba, 0x4a1b,
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
@@ -22,6 +22,7 @@
#include "launcher/shutdown.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "sdk/d3d9.h"
#include "util/detour.h"
#include "util/deferlog.h"
#include "util/flags_helper.h"
@@ -1511,6 +1512,8 @@ void graphics_d3d9_on_present(
device->EndScene();
}
sdk::d3d9::draw(hFocusWindow, device);
// after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
@@ -11,6 +11,7 @@
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "sdk/d3d9.h"
#include "util/flags_helper.h"
#include "util/utils.h"
#include "cfg/screen_resize.h"
@@ -136,6 +137,7 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
// release owned objects if there are no more references
if (local_refs == 0) {
sdk::d3d9::destroy(this->pReal);
if (this->main_swapchain) {
this->main_swapchain->Release();
this->main_swapchain = nullptr;
@@ -632,6 +634,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
}
}
sdk::d3d9::invalidate(pReal);
// reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate();
@@ -643,6 +647,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
d3d9_readback::discard_snapshot_targets(pReal);
HRESULT res = pReal->Reset(pPresentationParameters);
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
// recreate overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
@@ -673,7 +678,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
gfdm_small_head.compose(this);
}
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
const HRESULT result = pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
sdk::d3d9::present_complete(pReal, result);
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
@@ -2088,8 +2095,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::PresentEx(
graphics_d3d9_on_present(hFocusWindow, pReal, this);
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
const HRESULT result = static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
sdk::d3d9::present_complete(pReal, result);
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGPUThreadPriority(
@@ -2336,6 +2345,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
}
}
sdk::d3d9::invalidate(pReal);
// reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate();
@@ -2350,6 +2361,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
gfdm_parameters.presentation_parameters,
gfdm_parameters.fullscreen_display_modes);
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
if (is_gfdm_two_head_exclusive()
&& SUCCEEDED(res)
&& gfdm_parameters.recovery_candidate)
@@ -7,10 +7,14 @@
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "sdk/d3d9.h"
#include "util/logging.h"
#include "d3d9_device.h"
// windows.h defines many stupid things
#undef small
bool gfdm_two_head_exclusive() {
return games::gitadora::is_arena_model()
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
@@ -555,6 +559,7 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate;
temporary_modes[1] = alternate_small;
sdk::d3d9::invalidate(device);
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
const bool temporary_settled =
temporary_result == D3D_OK
@@ -571,6 +576,8 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
restore_result == D3D_OK
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]);
sdk::d3d9::reset_complete(device, SUCCEEDED(restore_result));
if (temporary_result != D3D_OK) {
return temporary_result;
}
@@ -5,6 +5,7 @@
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "sdk/d3d9.h"
#include "d3d9_backend.h"
#include "d3d9_device.h"
@@ -140,6 +141,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
result = recovery_failure;
}
}
if (should_run_hooks) {
sdk::d3d9::present_complete(pDev->pReal, result);
}
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
+7 -1
View File
@@ -7,6 +7,7 @@
#include <chrono>
#include <set>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>
@@ -396,7 +397,12 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
// terminate
if (uMsg == WM_CLOSE) {
log_info("graphics", "detected WM_CLOSE, terminating...");
launcher::shutdown(0);
static std::once_flag shutdown_requested;
std::call_once(shutdown_requested, [] {
std::thread([] {
launcher::shutdown(0);
}).detach();
});
return false;
}
+109 -26
View File
@@ -1,5 +1,6 @@
#include "icmphook_net.h"
#include "hooks/nicspoof.h"
#include "util/detour.h"
#include "util/logging.h"
@@ -353,7 +354,11 @@ int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) {
if (it != g_socks.end()) {
if (!name || namelen < (int) sizeof(sockaddr_in)) {
WSASetLastError(WSAEFAULT);
return SOCKET_ERROR;
}
auto *in = reinterpret_cast<const sockaddr_in *>(name);
if (in->sin_family != AF_INET) {
WSASetLastError(WSAEAFNOSUPPORT);
@@ -618,7 +623,10 @@ void install_icmphook_hooks() {
}
done = true;
const bool defer_divert = nicspoof_tunnel_enabled();
bool ok = true;
// Always own socket creation so raw ICMP sockets become emulated.
ok &= detour::trampoline_try(
"ws2_32.dll", "socket",
(void *) socket_hook, (void **) &socket_orig);
@@ -628,37 +636,52 @@ void install_icmphook_hooks() {
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASocketA",
(void *) WSASocketA_hook, (void **) &WSASocketA_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "closesocket",
(void *) closesocket_hook, (void **) &closesocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "bind",
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "sendto",
(void *) sendto_hook, (void **) &sendto_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "recvfrom",
(void *) recvfrom_hook, (void **) &recvfrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASendTo",
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSARecvFrom",
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "ioctlsocket",
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "setsockopt",
(void *) setsockopt_hook, (void **) &setsockopt_orig);
if (!ok) {
if (defer_divert) {
// NIC tunnel already hooked bind/sendto/recvfrom/closesocket/ioctlsocket.
// Tunnel hooks call icmphook_try_* for emulated ICMP sockets.
log_info("network",
"ICMP emulation: socket hooks installed; divert deferred to "
"NIC tunnel (icmphook_try_*)");
} else {
ok &= detour::trampoline_try(
"ws2_32.dll", "closesocket",
(void *) closesocket_hook, (void **) &closesocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "bind",
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "sendto",
(void *) sendto_hook, (void **) &sendto_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "recvfrom",
(void *) recvfrom_hook, (void **) &recvfrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASendTo",
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSARecvFrom",
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "ioctlsocket",
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
if (!ok) {
log_warning(
"network",
"ICMP emulation: one or more ws2_32 hooks failed to install");
} else {
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
}
}
if (defer_divert && !ok) {
log_warning(
"network",
"ICMP emulation: one or more ws2_32 hooks failed to install");
} else {
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
"ICMP emulation: one or more socket-creation hooks failed");
}
g_installed.store(true, std::memory_order_release);
@@ -686,6 +709,66 @@ bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *
return true;
}
bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags,
const sockaddr *to, int tolen, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::sendto_hook(s, buf, len, flags, to, tolen);
return true;
}
bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags,
sockaddr *from, int *fromlen, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::recvfrom_hook(s, buf, len, flags, from, fromlen);
return true;
}
bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo,
int iTolen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::WSASendTo_hook(
s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo,
iTolen, lpOverlapped, lpCompletionRoutine);
return true;
}
bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom,
LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::WSARecvFrom_hook(
s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpFrom,
lpFromlen, lpOverlapped, lpCompletionRoutine);
return true;
}
bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::ioctlsocket_hook(s, cmd, argp);
return true;
}
bool icmphook_try_closesocket(SOCKET s, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::closesocket_hook(s);
return true;
}
void icmphook_net_init() {
icmphook_internal::install_icmphook_hooks();
icmphook_iphlpapi_install();
+20 -1
View File
@@ -6,8 +6,27 @@ bool icmphook_is_emulated_socket(SOCKET s);
/*!
* Handle bind() for emulated ICMP sockets: records interface address and succeeds without kernel bind.
* Returns true if this socket was handled (caller should return 0).
* Returns true if this socket was handled (caller should return *out_result).
*/
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result);
/*!
* Divert-path helpers for when NIC tunnel owns the MinHook slots on these APIs.
* Return true if the socket is an emulated ICMP socket and was fully handled.
*/
bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags,
const sockaddr *to, int tolen, int *out_result);
bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags,
sockaddr *from, int *fromlen, int *out_result);
bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo,
int iTolen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result);
bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom,
LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result);
bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result);
bool icmphook_try_closesocket(SOCKET s, int *out_result);
void icmphook_net_init();
+645
View File
@@ -0,0 +1,645 @@
#define _WIN32_WINNT 0x0601
#include "nicspoof.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <iphlpapi.h>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include "util/detour.h"
#include "util/logging.h"
#include "hooks/nicspoof_tunnel.h"
namespace nicspoof_detail {
NicSpoofConfig g_cfg;
constexpr uint32_t k_default_local_ip = 0x0A64640Au; /* 10.100.100.10 */
constexpr uint32_t k_gateway = 0x0A646401u; /* 10.100.100.1 */
constexpr uint32_t k_dns1 = 0x0A02010Au; /* 10.2.1.10 */
constexpr uint32_t k_dns2 = 0x0A02011Eu; /* 10.2.1.30 */
constexpr uint32_t k_dhcp = 0xC0A80001u; /* 192.168.0.1 */
constexpr uint32_t k_mask = 0xFF000000u; /* 255.0.0.0 */
constexpr int k_prefix_len = 8;
constexpr DWORD k_ifindex = 77;
constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37};
constexpr char k_hostname[] = "N1C5P00F";
constexpr char k_domain[] = "sp2x";
constexpr char k_hostname_fqdn[] = "N1C5P00F.sp2x";
constexpr wchar_t k_domain_w[] = L"sp2x";
constexpr char k_adapter_name[] = "{NICSPOOF-0880-0001-4250-4E6963537066}";
constexpr char k_adapter_desc[] = "NicSpoof Virtual Ethernet";
constexpr wchar_t k_friendly_name[] = L"NicSpoof";
constexpr wchar_t k_adapter_desc_w[] = L"NicSpoof Virtual Ethernet";
uint32_t g_local_ip = k_default_local_ip;
uint32_t g_subnet = k_default_local_ip & k_mask;
#define ALIGN_UP_PTR(p, a) \
((BYTE *)(((ULONG_PTR)(p) + ((ULONG_PTR)(a) - 1)) & ~((ULONG_PTR)(a) - 1)))
bool g_log_adapters = true;
bool g_log_params = true;
[[maybe_unused]] decltype(GetAdaptersAddresses) *GetAdaptersAddresses_orig = nullptr;
[[maybe_unused]] decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
[[maybe_unused]] decltype(GetNetworkParams) *GetNetworkParams_orig = nullptr;
[[maybe_unused]] decltype(getaddrinfo) *getaddrinfo_orig = nullptr;
[[maybe_unused]] decltype(freeaddrinfo) *freeaddrinfo_orig = nullptr;
void ip_to_str(uint32_t ip, char *out, size_t n) {
if (!out || n == 0) {
return;
}
snprintf(out, n, "%u.%u.%u.%u",
(ip >> 24) & 255, (ip >> 16) & 255, (ip >> 8) & 255, ip & 255);
out[n - 1] = 0;
}
uint32_t parse_ipv4(const char *s) {
unsigned a = 0;
unsigned b = 0;
unsigned c = 0;
unsigned d = 0;
char trail = 0;
if (!s) {
return 0;
}
if (sscanf(s, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
return 0;
}
if (a > 255 || b > 255 || c > 255 || d > 255) {
return 0;
}
return (a << 24) | (b << 16) | (c << 8) | d;
}
bool name_is_local(const char *name) {
if (!name || !name[0]) {
return true;
}
if (_stricmp(name, "localhost") == 0) {
return true;
}
if (_stricmp(name, k_hostname) == 0) {
return true;
}
if (_stricmp(name, k_hostname_fqdn) == 0) {
return true;
}
return false;
}
bool name_is_numeric_ipv4(const char *name) {
unsigned a = 0;
unsigned b = 0;
unsigned c = 0;
unsigned d = 0;
char trail = 0;
if (!name) {
return false;
}
if (sscanf(name, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
return false;
}
return a <= 255 && b <= 255 && c <= 255 && d <= 255;
}
DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) {
ULONG need = sizeof(IP_ADAPTER_INFO) + 32;
char ip[16];
char gw[16];
char dhcp[16];
if (!s) {
return ERROR_INVALID_PARAMETER;
}
if (!p || *s < need) {
*s = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(p, 0, need);
p->Index = k_ifindex;
strcpy(p->AdapterName, k_adapter_name);
strcpy(p->Description, k_adapter_desc);
p->AddressLength = 6;
memcpy(p->Address, k_fake_mac, 6);
p->Type = MIB_IF_TYPE_ETHERNET;
p->DhcpEnabled = 1;
ip_to_str(g_local_ip, ip, sizeof(ip));
ip_to_str(k_gateway, gw, sizeof(gw));
ip_to_str(k_dhcp, dhcp, sizeof(dhcp));
strcpy(p->IpAddressList.IpAddress.String, ip);
ip_to_str(k_mask, p->IpAddressList.IpMask.String,
sizeof(p->IpAddressList.IpMask.String));
p->IpAddressList.Context = 0;
p->Next = nullptr;
strcpy(p->GatewayList.IpAddress.String, gw);
p->GatewayList.IpMask.String[0] = 0;
p->GatewayList.Context = 0;
p->GatewayList.Next = nullptr;
strcpy(p->DhcpServer.IpAddress.String, dhcp);
p->DhcpServer.IpMask.String[0] = 0;
p->DhcpServer.Context = 0;
p->DhcpServer.Next = nullptr;
*s = need;
if (g_log_adapters) {
log_misc("network", "NIC spoof GetAdaptersInfo ip={} gw={} ifindex={}",
ip, gw, static_cast<unsigned>(k_ifindex));
g_log_adapters = false;
}
return ERROR_SUCCESS;
}
DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) {
/* FIXED_INFO embeds the first IP_ADDR_STRING; second follows in the buffer. */
ULONG need = static_cast<ULONG>(sizeof(FIXED_INFO) + sizeof(IP_ADDR_STRING));
char dns1[16];
char dns2[16];
PIP_ADDR_STRING dns_second = nullptr;
if (!s) {
return ERROR_INVALID_PARAMETER;
}
if (!p || *s < need) {
*s = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(p, 0, need);
strncpy(p->HostName, k_hostname, sizeof(p->HostName) - 1);
strncpy(p->DomainName, k_domain, sizeof(p->DomainName) - 1);
ip_to_str(k_dns1, dns1, sizeof(dns1));
ip_to_str(k_dns2, dns2, sizeof(dns2));
strcpy(p->DnsServerList.IpAddress.String, dns1);
p->DnsServerList.IpMask.String[0] = 0;
p->DnsServerList.Context = 0;
dns_second = reinterpret_cast<PIP_ADDR_STRING>(
reinterpret_cast<BYTE *>(p) + sizeof(FIXED_INFO));
memset(dns_second, 0, sizeof(*dns_second));
strcpy(dns_second->IpAddress.String, dns2);
dns_second->IpMask.String[0] = 0;
dns_second->Context = 0;
dns_second->Next = nullptr;
p->DnsServerList.Next = dns_second;
p->CurrentDnsServer = &p->DnsServerList;
p->NodeType = 1;
p->EnableDns = 1;
*s = need;
if (g_log_params) {
log_misc("network",
"NIC spoof GetNetworkParams host={} domain={} dns={} dns2={}",
k_hostname, k_domain, dns1, dns2);
g_log_params = false;
}
return ERROR_SUCCESS;
}
ULONG WINAPI GetAdaptersAddresses_hook(
ULONG Family,
ULONG Flags,
PVOID Reserved,
PIP_ADAPTER_ADDRESSES AdapterAddresses,
PULONG SizePointer) {
enum { STR_SLOT = 64 };
/*
* Layout is fixed: adapter header, 4 string slots, then unicast/prefix/
* gateway/dns + sockaddrs with alignment slack. need is computed to always
* fit; never return BUFFER_OVERFLOW after a successful size probe.
*/
ULONG need = static_cast<ULONG>(sizeof(IP_ADAPTER_ADDRESSES) + STR_SLOT * 4 +
sizeof(void *) * 8 +
sizeof(IP_ADAPTER_UNICAST_ADDRESS) +
sizeof(IP_ADAPTER_PREFIX) +
sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) * 2 +
sizeof(IP_ADAPTER_GATEWAY_ADDRESS) +
sizeof(sockaddr_in) * 5 + 64);
BYTE *blob = nullptr;
PIP_ADAPTER_ADDRESSES a = nullptr;
PIP_ADAPTER_UNICAST_ADDRESS u = nullptr;
PIP_ADAPTER_PREFIX pref = nullptr;
PIP_ADAPTER_GATEWAY_ADDRESS gw = nullptr;
PIP_ADAPTER_DNS_SERVER_ADDRESS dns = nullptr;
PIP_ADAPTER_DNS_SERVER_ADDRESS dns2 = nullptr;
sockaddr_in *sa = nullptr;
sockaddr_in *sm = nullptr;
sockaddr_in *sg = nullptr;
sockaddr_in *ds = nullptr;
sockaddr_in *ds2 = nullptr;
(void)Reserved;
(void)Flags;
if (Family != AF_INET && Family != AF_UNSPEC) {
if (SizePointer) {
*SizePointer = 0;
}
return ERROR_NO_DATA;
}
if (!SizePointer) {
return ERROR_INVALID_PARAMETER;
}
if (!AdapterAddresses || *SizePointer < need) {
*SizePointer = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(AdapterAddresses, 0, need);
a = AdapterAddresses;
blob = reinterpret_cast<BYTE *>(a + 1);
a->Length = sizeof(IP_ADAPTER_ADDRESSES);
a->IfIndex = k_ifindex;
a->AdapterName = reinterpret_cast<PCHAR>(blob);
strcpy(reinterpret_cast<char *>(blob), k_adapter_name);
blob += STR_SLOT;
a->FriendlyName = reinterpret_cast<PWCHAR>(blob);
wcscpy(reinterpret_cast<wchar_t *>(blob), k_friendly_name);
blob += STR_SLOT;
a->Description = reinterpret_cast<PWCHAR>(blob);
wcscpy(reinterpret_cast<wchar_t *>(blob), k_adapter_desc_w);
blob += STR_SLOT;
a->PhysicalAddressLength = 6;
memcpy(a->PhysicalAddress, k_fake_mac, 6);
a->Flags = IP_ADAPTER_DHCP_ENABLED;
a->Mtu = 1500;
a->IfType = IF_TYPE_ETHERNET_CSMACD;
a->OperStatus = IfOperStatusUp;
a->Ipv4Enabled = 1;
a->DnsSuffix = reinterpret_cast<PWCHAR>(blob);
wcsncpy(reinterpret_cast<wchar_t *>(blob), k_domain_w,
(STR_SLOT / sizeof(wchar_t)) - 1);
reinterpret_cast<wchar_t *>(blob)[(STR_SLOT / sizeof(wchar_t)) - 1] = 0;
blob += STR_SLOT;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
u = reinterpret_cast<PIP_ADAPTER_UNICAST_ADDRESS>(blob);
blob += sizeof(*u);
memset(u, 0, sizeof(*u));
u->Length = sizeof(*u);
u->DadState = IpDadStatePreferred;
u->OnLinkPrefixLength = static_cast<UCHAR>(k_prefix_len);
sa = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sa);
memset(sa, 0, sizeof(*sa));
sa->sin_family = AF_INET;
sa->sin_addr.s_addr = htonl(g_local_ip);
u->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sa);
u->Address.iSockaddrLength = sizeof(*sa);
a->FirstUnicastAddress = u;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
pref = reinterpret_cast<PIP_ADAPTER_PREFIX>(blob);
blob += sizeof(*pref);
memset(pref, 0, sizeof(*pref));
pref->Length = sizeof(*pref);
pref->PrefixLength = static_cast<ULONG>(k_prefix_len);
sm = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sm);
memset(sm, 0, sizeof(*sm));
sm->sin_family = AF_INET;
sm->sin_addr.s_addr = htonl(g_subnet);
pref->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sm);
pref->Address.iSockaddrLength = sizeof(*sm);
a->FirstPrefix = pref;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
gw = reinterpret_cast<PIP_ADAPTER_GATEWAY_ADDRESS>(blob);
blob += sizeof(*gw);
memset(gw, 0, sizeof(*gw));
gw->Length = sizeof(*gw);
sg = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sg);
memset(sg, 0, sizeof(*sg));
sg->sin_family = AF_INET;
sg->sin_addr.s_addr = htonl(k_gateway);
gw->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sg);
gw->Address.iSockaddrLength = sizeof(*sg);
a->FirstGatewayAddress = gw;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
dns = reinterpret_cast<PIP_ADAPTER_DNS_SERVER_ADDRESS>(blob);
blob += sizeof(*dns);
memset(dns, 0, sizeof(*dns));
ds = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*ds);
memset(ds, 0, sizeof(*ds));
ds->sin_family = AF_INET;
ds->sin_addr.s_addr = htonl(k_dns1);
dns->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds);
dns->Address.iSockaddrLength = sizeof(*ds);
blob = ALIGN_UP_PTR(blob, sizeof(void *));
dns2 = reinterpret_cast<PIP_ADAPTER_DNS_SERVER_ADDRESS>(blob);
blob += sizeof(*dns2);
memset(dns2, 0, sizeof(*dns2));
ds2 = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*ds2);
memset(ds2, 0, sizeof(*ds2));
ds2->sin_family = AF_INET;
ds2->sin_addr.s_addr = htonl(k_dns2);
dns2->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds2);
dns2->Address.iSockaddrLength = sizeof(*ds2);
dns2->Next = nullptr;
dns->Next = dns2;
a->FirstDnsServerAddress = dns;
a->Next = nullptr;
*SizePointer = need;
return ERROR_SUCCESS;
}
constexpr uint32_t AI_MAGIC = 0x4E535041u; /* 'NSPA' */
struct SpoofAiHdr {
uint32_t magic;
ADDRINFOA ai;
};
SpoofAiHdr *ai_hdr_from_ai(ADDRINFOA *ai) {
SpoofAiHdr *h = nullptr;
if (!ai) {
return nullptr;
}
h = reinterpret_cast<SpoofAiHdr *>(
reinterpret_cast<BYTE *>(ai) - offsetof(SpoofAiHdr, ai));
if (h->magic != AI_MAGIC) {
return nullptr;
}
return h;
}
/* Wine packed getaddrinfo: sockaddr lives immediately after addrinfo. */
bool ai_is_wine_packed(ADDRINFOA *ai) {
BYTE *p = reinterpret_cast<BYTE *>(ai);
BYTE *addr = nullptr;
if (!ai || !ai->ai_addr) {
return false;
}
addr = reinterpret_cast<BYTE *>(ai->ai_addr);
return addr >= p + sizeof(*ai) &&
addr < p + sizeof(*ai) + sizeof(SOCKADDR_STORAGE) + 64;
}
/*
* Dual-safe free for Wine + native Windows:
* - Our results: magic header + separate mallocs for addr/canon.
* - Wine packed foreign: single free(ai).
* - Else (native Windows foreign): free canon, addr, node per link.
*/
void WSAAPI freeaddrinfo_hook(PADDRINFOA ai) {
while (ai) {
ADDRINFOA *next = ai->ai_next;
SpoofAiHdr *hdr = ai_hdr_from_ai(ai);
if (hdr) {
free(ai->ai_canonname);
free(ai->ai_addr);
free(hdr);
} else if (ai_is_wine_packed(ai)) {
free(ai);
break;
} else {
free(ai->ai_canonname);
free(ai->ai_addr);
free(ai);
}
ai = next;
}
}
INT WSAAPI getaddrinfo_hook(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult) {
SpoofAiHdr *hdr = nullptr;
ADDRINFOA *ai = nullptr;
sockaddr_in *sa = nullptr;
char *canon = nullptr;
const char *name_src = nullptr;
size_t name_len = 0;
uint32_t ip = 0;
int port = 0;
if (!ppResult) {
return EAI_FAIL;
}
*ppResult = nullptr;
if (pHints && pHints->ai_family == AF_INET6) {
return EAI_FAMILY;
}
if (!pNodeName || !pNodeName[0] || name_is_local(pNodeName)) {
if (pNodeName && (_stricmp(pNodeName, k_hostname) == 0 ||
_stricmp(pNodeName, k_hostname_fqdn) == 0)) {
ip = g_local_ip;
} else {
ip = 0x7F000001u;
}
} else if (name_is_numeric_ipv4(pNodeName)) {
ip = parse_ipv4(pNodeName);
} else {
/* offline=1: map external names to local_ip */
ip = g_local_ip;
}
if (pServiceName && pServiceName[0]) {
port = atoi(pServiceName);
if (port < 0 || port > 65535) {
port = 0;
}
}
name_src = (pNodeName && pNodeName[0]) ? pNodeName : "localhost";
name_len = strlen(name_src) + 1;
if (name_len > 256) {
name_len = 256;
}
hdr = static_cast<SpoofAiHdr *>(calloc(1, sizeof(*hdr)));
sa = static_cast<sockaddr_in *>(calloc(1, sizeof(*sa)));
canon = static_cast<char *>(malloc(name_len));
if (!hdr || !sa || !canon) {
free(hdr);
free(sa);
free(canon);
return EAI_MEMORY;
}
hdr->magic = AI_MAGIC;
ai = &hdr->ai;
memcpy(canon, name_src, name_len - 1);
canon[name_len - 1] = 0;
sa->sin_family = AF_INET;
sa->sin_addr.s_addr = htonl(ip);
if (port > 0) {
sa->sin_port = htons(static_cast<u_short>(port));
}
ai->ai_family = AF_INET;
ai->ai_socktype = pHints && pHints->ai_socktype ?
pHints->ai_socktype : SOCK_STREAM;
ai->ai_protocol = pHints && pHints->ai_protocol ?
pHints->ai_protocol : IPPROTO_TCP;
ai->ai_addrlen = sizeof(*sa);
ai->ai_addr = reinterpret_cast<struct sockaddr *>(sa);
ai->ai_canonname = canon;
ai->ai_next = nullptr;
*ppResult = ai;
return 0;
}
void install_nicspoof_hooks() {
static bool done = false;
if (done) {
return;
}
done = true;
char ipstr[16];
char maskstr[16];
char gwstr[16];
ip_to_str(g_local_ip, ipstr, sizeof(ipstr));
ip_to_str(k_mask, maskstr, sizeof(maskstr));
ip_to_str(k_gateway, gwstr, sizeof(gwstr));
log_info("network",
"NIC spoof enabled ip={} mask={}/{} gw={} "
"mac={:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X} host={} ifindex={}",
ipstr, maskstr, k_prefix_len, gwstr,
static_cast<unsigned>(k_fake_mac[0]),
static_cast<unsigned>(k_fake_mac[1]),
static_cast<unsigned>(k_fake_mac[2]),
static_cast<unsigned>(k_fake_mac[3]),
static_cast<unsigned>(k_fake_mac[4]),
static_cast<unsigned>(k_fake_mac[5]),
k_hostname, static_cast<unsigned>(k_ifindex));
bool ok = true;
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetAdaptersAddresses",
(void *) GetAdaptersAddresses_hook,
(void **) &GetAdaptersAddresses_orig);
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetAdaptersInfo",
(void *) GetAdaptersInfo_hook,
(void **) &GetAdaptersInfo_orig);
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetNetworkParams",
(void *) GetNetworkParams_hook,
(void **) &GetNetworkParams_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "getaddrinfo",
(void *) getaddrinfo_hook,
(void **) &getaddrinfo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "freeaddrinfo",
(void *) freeaddrinfo_hook,
(void **) &freeaddrinfo_orig);
{
HMODULE ws = GetModuleHandleA("ws2_32.dll");
void *faa = nullptr;
void *fai = nullptr;
if (ws) {
faa = reinterpret_cast<void *>(GetProcAddress(ws, "FreeAddrInfoA"));
fai = reinterpret_cast<void *>(GetProcAddress(ws, "freeaddrinfo"));
}
if (faa && faa != fai) {
decltype(freeaddrinfo) *FreeAddrInfoA_orig = nullptr;
if (!detour::trampoline_try(
"ws2_32.dll", "FreeAddrInfoA",
(void *) freeaddrinfo_hook,
(void **) &FreeAddrInfoA_orig)) {
log_warning("network",
"NIC spoof: FreeAddrInfoA hook was not installed");
}
}
}
if (!ok) {
log_warning("network",
"NIC spoof: one or more hooks failed to install");
} else {
log_info("network",
"NIC spoof hooks installed "
"(GAA/GAI/GNP/getaddrinfo/freeaddrinfo)");
}
}
bool ip_in_overlay_range(uint32_t ip) {
const uint32_t net = k_gateway & k_mask;
const uint32_t bcast = net | ~k_mask;
if ((ip & k_mask) != net) {
return false;
}
if (ip == net || ip == bcast || ip == k_gateway ||
ip == k_dns1 || ip == k_dns2) {
return false;
}
return true;
}
void init_impl() {
if (g_cfg.mode == NicSpoofMode::Off) {
return;
}
g_local_ip = g_cfg.local_ip ? g_cfg.local_ip : k_default_local_ip;
if (!ip_in_overlay_range(g_local_ip)) {
char bad[16];
ip_to_str(g_local_ip, bad, sizeof(bad));
log_warning("network",
"NIC spoof: IP {} is outside 10.0.0.0/8 usable range; "
"using 10.100.100.10",
bad);
g_local_ip = k_default_local_ip;
}
g_subnet = g_local_ip & k_mask;
const char *mode_str = "offline";
if (g_cfg.mode == NicSpoofMode::TunnelHost) {
mode_str = "tunnelhost";
} else if (g_cfg.mode == NicSpoofMode::TunnelClient) {
mode_str = "tunnelclient";
}
log_info("network", "NIC spoof mode={}", mode_str);
install_nicspoof_hooks();
if (g_cfg.mode == NicSpoofMode::TunnelHost ||
g_cfg.mode == NicSpoofMode::TunnelClient) {
nicspoof_tunnel_init(g_cfg);
}
}
} // namespace nicspoof_detail
void nicspoof_configure(const NicSpoofConfig &cfg) {
nicspoof_detail::g_cfg = cfg;
}
bool nicspoof_tunnel_enabled() {
return nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelHost ||
nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelClient;
}
uint32_t nicspoof_local_ip() {
return nicspoof_detail::g_local_ip;
}
uint32_t nicspoof_mask() {
return nicspoof_detail::k_mask;
}
uint32_t nicspoof_subnet() {
return nicspoof_detail::g_subnet;
}
void nicspoof_init() {
nicspoof_detail::init_impl();
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <string>
enum class NicSpoofMode {
Off,
Offline,
TunnelHost,
TunnelClient,
};
struct NicSpoofConfig {
NicSpoofMode mode = NicSpoofMode::Off;
uint32_t local_ip = 0x0A64640Au; /* 10.100.100.10 */
uint16_t tunnel_port = 51820;
std::string hub_host;
};
void nicspoof_configure(const NicSpoofConfig &cfg);
void nicspoof_init();
/*! True when mode is TunnelHost or TunnelClient (divert hooks own ws2 sendto/etc.). */
bool nicspoof_tunnel_enabled();
uint32_t nicspoof_local_ip();
uint32_t nicspoof_mask();
uint32_t nicspoof_subnet();
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include "nicspoof.h"
void nicspoof_tunnel_init(const NicSpoofConfig &cfg);
+51 -2
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <shlwapi.h>
#include <windows.h>
@@ -84,6 +85,7 @@
#include "hooks/lang.h"
#include "hooks/networkhook.h"
#include "hooks/icmphook_net.h"
#include "hooks/nicspoof.h"
#include "hooks/unisintrhook.h"
#include "launcher/launcher.h"
#include "launcher/logger.h"
@@ -257,6 +259,7 @@ int main_implementation(int argc, char *argv[]) {
bool load_stubs = false;
bool netfix_disable = false;
bool icmphook_enable = false;
NicSpoofConfig nicspoof_cfg;
bool lang_disable = false;
std::string process_priority_str = "high";
bool cardio_enabled = false;
@@ -799,6 +802,44 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::EnableICMPHook].value_bool()) {
icmphook_enable = true;
}
if (options[launcher::Options::EnableNICSpoof].is_active()) {
const auto &mode = options[launcher::Options::EnableNICSpoof].value_text();
if (mode == "offline" || mode == "/ENABLED") {
nicspoof_cfg.mode = NicSpoofMode::Offline;
} else if (mode == "tunnelhost") {
nicspoof_cfg.mode = NicSpoofMode::TunnelHost;
} else if (mode == "tunnelclient") {
nicspoof_cfg.mode = NicSpoofMode::TunnelClient;
} else {
log_warning("launcher", "unknown -nicspoof value '{}', ignoring", mode);
}
}
if (nicspoof_cfg.mode != NicSpoofMode::Off) {
if (options[launcher::Options::NICSpoofIP].is_active()) {
unsigned a = 0, b = 0, c = 0, d = 0;
char trail = 0;
const auto &ip = options[launcher::Options::NICSpoofIP].value_text();
if (sscanf(ip.c_str(), "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) == 4 &&
a <= 255 && b <= 255 && c <= 255 && d <= 255) {
nicspoof_cfg.local_ip = (a << 24) | (b << 16) | (c << 8) | d;
} else {
log_warning("launcher", "invalid -nicspoofip '{}'", ip);
}
}
if (options[launcher::Options::NICSpoofHostRealIP].is_active()) {
nicspoof_cfg.hub_host =
options[launcher::Options::NICSpoofHostRealIP].value_text();
}
if (options[launcher::Options::NICSpoofPort].is_active()) {
const uint32_t p =
options[launcher::Options::NICSpoofPort].value_uint32();
if (p == 0 || p > 65535) {
log_warning("launcher", "invalid -nicspoofport {}", p);
} else {
nicspoof_cfg.tunnel_port = static_cast<uint16_t>(p);
}
}
}
if (options[launcher::Options::DisableACPHook].value_bool()) {
lang_disable = true;
}
@@ -2553,7 +2594,15 @@ int main_implementation(int argc, char *argv[]) {
avs::core::load_dll();
avs::ea3::load_dll();
// ICMP emulation (opt-in; before games open raw ICMP sockets)
// NIC spoof / matching tunnel first so divert owns overlapping ws2_32
// MinHook slots (bind/sendto/recvfrom/...). ICMP then installs only the
// non-overlapping socket-creation hooks and is reached via icmphook_try_*.
if (nicspoof_cfg.mode != NicSpoofMode::Off) {
nicspoof_configure(nicspoof_cfg);
nicspoof_init();
}
// ICMP emulation (opt-in; after tunnel so hooks do not collide)
if (icmphook_enable) {
icmphook_net_init();
}
@@ -2859,7 +2908,7 @@ int main_implementation(int argc, char *argv[]) {
bt5api_dispose();
}
sdk::fini_sdk_modules();
sdk::fini_sdk_modules(true);
// stop raw input
hotkeys::disable_raw_input();
File diff suppressed because it is too large Load Diff
+6
View File
@@ -316,6 +316,10 @@ namespace launcher {
OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
EnableNICSpoof,
NICSpoofIP,
NICSpoofHostRealIP,
NICSpoofPort,
AutoElevate,
CfgForceSoftwareRender,
OBSWebSocketEnabled,
@@ -325,6 +329,8 @@ namespace launcher {
OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens,
_Count,
};
enum class OptionsCategory {
+4 -2
View File
@@ -331,8 +331,8 @@ int eamuse_coin_consume_stock() {
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
}
int eamuse_coin_add() {
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
void eamuse_coin_add(int amount) {
COIN_STOCK.fetch_add(amount, std::memory_order_relaxed);
}
void eamuse_coin_insert() {
@@ -621,6 +621,7 @@ int eamuse_get_game_keypads() {
avs::game::is_model("JDX") ||
avs::game::is_model("KDX") ||
avs::game::is_model("MDX") ||
avs::game::is_model("KDM") ||
avs::game::is_model("J33") ||
avs::game::is_model("K33") ||
avs::game::is_model("L33") ||
@@ -637,6 +638,7 @@ int eamuse_get_game_keypads_name() {
if (game_name == "Beatmania IIDX" ||
game_name == "Dance Dance Revolution" ||
game_name == "Dance Evolution" ||
game_name == "GitaDora")
{
return 2;
+1 -1
View File
@@ -59,7 +59,7 @@ void eamuse_coin_set_stock(int amount);
bool eamuse_coin_consume(int amount);
int eamuse_coin_consume_stock();
int eamuse_coin_add();
void eamuse_coin_add(int amount = 1);
void eamuse_coin_insert();
void eamuse_pin_macro_start_thread();
+403
View File
@@ -0,0 +1,403 @@
#include "d3d9.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
namespace sdk::d3d9 {
namespace {
struct Renderer {
spice_sdk_d3d9_callback_func *callback;
void *userdata;
bool ready = false;
bool started = false;
};
// keep registration separate from serialized device access and callback execution.
std::mutex registry_mutex;
std::recursive_mutex execution_mutex;
std::condition_variable_any shutdown_condition;
std::vector<std::shared_ptr<Renderer>> renderers;
SPICE_SDK_D3D9_FRAME frame{sizeof(SPICE_SDK_D3D9_FRAME), nullptr, nullptr, 0, 0};
bool stopped = false;
bool resetting = false;
bool device_lost = false;
bool reset_in_progress = false;
bool shutdown_complete = false;
DWORD device_thread = 0;
// reject reentrant SDK graphics calls made by a renderer callback.
thread_local bool dispatching = false;
struct DispatchScope {
DispatchScope() {
dispatching = true;
}
~DispatchScope() {
dispatching = false;
}
};
template<typename Interface>
struct ComReference {
Interface *value = nullptr;
~ComReference() {
if (value) {
value->Release();
}
}
};
// restore game state after each renderer, including early exits during setup.
struct GraphicsState {
IDirect3DDevice9 *device;
ComReference<IDirect3DStateBlock9> block;
std::array<ComReference<IDirect3DSurface9>, 4> targets;
ComReference<IDirect3DSurface9> depth;
D3DVIEWPORT9 viewport{};
std::array<D3DMATRIX, 3> transforms{};
std::array<bool, 3> have_transform{};
DWORD target_count = 0;
bool captured = false;
explicit GraphicsState(IDirect3DDevice9 *device) : device(device) {
D3DCAPS9 caps{};
if (FAILED(device->GetDeviceCaps(&caps)) ||
FAILED(device->CreateStateBlock(D3DSBT_ALL, &block.value)) ||
FAILED(block.value->Capture()) || FAILED(device->GetViewport(&viewport))) {
return;
}
// state blocks do not capture render targets or the depth surface.
target_count = std::min<DWORD>(caps.NumSimultaneousRTs, targets.size());
for (DWORD index = 0; index < target_count; ++index) {
const auto status = device->GetRenderTarget(index, &targets[index].value);
if (FAILED(status) && status != D3DERR_NOTFOUND) {
return;
}
}
const auto status = device->GetDepthStencilSurface(&depth.value);
if (FAILED(status) && status != D3DERR_NOTFOUND) {
return;
}
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
for (size_t index = 0; index < kinds.size(); ++index) {
have_transform[index] = SUCCEEDED(
device->GetTransform(kinds[index], &transforms[index]));
}
captured = true;
}
~GraphicsState() {
if (!captured) {
return;
}
// unbind auxiliary surfaces before restoring targets of potentially different sizes.
device->SetDepthStencilSurface(nullptr);
for (DWORD index = 1; index < target_count; ++index) {
device->SetRenderTarget(index, nullptr);
}
device->SetRenderTarget(0, targets[0].value);
for (DWORD index = 1; index < target_count; ++index) {
device->SetRenderTarget(index, targets[index].value);
}
device->SetDepthStencilSurface(depth.value);
block.value->Apply();
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
for (size_t index = 0; index < kinds.size(); ++index) {
if (have_transform[index]) {
device->SetTransform(kinds[index], &transforms[index]);
}
}
device->SetViewport(&viewport);
}
};
std::vector<std::shared_ptr<Renderer>> snapshot() {
// callbacks run without the registry lock, while their entries remain alive.
std::lock_guard lock(registry_mutex);
return renderers;
}
void destroy_renderers() {
for (const auto &renderer : snapshot()) {
if (renderer->started) {
renderer->callback(SPICE_SDK_D3D9_DESTROY, &frame, renderer->userdata);
}
renderer->ready = false;
renderer->started = false;
}
frame = {sizeof(frame), nullptr, nullptr, 0, 0};
resetting = false;
device_lost = false;
reset_in_progress = false;
device_thread = 0;
}
void finish_shutdown() {
if (shutdown_complete) {
return;
}
destroy_renderers();
{
std::lock_guard lock(registry_mutex);
renderers.clear();
}
shutdown_complete = true;
shutdown_condition.notify_all();
}
}
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
spice_sdk_d3d9_callback_func *callback, void *userdata) {
if (dispatching) {
return SPICE_SDK_STATUS_NOT_SUPPORTED;
}
if (!callback) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
// only registered SDK modules may supply callbacks.
HMODULE owner = nullptr;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(callback), &owner) ||
std::none_of(modules.begin(), modules.end(), [&](const SdkModule &module) {
return module.module == owner;
})) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
std::lock_guard lock(registry_mutex);
if (stopped) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (std::any_of(renderers.begin(), renderers.end(), [&](const auto &renderer) {
return renderer->callback == callback && renderer->userdata == userdata;
})) {
return SPICE_SDK_STATUS_SUCCESS;
}
// keep callback code loaded for the lifetime of the process.
try {
auto renderer = std::make_shared<Renderer>(Renderer{callback, userdata});
if (!GetModuleHandleExW(
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
reinterpret_cast<LPCWSTR>(callback), &owner)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
renderers.push_back(std::move(renderer));
} catch (...) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
return SPICE_SDK_STATUS_SUCCESS;
}
void draw(HWND window, IDirect3DDevice9 *device) {
if (dispatching) {
return;
}
std::lock_guard lock(execution_mutex);
DispatchScope scope;
if (frame.device && frame.device != device) {
return;
}
device_thread = GetCurrentThreadId();
if (stopped) {
if (!reset_in_progress) {
finish_shutdown();
}
return;
}
// latch the first presented device even before any renderers register.
frame.device = device;
frame.window = window;
const auto current = snapshot();
if (current.empty() || resetting || device_lost) {
return;
}
ComReference<IDirect3DSurface9> backbuffer;
D3DSURFACE_DESC description{};
if (FAILED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer.value)) ||
FAILED(backbuffer.value->GetDesc(&description))) {
return;
}
frame = {sizeof(frame), device, window, description.Width, description.Height};
for (const auto &renderer : current) {
// isolate each renderer and give it the full backbuffer in its own scene.
GraphicsState state(device);
if (!state.captured) {
continue;
}
if (FAILED(device->SetDepthStencilSurface(nullptr))) {
continue;
}
bool target_ready = true;
for (DWORD index = 1; index < state.target_count; ++index) {
if (FAILED(device->SetRenderTarget(index, nullptr))) {
target_ready = false;
}
}
const D3DVIEWPORT9 viewport{0, 0, frame.width, frame.height, 0.0f, 1.0f};
if (!target_ready || FAILED(device->SetRenderTarget(0, backbuffer.value)) ||
FAILED(device->SetViewport(&viewport)) || FAILED(device->BeginScene())) {
continue;
}
// dispatch READY to also recreate resources released before a successful reset.
if (!renderer->ready) {
renderer->started = true;
renderer->callback(SPICE_SDK_D3D9_READY, &frame, renderer->userdata);
renderer->ready = true;
}
renderer->callback(SPICE_SDK_D3D9_DRAW, &frame, renderer->userdata);
device->EndScene();
}
}
void present_complete(IDirect3DDevice9 *device, HRESULT result) {
if (dispatching) {
return;
}
std::lock_guard lock(execution_mutex);
DispatchScope scope;
if (frame.device == device) {
device_thread = GetCurrentThreadId();
if (stopped && !reset_in_progress) {
finish_shutdown();
return;
}
// a busy nonblocking present does not change the known device-loss state.
if (SUCCEEDED(result)) {
device_lost = false;
} else if (result != D3DERR_WASSTILLDRAWING) {
device_lost = true;
}
}
}
void invalidate(IDirect3DDevice9 *device) {
if (dispatching) {
return;
}
std::lock_guard lock(execution_mutex);
DispatchScope scope;
if (frame.device != device) {
return;
}
device_thread = GetCurrentThreadId();
if (stopped) {
finish_shutdown();
return;
}
// default-pool resources must be released before the native reset starts.
reset_in_progress = true;
resetting = true;
for (const auto &renderer : snapshot()) {
if (renderer->ready) {
renderer->callback(SPICE_SDK_D3D9_INVALIDATE, &frame, renderer->userdata);
}
renderer->ready = false;
}
}
void reset_complete(IDirect3DDevice9 *device, bool success) {
if (dispatching) {
return;
}
std::lock_guard lock(execution_mutex);
DispatchScope scope;
if (frame.device == device) {
device_thread = GetCurrentThreadId();
reset_in_progress = false;
resetting = !success;
device_lost = !success;
if (stopped) {
finish_shutdown();
}
}
}
void destroy(IDirect3DDevice9 *device) {
if (dispatching) {
return;
}
std::lock_guard lock(execution_mutex);
DispatchScope scope;
if (frame.device == device) {
if (stopped) {
finish_shutdown();
} else {
destroy_renderers();
}
}
}
bool shutdown(bool graphics_stopped) {
std::unique_lock lock(execution_mutex);
if (shutdown_complete) {
return true;
}
// close registration before deciding where device resources can be released.
{
std::lock_guard registry_lock(registry_mutex);
stopped = true;
}
const auto current = snapshot();
const bool resources_started = std::any_of(
current.begin(), current.end(), [](const auto &renderer) {
return renderer->started;
});
if (!resources_started || (!reset_in_progress &&
(graphics_stopped || device_thread == GetCurrentThreadId()))) {
DispatchScope scope;
finish_shutdown();
return true;
}
// a graphics boundary must drain callbacks when another thread requests shutdown.
return shutdown_condition.wait_for(lock, std::chrono::milliseconds(100), [] {
return shutdown_complete;
});
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include <d3d9.h>
#include "modules.h"
namespace sdk::d3d9 {
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
spice_sdk_d3d9_callback_func *callback, void *userdata);
void draw(HWND window, IDirect3DDevice9 *device);
void present_complete(IDirect3DDevice9 *device, HRESULT result);
void invalidate(IDirect3DDevice9 *device);
void reset_complete(IDirect3DDevice9 *device, bool success);
void destroy(IDirect3DDevice9 *device);
bool shutdown(bool graphics_stopped);
}
+107
View File
@@ -4,6 +4,7 @@
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#ifdef __cplusplus
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
@@ -237,6 +238,105 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
const char *text
);
// insert_coin (v0.3 and up)
// adds to the shared coin stock, bypassing the coin blocker like the Spice API
//
// amount: number of coins to insert (0 to 255); use 1 for a single coin
// zero succeeds without changing the stock
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)(
uint8_t amount
);
typedef struct SPICE_SDK_MODULE_INFO {
uint32_t size; // initialize to sizeof(SPICE_SDK_MODULE_INFO)
uintptr_t base; // loaded address, not the preferred PE image base
uint32_t image_size;
uint32_t timestamp;
uint32_t entry_point; // RVA, not an absolute address
uint16_t machine; // PE IMAGE_FILE_MACHINE_* value
} SPICE_SDK_MODULE_INFO;
// get_module_info (v0.4 and up)
// gets PE info about an already loaded module
//
// does not load or permanently pin the module; the caller must keep it loaded
// while using the returned base address
// returns GENERIC_ERROR if not found or unreadable, NOT_SUPPORTED for non-PE32/PE32+
//
// module_name: null-terminated UTF-16 module name or full path
// info: receives module info; initialize size to sizeof(SPICE_SDK_MODULE_INFO)
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_module_info_func)(
const wchar_t *module_name,
SPICE_SDK_MODULE_INFO *info
);
// get_plugin_directory (v0.4 and up)
// gets the directory containing a registered SDK DLL
//
// available during entry-point initialization and until destroy callbacks finish
// returns INVALID_ARGUMENT_1 for addresses outside registered SDK DLLs
//
// plugin_address: address of a function or static object in the plugin DLL
// used only to identify the DLL; the pointed-to object is not read
// address of any global variable in your DLL will work
// buffer: caller-owned UTF-16 output, or NULL to query the required size
// size: input capacity and output required wchar_t count, including the terminator
// NULL buffer or insufficient capacity returns TOO_SMALL and sets the
// required size without partial output
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_plugin_directory_func)(
const void *plugin_address,
wchar_t *buffer,
uint32_t *size
);
typedef enum SPICE_SDK_D3D9_EVENT {
SPICE_SDK_D3D9_READY = 0,
SPICE_SDK_D3D9_DRAW = 1,
SPICE_SDK_D3D9_INVALIDATE = 2,
SPICE_SDK_D3D9_DESTROY = 3,
} SPICE_SDK_D3D9_EVENT;
typedef struct SPICE_SDK_D3D9_FRAME {
uint32_t size;
void *device; // borrowed IDirect3DDevice9*, not Spice's wrapper
void *window; // HWND
uint32_t width;
uint32_t height;
} SPICE_SDK_D3D9_FRAME;
typedef void (__cdecl spice_sdk_d3d9_callback_func)(
SPICE_SDK_D3D9_EVENT event,
const SPICE_SDK_D3D9_FRAME *frame,
void *userdata
);
// register_d3d9 (v0.4 and up)
// registers a process-lifetime renderer for the primary D3D9 presentation target
//
// READY precedes drawing on a usable device and follows each successful reset
// DRAW runs on top of all Spice overlays, even when the overlay is closed; the host
// binds the backbuffer, brackets the scene, and restores graphics state
// INVALIDATE precedes reset; release default-pool resources, even if reset fails
// DESTROY releases all device references before device destruction or SDK shutdown
// callbacks are serialized; DRAW/READY run on the presentation thread; teardown
// runs at a graphics-thread boundary, or after the game stops rendering
// do not throw, block, reset/present the
// device, or shut down Spice from a callback; frame is valid only during the call
// no callbacks run after the plugin's SDK destroy callback begins
// registration from inside a render callback is not supported
// input capture and non-D3D9 backends are not provided
//
// callback: function in a registered plugin DLL; the DLL is retained until exit
// userdata: opaque plugin state passed unchanged to each callback
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_register_d3d9_func)(
spice_sdk_d3d9_callback_func *callback,
void *userdata
);
typedef struct SPICE_SDK_V0 {
uint32_t size;
@@ -262,6 +362,13 @@ typedef struct SPICE_SDK_V0 {
spice_sdk_add_toast_func *add_toast;
spice_sdk_insert_coin_func *insert_coin;
spice_sdk_get_module_info_func *get_module_info;
spice_sdk_get_plugin_directory_func *get_plugin_directory;
spice_sdk_register_d3d9_func *register_d3d9;
} SPICE_SDK_V0;
typedef void (__cdecl spice_sdk_destroy_callback_func)(
+161
View File
@@ -0,0 +1,161 @@
#include "modules.h"
#include <algorithm>
#include <array>
#include <filesystem>
#include <limits>
namespace sdk::modules {
namespace {
// RAII wrapper for a module handle
struct ModuleReference {
HMODULE handle = nullptr;
ModuleReference() = default;
ModuleReference(const ModuleReference &) = delete;
ModuleReference &operator=(const ModuleReference &) = delete;
~ModuleReference() {
if (handle) {
FreeLibrary(handle);
}
}
};
template<typename Value>
bool read_module_value(uintptr_t base, size_t offset, Value &value) {
if (offset > std::numeric_limits<uintptr_t>::max() - base ||
sizeof(Value) > std::numeric_limits<uintptr_t>::max() - (base + offset)) {
return false;
}
SIZE_T copied = 0;
return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void *>(base + offset),
&value, sizeof(value), &copied) && copied == sizeof(value);
}
}
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
if (!module_name || !module_name[0]) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!info) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
if (info->size < sizeof(SPICE_SDK_MODULE_INFO)) {
return SPICE_SDK_STATUS_TOO_SMALL;
}
// Keep the module loaded while inspecting its headers.
ModuleReference module;
if (!GetModuleHandleExW(0, module_name, &module.handle)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
// Locate the PE header through the DOS header.
const auto base = reinterpret_cast<uintptr_t>(module.handle);
IMAGE_DOS_HEADER dos{};
if (!read_module_value(base, 0, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
dos.e_lfanew < static_cast<LONG>(sizeof(dos))) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
const auto nt_offset = static_cast<size_t>(dos.e_lfanew);
DWORD signature{};
IMAGE_FILE_HEADER header{};
if (!read_module_value(base, nt_offset, signature) || signature != IMAGE_NT_SIGNATURE ||
!read_module_value(base, nt_offset + sizeof(signature), header)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
// The optional-header magic selects the PE32 or PE32+ layout.
const auto optional_offset = nt_offset + sizeof(signature) + sizeof(header);
WORD magic{};
if (header.SizeOfOptionalHeader < sizeof(magic) || !read_module_value(base, optional_offset, magic)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
SPICE_SDK_MODULE_INFO result{};
result.size = info->size;
result.base = base;
result.timestamp = header.TimeDateStamp;
result.machine = header.Machine;
if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
IMAGE_OPTIONAL_HEADER64 optional{};
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
result.image_size = optional.SizeOfImage;
result.entry_point = optional.AddressOfEntryPoint;
} else if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
IMAGE_OPTIONAL_HEADER32 optional{};
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
result.image_size = optional.SizeOfImage;
result.entry_point = optional.AddressOfEntryPoint;
} else {
return SPICE_SDK_STATUS_NOT_SUPPORTED;
}
// Reject headers or entry points outside the image.
if (optional_offset > result.image_size || header.SizeOfOptionalHeader > result.image_size - optional_offset ||
result.entry_point >= result.image_size) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
// Leave the caller's output untouched on failure.
*info = result;
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> &registered_modules,
const void *plugin_address, wchar_t *buffer, uint32_t *size) {
if (!plugin_address) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!size) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_3;
}
// Resolve the address and require a registered plugin.
ModuleReference module;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
reinterpret_cast<LPCWSTR>(plugin_address), &module.handle) ||
std::none_of(registered_modules.begin(), registered_modules.end(), [&](const SdkModule &entry) {
return entry.module == module.handle;
})) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
try {
// Reject truncated paths before extracting the directory.
std::array<wchar_t, 32768> filename{};
const auto length = GetModuleFileNameW(module.handle, filename.data(), static_cast<DWORD>(filename.size()));
if (!length || length >= filename.size()) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
const auto path = std::filesystem::path(filename.data()).parent_path();
// Report the full UTF-16 capacity, including the terminator.
const auto &text = path.native();
const auto required = static_cast<uint32_t>(text.size() + 1);
const auto capacity = *size;
*size = required;
if (!buffer || capacity < required) {
return SPICE_SDK_STATUS_TOO_SMALL;
}
// Copy only when the entire path fits.
std::copy_n(text.c_str(), required, buffer);
return SPICE_SDK_STATUS_SUCCESS;
} catch (const std::exception &) {
return SPICE_SDK_STATUS_GENERIC_ERROR;
}
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <string>
#include <vector>
#include <windows.h>
#include "sdk/include/spicesdk.h"
namespace sdk {
struct SdkModule {
std::string dll;
HINSTANCE module;
};
namespace modules {
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info);
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> &registered_modules,
const void *plugin_address, wchar_t *buffer, uint32_t *size);
}
}
+89 -2
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;
@@ -76,9 +129,43 @@ 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()) {
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);
if (coin_pressed && !coin_previous_state[amount] && spice.insert_coin) {
const auto status = spice.insert_coin(amount);
if (status != SPICE_SDK_STATUS_SUCCESS) {
LOG_INFO(std::format(
"coin insertion failed: {}",
static_cast<int>(status)).c_str());
} else {
const auto message = std::format(
"v0_cpp: inserted {} coin{}", amount, amount == 1 ? "" : "s");
LOG_INFO(message.c_str());
if (spice.add_toast) {
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, message.c_str());
}
}
}
coin_previous_state[amount] = coin_pressed;
}
// check for ctrl + arrow keys and trigger p1 pad arrows
for (auto& arrow : arrow_buttons) {
// check for ctrl + arrow keys and trigger p1 pad arrows
if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) &&
((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) {
spice.set_button(arrow.button, true, 1.f);
@@ -88,7 +175,7 @@ static void worker_thread_main(std::stop_token stop_token) {
if (spice.add_toast) {
spice.add_toast(
SPICE_SDK_TOAST_LEVEL_INFO,
std::format("let me hear you say: {}", arrow.name).c_str());
std::format("v0_cpp: let me hear you say: {}", arrow.name).c_str());
} else {
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
}
@@ -0,0 +1,218 @@
#include "v0_cpp_imgui.h"
#include <atomic>
#include <chrono>
#include <cstdlib>
#include <cfloat>
#include <windows.h>
#include "imgui.h"
#include "backends/imgui_impl_dx9.h"
namespace sample_imgui {
namespace {
struct SampleRenderer {
ImGuiContext *context = nullptr;
bool backend_initialized = false;
bool was_visible = false;
bool dummy_enabled = true;
float dummy_value = 0.5f;
int32_t dummy_mode = 0;
uint32_t click_count = 0;
std::chrono::steady_clock::time_point previous_frame;
};
SampleRenderer renderer;
std::atomic_bool window_visible = false;
void update_mouse(const SPICE_SDK_D3D9_FRAME &frame) {
auto &io = ImGui::GetIO();
const auto window = static_cast<HWND>(frame.window);
const bool focused = window &&
GetAncestor(GetForegroundWindow(), GA_ROOT) == GetAncestor(window, GA_ROOT);
io.AddFocusEvent(focused);
POINT position{};
RECT client{};
// can window receive mouse input?
const bool mouse_available =
focused &&
GetCursorPos(&position) &&
ScreenToClient(window, &position) &&
GetClientRect(window, &client) &&
client.right > 0 &&
client.bottom > 0;
// add mouse position
if (mouse_available) {
io.AddMousePosEvent(
static_cast<float>(position.x) * frame.width / client.right,
static_cast<float>(position.y) * frame.height / client.bottom);
} else {
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
}
// add click to imgui
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
io.AddMouseButtonEvent(0, mouse_available &&
((GetAsyncKeyState(swapped ? VK_RBUTTON : VK_LBUTTON) & 0x8000) != 0));
io.AddMouseButtonEvent(1, mouse_available &&
((GetAsyncKeyState(swapped ? VK_LBUTTON : VK_RBUTTON) & 0x8000) != 0));
}
void draw_window(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
auto &io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
ImGuiCond_Once, ImVec2(0.5f, 0.5f));
ImGui::SetNextWindowBgAlpha(0.8f);
if (ImGui::Begin("SDK sample", nullptr,
ImGuiWindowFlags_AlwaysAutoResize |
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse)) {
ImGui::Text("%u x %u", frame.width, frame.height);
ImGui::Text("%.1f FPS", io.Framerate);
ImGui::Separator();
ImGui::Checkbox("Enable dummy widgets", &state.dummy_enabled);
ImGui::BeginDisabled(!state.dummy_enabled);
{
ImGui::SliderFloat("Value", &state.dummy_value, 0.0f, 1.0f, "%.2f",
ImGuiSliderFlags_NoInput);
ImGui::Combo("Mode", &state.dummy_mode, "Off\0Low\0High\0");
if (ImGui::Button("Increment")) {
++state.click_count;
}
ImGui::SameLine();
if (ImGui::Button("Reset")) {
state.click_count = 0;
state.dummy_value = 0.5f;
state.dummy_mode = 0;
}
ImGui::Text("Count: %u", state.click_count);
}
ImGui::EndDisabled();
}
ImGui::End();
}
void draw_frame(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
if (!state.backend_initialized) {
return;
}
const auto now = std::chrono::steady_clock::now();
const float elapsed = std::chrono::duration<float>(now - state.previous_frame).count();
state.previous_frame = now;
const bool visible = window_visible.load(std::memory_order_relaxed);
if (!visible && !state.was_visible) {
return;
}
state.was_visible = visible;
// update imgui state
auto &io = ImGui::GetIO();
io.DisplaySize = ImVec2(
static_cast<float>(frame.width), static_cast<float>(frame.height));
io.DeltaTime = elapsed > 0.0f ? elapsed : 1.0f / 60.0f;
// update mouse i/o
if (visible) {
update_mouse(frame);
} else {
io.ClearEventsQueue();
io.ClearInputMouse();
io.AddFocusEvent(false);
}
ImGui_ImplDX9_NewFrame();
ImGui::NewFrame();
if (!visible) {
ImGui::SetWindowFocus(nullptr);
ImGui::EndFrame();
return;
}
draw_window(state, frame);
ImGui::Render();
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
}
void __cdecl render_callback(
SPICE_SDK_D3D9_EVENT event,
const SPICE_SDK_D3D9_FRAME *frame,
void *userdata) {
auto &state = *static_cast<SampleRenderer *>(userdata);
auto *previous_context = ImGui::GetCurrentContext();
// init imgui context
if (event == SPICE_SDK_D3D9_READY && !state.context) {
ImGui::SetAllocatorFunctions(
[](size_t size, void *) -> void * { return std::malloc(size); },
[](void *memory, void *) { std::free(memory); });
IMGUI_CHECKVERSION();
state.context = ImGui::CreateContext();
ImGui::SetCurrentContext(state.context);
auto &io = ImGui::GetIO();
io.IniFilename = nullptr;
io.LogFilename = nullptr;
io.MouseDrawCursor = true;
ImGui::StyleColorsDark();
state.backend_initialized = ImGui_ImplDX9_Init(
static_cast<IDirect3DDevice9 *>(frame->device));
}
if (state.context) {
ImGui::SetCurrentContext(state.context);
switch (event) {
case SPICE_SDK_D3D9_READY:
state.previous_frame = std::chrono::steady_clock::now();
if (state.backend_initialized) {
ImGui_ImplDX9_CreateDeviceObjects();
}
break;
case SPICE_SDK_D3D9_DRAW:
draw_frame(state, *frame);
break;
case SPICE_SDK_D3D9_INVALIDATE:
if (state.backend_initialized) {
ImGui_ImplDX9_InvalidateDeviceObjects();
}
break;
case SPICE_SDK_D3D9_DESTROY:
if (state.backend_initialized) {
ImGui_ImplDX9_Shutdown();
}
ImGui::DestroyContext(state.context);
state.context = nullptr;
state.backend_initialized = false;
state.was_visible = false;
break;
}
}
ImGui::SetCurrentContext(previous_context);
}
}
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice) {
if (!spice.register_d3d9) {
return SPICE_SDK_STATUS_NOT_SUPPORTED;
}
return spice.register_d3d9(render_callback, &renderer);
}
void toggle() {
const bool visible = !window_visible.load(std::memory_order_relaxed);
window_visible.store(visible, std::memory_order_relaxed);
}
}
@@ -0,0 +1,10 @@
#pragma once
#include "sdk/include/spicesdk.h"
namespace sample_imgui {
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice);
void toggle();
}
+82 -8
View File
@@ -1,8 +1,11 @@
#include <vector>
#include <mutex>
#include <shared_mutex>
#include <algorithm>
#include "sdk.h"
#include "modules.h"
#include "d3d9.h"
#include "avs/game.h"
#include "games/io.h"
#include "launcher/launcher.h"
@@ -31,11 +34,10 @@ static spice_sdk_clear_touch_func sdk_clear_touch;
static spice_sdk_insert_card_func sdk_insert_card;
static spice_sdk_set_keypad_func sdk_set_keypad;
static spice_sdk_add_toast_func sdk_add_toast;
struct SdkModule {
std::string dll;
HINSTANCE module;
};
static spice_sdk_insert_coin_func sdk_insert_coin;
static spice_sdk_get_module_info_func sdk_get_module_info;
static spice_sdk_get_plugin_directory_func sdk_get_plugin_directory;
static spice_sdk_register_d3d9_func sdk_register_d3d9;
// DLLs
static int sdk_modules_count = 0;
@@ -45,6 +47,7 @@ static std::shared_mutex sdk_global_mutex;
// internal
static bool sdk_initialized = false;
static bool sdk_shutting_down = false;
static bool sdk_finalizing = false;
static std::vector<Button> *buttons;
static std::vector<Analog> *analogs;
static std::vector<Light> *lights;
@@ -90,14 +93,22 @@ void init_sdk_modules() {
}
}
void fini_sdk_modules() {
void fini_sdk_modules(bool graphics_stopped) {
// prevent multiple calls and further calls into sdk_init
{
std::unique_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
if (!sdk_initialized || sdk_finalizing) {
return;
}
sdk_shutting_down = true;
sdk_finalizing = true;
}
if (!d3d9::shutdown(graphics_stopped)) {
std::unique_lock lock(sdk_global_mutex);
sdk_finalizing = false;
log_warning("sdk", "deferring plugin teardown: waiting for D3D9 rendering to stop");
return;
}
// call into destroy callback of each DLL
@@ -175,8 +186,24 @@ sdk_init(
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
v0->add_toast = sdk_add_toast;
}
// end of 0.2
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, insert_coin)) {
v0->insert_coin = sdk_insert_coin;
}
// end of 0.3
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_module_info)) {
v0->get_module_info = sdk_get_module_info;
}
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_plugin_directory)) {
v0->get_plugin_directory = sdk_get_plugin_directory;
}
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, register_d3d9)) {
v0->register_d3d9 = sdk_register_d3d9;
}
// end of 0.4
// any newer minor iterations will need to check the size
{
@@ -189,6 +216,36 @@ sdk_init(
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
return modules::get_module_info(module_name, info);
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_plugin_directory(const void *plugin_address, wchar_t *buffer, uint32_t *size) {
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
return modules::get_plugin_directory(sdk_modules_list, plugin_address, buffer, size);
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_register_d3d9(spice_sdk_d3d9_callback_func *callback, void *userdata) {
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized || sdk_shutting_down) {
return SPICE_SDK_STATUS_TOO_LATE;
}
return d3d9::register_d3d9(sdk_modules_list, callback, userdata);
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_log(
@@ -219,6 +276,7 @@ sdk_log(
log_warning(facility_str.c_str(), "{}", message);
break;
case SPICE_SDK_LOG_LEVEL_FATAL:
lock.unlock();
log_fatal(facility_str.c_str(), "{}", message);
break;
default:
@@ -640,5 +698,21 @@ sdk_add_toast(
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_insert_coin(
uint8_t amount
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (amount > 0) {
eamuse_coin_add(amount);
}
return SPICE_SDK_STATUS_SUCCESS;
}
} // namespace sdk
+1 -1
View File
@@ -7,6 +7,6 @@ namespace sdk {
void register_sdk_hooks(std::string dll, HINSTANCE module);
void init_sdk_modules();
void fini_sdk_modules();
void fini_sdk_modules(bool graphics_stopped = false);
}
+2 -1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <windows.h>
#include <winternl.h>
#define LDR_DLL_NOTIFICATION_REASON_LOADED 1
@@ -43,4 +44,4 @@ NTSTATUS NTAPI LdrRegisterDllNotification(
NTSTATUS NTAPI LdrUnregisterDllNotification(
PVOID Cookie
);
);