Compare commits

...

3 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
25 changed files with 3591 additions and 88 deletions
+12 -1
View File
@@ -617,6 +617,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/nicspoof.cpp
hooks/nicspoof_tunnel.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
@@ -718,6 +719,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/crypt.cpp
# sdk
sdk/d3d9.cpp
sdk/modules.cpp
sdk/sdk.cpp
# stubs
@@ -1046,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")
@@ -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,6 +7,7 @@
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "sdk/d3d9.h"
#include "util/logging.h"
#include "d3d9_device.h"
@@ -558,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
@@ -574,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();
+131 -32
View File
@@ -14,26 +14,32 @@
#include "util/detour.h"
#include "util/logging.h"
#include "hooks/nicspoof_tunnel.h"
namespace {
constexpr uint32_t k_local_ip = 0x0A390302u; /* 10.57.3.2 */
constexpr uint32_t k_gateway = 0x0A390301u; /* 10.57.3.1 */
constexpr uint32_t k_dns = 0x0A390301u; /* 10.57.3.1 */
constexpr uint32_t k_dhcp = 0x0A390301u; /* 10.57.3.1 */
constexpr uint32_t k_mask = 0xFFFFFF00u; /* 255.255.255.0 */
constexpr uint32_t k_subnet = 0x0A390300u;
constexpr int k_prefix_len = 24;
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[] = "N1CSP00F";
constexpr char k_domain[] = "local";
constexpr char k_hostname_fqdn[] = "N1CSP00F.local";
constexpr wchar_t k_domain_w[] = L"local";
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)))
@@ -125,7 +131,7 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) {
memcpy(p->Address, k_fake_mac, 6);
p->Type = MIB_IF_TYPE_ETHERNET;
p->DhcpEnabled = 1;
ip_to_str(k_local_ip, ip, sizeof(ip));
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);
@@ -151,8 +157,11 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) {
}
DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) {
ULONG need = static_cast<ULONG>(sizeof(FIXED_INFO));
char dns[16];
/* 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;
@@ -164,18 +173,27 @@ DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) {
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_dns, dns, sizeof(dns));
strcpy(p->DnsServerList.IpAddress.String, dns);
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;
p->DnsServerList.Next = nullptr;
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={}",
k_hostname, k_domain, dns);
log_misc("network",
"NIC spoof GetNetworkParams host={} domain={} dns={} dns2={}",
k_hostname, k_domain, dns1, dns2);
g_log_params = false;
}
return ERROR_SUCCESS;
@@ -197,19 +215,21 @@ ULONG WINAPI GetAdaptersAddresses_hook(
sizeof(void *) * 8 +
sizeof(IP_ADAPTER_UNICAST_ADDRESS) +
sizeof(IP_ADAPTER_PREFIX) +
sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) +
sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) * 2 +
sizeof(IP_ADAPTER_GATEWAY_ADDRESS) +
sizeof(sockaddr_in) * 4 + 64);
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;
@@ -267,7 +287,7 @@ ULONG WINAPI GetAdaptersAddresses_hook(
blob += sizeof(*sa);
memset(sa, 0, sizeof(*sa));
sa->sin_family = AF_INET;
sa->sin_addr.s_addr = htonl(k_local_ip);
sa->sin_addr.s_addr = htonl(g_local_ip);
u->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sa);
u->Address.iSockaddrLength = sizeof(*sa);
a->FirstUnicastAddress = u;
@@ -282,7 +302,7 @@ ULONG WINAPI GetAdaptersAddresses_hook(
blob += sizeof(*sm);
memset(sm, 0, sizeof(*sm));
sm->sin_family = AF_INET;
sm->sin_addr.s_addr = htonl(k_subnet);
sm->sin_addr.s_addr = htonl(g_subnet);
pref->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sm);
pref->Address.iSockaddrLength = sizeof(*sm);
a->FirstPrefix = pref;
@@ -309,10 +329,23 @@ ULONG WINAPI GetAdaptersAddresses_hook(
blob += sizeof(*ds);
memset(ds, 0, sizeof(*ds));
ds->sin_family = AF_INET;
ds->sin_addr.s_addr = htonl(k_dns);
ds->sin_addr.s_addr = htonl(k_dns1);
dns->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds);
dns->Address.iSockaddrLength = sizeof(*ds);
dns->Next = nullptr;
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;
@@ -404,7 +437,7 @@ INT WSAAPI getaddrinfo_hook(
if (!pNodeName || !pNodeName[0] || name_is_local(pNodeName)) {
if (pNodeName && (_stricmp(pNodeName, k_hostname) == 0 ||
_stricmp(pNodeName, k_hostname_fqdn) == 0)) {
ip = k_local_ip;
ip = g_local_ip;
} else {
ip = 0x7F000001u;
}
@@ -412,7 +445,7 @@ INT WSAAPI getaddrinfo_hook(
ip = parse_ipv4(pNodeName);
} else {
/* offline=1: map external names to local_ip */
ip = k_local_ip;
ip = g_local_ip;
}
if (pServiceName && pServiceName[0]) {
@@ -471,7 +504,7 @@ void install_nicspoof_hooks() {
char ipstr[16];
char maskstr[16];
char gwstr[16];
ip_to_str(k_local_ip, ipstr, sizeof(ipstr));
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",
@@ -508,7 +541,6 @@ void install_nicspoof_hooks() {
(void *) freeaddrinfo_hook,
(void **) &freeaddrinfo_orig);
/* FreeAddrInfoA is often the same address; only hook if distinct. */
{
HMODULE ws = GetModuleHandleA("ws2_32.dll");
void *faa = nullptr;
@@ -539,8 +571,75 @@ void install_nicspoof_hooks() {
}
}
} // namespace
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() {
install_nicspoof_hooks();
nicspoof_detail::init_impl();
}
+25
View File
@@ -1,3 +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);
+49 -10
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <shlwapi.h>
#include <windows.h>
@@ -258,7 +259,7 @@ int main_implementation(int argc, char *argv[]) {
bool load_stubs = false;
bool netfix_disable = false;
bool icmphook_enable = false;
bool nicspoof_enable = false;
NicSpoofConfig nicspoof_cfg;
bool lang_disable = false;
std::string process_priority_str = "high";
bool cardio_enabled = false;
@@ -801,8 +802,43 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::EnableICMPHook].value_bool()) {
icmphook_enable = true;
}
if (options[launcher::Options::EnableNICSpoof].value_bool()) {
nicspoof_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;
@@ -2558,14 +2594,17 @@ 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)
if (icmphook_enable) {
icmphook_net_init();
// 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();
}
// NIC spoof (opt-in; fake Ethernet with no real NIC)
if (nicspoof_enable) {
nicspoof_init();
// ICMP emulation (opt-in; after tunnel so hooks do not collide)
if (icmphook_enable) {
icmphook_net_init();
}
// net fix
@@ -2869,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();
+50 -5
View File
@@ -3188,12 +3188,57 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = std::invoke([]()
.category = "Network Dev",
};
c[launcher::Options::EnableNICSpoof] = {
.title = "Enable NIC Spoof",
.title = "NIC Spoof",
.name = "nicspoof",
.desc = "Report a fake Ethernet IPv4 adapter (10.57.3.2) so the game can "
"boot when no real NIC is present. DNS names are mapped to that "
"address. Does not create a real adapter.",
.type = OptionType::Bool,
.desc =
"Use with -icmphook.\n\n"
"offline: report a fake Ethernet IPv4 adapter and map DNS names to "
"that address so the game can boot when no real NIC is present. "
"Does not create a real adapter.\n\n"
"tunnelhost: same fake NIC, plus listen for a matching tunnel "
"from a peer.\n\n"
"tunnelclient: same fake NIC, plus connect to a tunnel host. "
"Set -nicspoofhostrealip.",
.type = OptionType::Enum,
.category = "Network Dev",
.elements = {
{"offline", ""},
{"tunnelhost", ""},
{"tunnelclient", ""},
},
};
c[launcher::Options::NICSpoofIP] = {
.title = "NIC Spoof IP",
.name = "nicspoofip",
.desc =
"Overlay IPv4 for the fake adapter. Used when NIC Spoof is not Default. "
"If unset, 10.100.100.10 is used.\n\n"
"Must be on the same subnet as gateway 10.100.100.1 with mask "
"255.0.0.0 (10.0.0.0/8). Examples: 10.100.100.10, 10.0.0.10. "
"Do not use 10.0.0.0, 10.255.255.255, the gateway 10.100.100.1, "
"or DNS 10.2.1.10 / 10.2.1.30. Host and client must use different IPs.",
.type = OptionType::Text,
.setting_name = "10.100.100.10",
.category = "Network Dev",
};
c[launcher::Options::NICSpoofHostRealIP] = {
.title = "NIC Spoof Tunnel Host",
.name = "nicspoofhostrealip",
.desc =
"Real IP address or hostname of the tunnel host. Required for "
"tunnel client mode. Not used for offline or tunnel host.",
.type = OptionType::Text,
.setting_name = "192.168.1.10",
.category = "Network Dev",
};
c[launcher::Options::NICSpoofPort] = {
.title = "NIC Spoof Tunnel Port",
.name = "nicspoofport",
.desc =
"UDP port for the matching tunnel. Host listens on this port; "
"client connects to it. If unset, 51820 is used.",
.type = OptionType::Integer,
.setting_name = "51820",
.category = "Network Dev",
};
c[launcher::Options::AutoElevate] = {
+3
View File
@@ -317,6 +317,9 @@ namespace launcher {
DisableHighResTimer,
EnableICMPHook,
EnableNICSpoof,
NICSpoofIP,
NICSpoofHostRealIP,
NICSpoofPort,
AutoElevate,
CfgForceSoftwareRender,
OBSWebSocketEnabled,
+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);
}
+95
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
@@ -247,6 +248,95 @@ 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;
@@ -274,6 +364,11 @@ typedef struct SPICE_SDK_V0 {
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);
}
}
+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);
@@ -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();
}
+58 -7
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"
@@ -32,11 +35,9 @@ 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;
static spice_sdk_insert_coin_func sdk_insert_coin;
struct SdkModule {
std::string dll;
HINSTANCE module;
};
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;
@@ -46,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;
@@ -91,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
@@ -183,6 +193,17 @@ sdk_init(
}
// 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
{
@@ -195,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(
+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);
}