Compare commits

..

11 Commits

Author SHA1 Message Date
bicarus 7aba7da3b6 sdk: get/set coin blocker (#918) 2026-09-22 21:42:34 -07:00
aixxe 197d76755b iidxcamhook: detect game camera count during init (#917)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #916

## Description of change
- Read the camera manager actual vector length instead of assuming 2
cameras. Front camera is gone in IIDX 34. :(
- Resolve texture-registry entries through AFP texture handles instead
of assuming slots 0 and 2. Generally fix up the internal game structure
layouts, as I made some incorrect assumptions about them last time.
- Replace the existing D3D-device offset pattern, as it was resolving to
an incorrect offset in IIDX 34.

## Testing
- Booted IIDX 27-34 to gameplay with camera on, though with only one
attached.
- Batch scanned all known game DLLs to ensure the pattern was resolving
correctly.

Co-authored-by: Codex <codex@openai.com>
2026-09-17 14:19:03 -07:00
Jiongjia Lu 2b6e76730b Dance around early support (#897)
## Link to GitHub Issue or related Pull Request, if one exists
N/A

## Description of change
This PR add initial supports for running Dance aRound in window mode,
with keyboard, cardio supports.

## Testing
- Tested game play under Windows 10 using Kinect V1 with my visionpose
compatible layer (https://github.com/Avimitin/dance-around-anygear)


https://github.com/user-attachments/assets/4b821dcf-751d-4953-b626-4a9491569183

- Tested full game play, user profile saving with Asphyxia

Assisted by AI.

---------

Co-authored-by: Codex <codex@openai.com>
2026-09-12 09:51:23 -07:00
Jiongjia Lu 4042b77065 danevo: retry card UID reads (#914)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change

Dance Evolution requires the same FeliCa UID to be returned repeatedly
before accepting a card. Add KDM to the existing retry logic so physical
cards register without needing to be held unusually long.

## Testing
- Validated by compiling spice2x successfully
- Tested with latest action compiled spice 32bit executable, card insert
work instantly
2026-09-12 09:50:36 -07:00
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
50 changed files with 6493 additions and 929 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 }}
+17 -1
View File
@@ -554,6 +554,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/pc/pc.cpp
games/pc/io.cpp
games/pc/bi2x_hook.cpp
games/udn/udn.cpp
games/udn/io.cpp
games/udn/bi2x_hook.cpp
# hooks
hooks/audio/acm.cpp
@@ -605,6 +608,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/backends/d3d9/d3d9_texture.cpp
hooks/graphics/backends/d3d11/d3d11_backend.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain_udn.cpp
hooks/graphics/backends/d3d11/d3d11_factory.cpp
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
@@ -616,6 +620,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 +723,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/crypt.cpp
# sdk
sdk/d3d9.cpp
sdk/modules.cpp
sdk/sdk.cpp
# stubs
@@ -1045,8 +1053,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")
+2 -2
View File
@@ -118,11 +118,11 @@ static inline void update_card(int unit_id) {
const bool card_presented = eamuse_card_insert_consume(icca_get_active_count(), unit_id);
const bool key_pressed = (kb_insert_press && !kb_insert_press_old[unit_id]);
// beatstream and nostalgia have logic that requires ac_io_icca_get_uid_felica to return the
// beatstream, nostalgia and Dance Evolution have logic that requires ac_io_icca_get_uid_felica to return the
// exact same card number multiple times in a row in order for the card number to be read...
// for whatever reason setting this to 3-4 doesn't work the very first time the game boots up
// so we use 10 just to be safe
const bool need_felica_retries = avs::game::is_model({"NBT", "PAN"});
const bool need_felica_retries = avs::game::is_model({"KDM", "NBT", "PAN"});
if (need_felica_retries && (card_presented || key_pressed)) {
unit->felica_retries = 10;
}
+7
View File
@@ -573,6 +573,13 @@ static inline void easrv_init_messages() {
"\x5b\x99\xa1\x15\x78\x16\x84\xe0\x67\x2c\x75\xf7\xc0\x38\xd5\x20\xf7\x81\xae\x1f\x77\x02\xdf\x0c"
"\x60\x68\x2e\x26\xe5\xbb\xb7\xb4\x41\x72\xc4\xa4\x43\x2a\xc8\x81\xd3\x8a\x1f\xe8\x94\x39\x7e\xf6"
"\x69\x56\x9e\xaf\x06\x10\xfd\x47\x69\xb8\x14\xe6\x48\x36\xd6\x25\x18\x0c\x2b\x3d\x69");
if (avs::game::is_model("UDN")) {
// Only UDN needs an ASCII location/name for the Unity score-screen marshaler.
// These 12 encoded bytes replace the name with "SPICE2 LOCAL";
// its terminator, the other fields and the response length stay unchanged.
EA_FACILITY_GET.replace(311, 12,
"\xf2\x20\xcc\x9a\xe3\x32\x78\x80\x9a\xba\xcd\xc3");
}
EA_PCBEVENT_PUT = std::string(
"\x17\x7e\x7c\x0e\x1a\x15\x41\x0b\xae\xd5\xa0\xf5\x2c\xa1\xb9\xd2\x53\xca\xd2\x0d\x81\x34\xdb\x94\x79\xd0"
"\xc8\x9d\xb5\xfb\xef\x97");
+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;
+38 -25
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;
}
}
@@ -200,11 +199,11 @@ namespace games::iidx {
addr_camera_manager = (addr_camera_manager_ptr - (uint8_t*) iidx_module) + disp_camera_manager + 7;
// addr_device_offset
search_from = (addr_hook_a_ptr - (uint8_t*) iidx_module);
search_from = addr_hook_a;
uint8_t *addr_device_ptr = reinterpret_cast<uint8_t *>(find_pattern_from(
iidx_module,
"488B89",
"XXX",
"488B8F00000000488B014C8B870000000033D2FF9028010000",
"XXX????XXXXXX????XXXXXXXX",
3, 0, search_from));
if (addr_device_ptr == nullptr) {
@@ -212,7 +211,7 @@ namespace games::iidx {
return FALSE;
}
addr_device_offset = *addr_device_ptr;
addr_device_offset = *reinterpret_cast<uint32_t*>(addr_device_ptr);
// --- addr_afp_texture_offset ---
uint8_t *addr_afp_texture_ptr = reinterpret_cast<uint8_t *>(find_pattern(
@@ -234,13 +233,27 @@ namespace games::iidx {
static void **__fastcall camera_hook_a(PBYTE a1) {
std::call_once(hook_a_init, [&]{
device = *reinterpret_cast<LPDIRECT3DDEVICE9EX*>(a1 + addr_device_offset);
auto const preview = *reinterpret_cast<LPDIRECT3DTEXTURE9**>((uint8_t*)iidx_module + addr_textures);
auto const registry = reinterpret_cast<Camera::TextureRegistry*>(
reinterpret_cast<uint8_t*>(iidx_module) + addr_textures - offsetof(Camera::TextureRegistry, begin));
auto const manager = reinterpret_cast<Camera::CCameraManager2*>((uint8_t*)iidx_module + addr_camera_manager);
camera_texture_a = manager->cameras->a->d3d9_texture(addr_afp_texture_offset);
camera_texture_b = manager->cameras->b->d3d9_texture(addr_afp_texture_offset);
preview_texture_a = preview;
preview_texture_b = preview + 2;
auto const camera_count = manager->begin ? manager->end - manager->begin : 0;
auto const bind_camera = [&](size_t index, LPDIRECT3DTEXTURE9*& texture, LPDIRECT3DTEXTURE9*& preview) {
if (index >= static_cast<size_t>(camera_count)) {
return; // IIDX 34 only has the TOP camera.
}
auto const camera = manager->begin[index];
auto const afp = camera ? camera->afp_texture(addr_afp_texture_offset) : nullptr;
if (!afp || !registry->begin || afp->handle < registry->handle_base ||
afp->handle - registry->handle_base >= registry->end - registry->begin) {
log_warning("iidx:camhook", "invalid texture destination for camera {}", index);
return;
}
texture = &afp->texture;
preview = registry->begin + (afp->handle - registry->handle_base);
};
bind_camera(0, camera_texture_a, preview_texture_a);
bind_camera(1, camera_texture_b, preview_texture_b);
init_local_camera();
});
@@ -382,10 +395,10 @@ namespace games::iidx {
log_misc("iidx:camhook", "Adding user-preferred cameras");
for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
auto pActivate = ppDevices[i];
if ((int)i == preferred_top_index && top_camera == nullptr) {
if ((int)i == preferred_top_index && top_camera == nullptr && camera_texture_a != nullptr) {
log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id);
top_camera = add_top_camera(pActivate);
} else if ((int)i == preferred_front_index && front_camera == nullptr) {
} else if ((int)i == preferred_front_index && front_camera == nullptr && camera_texture_b != nullptr) {
log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id);
front_camera = add_front_camera(pActivate);
}
@@ -403,16 +416,16 @@ namespace games::iidx {
if (!FLIP_CAMS) {
// top camera first, then front
if (top_camera == nullptr) {
if (top_camera == nullptr && camera_texture_a != nullptr) {
top_camera = add_top_camera(pActivate);
} else if (front_camera == nullptr) {
} else if (front_camera == nullptr && camera_texture_b != nullptr) {
front_camera = add_front_camera(pActivate);
}
} else {
// front first, then top
if (front_camera == nullptr) {
if (front_camera == nullptr && camera_texture_b != nullptr) {
front_camera = add_front_camera(pActivate);
} else if (top_camera == nullptr) {
} else if (top_camera == nullptr && camera_texture_a != nullptr) {
top_camera = add_top_camera(pActivate);
}
}
+19 -9
View File
@@ -70,21 +70,31 @@ namespace games::iidx {
class IIDXCameraSourceReaderCallback;
namespace Camera {
struct AfpTexture {
void* vftbl;
IDirect3DTexture9* texture;
uint32_t handle;
};
struct TextureRegistry {
uint32_t handle_base;
uint32_t next_slot;
IDirect3DTexture9** begin;
IDirect3DTexture9** end;
IDirect3DTexture9** capacity_end;
};
struct PlayVideoCamera {
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
auto const afp_texture = *reinterpret_cast<uint8_t**>
(reinterpret_cast<uint8_t*>(this) + offset);
return reinterpret_cast<IDirect3DTexture9**>(afp_texture + 0x8);
AfpTexture* afp_texture(const uintptr_t offset) {
return *reinterpret_cast<AfpTexture**>(reinterpret_cast<uint8_t*>(this) + offset);
}
};
struct CCameraManager2 {
using camera_pointers = struct {
PlayVideoCamera* a;
PlayVideoCamera* b;
};
void* vftbl;
camera_pointers* cameras;
PlayVideoCamera** begin;
PlayVideoCamera** end;
PlayVideoCamera** capacity_end;
};
}
+9
View File
@@ -37,6 +37,7 @@
#include "qks/io.h"
#include "mfg/io.h"
#include "pc/io.h"
#include "udn/io.h"
namespace games {
@@ -332,6 +333,14 @@ namespace games {
analogs.insert({ pc, pc::get_analogs() });
lights.insert({ pc, pc::get_lights() });
file_hints[pc].push_back({"game/svm.exe"});
// DANCE aROUND
const std::string udn("DANCE aROUND");
games.push_back(udn);
buttons.insert({ udn, udn::get_buttons() });
buttons_help.insert({ udn, udn::get_buttons_help() });
lights.insert({ udn, udn::get_lights() });
file_hints[udn].push_back({"game/dancearound_Data"});
}
const std::vector<std::string> &get_games() {
+686
View File
@@ -0,0 +1,686 @@
#include "bi2x_hook.h"
#if SPICE64
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include "acio/icca/icca.h"
#include "games/io.h"
#include "io.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/execexe.h"
#include "util/logging.h"
#include "util/tapeled.h"
#include "util/utils.h"
namespace games::udn {
struct AIO_SCI_COMM {
uint8_t data[0xf8];
};
struct AIO_NMGR_IOB2 {
uint8_t data[0xe30];
};
struct AIO_IOB2_BI2X_UDN {
uint8_t data[0x3b00];
};
struct AIO_IOB2_BI2X_WRFIRM {
uint8_t data[0x20f48];
};
struct AIO_IOB_ICCA;
#pragma pack(push, 1)
struct AIO_IOB2_BI2X_UDN__INPUT {
uint8_t DevIoCounter;
uint8_t bExIoAErr;
uint8_t bExIoBErr;
uint8_t bPcPowerOn;
uint8_t bPcPowerCheck;
uint8_t CoinCount;
uint8_t bTest;
uint8_t bService;
uint8_t bCoinSw;
uint8_t bCoinJam;
uint8_t bStart;
uint8_t bUp;
uint8_t bDown;
uint8_t bLeft;
uint8_t bRight;
};
struct AIO_IOB2_BI2X_UDN__DEVSTATUS {
uint8_t InputCounter;
uint8_t OutputCounter;
uint8_t TapeLedCounter;
uint8_t TapeLedRate[8];
AIO_IOB2_BI2X_UDN__INPUT Input;
uint8_t InputData[241];
uint8_t OutputData[48];
uint8_t IoResetData[4];
uint8_t PlyrInHist[18];
};
// Fixed card-status wire format used by the compatibility interface.
// The payload is exactly 58 bytes.
// Field widths remain explicit to keep the boundary stable.
struct AIO_IOB_ICCA__CARDID {
uint8_t CardType;
uint8_t cbCardId;
uint8_t CardId[10];
};
struct AIO_IOB_ICCA__INPUT {
uint8_t cuStatus;
uint8_t cuError;
AIO_IOB_ICCA__CARDID CardId;
uint8_t KeyNo;
uint8_t KeyCount;
uint8_t bKeyDataValid;
uint8_t Key2No;
uint8_t Key2Count;
uint8_t bKey2DataValid;
uint8_t bKey0;
uint8_t bKey1;
uint8_t bKey4;
uint8_t bKey7;
uint8_t bKey00;
uint8_t bKey2;
uint8_t bKey5;
uint8_t bKey8;
uint8_t bKeyBK;
uint8_t bKey3;
uint8_t bKey6;
uint8_t bKey9;
uint8_t CN1_1;
uint8_t CN1_2;
uint8_t CN1_3;
uint8_t CN1_4;
};
struct AIO_IOB_ICCA__DEVSTATUS {
uint8_t InputCounter;
uint8_t OutputCounter;
AIO_IOB_ICCA__INPUT Input;
uint8_t InputData[16];
uint8_t OutputData[4];
};
#pragma pack(pop)
static_assert(sizeof(AIO_SCI_COMM) == 0xf8);
static_assert(sizeof(AIO_NMGR_IOB2) == 0xe30);
static_assert(sizeof(AIO_IOB2_BI2X_UDN) == 0x3b00);
static_assert(sizeof(AIO_IOB2_BI2X_WRFIRM) == 0x20f48);
static_assert(sizeof(AIO_IOB2_BI2X_UDN__INPUT) == 15);
static_assert(sizeof(AIO_IOB2_BI2X_UDN__DEVSTATUS) == 337);
static_assert(sizeof(AIO_IOB_ICCA__CARDID) == 12);
static_assert(sizeof(AIO_IOB_ICCA__INPUT) == 36);
static_assert(sizeof(AIO_IOB_ICCA__DEVSTATUS) == 58);
using aioIob2Bi2xUDN_Create_t = AIO_IOB2_BI2X_UDN *(__fastcall *)(AIO_NMGR_IOB2 *, uint32_t);
using aioIob2Bi2xUDN_GetDeviceStatus_t = uint32_t (__fastcall *)(
AIO_IOB2_BI2X_UDN *, AIO_IOB2_BI2X_UDN__DEVSTATUS *, uint32_t);
using aioIob2Bi2xAC1_GetDeviceStatus_t = uint32_t (__fastcall *)(void *, void *, uint32_t);
using aioIob2Bi2xUDN_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t);
using aioIob2Bi2xUDN_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint8_t);
using aioIob2Bi2xUDN_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool);
using aioIob2Bi2xUDN_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint32_t);
using aioIob2Bi2xUDN_SetStartLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, bool);
using aioIob2Bi2xUDN_SetPlayerButtonLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool);
using aioIob2Bi2xUDN_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t);
using aioIob2Bi2xUDN_SetTapeLedDataLimit_t = void (__fastcall *)(
AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t, uint8_t);
using aioIob2Bi2xUDN_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t *);
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t);
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(uint32_t, uint32_t);
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *);
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *);
using aioIobIcca_Create_t = AIO_IOB_ICCA *(__fastcall *)(void *, uint32_t, uint8_t);
using aioIobIcca_GetDeviceStatus_t = uint32_t (__fastcall *)(
AIO_IOB_ICCA *, AIO_IOB_ICCA__DEVSTATUS *, uint32_t);
using aioIobIcca_BeginGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *);
using aioIobIcca_EndGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *);
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *, uint32_t);
using aioNMgrIob_BeginManage_t = void (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_GetState_t = int32_t (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_IsReady_t = bool (__fastcall *)(AIO_NMGR_IOB2 *, int32_t);
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *);
using aioNodeCtl_GetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_UDN *);
using aioNodeCtl_IsReady_t = bool (__fastcall *)(AIO_IOB2_BI2X_UDN *, int32_t);
static aioIob2Bi2xUDN_Create_t aioIob2Bi2xUDN_Create_orig = nullptr;
static aioIob2Bi2xUDN_GetDeviceStatus_t aioIob2Bi2xUDN_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xAC1_GetDeviceStatus_t aioIob2Bi2xAC1_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xUDN_IoReset_t aioIob2Bi2xUDN_IoReset_orig = nullptr;
static aioIob2Bi2xUDN_SetWatchDogTimer_t aioIob2Bi2xUDN_SetWatchDogTimer_orig = nullptr;
static aioIob2Bi2xUDN_ControlCoinBlocker_t aioIob2Bi2xUDN_ControlCoinBlocker_orig = nullptr;
static aioIob2Bi2xUDN_AddCounter_t aioIob2Bi2xUDN_AddCounter_orig = nullptr;
static aioIob2Bi2xUDN_SetStartLamp_t aioIob2Bi2xUDN_SetStartLamp_orig = nullptr;
static aioIob2Bi2xUDN_SetPlayerButtonLamp_t aioIob2Bi2xUDN_SetPlayerButtonLamp_orig = nullptr;
static aioIob2Bi2xUDN_SetIccrLed_t aioIob2Bi2xUDN_SetIccrLed_orig = nullptr;
static aioIob2Bi2xUDN_SetTapeLedDataLimit_t aioIob2Bi2xUDN_SetTapeLedDataLimit_orig = nullptr;
static aioIob2Bi2xUDN_SetTapeLedData_t aioIob2Bi2xUDN_SetTapeLedData_orig = nullptr;
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
static aioIobIcca_Create_t aioIobIcca_Create_orig = nullptr;
static aioIobIcca_GetDeviceStatus_t aioIobIcca_GetDeviceStatus_orig = nullptr;
static aioIobIcca_BeginGetCardId_t aioIobIcca_BeginGetCardId_orig = nullptr;
static aioIobIcca_EndGetCardId_t aioIobIcca_EndGetCardId_orig = nullptr;
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
static aioNMgrIob_BeginManage_t aioNMgrIob_BeginManage_orig = nullptr;
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
static aioNodeMgr_GetState_t aioNodeMgr_GetState_orig = nullptr;
static aioNodeMgr_IsReady_t aioNodeMgr_IsReady_orig = nullptr;
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
static aioNodeCtl_GetState_t aioNodeCtl_GetState_orig = nullptr;
static aioNodeCtl_IsReady_t aioNodeCtl_IsReady_orig = nullptr;
static std::unique_ptr<AIO_SCI_COMM> aio_sci_comm;
static std::unique_ptr<AIO_NMGR_IOB2> aio_node_manager;
static AIO_NMGR_IOB2 *aio_icca_node_manager = nullptr;
static std::unique_ptr<AIO_IOB2_BI2X_UDN> aio_udn;
static std::unique_ptr<AIO_IOB2_BI2X_WRFIRM> aio_write_firm;
static uint8_t input_counter = 0;
// Native ICCA status/begin/end operations also lock the reader state.
// The managed wrapper can destroy its node from a finalizer thread.
static std::mutex icca_mutex;
static AIO_IOB_ICCA *aio_icca = nullptr;
static bool icca_reading = false;
static bool icca_card_ready = false;
static uint8_t icca_card_uid[8] {};
static uint8_t icca_input_counter = 0;
static void write_light(Lights::udn_lights_t light, float value) {
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights.at(light), value);
}
static AIO_IOB2_BI2X_UDN *__fastcall aioIob2Bi2xUDN_Create(
AIO_NMGR_IOB2 *node_manager, uint32_t device_id) {
if (node_manager != aio_node_manager.get()) {
return aioIob2Bi2xUDN_Create_orig(node_manager, device_id);
}
if (!aio_udn) {
aio_udn = std::make_unique<AIO_IOB2_BI2X_UDN>();
log_info("udn_bi2x", "UDN node created");
}
return aio_udn.get();
}
static uint32_t __fastcall aioIob2Bi2xAC1_GetDeviceStatus(
void *node, void *status, uint32_t status_size) {
// aioIob2Bi2xUDN.GetDeviceStatus() first invokes its managed AC1 base method.
if (node == aio_udn.get()) {
if (status && status_size > 0) {
std::memset(status, 0, status_size);
}
return status_size;
}
return aioIob2Bi2xAC1_GetDeviceStatus_orig(node, status, status_size);
}
static uint32_t __fastcall aioIob2Bi2xUDN_GetDeviceStatus(
AIO_IOB2_BI2X_UDN *node, AIO_IOB2_BI2X_UDN__DEVSTATUS *status,
uint32_t status_size) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_GetDeviceStatus_orig(node, status, status_size);
}
RI_MGR->devices_flush_output();
AIO_IOB2_BI2X_UDN__DEVSTATUS current {};
current.InputCounter = input_counter;
current.Input.DevIoCounter = input_counter++;
auto &buttons = get_buttons();
current.Input.bService = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 1 : 0;
current.Input.bTest = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 1 : 0;
current.Input.bCoinSw = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 1 : 0;
current.Input.bStart = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Start]) ? 1 : 0;
current.Input.bUp = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Up]) ? 1 : 0;
current.Input.bDown = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Down]) ? 1 : 0;
current.Input.bLeft = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Left]) ? 1 : 0;
current.Input.bRight = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Right]) ? 1 : 0;
// The hardware counter wraps at 256; clamping it would stop accepting
// coins once the shared stock reaches 255.
current.Input.CoinCount = static_cast<uint8_t>(eamuse_coin_get_stock());
const auto copy_size = std::min<uint32_t>(status_size, sizeof(current));
if (status && copy_size > 0) {
std::memcpy(status, &current, copy_size);
}
return copy_size;
}
static void __fastcall aioIob2Bi2xUDN_IoReset(AIO_IOB2_BI2X_UDN *node, uint32_t reset_bits) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_IoReset_orig(node, reset_bits);
}
}
static void __fastcall aioIob2Bi2xUDN_SetWatchDogTimer(AIO_IOB2_BI2X_UDN *node, uint8_t count) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetWatchDogTimer_orig(node, count);
}
}
static void __fastcall aioIob2Bi2xUDN_ControlCoinBlocker(
AIO_IOB2_BI2X_UDN *node, uint32_t slot, bool open) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_ControlCoinBlocker_orig(node, slot, open);
}
eamuse_coin_set_block(!open);
}
static void __fastcall aioIob2Bi2xUDN_AddCounter(
AIO_IOB2_BI2X_UDN *node, uint32_t counter, uint32_t count) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_AddCounter_orig(node, counter, count);
}
}
static void __fastcall aioIob2Bi2xUDN_SetStartLamp(AIO_IOB2_BI2X_UDN *node, bool on) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetStartLamp_orig(node, on);
}
write_light(Lights::Start, on ? 1.f : 0.f);
}
static void __fastcall aioIob2Bi2xUDN_SetPlayerButtonLamp(
AIO_IOB2_BI2X_UDN *node, uint32_t button, bool on) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetPlayerButtonLamp_orig(node, button, on);
}
static const Lights::udn_lights_t mapping[] {
Lights::Start,
Lights::Up,
Lights::Down,
Lights::Left,
Lights::Right,
};
if (button < std::size(mapping)) {
write_light(mapping[button], on ? 1.f : 0.f);
}
}
static void __fastcall aioIob2Bi2xUDN_SetIccrLed(AIO_IOB2_BI2X_UDN *node, uint32_t rgb) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetIccrLed_orig(node, rgb);
}
write_light(Lights::CardReaderR, ((rgb >> 16) & 0xff) / 255.f);
write_light(Lights::CardReaderG, ((rgb >> 8) & 0xff) / 255.f);
write_light(Lights::CardReaderB, (rgb & 0xff) / 255.f);
}
static void __fastcall aioIob2Bi2xUDN_SetTapeLedDataLimit(
AIO_IOB2_BI2X_UDN *node, uint32_t channel, uint8_t scale, uint8_t limit) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetTapeLedDataLimit_orig(node, channel, scale, limit);
}
}
static void __fastcall aioIob2Bi2xUDN_SetTapeLedData(
AIO_IOB2_BI2X_UDN *node, uint32_t tape, uint8_t *data) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetTapeLedData_orig(node, tape, data);
}
struct TapeMapping {
size_t color_count;
Lights::udn_lights_t r;
Lights::udn_lights_t g;
Lights::udn_lights_t b;
};
static const TapeMapping mapping[] {
{62, Lights::TitleR, Lights::TitleG, Lights::TitleB},
{152, Lights::PillarRightTopR, Lights::PillarRightTopG, Lights::PillarRightTopB},
{165, Lights::PillarRightBottomR, Lights::PillarRightBottomG, Lights::PillarRightBottomB},
{9, Lights::StageLeftR, Lights::StageLeftG, Lights::StageLeftB},
{17, Lights::CabinetLeftR, Lights::CabinetLeftG, Lights::CabinetLeftB},
{152, Lights::PillarLeftTopR, Lights::PillarLeftTopG, Lights::PillarLeftTopB},
{165, Lights::PillarLeftBottomR, Lights::PillarLeftBottomG, Lights::PillarLeftBottomB},
{9, Lights::StageRightR, Lights::StageRightG, Lights::StageRightB},
{17, Lights::CabinetRightR, Lights::CabinetRightG, Lights::CabinetRightB},
};
if (data && tapeledutils::is_enabled() && tape < std::size(mapping)) {
const auto &entry = mapping[tape];
const auto color = tapeledutils::pick_color_from_led_tape(data, entry.color_count);
write_light(entry.r, color.r);
write_light(entry.g, color.g);
write_light(entry.b, color.b);
}
}
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t serial_number) {
if (!aio_sci_comm) {
aio_sci_comm = std::make_unique<AIO_SCI_COMM>();
}
return aio_sci_comm.get();
}
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(
uint32_t serial_number, uint32_t iob_mask) {
if (!aio_write_firm) {
aio_write_firm = std::make_unique<AIO_IOB2_BI2X_WRFIRM>();
}
return aio_write_firm.get();
}
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm.get()) {
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
}
aio_write_firm.reset();
}
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
if (context == aio_write_firm.get()) {
// The native state machine treats both 7 (NODEV) and 8
// (COMPLETED) as a successful terminal state. UDN additionally
// interprets state 8 as "firmware was written this boot" and
// deliberately powers the cabinet down after loading so the real
// I/O board can reboot. There is no physical BI2X board behind
// this emulation, so report NODEV: the check is complete without
// scheduling that otherwise endless firmware-update reboot.
return 7;
}
return aioIob2Bi2x_WriteFirmGetState_orig(context);
}
static AIO_IOB_ICCA *__fastcall aioIobIcca_Create(
void *node_manager, uint32_t device_id, uint8_t init_mode) {
auto *node = aioIobIcca_Create_orig(node_manager, device_id, init_mode);
if (node) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
aio_icca = node;
aio_icca_node_manager =
reinterpret_cast<AIO_NMGR_IOB2 *>(node_manager);
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
log_info("udn_bi2x", "ICCA node attached to Spice CardIO bridge");
}
return node;
}
static uint32_t __fastcall aioIobIcca_GetDeviceStatus(
AIO_IOB_ICCA *node, AIO_IOB_ICCA__DEVSTATUS *status, uint32_t status_size) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_GetDeviceStatus_orig(node, status, status_size);
}
AIO_IOB_ICCA__DEVSTATUS current {};
current.InputCounter = icca_input_counter++;
current.OutputCounter = current.InputCounter;
// DNI while idle, BUSY while waiting for a card, READY once a UID is
// available. The card-reader state machine accepts a card only on READY.
current.Input.cuStatus = icca_reading ? 1 : 0;
if (icca_reading && !icca_card_ready && eamuse_card_insert_consume(1, 0)) {
uint8_t uid[8] {};
if (eamuse_get_card(1, 0, uid)) {
std::memcpy(icca_card_uid, uid, sizeof(icca_card_uid));
icca_card_ready = true;
log_info(
"udn_bi2x",
"ICCA accepted UID {} from the Spice card queue; reporting READY to Unity",
bin2hex(icca_card_uid, sizeof(icca_card_uid)));
} else {
log_warning(
"udn_bi2x",
"ICCA consumed a card-insert event but Spice returned no UID");
}
}
if (icca_card_ready) {
current.Input.cuStatus = 2;
current.Input.CardId.CardType = is_card_uid_felica(icca_card_uid) ? 2 : 1;
current.Input.CardId.cbCardId = sizeof(icca_card_uid);
std::memcpy(current.Input.CardId.CardId, icca_card_uid, sizeof(icca_card_uid));
}
// The ICCA keypad scan-code order is identical to eamuse.h. Keeping
// these bits populated also preserves the usual Spice numpad PIN flow.
const auto keypad = eamuse_get_keypad_state(0);
auto *key_states = &current.Input.bKey0;
for (size_t key = 0; key < EAM_IO_KEYPAD_COUNT; ++key) {
key_states[key] = (keypad & (1u << key)) ? 1 : 0;
}
const auto copy_size = std::min<uint32_t>(status_size, sizeof(current));
if (status && copy_size > 0) {
std::memcpy(status, &current, copy_size);
}
return copy_size;
}
static void __fastcall aioIobIcca_BeginGetCardId(AIO_IOB_ICCA *node) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_BeginGetCardId_orig(node);
}
if (!icca_reading) {
log_info("udn_bi2x", "ICCA BeginGetCardId: waiting for the Spice/CardIO queue");
}
icca_reading = true;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
static void __fastcall aioIobIcca_EndGetCardId(AIO_IOB_ICCA *node) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_EndGetCardId_orig(node);
}
if (icca_reading || icca_card_ready) {
log_info("udn_bi2x", "ICCA EndGetCardId: returning to idle");
}
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
if (sci != aio_sci_comm.get()) {
return aioNMgrIob2_Create_orig(sci, mode);
}
if (!aio_node_manager) {
aio_node_manager = std::make_unique<AIO_NMGR_IOB2>();
}
return aio_node_manager.get();
}
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *node_manager) {
if (node_manager != aio_node_manager.get()) {
return aioNMgrIob_BeginManage_orig(node_manager);
}
}
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_manager) {
if (node_manager != aio_node_manager.get()) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
if (node_manager == aio_icca_node_manager) {
aio_icca_node_manager = nullptr;
}
}
return aioNodeMgr_Destroy_orig(node_manager);
}
aio_node_manager.reset();
}
static int32_t __fastcall aioNodeMgr_GetState(AIO_NMGR_IOB2 *node_manager) {
bool virtual_manager = node_manager == aio_node_manager.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_manager |= node_manager == aio_icca_node_manager;
}
if (virtual_manager) {
return 0;
}
return aioNodeMgr_GetState_orig(node_manager);
}
static bool __fastcall aioNodeMgr_IsReady(AIO_NMGR_IOB2 *node_manager, int32_t state) {
bool virtual_manager = node_manager == aio_node_manager.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_manager |= node_manager == aio_icca_node_manager;
}
if (virtual_manager) {
return true;
}
return aioNodeMgr_IsReady_orig(node_manager, state);
}
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_UDN *node) {
if (node != aio_udn.get()) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
if (reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca) {
aio_icca = nullptr;
aio_icca_node_manager = nullptr;
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
}
return aioNodeCtl_Destroy_orig(node);
}
aio_udn.reset();
}
static int32_t __fastcall aioNodeCtl_GetState(AIO_IOB2_BI2X_UDN *node) {
bool virtual_node = node == aio_udn.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_node |= reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca;
}
if (virtual_node) {
return 0;
}
return aioNodeCtl_GetState_orig(node);
}
static bool __fastcall aioNodeCtl_IsReady(AIO_IOB2_BI2X_UDN *node, int32_t state) {
bool virtual_node = node == aio_udn.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_node |= reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca;
}
if (virtual_node) {
return true;
}
return aioNodeCtl_IsReady_orig(node, state);
}
void bi2x_hook_init() {
log_info("udn_bi2x", "init (BI2X + ICCA/CardIO bridge)");
// The runtime imports the alias DLL names, not their lib-prefixed originals.
const auto aio_iob2_video = "aio-iob2_video.dll";
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_Create",
aioIob2Bi2xUDN_Create, &aioIob2Bi2xUDN_Create_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_GetDeviceStatus",
aioIob2Bi2xUDN_GetDeviceStatus, &aioIob2Bi2xUDN_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xAC1_GetDeviceStatus",
aioIob2Bi2xAC1_GetDeviceStatus, &aioIob2Bi2xAC1_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_IoReset",
aioIob2Bi2xUDN_IoReset, &aioIob2Bi2xUDN_IoReset_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetWatchDogTimer",
aioIob2Bi2xUDN_SetWatchDogTimer, &aioIob2Bi2xUDN_SetWatchDogTimer_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_ControlCoinBlocker",
aioIob2Bi2xUDN_ControlCoinBlocker, &aioIob2Bi2xUDN_ControlCoinBlocker_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_AddCounter",
aioIob2Bi2xUDN_AddCounter, &aioIob2Bi2xUDN_AddCounter_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetStartLamp",
aioIob2Bi2xUDN_SetStartLamp, &aioIob2Bi2xUDN_SetStartLamp_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetPlayerButtonLamp",
aioIob2Bi2xUDN_SetPlayerButtonLamp, &aioIob2Bi2xUDN_SetPlayerButtonLamp_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetIccrLed",
aioIob2Bi2xUDN_SetIccrLed, &aioIob2Bi2xUDN_SetIccrLed_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedDataLimit",
aioIob2Bi2xUDN_SetTapeLedDataLimit, &aioIob2Bi2xUDN_SetTapeLedDataLimit_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedData",
aioIob2Bi2xUDN_SetTapeLedData, &aioIob2Bi2xUDN_SetTapeLedData_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_OpenSciUsbCdc",
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_CreateWriteFirmContext",
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_DestroyWriteFirmContext",
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_WriteFirmGetState",
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
const auto aio_iob_video = "aio-iob_video.dll";
execexe::trampoline_try(aio_iob_video, "aioIobIcca_Create",
aioIobIcca_Create, &aioIobIcca_Create_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_GetDeviceStatus",
aioIobIcca_GetDeviceStatus, &aioIobIcca_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_BeginGetCardId",
aioIobIcca_BeginGetCardId, &aioIobIcca_BeginGetCardId_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_EndGetCardId",
aioIobIcca_EndGetCardId, &aioIobIcca_EndGetCardId_orig);
const auto aio_iob = "aio-iob.dll";
execexe::trampoline_try(aio_iob, "aioNMgrIob2_Create",
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
execexe::trampoline_try(aio_iob, "aioNMgrIob_BeginManage",
aioNMgrIob_BeginManage, &aioNMgrIob_BeginManage_orig);
const auto aio = "aio.dll";
execexe::trampoline_try(aio, "aioNodeMgr_Destroy",
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
execexe::trampoline_try(aio, "aioNodeMgr_GetState",
aioNodeMgr_GetState, &aioNodeMgr_GetState_orig);
execexe::trampoline_try(aio, "aioNodeMgr_IsReady",
aioNodeMgr_IsReady, &aioNodeMgr_IsReady_orig);
execexe::trampoline_try(aio, "aioNodeCtl_Destroy",
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
execexe::trampoline_try(aio, "aioNodeCtl_GetState",
aioNodeCtl_GetState, &aioNodeCtl_GetState_orig);
execexe::trampoline_try(aio, "aioNodeCtl_IsReady",
aioNodeCtl_IsReady, &aioNodeCtl_IsReady_orig);
}
}
#endif
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::udn {
void bi2x_hook_init();
}
+91
View File
@@ -0,0 +1,91 @@
#include "io.h"
#include <windows.h>
std::vector<Button> &games::udn::get_buttons() {
static std::vector<Button> buttons;
if (buttons.empty()) {
buttons = GameAPI::Buttons::getButtons("DANCE aROUND");
const std::vector<std::string> names {
"Service",
"Test",
"Coin Mech",
"Start",
"Up",
"Down",
"Left",
"Right",
};
const std::vector<unsigned short> vkey_defaults {
0xFF,
0xFF,
0xFF,
VK_RETURN,
VK_UP,
VK_DOWN,
VK_LEFT,
VK_RIGHT,
};
buttons = GameAPI::Buttons::sortButtons(buttons, names, &vkey_defaults);
}
return buttons;
}
std::string games::udn::get_buttons_help() {
return
" Arrow keys: menu directions\n"
" Enter: start/confirm\n"
" MIDI input is disabled for this game.\n";
}
std::vector<Light> &games::udn::get_lights() {
static std::vector<Light> lights;
if (lights.empty()) {
lights = GameAPI::Lights::getLights("DANCE aROUND");
GameAPI::Lights::sortLights(
&lights,
"Card Reader R",
"Card Reader G",
"Card Reader B",
"Start",
"Up",
"Down",
"Left",
"Right",
"Title R",
"Title G",
"Title B",
"Pillar Right Top R",
"Pillar Right Top G",
"Pillar Right Top B",
"Pillar Right Bottom R",
"Pillar Right Bottom G",
"Pillar Right Bottom B",
"Stage Left R",
"Stage Left G",
"Stage Left B",
"Cabinet Left R",
"Cabinet Left G",
"Cabinet Left B",
"Pillar Left Top R",
"Pillar Left Top G",
"Pillar Left Top B",
"Pillar Left Bottom R",
"Pillar Left Bottom G",
"Pillar Left Bottom B",
"Stage Right R",
"Stage Right G",
"Stage Right B",
"Cabinet Right R",
"Cabinet Right G",
"Cabinet Right B"
);
}
return lights;
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <vector>
#include "cfg/api.h"
namespace games::udn {
namespace Buttons {
enum {
Service,
Test,
CoinMech,
Start,
Up,
Down,
Left,
Right,
};
}
namespace Lights {
enum udn_lights_t {
CardReaderR,
CardReaderG,
CardReaderB,
Start,
Up,
Down,
Left,
Right,
TitleR,
TitleG,
TitleB,
PillarRightTopR,
PillarRightTopG,
PillarRightTopB,
PillarRightBottomR,
PillarRightBottomG,
PillarRightBottomB,
StageLeftR,
StageLeftG,
StageLeftB,
CabinetLeftR,
CabinetLeftG,
CabinetLeftB,
PillarLeftTopR,
PillarLeftTopG,
PillarLeftTopB,
PillarLeftBottomR,
PillarLeftBottomG,
PillarLeftBottomB,
StageRightR,
StageRightG,
StageRightB,
CabinetRightR,
CabinetRightG,
CabinetRightB,
};
}
std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::vector<Light> &get_lights();
}
+168
View File
@@ -0,0 +1,168 @@
// QueryDisplayConfig
#define _WIN32_WINNT 0x0601
#include "udn.h"
#include <format>
#include "acioemu/handle.h"
#include "bi2x_hook.h"
#include "cfg/screen_resize.h"
#include "hooks/devicehook.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/rawinput.h"
#include "util/detour.h"
#include "util/execexe.h"
#include "util/logging.h"
#include "util/unity_player.h"
#include "util/utils.h"
namespace games::udn {
std::string UDN_INJECT_ARGS = "";
bool UDN_NO_IO = false;
static acioemu::ACIOHandle *acio_handle = nullptr;
static const std::wstring port_name = L"COM1";
static decltype(RegisterRawInputDevices) *RegisterRawInputDevices_orig = nullptr;
static decltype(EnumDisplaySettingsW) *EnumDisplaySettingsW_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
// Unity reapplies its cabinet window settings after startup. Use the
// existing graphics message callback so changes stay on the window thread.
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
if (!GRAPHICS_WINDOWED || GRAPHICS_HOOKED_WINDOW != window) {
return 0;
}
static bool interactive_resize = false;
switch (message) {
case WM_ENTERSIZEMOVE:
interactive_resize = true;
break;
case WM_EXITSIZEMOVE:
interactive_resize = false;
break;
case WM_STYLECHANGING:
if (w_param == static_cast<WPARAM>(GWL_STYLE)) {
auto styles = reinterpret_cast<STYLESTRUCT *>(l_param);
styles->styleNew &= ~WS_POPUP;
if (cfg::SCREENRESIZE->window_decoration == cfg::WindowDecorationMode::Borderless) {
styles->styleNew &= ~WS_OVERLAPPEDWINDOW;
} else {
styles->styleNew |= WS_OVERLAPPEDWINDOW;
}
}
break;
case WM_WINDOWPOSCHANGING:
if (!interactive_resize && cfg::SCREENRESIZE->enable_window_resize) {
auto position = reinterpret_cast<WINDOWPOS *>(l_param);
// The shared WM_MOVE/WM_SIZE handlers keep these values in
// sync with interactive changes and the overlay settings.
if (!(position->flags & SWP_NOMOVE)) {
position->x = cfg::SCREENRESIZE->window_offset_x;
position->y = cfg::SCREENRESIZE->window_offset_y;
}
if (!(position->flags & SWP_NOSIZE)) {
position->cx = cfg::SCREENRESIZE->client_width +
cfg::SCREENRESIZE->window_deco_width;
position->cy = cfg::SCREENRESIZE->client_height +
cfg::SCREENRESIZE->window_deco_height;
}
}
break;
}
return 0;
}
static BOOL WINAPI RegisterRawInputDevices_hook(
PCRAWINPUTDEVICE devices, UINT device_count, UINT structure_size) {
if (devices && device_count > 0 && devices[0].hwndTarget == RI_MGR->input_hwnd) {
return RegisterRawInputDevices_orig(devices, device_count, structure_size);
}
SetLastError(0xDEADBEEF);
return FALSE;
}
static BOOL WINAPI EnumDisplaySettingsW_hook(
LPCWSTR device_name, DWORD mode_number, DEVMODEW *mode) {
const auto result = EnumDisplaySettingsW_orig(device_name, mode_number, mode);
if (result && mode_number == ENUM_CURRENT_SETTINGS) {
mode->dmPelsWidth = 1920;
mode->dmPelsHeight = 1080;
}
return result;
}
static LONG WINAPI QueryDisplayConfig_hook(
UINT32, UINT32 *, DISPLAYCONFIG_PATH_INFO *, UINT32 *, DISPLAYCONFIG_MODE_INFO *,
DISPLAYCONFIG_TOPOLOGY_ID *) {
return ERROR_NOT_SUPPORTED;
}
void UDNGame::pre_attach() {
// The title has no MIDI controls. The legacy WinMM provider can fault in
// MMDevAPI while Unity and its audio backend initialize concurrently, so
// disable MIDI enumeration for this game.
rawinput::DISABLE_MIDI = true;
log_info("udn", "MIDI input disabled to avoid WinMM initialization errors");
}
void UDNGame::attach() {
Game::attach();
acio_handle = new acioemu::ACIOHandle(port_name.c_str(), 1);
devicehook_init_trampoline();
devicehook_add(acio_handle);
execexe::init();
execexe::init_deferred([]() {
execexe::init_port_hook(port_name, acio_handle);
// Runtime import names omit the "lib" prefix.
execexe::load_library("aio.dll");
execexe::load_library("aio-iob.dll");
execexe::load_library("aio-iob_video.dll");
execexe::load_library("aio-iob2_video.dll");
execexe::load_library("win10actlog.dll", false);
#if SPICE64
if (!UDN_NO_IO) {
bi2x_hook_init();
}
#endif
});
const auto user32_dll = "user32.dll";
detour::trampoline_try(user32_dll, "RegisterRawInputDevices",
RegisterRawInputDevices_hook, &RegisterRawInputDevices_orig);
if (GRAPHICS_WINDOWED) {
// Keep Unity's cabinet-sized UI in windowed mode. Fullscreen uses
// the real display topology and resolution.
detour::trampoline_try(user32_dll, "QueryDisplayConfig",
QueryDisplayConfig_hook, &QueryDisplayConfig_orig);
detour::trampoline_try(user32_dll, "EnumDisplaySettingsW",
EnumDisplaySettingsW_hook, &EnumDisplaySettingsW_orig);
graphics_add_wnd_proc(window_proc);
}
if (GRAPHICS_SHOW_CURSOR) {
unity_utils::force_show_cursor(true);
}
unity_utils::set_args(
std::format("{} {}{}",
GetCommandLineA(),
UDN_INJECT_ARGS,
unity_utils::get_unity_player_args()));
}
void UDNGame::detach() {
Game::detach();
graphics_remove_wnd_proc(window_proc);
devicehook_dispose();
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "games/game.h"
namespace games::udn {
extern std::string UDN_INJECT_ARGS;
extern bool UDN_NO_IO;
class UDNGame : public games::Game {
public:
UDNGame() : Game("DANCE aROUND") {}
virtual void pre_attach() override;
virtual void attach() override;
virtual void detach() override;
};
}
@@ -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);
@@ -26,6 +26,7 @@ namespace d3d11_hooks {
// ignore_hwnd.
void note_main_hwnd(HWND hwnd);
HWND main_hwnd();
bool is_main_game_swapchain(IDXGISwapChain *swapchain);
void ignore_hwnd(HWND hwnd);
// capture backbuffer to PNG if a screenshot was requested.
@@ -18,6 +18,7 @@
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "d3d11_swapchain_udn.h"
#include "external/imgui/imgui.h"
#include "external/imgui/backends/imgui_impl_dx11.h"
@@ -115,34 +116,6 @@ bool looks_like_game_window(HWND hwnd) {
&& client.bottom > client.top;
}
// only the main game window; ignore sub-screens / IME helpers.
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
// checks are ordered cheapest first, since this runs on every present
void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain) {
@@ -160,7 +133,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
}
// ignore sub windows
if (!is_main_game_swapchain(swapchain)) {
if (!d3d11_hooks::is_main_game_swapchain(swapchain)) {
return;
}
@@ -198,7 +171,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
void pump_frame(IDXGISwapChain *swapchain) {
const bool has_overlay =
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
if (!has_overlay && !d3d11_hooks::is_main_game_swapchain(swapchain)) {
return;
}
@@ -240,8 +213,10 @@ HRESULT STDMETHODCALLTYPE Present_hook(
{
// a test present doesn't display anything; don't pick a window or take a screenshot off it
if (!(Flags & DXGI_PRESENT_TEST)) {
d3d11_hooks::udn::schedule_window_attach(swapchain);
try_create_overlay(swapchain);
pump_frame(swapchain);
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
}
return Present_orig(swapchain, SyncInterval, Flags);
}
@@ -251,8 +226,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
const DXGI_PRESENT_PARAMETERS *pParams)
{
if (!(Flags & DXGI_PRESENT_TEST)) {
d3d11_hooks::udn::schedule_window_attach(swapchain);
try_create_overlay(swapchain);
pump_frame(swapchain);
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
}
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
}
@@ -287,6 +264,33 @@ namespace {
std::atomic<HWND> g_ignored_hwnd { nullptr };
}
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
void note_main_hwnd(HWND hwnd) {
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
return;
@@ -326,6 +330,8 @@ void install_swapchain_hooks(IDXGISwapChain *swapchain) {
}
}
udn::install_swapchain_hooks(swapchain);
if (!g_swapchain1_hooked) {
IDXGISwapChain1 *sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
@@ -0,0 +1,223 @@
// DANCE aROUND window attachment, frame pacing and display-mode overrides.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <chrono>
#include <cstdint>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include "d3d11_internal.h"
#include "d3d11_swapchain_udn.h"
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "util/precise_timer.h"
namespace d3d11_hooks::udn {
namespace {
using SetFullscreenState_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, BOOL, IDXGIOutput *);
using ResizeTarget_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, const DXGI_MODE_DESC *);
SetFullscreenState_t SetFullscreenState_orig = nullptr;
ResizeTarget_t ResizeTarget_orig = nullptr;
bool g_set_fullscreen_hooked = false;
bool g_resize_target_hooked = false;
// Present and Present1 share one waitable timer and frame deadline.
std::mutex g_forced_present_pacing_mutex;
std::chrono::steady_clock::time_point g_next_forced_present {};
// Unity creates and owns its top-level window on a different thread from the
// D3D11 Present thread. Subclassing or resizing that HWND from Present can
// synchronously wait for the owner thread and deadlock Unity. A one-time
// message-queue hook performs graphics_hook_window on the HWND's owner thread.
std::mutex g_window_attach_mutex;
HHOOK g_window_attach_hook = nullptr;
HWND g_window_attach_target = nullptr;
bool g_window_attach_complete = false;
UINT window_attach_message() {
static const UINT message = RegisterWindowMessageW(
L"SpiceTools.D3D11.AttachGameWindow");
return message;
}
void pace_forced_present(UINT frame_rate) {
static timeutils::PreciseSleepTimer timer;
const auto period = std::chrono::nanoseconds(
1'000'000'000ULL / static_cast<uint64_t>(frame_rate));
std::lock_guard<std::mutex> lock(g_forced_present_pacing_mutex);
const auto now = std::chrono::steady_clock::now();
if (g_next_forced_present.time_since_epoch().count() == 0 ||
now > g_next_forced_present + period) {
g_next_forced_present = now + period;
return;
}
if (now < g_next_forced_present) {
timer.sleep(g_next_forced_present - now);
}
g_next_forced_present += period;
}
LRESULT CALLBACK window_attach_getmessage(
int code, WPARAM w_param, LPARAM l_param) {
HWND target = nullptr;
HHOOK hook = nullptr;
if (code >= 0 && w_param == PM_REMOVE) {
const auto message = reinterpret_cast<const MSG *>(l_param);
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
if (!g_window_attach_complete && message->hwnd == g_window_attach_target &&
message->message == window_attach_message()) {
target = g_window_attach_target;
hook = g_window_attach_hook;
g_window_attach_hook = nullptr;
g_window_attach_complete = true;
}
}
const LRESULT result = CallNextHookEx(nullptr, code, w_param, l_param);
if (target) {
UnhookWindowsHookEx(hook);
graphics_hook_window(target, nullptr);
log_info(
"graphics::d3d11",
"attached Spice window hooks on owner thread for hwnd=0x{:x}",
(uintptr_t) target);
}
return result;
}
HRESULT STDMETHODCALLTYPE SetFullscreenState_hook(
IDXGISwapChain *swapchain, BOOL Fullscreen, IDXGIOutput *Target) {
if (Fullscreen && GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
is_main_game_swapchain(swapchain)) {
log_info(
"graphics::d3d11",
"blocked UDN request to enter DXGI exclusive fullscreen");
return SetFullscreenState_orig(swapchain, FALSE, nullptr);
}
return SetFullscreenState_orig(swapchain, Fullscreen, Target);
}
HRESULT STDMETHODCALLTYPE ResizeTarget_hook(
IDXGISwapChain *swapchain, const DXGI_MODE_DESC *NewTargetParameters) {
if (GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
is_main_game_swapchain(swapchain)) {
if (NewTargetParameters) {
log_info(
"graphics::d3d11",
"blocked UDN output-mode resize to {}x{}",
NewTargetParameters->Width,
NewTargetParameters->Height);
}
return S_OK;
}
return ResizeTarget_orig(swapchain, NewTargetParameters);
}
} // namespace
void schedule_window_attach(IDXGISwapChain *swapchain) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
!is_main_game_swapchain(swapchain)) {
return;
}
DXGI_SWAP_CHAIN_DESC desc {};
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return;
}
const HWND hwnd = desc.OutputWindow;
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
if (g_window_attach_complete || g_window_attach_hook != nullptr) {
return;
}
if (GRAPHICS_HOOKED_WINDOW.has_value()) {
g_window_attach_complete = true;
return;
}
DWORD process_id = 0;
const DWORD thread_id = GetWindowThreadProcessId(hwnd, &process_id);
if (!thread_id || process_id != GetCurrentProcessId()) {
log_warning(
"graphics::d3d11",
"could not identify owner thread for hwnd=0x{:x}",
(uintptr_t) hwnd);
return;
}
const HHOOK hook = SetWindowsHookExW(
WH_GETMESSAGE, window_attach_getmessage, nullptr, thread_id);
if (!hook) {
log_warning(
"graphics::d3d11",
"could not schedule owner-thread window attach for hwnd=0x{:x}: {}",
(uintptr_t) hwnd,
GetLastError());
return;
}
g_window_attach_target = hwnd;
g_window_attach_hook = hook;
if (!PostMessageW(hwnd, window_attach_message(), 0, 0)) {
const DWORD error = GetLastError();
g_window_attach_hook = nullptr;
g_window_attach_target = nullptr;
UnhookWindowsHookEx(hook);
log_warning(
"graphics::d3d11",
"could not post owner-thread window attach for hwnd=0x{:x}: {}",
(uintptr_t) hwnd,
error);
return;
}
log_info(
"graphics::d3d11",
"scheduled Spice window attach on owner thread {} for hwnd=0x{:x}",
thread_id,
(uintptr_t) hwnd);
}
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
!is_main_game_swapchain(swapchain)) {
return;
}
// Interval two gives 60 FPS on the cabinet's 120 Hz display, but only
// 37.5 FPS on a 75 Hz desktop. Pace windowed mode with an explicit clock.
// Fullscreen keeps the game's own presentation interval and flags.
pace_forced_present(60);
sync_interval = 0;
}
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN")) {
return;
}
// The shared installer holds its mutex while calling this function.
if (!g_set_fullscreen_hooked) {
g_set_fullscreen_hooked = hook_vtbl(
swapchain, 10, (void *) SetFullscreenState_hook,
(void **) &SetFullscreenState_orig,
"IDXGISwapChain::SetFullscreenState");
}
if (!g_resize_target_hooked) {
g_resize_target_hooked = hook_vtbl(
swapchain, 14, (void *) ResizeTarget_hook,
(void **) &ResizeTarget_orig, "IDXGISwapChain::ResizeTarget");
}
}
} // namespace d3d11_hooks::udn
#endif // SPICE_D3D11
@@ -0,0 +1,17 @@
#pragma once
#ifdef SPICE_D3D11
#include <windows.h>
struct IDXGISwapChain;
namespace d3d11_hooks::udn {
void schedule_window_attach(IDXGISwapChain *swapchain);
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval);
void install_swapchain_hooks(IDXGISwapChain *swapchain);
}
#endif
@@ -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();
+13 -1
View File
@@ -1,11 +1,16 @@
#include "libraryhook.h"
#include <mutex>
#include <shared_mutex>
#include "external/robin_hood.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
static bool LHOOK_ENABLED = false;
// SDK plugins can add aliases while other threads load DLLs.
static std::shared_mutex LIBRARIES_MUTEX;
static robin_hood::unordered_map<std::string, HMODULE> LIBRARIES_A;
static robin_hood::unordered_map<std::wstring, HMODULE> LIBRARIES_W;
static robin_hood::unordered_map<std::string, FARPROC> PROCS;
@@ -21,6 +26,7 @@ static HMODULE WINAPI LoadLibraryA_hook(LPCTSTR lpFileName) {
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_A.find(lpFileName);
if (module != LIBRARIES_A.end()) {
return module->second;
@@ -35,6 +41,7 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -49,6 +56,7 @@ static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWOR
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -63,6 +71,7 @@ static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
// check hooks
if (lpModuleName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_A.find(lpModuleName);
if (module != LIBRARIES_A.end()) {
return module->second;
@@ -77,6 +86,7 @@ static HMODULE WINAPI GetModuleHandleW_hook(LPCWSTR lpModuleName) {
// check hooks
if (lpModuleName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpModuleName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -129,8 +139,10 @@ void libraryhook_enable(HMODULE module) {
void libraryhook_hook_library(std::string library_name, HMODULE library_address) {
const auto library_name_w = s2ws(library_name);
std::unique_lock lock(LIBRARIES_MUTEX);
// add library to list
LIBRARIES_W.insert_or_assign(s2ws(library_name), library_address);
LIBRARIES_W.insert_or_assign(library_name_w, library_address);
LIBRARIES_A.insert_or_assign(std::move(library_name), library_address);
}
+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);
+76 -2
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <shlwapi.h>
#include <windows.h>
@@ -71,6 +72,7 @@
#include "games/qks/qks.h"
#include "games/mfg/mfg.h"
#include "games/pc/pc.h"
#include "games/udn/udn.h"
#include "games/museca/museca.h"
#include "hooks/avshook.h"
#include "hooks/audio/audio.h"
@@ -84,6 +86,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"
@@ -246,6 +249,7 @@ int main_implementation(int argc, char *argv[]) {
bool attach_qks = false;
bool attach_mfg = false;
bool attach_pc = false;
bool attach_udn = false;
bool attach_museca = false;
bool show_cursor_if_no_touch = false;
@@ -257,6 +261,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;
@@ -781,6 +786,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadPCModule].value_bool()) {
attach_pc = true;
}
if (options[launcher::Options::LoadUDNModule].value_bool()) {
attach_udn = true;
}
if (options[launcher::Options::LoadMusecaModule].value_bool()) {
attach_museca = true;
}
@@ -799,6 +807,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;
}
@@ -1448,6 +1494,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PCKnobMode].value_bool()) {
games::pc::PC_KNOB_MODE = true;
}
if (options[launcher::Options::UDNArgs].is_active()) {
games::udn::UDN_INJECT_ARGS = options[launcher::Options::UDNArgs].value_text();
}
if (options[launcher::Options::UDNNoIO].is_active()) {
games::udn::UDN_NO_IO = options[launcher::Options::UDNNoIO].value_bool();
}
if (options[launcher::Options::spice2x_EnableSMXStage].value_bool()) {
rawinput::ENABLE_SMX_STAGE = true;
}
@@ -2262,6 +2314,16 @@ int main_implementation(int argc, char *argv[]) {
break;
}
// DANCE aROUND
if (check_dll("kamunity.dll") && fileutils::dir_exists("game/dancearound_Data")) {
avs::game::DLL_NAME = "kamunity.dll";
attach_io = true;
attach_udn = true;
launcher::signal::USE_VEH_WORKAROUND = true;
show_cursor_if_no_touch = true;
break;
}
// Busou Shinki: Armored Princess Battle Conductor
if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) {
avs::game::DLL_NAME = "kamunity.dll";
@@ -2427,6 +2489,10 @@ int main_implementation(int argc, char *argv[]) {
avs::core::HEAP_SIZE = 536870912; // 512MB
games.push_back(new games::pc::PCGame());
}
if (attach_udn) {
avs::core::HEAP_SIZE = 1073741824; // 1GB
games.push_back(new games::udn::UDNGame());
}
// apply user heap size, if defined
if (user_heap_size > 0) {
@@ -2553,7 +2619,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 +2933,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
+9
View File
@@ -150,6 +150,7 @@ namespace launcher {
LoadQKSModule,
LoadMFGModule,
LoadPCModule,
LoadUDNModule,
LoadMusecaModule,
PathToModules,
ScreenshotFolder,
@@ -250,6 +251,8 @@ namespace launcher {
PCArgs,
PCNoIO,
PCKnobMode,
UDNArgs,
UDNNoIO,
spice2x_LightsOverallBrightness,
spice2x_WindowBorder,
spice2x_WindowSize,
@@ -316,6 +319,10 @@ namespace launcher {
OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
EnableNICSpoof,
NICSpoofIP,
NICSpoofHostRealIP,
NICSpoofPort,
AutoElevate,
CfgForceSoftwareRender,
OBSWebSocketEnabled,
@@ -325,6 +332,8 @@ namespace launcher {
OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens,
_Count,
};
enum class OptionsCategory {
+2
View File
@@ -732,6 +732,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("Mahjong Fight Girl");
else if (avs::game::is_model("XIF"))
eamuse_set_game("Polaris Chord");
else if (avs::game::is_model("UDN"))
eamuse_set_game("DANCE aROUND");
else if (avs::game::is_model("NDD"))
eamuse_set_game("Silent Scope: Bone Eater");
else {
+5
View File
@@ -9,6 +9,7 @@
namespace rawinput {
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
bool DISABLE_MIDI = false;
}
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
@@ -37,6 +38,10 @@ void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
void rawinput::RawInputManager::midi_scan_start() {
if (rawinput::DISABLE_MIDI) {
return;
}
// single-flight: only one scan runs at a time. if one is already running, set
// the pending flag so it rescans once more when it finishes - MIDI hotplug
// events fire while the slow enumeration is still going and must not be lost.
+1
View File
@@ -32,6 +32,7 @@ namespace rawinput {
extern bool DUMP_HID_DEVICES_TO_LOG;
extern uint32_t MIDI_NOTE_SUSTAIN;
extern bool DISABLE_MIDI;
extern bool NAIVE_REQUIRE_FOCUS;
extern bool RAWINPUT_REQUIRE_FOCUS;
+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);
}
+133
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,128 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)(
uint8_t amount
);
// get_coin_blocker (v0.6 and up)
// gets the current shared coin blocker state
// insert_coin bypasses the blocker regardless of this state
//
// blocked: receives true when coins are blocked, false when allowed
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_coin_blocker_func)(
bool *blocked
);
// set_coin_blocker (v0.6 and up)
// sets the shared coin blocker state used by normal coin input
// this is not a persistent override; the game can change the state again
// insert_coin bypasses the blocker regardless of this state
//
// blocked: true to block coins, false to allow them
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_coin_blocker_func)(
bool blocked
);
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
);
// hook_library (v0.5 and up)
//
// maps an exact LoadLibrary name to a module that the plugin has already loaded
//
// library_name: exact ANSI library name or path used by the target
// module: module handle returned by LoadLibrary; keep it loaded until shutdown
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_hook_library_func)(
const char *library_name,
void *module
);
typedef struct SPICE_SDK_V0 {
uint32_t size;
@@ -274,6 +397,16 @@ 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_hook_library_func *hook_library;
spice_sdk_get_coin_blocker_func *get_coin_blocker;
spice_sdk_set_coin_blocker_func *set_coin_blocker;
} SPICE_SDK_V0;
typedef void (__cdecl spice_sdk_destroy_callback_func)(
+51
View File
@@ -1899,6 +1899,57 @@ typedef enum SPICE_SDK_POLARIS_CHORD_LIGHTS {
PC_Light_Button12_B,
} SPICE_SDK_POLARIS_CHORD_LIGHTS;
// DANCE aROUND
typedef enum SPICE_SDK_DANCE_AROUND_BUTTONS {
UDN_Button_Service,
UDN_Button_Test,
UDN_Button_CoinMech,
UDN_Button_Start,
UDN_Button_Up,
UDN_Button_Down,
UDN_Button_Left,
UDN_Button_Right,
} SPICE_SDK_DANCE_AROUND_BUTTONS;
typedef enum SPICE_SDK_DANCE_AROUND_LIGHTS {
UDN_Light_CardReaderR,
UDN_Light_CardReaderG,
UDN_Light_CardReaderB,
UDN_Light_Start,
UDN_Light_Up,
UDN_Light_Down,
UDN_Light_Left,
UDN_Light_Right,
UDN_Light_TitleR,
UDN_Light_TitleG,
UDN_Light_TitleB,
UDN_Light_PillarRightTopR,
UDN_Light_PillarRightTopG,
UDN_Light_PillarRightTopB,
UDN_Light_PillarRightBottomR,
UDN_Light_PillarRightBottomG,
UDN_Light_PillarRightBottomB,
UDN_Light_StageLeftR,
UDN_Light_StageLeftG,
UDN_Light_StageLeftB,
UDN_Light_CabinetLeftR,
UDN_Light_CabinetLeftG,
UDN_Light_CabinetLeftB,
UDN_Light_PillarLeftTopR,
UDN_Light_PillarLeftTopG,
UDN_Light_PillarLeftTopB,
UDN_Light_PillarLeftBottomR,
UDN_Light_PillarLeftBottomG,
UDN_Light_PillarLeftBottomB,
UDN_Light_StageRightR,
UDN_Light_StageRightG,
UDN_Light_StageRightB,
UDN_Light_CabinetRightR,
UDN_Light_CabinetRightG,
UDN_Light_CabinetRightB,
} SPICE_SDK_DANCE_AROUND_LIGHTS;
#ifdef __cplusplus
} // extern "C"
#endif
+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();
}
+132 -7
View File
@@ -1,10 +1,14 @@
#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 "hooks/libraryhook.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
#include "overlay/notifications.h"
@@ -32,11 +36,12 @@ 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_coin_blocker_func sdk_get_coin_blocker;
static spice_sdk_set_coin_blocker_func sdk_set_coin_blocker;
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;
static spice_sdk_hook_library_func sdk_hook_library;
// DLLs
static int sdk_modules_count = 0;
@@ -46,6 +51,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 +97,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 +197,30 @@ 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
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, hook_library)) {
v0->hook_library = sdk_hook_library;
}
// end of 0.5
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_coin_blocker)) {
v0->get_coin_blocker = sdk_get_coin_blocker;
}
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, set_coin_blocker)) {
v0->set_coin_blocker = sdk_set_coin_blocker;
}
// end of 0.6
// any newer minor iterations will need to check the size
{
@@ -195,6 +233,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(
@@ -647,6 +715,30 @@ sdk_add_toast(
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_hook_library(
const char *library_name,
void *module
)
{
std::shared_lock lock(sdk_global_mutex);
if (sdk_shutting_down || !sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!library_name || !library_name[0]) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
if (!module) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
}
libraryhook_hook_library(library_name, static_cast<HMODULE>(module));
// launcher::signal::attach enables library hooks before SDK entry points run.
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_insert_coin(
@@ -664,4 +756,37 @@ sdk_insert_coin(
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_get_coin_blocker(
bool *blocked
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (!blocked) {
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
}
*blocked = eamuse_coin_get_block();
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_set_coin_blocker(
bool blocked
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
eamuse_coin_set_block(blocked);
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);
}
+68
View File
@@ -1,5 +1,8 @@
#include "execexe.h"
#include <cstring>
#include "avs/game.h"
#include "util/logging.h"
#include "util/libutils.h"
#include "util/utils.h"
@@ -22,6 +25,23 @@ namespace execexe {
static bool port_opened = false;
static std::function<void()> deferred_function = nullptr;
template<typename T>
static T resolve_export_thunk(T entry) {
// Some execexe builds expose ordinal-only five-byte near jumps backed
// only by INT3 padding. MinHook cannot construct a trampoline from
// those tiny export stubs, while the jump destination has a normal
// prologue.
const auto bytes = reinterpret_cast<const uint8_t *>(entry);
if (!bytes || bytes[0] != 0xE9) {
return entry;
}
int32_t displacement = 0;
std::memcpy(&displacement, bytes + 1, sizeof(displacement));
return reinterpret_cast<T>(
const_cast<uint8_t *>(bytes + 5 + displacement));
}
static HANDLE WINAPI execexe_CreateFileA_hook(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition,
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
@@ -92,6 +112,13 @@ namespace execexe {
execexe_CreateFileW = libutils::get_proc<decltype(&CreateFileW)>(execexe_module, MAKEINTRESOURCE(11));
execexe_PreLoadLibraries = libutils::get_proc<decltype(execexe_PreLoadLibraries)>(execexe_module, MAKEINTRESOURCE(48));
if (avs::game::is_model("UDN")) {
execexe_CloseHandle = resolve_export_thunk(execexe_CloseHandle);
execexe_CreateFileA = resolve_export_thunk(execexe_CreateFileA);
execexe_CreateFileW = resolve_export_thunk(execexe_CreateFileW);
execexe_PreLoadLibraries = resolve_export_thunk(execexe_PreLoadLibraries);
}
auto module_path = libutils::module_file_name(nullptr);
module_path = module_path.replace_extension("");
module_path = module_path.replace_filename(module_path.filename().wstring() + L"_Data");
@@ -105,6 +132,30 @@ namespace execexe {
log_fatal("execexe", "deferred init function is already set");
}
deferred_function = std::move(init_func);
// DANCE aROUND calls PreLoadLibraries through kamunity's ordinal
// import. Hook that call site directly: execexe can be loaded in the
// low address range immediately next to the AVS heap, where MinHook
// may be unable to reserve a nearby trampoline page.
if (avs::game::is_model("UDN")) {
const auto kamunity_module = GetModuleHandleW(L"kamunity.dll");
const auto imported_pre_load_libraries = kamunity_module ? detour::iat_try_ordinal(
"execexe.dll",
48,
execexe_PreLoadLibraries_hook,
kamunity_module) : nullptr;
if (imported_pre_load_libraries) {
execexe_PreLoadLibraries = reinterpret_cast<decltype(execexe_PreLoadLibraries)>(
imported_pre_load_libraries);
log_info("execexe", "hooked PreLoadLibraries through kamunity import");
return;
}
detour::trampoline(execexe_PreLoadLibraries,
execexe_PreLoadLibraries_hook, &execexe_PreLoadLibraries);
return;
}
detour::trampoline("execexe.dll", MAKEINTRESOURCE(48),
execexe_PreLoadLibraries_hook, &execexe_PreLoadLibraries);
}
@@ -117,6 +168,16 @@ namespace execexe {
port_name = portName;
acio = acioHandle;
if (avs::game::is_model("UDN")) {
detour::trampoline_try(execexe_CloseHandle,
execexe_CloseHandle_hook, &execexe_CloseHandle);
detour::trampoline_try(execexe_CreateFileA,
execexe_CreateFileA_hook, &execexe_CreateFileA);
detour::trampoline_try(execexe_CreateFileW,
execexe_CreateFileW_hook, &execexe_CreateFileW);
return;
}
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(7),
execexe_CloseHandle_hook, &execexe_CloseHandle);
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(9),
@@ -183,6 +244,10 @@ namespace execexe {
HMODULE module = get_module(dll);
FARPROC proc = get_proc(module, func);
if (avs::game::is_model("UDN")) {
// The existing detour helper reads *orig as the target to enable.
*orig = reinterpret_cast<void *>(proc);
}
return detour::trampoline(
reinterpret_cast<void *>(proc),
hook,
@@ -201,6 +266,9 @@ namespace execexe {
return false;
}
if (avs::game::is_model("UDN")) {
*orig = reinterpret_cast<void *>(proc);
}
return detour::trampoline(
reinterpret_cast<void *>(proc),
hook,
+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
);
);