From 0169fe1e3174823a05bb7bfb4abd53050bd8b6bf Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 30 Aug 2026 22:35:12 -0700 Subject: [PATCH 01/19] gitadora: show LEFT / RIGHT screens on sub screen in landscape mode (#900) ## Link to GitHub Issue or related Pull Request, if one exists Continues #899 ## Description of change #899 added the ability to display the gitadora subscreen in lanscape mode with black bars on the side. With the new option the black bars can instead display the LEFT / RIGHT screens, completing the two-monitor full screen use case for arena model. ## Testing --- src/spice2x/games/gitadora/gitadora.cpp | 8 +- src/spice2x/games/gitadora/gitadora.h | 18 ++- .../graphics/backends/d3d9/d3d9_device.cpp | 2 +- .../graphics/backends/d3d9/d3d9_gfdm.cpp | 122 +++++++++++++++++- .../hooks/graphics/backends/d3d9/d3d9_gfdm.h | 4 +- .../graphics/backends/d3d9/d3d9_swapchain.cpp | 2 +- src/spice2x/hooks/graphics/graphics.cpp | 4 + src/spice2x/hooks/graphics/graphics.h | 3 + src/spice2x/launcher/launcher.cpp | 13 +- src/spice2x/launcher/options.cpp | 32 +++-- src/spice2x/launcher/options.h | 2 +- 11 files changed, 178 insertions(+), 32 deletions(-) diff --git a/src/spice2x/games/gitadora/gitadora.cpp b/src/spice2x/games/gitadora/gitadora.cpp index 460b1b6..d99c770 100644 --- a/src/spice2x/games/gitadora/gitadora.cpp +++ b/src/spice2x/games/gitadora/gitadora.cpp @@ -39,7 +39,7 @@ namespace games::gitadora { std::optional PICK_ALGO = socd::SocdAlgorithm::PreferRecent; std::optional ARENA_WINDOW_COUNT = std::nullopt; bool ARENA_TWO_HEAD_EXCLUSIVE = false; - bool ARENA_SUBSCREEN_LANDSCAPE = false; + ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off; std::optional ASIO_DRIVER = std::nullopt; bool ALLOW_REALTEK_AUDIO = false; bool NATIVE_TOUCH = false; @@ -48,7 +48,7 @@ namespace games::gitadora { if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) { return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value(); } - if (ARENA_SUBSCREEN_LANDSCAPE) { + if (arena_subscreen_landscape()) { return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT }; } return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT }; @@ -372,11 +372,11 @@ namespace games::gitadora { "arena model: unsupported window count: {}", count); } - if (ARENA_SUBSCREEN_LANDSCAPE && !ARENA_TWO_HEAD_EXCLUSIVE) { + if (arena_subscreen_landscape() && !ARENA_TWO_HEAD_EXCLUSIVE) { log_warning( "gitadora", "arena model: landscape subscreen needs full screen two-window mode, ignoring"); - ARENA_SUBSCREEN_LANDSCAPE = false; + ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off; } if (ARENA_TWO_HEAD_EXCLUSIVE) { const auto [host_width, host_height] = arena_subscreen_host_size(); diff --git a/src/spice2x/games/gitadora/gitadora.h b/src/spice2x/games/gitadora/gitadora.h index 7e6edaf..0dd2218 100644 --- a/src/spice2x/games/gitadora/gitadora.h +++ b/src/spice2x/games/gitadora/gitadora.h @@ -12,6 +12,14 @@ namespace games::gitadora { + // what a landscape monitor driving the SMALL head does with the space beside the + // portrait subscreen image + enum class ArenaSubscreenLandscape { + Off, // SMALL head stays portrait + Small, // subscreen centered, the space either side stays black + All, // that space shows the LEFT and RIGHT heads instead + }; + // settings extern bool TWOCHANNEL; extern bool DISABLE_FRAME_LIMITER; @@ -22,11 +30,19 @@ namespace games::gitadora { extern std::optional PICK_ALGO; extern std::optional ARENA_WINDOW_COUNT; extern bool ARENA_TWO_HEAD_EXCLUSIVE; - extern bool ARENA_SUBSCREEN_LANDSCAPE; + extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE; extern std::optional ASIO_DRIVER; extern bool ALLOW_REALTEK_AUDIO; extern bool NATIVE_TOUCH; + static inline bool arena_subscreen_landscape() { + return ARENA_SUBSCREEN_LANDSCAPE != ArenaSubscreenLandscape::Off; + } + + static inline bool arena_subscreen_shows_sides() { + return ARENA_SUBSCREEN_LANDSCAPE == ArenaSubscreenLandscape::All; + } + // arena SMALL subscreen (touch panel) resolution static constexpr int ARENA_SUBSCREEN_WIDTH = 800; static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280; diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index e7bba5f..d03b9c8 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -670,7 +670,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present( // an adapter group device presents every head at once, so the SMALL head has to be // composed here as well as in its own swap chain if (gfdm_small_head.scaled()) { - gfdm_small_head.compose(pReal); + gfdm_small_head.compose(this); } CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion)); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp index 280c4c7..89e4711 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include "games/gitadora/gitadora.h" @@ -739,15 +740,118 @@ void GfdmSmallHead::release() { } } -HRESULT GfdmSmallHead::compose(IDirect3DDevice9 *device) { +// largest centered rect inside bounds that keeps the source's aspect ratio +static RECT gfdm_fit_centered(const RECT &bounds, LONG source_width, LONG source_height) { + const LONG bounds_width = bounds.right - bounds.left; + const LONG bounds_height = bounds.bottom - bounds.top; + if (bounds_width <= 0 || bounds_height <= 0 || source_width <= 0 || source_height <= 0) { + return RECT {}; + } + + LONG width = MulDiv(bounds_height, source_width, source_height); + LONG height = bounds_height; + if (width > bounds_width) { + width = bounds_width; + height = MulDiv(bounds_width, source_height, source_width); + } + + const LONG left = bounds.left + (bounds_width - width) / 2; + const LONG top = bounds.top + (bounds_height - height) / 2; + return RECT { left, top, left + width, top + height }; +} + +static void gfdm_compose_side_heads( + WrappedIDirect3DDevice9 *device, + IDirect3DSurface9 *head, + LONG host_width, + LONG host_height, + const RECT &content) +{ + if (!games::gitadora::arena_subscreen_shows_sides()) { + return; + } + + const RECT bars[2] = { + { 0, 0, content.left, host_height }, + { content.right, 0, host_width, host_height }, + }; + + for (UINT swapchain = 1; swapchain < GFDM_LOGICAL_HEAD_COUNT; swapchain++) { + if (!device->is_gfdm_logical_side_swapchain(swapchain)) { + continue; + } + + // nothing has been drawn yet if the game never asked for this head's back buffer + const auto *chain = device->fake_sub_swapchain[ + device->gfdm_hidden_side_swapchain_slot(swapchain)]; + if (chain == nullptr || chain->render_targets.empty()) { + continue; + } + + // the head's own window is the only thing that says which side it is, so an + // unnamed one is left out rather than guessed at and possibly mirrored + const char *name = graphics_gitadora_window_name( + device->gfdm_logical_group_parameters[swapchain].hDeviceWindow); + const bool named_left = name != nullptr && strcmp(name, "LEFT") == 0; + const bool named_right = name != nullptr && strcmp(name, "RIGHT") == 0; + if (!named_left && !named_right) { + static std::once_flag warned; + std::call_once(warned, [] { + log_warning( + "graphics::d3d9", + "two-head exclusive: a side head has no LEFT or RIGHT window, " + "leaving its bar black"); + }); + continue; + } + + const RECT &bar = bars[named_left ? 0 : 1]; + if (bar.right <= bar.left) { + continue; + } + + IDirect3DSurface9 *source = chain->render_targets[0]; + D3DSURFACE_DESC source_desc {}; + if (FAILED(source->GetDesc(&source_desc))) { + continue; + } + + const RECT target = gfdm_fit_centered( + bar, + static_cast(source_desc.Width), + static_cast(source_desc.Height)); + const HRESULT result = device->pReal->StretchRect( + source, + nullptr, + head, + &target, + D3DTEXF_LINEAR); + if (FAILED(result)) { + static std::once_flag warned; + std::call_once(warned, [result] { + log_warning( + "graphics::d3d9", + "two-head exclusive: could not draw a side head into the bars, hr={}", + FMT_HRESULT(result)); + }); + } + } +} + +HRESULT GfdmSmallHead::compose(WrappedIDirect3DDevice9 *device) { + if (device == nullptr || device->pReal == nullptr) { + return D3DERR_INVALIDCALL; + } + + IDirect3DDevice9 *real = device->pReal; IDirect3DSurface9 *proxy = nullptr; - HRESULT result = backbuffer(device, &proxy); + HRESULT result = backbuffer(real, &proxy); if (FAILED(result)) { return result; } IDirect3DSurface9 *head = nullptr; - result = device->GetBackBuffer( + result = real->GetBackBuffer( GFDM_NATIVE_SMALL_SWAPCHAIN, 0, D3DBACKBUFFER_TYPE_MONO, @@ -760,14 +864,18 @@ HRESULT GfdmSmallHead::compose(IDirect3DDevice9 *device) { D3DSURFACE_DESC desc {}; result = head->GetDesc(&desc); if (SUCCEEDED(result)) { + const auto host_width = static_cast(desc.Width); + const auto host_height = static_cast(desc.Height); const RECT content = games::gitadora::arena_subscreen_content_rect( - static_cast(desc.Width), - static_cast(desc.Height)); + host_width, + host_height); // discard swap effect leaves the whole head undefined every frame, so the bars // have to be repainted along with the image - device->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0)); - result = device->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR); + real->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0)); + result = real->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR); + + gfdm_compose_side_heads(device, head, host_width, host_height, content); } head->Release(); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h index 87a61fe..4cabb31 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.h @@ -13,6 +13,8 @@ inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGH inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4; inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1; +struct WrappedIDirect3DDevice9; + // Owns the portrait surface the game draws into while the SMALL head is scanned out at a // different resolution, and composes it onto the real head. Allocates nothing and reports // nothing until resolve() finds a head that is not the panel size. @@ -27,7 +29,7 @@ struct GfdmSmallHead { bool scaled() const { return active; } HRESULT backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out); - HRESULT compose(IDirect3DDevice9 *device); + HRESULT compose(WrappedIDirect3DDevice9 *device); void release(); // the game only ever sees the portrait size it asked for diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp index e035972..dacb126 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp @@ -96,7 +96,7 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc } if (scales_small_head(this)) { - pDev->gfdm_small_head.compose(pDev->pReal); + pDev->gfdm_small_head.compose(pDev); } HRESULT result = pReal->Present( diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index 7bb967b..e3fb016 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -233,6 +233,10 @@ bool graphics_gitadora_has_dedicated_subscreen() { return GFDM_SUBSCREEN_WINDOW != nullptr; } +const char *graphics_gitadora_window_name(HWND hWnd) { + return gitadora_window_name_for_hwnd(hWnd); +} + bool graphics_gitadora_prepare_two_head_device_window( HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) { if (hWnd == nullptr || !IsWindow(hWnd)) { diff --git a/src/spice2x/hooks/graphics/graphics.h b/src/spice2x/hooks/graphics/graphics.h index fe83955..e7baa9e 100644 --- a/src/spice2x/hooks/graphics/graphics.h +++ b/src/spice2x/hooks/graphics/graphics.h @@ -122,6 +122,9 @@ extern bool D3D9_DEVICE_HOOK_DISABLE; void graphics_init(); void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters); bool graphics_gitadora_has_dedicated_subscreen(); + +// "GITADORA", "LEFT", "RIGHT", "SMALL", or nullptr for anything else +const char *graphics_gitadora_window_name(HWND hWnd); // The native GITADORA two-head D3D9 group uses the game's named SMALL // device window for the native physical SMALL head. The game requests // D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index f130fc5..d1e0cf7 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -671,6 +671,16 @@ int main_implementation(int argc, char *argv[]) { } } + // gitadora arena sub layout + if (options[launcher::Options::GitaDoraArenaSubLayout].is_active()) { + const auto text = options[launcher::Options::GitaDoraArenaSubLayout].value_text(); + if (text == "landscape") { + games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = games::gitadora::ArenaSubscreenLandscape::Small; + } else if (text == "combine") { + games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = games::gitadora::ArenaSubscreenLandscape::All; + } + } + if (options[launcher::Options::GitaDoraWailHold].is_active()) { socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32(); } @@ -693,9 +703,6 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::GitaDoraArenaRealtekAccess].value_bool()) { games::gitadora::ALLOW_REALTEK_AUDIO = true; } - if (options[launcher::Options::GitaDoraSubscreenLandscape].value_bool()) { - games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = true; - } if (options[launcher::Options::LoadNostalgiaModule].value_bool()) { attach_nostalgia = true; } diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index b10715c..6451edd 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -1330,6 +1330,25 @@ static const std::vector OPTION_DEFINITIONS = { }, .quick_setting_category = "Game", }, + { + // GitaDoraArenaSubLayout + .title = "GitaDora Arena Subscreen Layout (EXPERIMENTAL)", + .name = "gdasublayout", + .desc = "For Arena Model full screen two-window mode: select the image layout on the second monitor.\n\n" + "portrait (default): default subscreen @ 800x1280\n\n" + "landscape: pillarboxed subscreen @ 1920x1080\n\n" + "combine: LEFT + SMALL + RIGHT screens side by side @ 1920x1080\n\n" + "For landscape and combine, -forceressub can be used to override the resolution.", + .type = OptionType::Enum, + .game_name = "GitaDora", + .category = "Game Options", + .elements = { + {"portrait", ""}, + {"landscape", ""}, + {"combine", ""} + }, + .quick_setting_category = "Game", + }, { // GitaDoraArenaAsioDriver .title = "GitaDora Arena ASIO driver", @@ -3425,19 +3444,6 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "General Overlay", }, - { - // GitaDoraSubscreenLandscape - .title = "GitaDora Arena Landscape Subscreen (EXPERIMENTAL)", - .name = "gdsublandscape", - .desc = "For Arena Model full screen two-window mode: drive the SMALL touch subscreen " - "with a landscape monitor instead of a portrait one.\n\n" - "The portrait image is centered and black bars fill the space on either side.\n\n" - "Launches at 1920x1080; use -forceressub if you need a different resolution.", - .type = OptionType::Bool, - .game_name = "GitaDora", - .category = "Game Options", - .quick_setting_category = "Game", - }, }; const std::vector &launcher::get_categories(Options::OptionsCategory category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 6a7627d..89dde71 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -122,6 +122,7 @@ namespace launcher { GitaDoraSubOverlaySize, GitaDoraArenaSingleWindow, GitaDoraArenaWindowLayout, + GitaDoraArenaSubLayout, GitaDoraArenaAsioDriver, GitaDoraArenaRealtekAccess, LoadJubeatModule, @@ -324,7 +325,6 @@ namespace launcher { OBSWebSocketDebug, ScreenshotIncludeOverlay, ScreenshotSubscreens, - GitaDoraSubscreenLandscape }; enum class OptionsCategory { From 81c913726d4452837ba56823948e4a824305539e Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 5 Sep 2026 00:00:41 -0700 Subject: [PATCH 02/19] danevo: add 2p card reader (#902) ## Link to GitHub Issue or related Pull Request, if one exists Fixes #901 ## Description of change This adds 2p card reader support to DanEvo. The cab only has one keypad but this is not a configuration spice supports today so spicecfg will show two keypads. ## Testing 2p card was recognized in test mode. No other testing performed. --- src/spice2x/misc/eamuse.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index bacf85a..d16ee5e 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -621,6 +621,7 @@ int eamuse_get_game_keypads() { avs::game::is_model("JDX") || avs::game::is_model("KDX") || avs::game::is_model("MDX") || + avs::game::is_model("KDM") || avs::game::is_model("J33") || avs::game::is_model("K33") || avs::game::is_model("L33") || @@ -637,6 +638,7 @@ int eamuse_get_game_keypads_name() { if (game_name == "Beatmania IIDX" || game_name == "Dance Dance Revolution" || + game_name == "Dance Evolution" || game_name == "GitaDora") { return 2; From a48eccab93c9ce11b28d06b3b8ecfe5a018f9def Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 5 Sep 2026 04:26:44 -0700 Subject: [PATCH 03/19] sdk: insert coin (#903) ## Link to GitHub Issue or related Pull Request, if one exists developer request ## Description of change Expose coin insert API over the SDK ## Testing see sample sdk code --- src/spice2x/api/modules/coin.cpp | 2 +- src/spice2x/misc/eamuse.cpp | 4 ++-- src/spice2x/misc/eamuse.h | 2 +- src/spice2x/sdk/include/spicesdk.h | 12 ++++++++++ src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp | 30 ++++++++++++++++++++++-- src/spice2x/sdk/sdk.cpp | 24 ++++++++++++++++++- 6 files changed, 67 insertions(+), 7 deletions(-) diff --git a/src/spice2x/api/modules/coin.cpp b/src/spice2x/api/modules/coin.cpp index 4f4937d..14e955e 100644 --- a/src/spice2x/api/modules/coin.cpp +++ b/src/spice2x/api/modules/coin.cpp @@ -61,7 +61,7 @@ namespace api::modules { return error_type(res, "amount", "int"); // add to coin stock - eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt())); + eamuse_coin_add(std::max(0, req.params[0].GetInt())); } /* diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index d16ee5e..91186ff 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -331,8 +331,8 @@ int eamuse_coin_consume_stock() { return COIN_STOCK.exchange(0, std::memory_order_relaxed); } -int eamuse_coin_add() { - return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1; +void eamuse_coin_add(int amount) { + COIN_STOCK.fetch_add(amount, std::memory_order_relaxed); } void eamuse_coin_insert() { diff --git a/src/spice2x/misc/eamuse.h b/src/spice2x/misc/eamuse.h index 366ba10..d495b44 100644 --- a/src/spice2x/misc/eamuse.h +++ b/src/spice2x/misc/eamuse.h @@ -59,7 +59,7 @@ void eamuse_coin_set_stock(int amount); bool eamuse_coin_consume(int amount); int eamuse_coin_consume_stock(); -int eamuse_coin_add(); +void eamuse_coin_add(int amount = 1); void eamuse_coin_insert(); void eamuse_pin_macro_start_thread(); diff --git a/src/spice2x/sdk/include/spicesdk.h b/src/spice2x/sdk/include/spicesdk.h index 40450df..2a52603 100644 --- a/src/spice2x/sdk/include/spicesdk.h +++ b/src/spice2x/sdk/include/spicesdk.h @@ -237,6 +237,16 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)( const char *text ); +// insert_coin (v0.3 and up) +// adds to the shared coin stock, bypassing the coin blocker like the Spice API +// +// amount: number of coins to insert (0 to 255); use 1 for a single coin +// zero succeeds without changing the stock + +typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)( + uint8_t amount +); + typedef struct SPICE_SDK_V0 { uint32_t size; @@ -262,6 +272,8 @@ typedef struct SPICE_SDK_V0 { spice_sdk_add_toast_func *add_toast; + spice_sdk_insert_coin_func *insert_coin; + } SPICE_SDK_V0; typedef void (__cdecl spice_sdk_destroy_callback_func)( diff --git a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp index 8511230..fe7b2e1 100644 --- a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp +++ b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp @@ -76,9 +76,35 @@ static ArrowButton arrow_buttons[] = { // worker thread for I/O static void worker_thread_main(std::stop_token stop_token) { + bool coin_previous_state[10] = {}; while (!stop_token.stop_requested()) { + + // insert coin + const bool control_pressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0; + for (uint8_t amount = 0; amount < 10; amount++) { + const bool coin_pressed = control_pressed && + ((GetAsyncKeyState('0' + amount) & 0x8000) != 0); + + if (coin_pressed && !coin_previous_state[amount] && spice.insert_coin) { + const auto status = spice.insert_coin(amount); + if (status != SPICE_SDK_STATUS_SUCCESS) { + LOG_INFO(std::format( + "coin insertion failed: {}", + static_cast(status)).c_str()); + } else { + const auto message = std::format( + "v0_cpp: inserted {} coin{}", amount, amount == 1 ? "" : "s"); + LOG_INFO(message.c_str()); + if (spice.add_toast) { + spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, message.c_str()); + } + } + } + coin_previous_state[amount] = coin_pressed; + } + + // check for ctrl + arrow keys and trigger p1 pad arrows for (auto& arrow : arrow_buttons) { - // check for ctrl + arrow keys and trigger p1 pad arrows if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) && ((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) { spice.set_button(arrow.button, true, 1.f); @@ -88,7 +114,7 @@ static void worker_thread_main(std::stop_token stop_token) { if (spice.add_toast) { spice.add_toast( SPICE_SDK_TOAST_LEVEL_INFO, - std::format("let me hear you say: {}", arrow.name).c_str()); + std::format("v0_cpp: let me hear you say: {}", arrow.name).c_str()); } else { LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str()); } diff --git a/src/spice2x/sdk/sdk.cpp b/src/spice2x/sdk/sdk.cpp index e860b52..e61bd05 100644 --- a/src/spice2x/sdk/sdk.cpp +++ b/src/spice2x/sdk/sdk.cpp @@ -31,6 +31,7 @@ static spice_sdk_clear_touch_func sdk_clear_touch; static spice_sdk_insert_card_func sdk_insert_card; static spice_sdk_set_keypad_func sdk_set_keypad; static spice_sdk_add_toast_func sdk_add_toast; +static spice_sdk_insert_coin_func sdk_insert_coin; struct SdkModule { std::string dll; @@ -175,8 +176,13 @@ sdk_init( if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) { v0->add_toast = sdk_add_toast; } - // end of 0.2 + + if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, insert_coin)) { + v0->insert_coin = sdk_insert_coin; + } + // end of 0.3 + // any newer minor iterations will need to check the size { @@ -640,5 +646,21 @@ sdk_add_toast( return SPICE_SDK_STATUS_SUCCESS; } +SPICE_SDK_STATUS_CODE +__cdecl +sdk_insert_coin( + uint8_t amount +) +{ + std::shared_lock lock(sdk_global_mutex); + if (!sdk_initialized) { + return SPICE_SDK_STATUS_TOO_LATE; + } + + if (amount > 0) { + eamuse_coin_add(amount); + } + return SPICE_SDK_STATUS_SUCCESS; +} } // namespace sdk \ No newline at end of file From eaab4698ebdb80e71abeee329762011076fd0054 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 5 Sep 2026 05:32:50 -0700 Subject: [PATCH 04/19] api: log JSON parse error (#904) --- src/spice2x/api/controller.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 1a6c4dd..c437ba8 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -8,6 +8,7 @@ #include "client.h" #include "cfg/configurator.h" #include "external/rapidjson/document.h" +#include "external/rapidjson/error/en.h" #include "util/crypt.h" #include "util/logging.h" #include "util/utils.h" @@ -312,6 +313,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s // check for parse error if (document.HasParseError()) { + log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)", + GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size); // return empty response and close connection out->push_back(0); From 00e904539e8e25298ab5f6c3f0b775c8499dc271 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sat, 5 Sep 2026 17:20:02 -0700 Subject: [PATCH 05/19] Update C writing guidelines in CONTRIBUTING.md Clarified guidelines for writing in C, emphasizing RAII patterns. --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17a64d4..9bde183 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,5 +105,5 @@ if (conditional) * We don't use GSL. * Don't throw exceptions. * Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`. - * Writing in C is also completely acceptable. + * Writing in C is also completely acceptable but prefer to use RAII patterns. * Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying. From 08e99dea4a9fceb0a6086c8489eaa4f575f09acc Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Sun, 6 Sep 2026 15:43:37 -0700 Subject: [PATCH 06/19] misc: fix minor bugs (#906) ## Link to GitHub Issue or related Pull Request, if one exists n/a ## Description of change Fix hang when a DLL using the SDK calls `log_fatal` (deadlock) Fix spicecfg crashing if API server fails to bind (make websocket teardown conditional) ## Testing --- src/spice2x/api/controller.cpp | 6 +++++- src/spice2x/api/controller.h | 6 +++--- src/spice2x/sdk/sdk.cpp | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index c437ba8..d1400e6 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -93,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty) // bind socket to address if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) { log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string()); + closesocket(this->server); this->server = INVALID_SOCKET; if (!cfg::CONFIGURATOR_STANDALONE) { log_fatal("api", "failed to start server"); @@ -103,6 +104,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty) // set socket to listen if (listen(this->server, server_backlog) == -1) { log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string()); + closesocket(this->server); this->server = INVALID_SOCKET; if (!cfg::CONFIGURATOR_STANDALONE) { log_fatal("api", "failed to start server"); @@ -446,7 +448,9 @@ void Controller::free_socket() { this->server = INVALID_SOCKET; } - this->websocket->free_socket(); + if (this->websocket) { + this->websocket->free_socket(); + } for (auto &s : this->serial) { s->free_port(); diff --git a/src/spice2x/api/controller.h b/src/spice2x/api/controller.h index 5413f44..7a4a356 100644 --- a/src/spice2x/api/controller.h +++ b/src/spice2x/api/controller.h @@ -41,21 +41,21 @@ namespace api { bool pretty; // server - WebSocketController *websocket; + WebSocketController *websocket = nullptr; std::vector serial; std::vector server_workers; std::vector server_handlers; std::mutex server_handlers_m; std::vector client_states; std::mutex client_states_m; - SOCKET server; + SOCKET server = INVALID_SOCKET; void server_worker(); void connection_handler(ClientState client_state); public: // state - bool server_running; + bool server_running = false; // constructor / destructor Controller(unsigned short port, std::string password, bool pretty); diff --git a/src/spice2x/sdk/sdk.cpp b/src/spice2x/sdk/sdk.cpp index e61bd05..5677ded 100644 --- a/src/spice2x/sdk/sdk.cpp +++ b/src/spice2x/sdk/sdk.cpp @@ -225,6 +225,7 @@ sdk_log( log_warning(facility_str.c_str(), "{}", message); break; case SPICE_SDK_LOG_LEVEL_FATAL: + lock.unlock(); log_fatal(facility_str.c_str(), "{}", message); break; default: From de466db29ffc38bb3b46f6a622d4f2dbe93916c6 Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 7 Sep 2026 14:52:18 +1000 Subject: [PATCH 07/19] CI: add MSVC build and fix building with MSVC (#907) Yess yessss let's continue supporting the worst compiler toolchain :smiling_imp: 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) --- .github/workflows/ci.yml | 31 ++++++++++++++++++- src/spice2x/games/iidx/camera.cpp | 19 ++++++------ .../hooks/audio/backends/wasapi/defs.h | 4 +++ .../graphics/backends/d3d9/d3d9_gfdm.cpp | 3 ++ src/spice2x/util/nt_loader.h | 3 +- 5 files changed, 48 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6afb357..73e4bd7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,4 +29,33 @@ jobs: with: name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }} path: src/spice2x/bin - if-no-files-found: error \ No newline at end of file + 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 }} diff --git a/src/spice2x/games/iidx/camera.cpp b/src/spice2x/games/iidx/camera.cpp index 68a7d48..0e7ac7a 100644 --- a/src/spice2x/games/iidx/camera.cpp +++ b/src/spice2x/games/iidx/camera.cpp @@ -2,6 +2,7 @@ #if SPICE64 && !SPICE_XP +#include #include #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 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; } } diff --git a/src/spice2x/hooks/audio/backends/wasapi/defs.h b/src/spice2x/hooks/audio/backends/wasapi/defs.h index 7ea3080..c13c4f0 100644 --- a/src/spice2x/hooks/audio/backends/wasapi/defs.h +++ b/src/spice2x/hooks/audio/backends/wasapi/defs.h @@ -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); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp index 89e4711..c00246d 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp @@ -11,6 +11,9 @@ #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 diff --git a/src/spice2x/util/nt_loader.h b/src/spice2x/util/nt_loader.h index 01fd2d3..223e7db 100644 --- a/src/spice2x/util/nt_loader.h +++ b/src/spice2x/util/nt_loader.h @@ -1,5 +1,6 @@ #pragma once +#include #include #define LDR_DLL_NOTIFICATION_REASON_LOADED 1 @@ -43,4 +44,4 @@ NTSTATUS NTAPI LdrRegisterDllNotification( NTSTATUS NTAPI LdrUnregisterDllNotification( PVOID Cookie -); \ No newline at end of file +); From 112b1aecb0897735bbf12a9730716fccefe4609a Mon Sep 17 00:00:00 2001 From: Will Date: Mon, 7 Sep 2026 18:13:37 +1000 Subject: [PATCH 08/19] 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. --- src/spice2x/launcher/options.cpp | 1450 +++++++++++++----------------- src/spice2x/launcher/options.h | 2 + 2 files changed, 638 insertions(+), 814 deletions(-) diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 6451edd..c6de7bd 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -91,10 +91,10 @@ bool launcher::USE_CMD_OVERRIDE = false; /* * Option Definitions - * Be aware that the order must be the same as in the enum launcher::Options! */ -static const std::vector OPTION_DEFINITIONS = { - { +static const std::vector OPTION_DEFINITIONS = std::invoke([]() { + std::vector c{launcher::Options::_Count}; + c[launcher::Options::GameExecutable] = { .title = "Override Game Executable & Disable Hooks", .name = "exec", .desc = "Path to the game DLL file.\n\n" @@ -104,8 +104,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "*.dll", .category = "Path Overrides", // intentionally not setting a file picker here to discourage people setting this without a good reason - }, - { + }; + c[launcher::Options::OpenConfigurator] = { .title = "Open Configurator", .name = "cfg", .desc = "Opens configuration window. This can only be launched via the command line " @@ -113,25 +113,24 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .disabled = true, - }, - { + }; + c[launcher::Options::OpenKFControl] = { .title = "(REMOVED) Open KFControl", .name = "kfcontrol", .desc = "This feature has been removed; please use an older version.", .type = OptionType::Bool, .hidden = true, .disabled = true, - }, - { + }; + c[launcher::Options::EAmusementEmulation] = { .title = "Basic Local EA Emulation", .name = "ea", .desc = "Enables the integrated local EA server, just enough to boot the game; no card in, no data saving.", .type = OptionType::Bool, .category = "Network", .quick_setting_category = "Network", - }, - { - // ServiceURL + }; + c[launcher::Options::ServiceURL] = { .title = "EA Service URL", .name = "url", .desc = "Sets a custom service URL override.", @@ -139,8 +138,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "example.com:8083", .category = "Network", .quick_setting_category = "Network", - }, - { + }; + c[launcher::Options::PCBID] = { .title = "PCBID", .name = "p", .desc = "Sets a custom PCBID override.", @@ -149,8 +148,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Network", .sensitive = true, .quick_setting_category = "Network", - }, - { + }; + c[launcher::Options::Player1Card] = { .title = "Player 1 Card", .name = "card0", .desc = "Set a card number for reader 1. Overrides the selected card file.", @@ -160,8 +159,8 @@ static const std::vector OPTION_DEFINITIONS = { .sensitive = true, .picker = OptionPickerType::EACard, .quick_setting_category = "Network", - }, - { + }; + c[launcher::Options::Player2Card] = { .title = "Player 2 Card", .name = "card1", .desc = "Set a card number for reader 2. Overrides the selected card file.", @@ -171,9 +170,8 @@ static const std::vector OPTION_DEFINITIONS = { .sensitive = true, .picker = OptionPickerType::EACard, .quick_setting_category = "Network", - }, - { - // Player1PinMacro + }; + c[launcher::Options::Player1PinMacro] = { .title = "Player 1 PIN Macro", .name = "pinmacro0", .desc = "Set a PIN for Player 1 that will cause the PIN to be automatically typed when Player 1 PIN Macro overlay key is pressed.", @@ -181,9 +179,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "1234", .category = "Auto PIN", .sensitive = true, - }, - { - // Player2PinMacro + }; + c[launcher::Options::Player2PinMacro] = { .title = "Player 2 PIN Macro", .name = "pinmacro1", .desc = "Set a PIN for Player 2 that will cause the PIN to be automatically typed when Player 2 PIN Macro overlay key is pressed.", @@ -191,9 +188,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "5678", .category = "Auto PIN", .sensitive = true, - }, - { - // AutoPinMacroTrigger0 + }; + c[launcher::Options::AutoPinMacroTrigger0] = { .title = "Player 1 PIN Macro Auto Trigger on Log", .name = "autopinmacrotrigger0", .desc = @@ -202,9 +198,8 @@ static const std::vector OPTION_DEFINITIONS = { "typed for Player 1 only. Leave blank to disable auto-trigger for P1.", .type = OptionType::Text, .category = "Auto PIN", - }, - { - // AutoPinMacroTrigger1 + }; + c[launcher::Options::AutoPinMacroTrigger1] = { .title = "Player 2 PIN Macro Auto Trigger on Log", .name = "autopinmacrotrigger1", .desc = @@ -213,17 +208,16 @@ static const std::vector OPTION_DEFINITIONS = { "typed for Player 2 only. Leave blank to disable auto-trigger for P2.", .type = OptionType::Text, .category = "Auto PIN", - }, - { + }; + c[launcher::Options::WindowedMode] = { .title = "Windowed Mode", .name = "w", .desc = "Enables windowed mode.", .type = OptionType::Bool, .category = "Windowed Settings", .quick_setting_category = "Display", - }, - { - // InjectHook + }; + c[launcher::Options::InjectHook] = { .title = "Inject DLL Hooks", .name = "k", .desc = "Multiple files are allowed; use multiple -k flags, or in SpiceCfg, separate by " @@ -233,9 +227,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "a.dll b.dll c.dll", .category = "DLL Hooks", .quick_setting_category = "Common", - }, - { - // EarlyInjectHook + }; + c[launcher::Options::EarlyInjectHook] = { .title = "Inject Early DLL Hooks", .name = "z", .desc = "Equivalent to 'Inject DLL Hooks' option, but ensures hooks are injected before " @@ -243,8 +236,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "a.dll b.dll c.dll", .category = "DLL Hooks", - }, - { + }; + c[launcher::Options::ExecuteScript] = { .title = "(REMOVED) Execute Lua Script", .name = "script", .desc = "This feature has been removed.", @@ -252,24 +245,23 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .category = "Miscellaneous", .disabled = true, - }, - { + }; + c[launcher::Options::CaptureCursor] = { .title = "Lock Cursor to Window", .name = "c", .desc = "Confines the cursor to be within the game window.", .type = OptionType::Bool, .category = "Mouse", - }, - { + }; + c[launcher::Options::ShowCursor] = { .title = "Show Cursor & Touch Emulation Enable", .name = "s", .desc = "Shows the cursor in the game window; also turns on touch emulation. Do not enable this if you have a real touch screen.", .type = OptionType::Bool, .category = "Mouse", .quick_setting_category = "Common", - }, - { - // PrimaryMonitor + }; + c[launcher::Options::PrimaryMonitor] = { .title = "Change Main Monitor", .name = "mainmonitor", .desc = "Changes the primary monitor before launching the game. It will be restored on exit.\n\n" @@ -278,9 +270,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "\\\\.\\DISPLAY2", .category = "Monitor", .picker = OptionPickerType::Monitor, - }, - { - // DXDisplayAdapter + }; + c[launcher::Options::DXDisplayAdapter] = { .title = "DX Primary Display Adapter Override", .name = "monitor", .display_name = "dxmainadapter", @@ -292,8 +283,8 @@ static const std::vector OPTION_DEFINITIONS = { "Disable Full Screen Optimizations for best results.", .type = OptionType::Integer, .category = "Full Screen Settings", - }, - { + }; + c[launcher::Options::GraphicsForceSingleAdapter] = { .title = "Only Use Main Monitor For Full Screen", .name = "graphics-force-single-adapter", .desc = "Force the graphics device to be opened utilizing only one adapter in multi-monitor systems.\n\n" @@ -301,17 +292,16 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "Full Screen Settings", .quick_setting_category = "Display", - }, - { + }; + c[launcher::Options::GraphicsForceRefresh] = { .title = "Monitor Refresh Rate", .name = "graphics-force-refresh", .desc = "Change the refresh rate for the primary monitor before launching the game. It will be restored on exit.", .type = OptionType::Integer, .category = "Monitor", .quick_setting_category = "Display", - }, - { - // FullscreenResolution + }; + c[launcher::Options::FullscreenResolution] = { .title = "Force Full Screen Resolution", .name = "forceres", .desc = @@ -322,9 +312,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "1280,720", .category = "Full Screen Settings" - }, - { - // FullscreenOrientationFlip + }; + c[launcher::Options::FullscreenOrientationFlip] = { .title = "Full Screen Orientation Swap (EXPERIMENTAL)", .name = "forceresswap", .desc = @@ -335,9 +324,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .category = "Full Screen Settings" - }, - { - // FullscreenSubResolution + }; + c[launcher::Options::FullscreenSubResolution] = { .title = "Force FS Subscreen Resolution (EXPERIMENTAL)", .name = "forceressub", .desc = @@ -347,9 +335,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "1280,720", .category = "Full Screen Settings" - }, - { - // FullscreenSubRefreshRate + }; + c[launcher::Options::FullscreenSubRefreshRate] = { .title = "Force FS Subscreen Refresh Rate (EXPERIMENTAL)", .name = "graphics-force-refresh-sub", .desc = @@ -358,18 +345,16 @@ static const std::vector OPTION_DEFINITIONS = { "WARNING: experimental as we have not done extensive testing to see if this causes desyncs.", .type = OptionType::Integer, .category = "Full Screen Settings" - }, - { - // Graphics9On12 + }; + c[launcher::Options::Graphics9On12] = { .title = "DirectX 9 on 12 (DEPRECATED - use -dx9on12 instead)", .name = "9on12", .desc = "Use D3D9On12 wrapper library, requires Windows 10. Deprecated - use -dx9on12 instead.", .type = OptionType::Bool, .hidden = true, .category = "Graphics", - }, - { - // spice2x_Dx9On12 + }; + c[launcher::Options::spice2x_Dx9On12] = { .title = "DirectX 9 on 12", .name = "sp2x-dx9on12", .display_name = "dx9on12", @@ -383,37 +368,36 @@ static const std::vector OPTION_DEFINITIONS = { {"0", "Use DX9"}, {"1", "Use DX9on12"}, }, - }, - { + }; + c[launcher::Options::NoLegacy] = { .title = "Disable Win/Media/Special Keys", .name = "nolegacy", .desc = "Disables legacy key activation in-game.", .type = OptionType::Bool, .category = "I/O Options", - }, - { + }; + c[launcher::Options::RichPresence] = { .title = "Discord Rich Presence", .name = "richpresence", .desc = "Enables Discord Rich Presence support.", .type = OptionType::Bool, .category = "Miscellaneous", - }, - { + }; + c[launcher::Options::DiscordAppID] = { .title = "Discord RPC AppID Override", .name = "discordappid", .desc = "Set the discord RPC AppID override.", .type = OptionType::Text, .category = "Miscellaneous", - }, - { + }; + c[launcher::Options::SmartEAmusement] = { .title = "Smart Local EA", .name = "smartea", .desc = "Automatically enables -ea when server is offline.", .type = OptionType::Bool, .category = "Advanced Network", - }, - { - // EAmusementMaintenance + }; + c[launcher::Options::EAmusementMaintenance] = { .title = "EA Maintenance (DEPRECATED - use -forceeamaint instead)", .name = "eamaint", .desc = "Enables EA Maintenance, 1 for on, 0 for off.", @@ -421,9 +405,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .category = "Advanced Network", .elements = {{"0", "Off"}, {"1", "On"}}, - }, - { - // spice2x_EAmusementMaintenance + }; + c[launcher::Options::spice2x_EAmusementMaintenance] = { .title = "Local EA Maintenance", .name = "sp2x-eamaint", .display_name = "forceeamaint", @@ -431,8 +414,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Causes local EA to start in maintenance mode. Must be used with -ea or -smartea.", .type = OptionType::Bool, .category = "Advanced Network", - }, - { + }; + c[launcher::Options::AdapterNetwork] = { .title = "Preferred Network Adapter's IP", .name = "network", .display_name = "netadapterip", @@ -442,8 +425,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .category = "Advanced Network", .sensitive = true, - }, - { + }; + c[launcher::Options::AdapterSubnet] = { .title = "Preferred Network Adapter's Subnet", .name = "subnet", .display_name = "netadaptersubnet", @@ -452,46 +435,46 @@ static const std::vector OPTION_DEFINITIONS = { "with the specified subnet. You must also set -netadapterip.", .type = OptionType::Text, .category = "Advanced Network", - }, - { + }; + c[launcher::Options::DisableNetworkFixes] = { .title = "Disable Network Fixes", .name = "netfixdisable", .desc = "Force disables network fixes.", .type = OptionType::Bool, .category = "Network Dev", - }, - { + }; + c[launcher::Options::HTTP11] = { .title = "HTTP/1.1", .name = "http11", .desc = "Sets EA3 http11 value.", .type = OptionType::Enum, .category = "Network Dev", .elements = {{"0", "Off"}, {"1", "On"}}, - }, - { + }; + c[launcher::Options::DisableSSL] = { .title = "Disable SSL Protocol", .name = "ssldisable", .desc = "Prevents the SSL protocol from being registered.", .type = OptionType::Bool, .category = "Network Dev", - }, - { + }; + c[launcher::Options::URLSlash] = { .title = "URL Slash", .name = "urlslash", .desc = "Sets EA3 urlslash value.", .type = OptionType::Enum, .category = "Advanced Network", .elements = {{"0", "Off"}, {"1", "On"}}, - }, - { + }; + c[launcher::Options::SOFTID] = { .title = "SOFTID", .name = "r", .desc = "Set custom SOFTID override.", .type = OptionType::Text, .category = "Development", .sensitive = true, - }, - { + }; + c[launcher::Options::VREnable] = { .title = "(REMOVED) VR controls (experimental)", .name = "vr", .desc = "This feature has been removed.", @@ -500,24 +483,22 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "DANCERUSH", .category = "Game Options", .disabled = true, - }, - { - // DisableOverlay + }; + c[launcher::Options::DisableOverlay] = { .title = "Disable Spice Overlay", .name = "overlaydisable", .desc = "Disables the in-game overlay.", .type = OptionType::Bool, .category = "General Overlay", - }, - { + }; + c[launcher::Options::OverlayKeyboardNavigation] = { .title = "Overlay Keyboard Navigation", .name = "keyboardnav", .desc = "Enables keyboard navigation in the in-game overlay.", .type = OptionType::Bool, .category = "General Overlay", - }, - { - // OverlayScaling + }; + c[launcher::Options::OverlayScaling] = { .title = "Spice Overlay UI Scale %", .name = "overlayscale", .desc = "Forces UI scaling for the overlay, " @@ -526,9 +507,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "200", .category = "General Overlay", - }, - { - // NotificationPosition + }; + c[launcher::Options::NotificationPosition] = { .title = "Notifications", .name = "toast", .desc = "Select where notifications will appear on the screen.", @@ -541,9 +521,8 @@ static const std::vector OPTION_DEFINITIONS = { {"bottomleft", ""}, {"bottomright", ""}, }, - }, - { - // spice2x_FpsAutoShow + }; + c[launcher::Options::spice2x_FpsAutoShow] = { .title = "Auto Show FPS/Clock", .name = "sp2x-autofps", .display_name = "autofps", @@ -551,9 +530,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Automatically show FPS / clock / timer overlay window when the game starts.", .type = OptionType::Bool, .category = "General Overlay", - }, - { - // spice2x_FpsOpposite + }; + c[launcher::Options::spice2x_FpsOpposite] = { .title = "Show FPS/Clock top-left (DEPRECATED - use -fpslocation instead)", .name = "fpsflip", .desc = "Show FPS / clock / timer overlay on the top left of the screen instead of the top right. " @@ -561,9 +539,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .category = "General Overlay", - }, - { - // FpsLocation + }; + c[launcher::Options::FpsLocation] = { .title = "FPS/Clock Location", .name = "fpslocation", .desc = "Select which corner of the screen the FPS / clock / timer overlay appears in.", @@ -575,9 +552,8 @@ static const std::vector OPTION_DEFINITIONS = { {"bottomleft", ""}, {"bottomright", ""}, }, - }, - { - // spice2x_SubScreenAutoShow + }; + c[launcher::Options::spice2x_SubScreenAutoShow] = { .title = "Auto Show Subscreen", .name = "sp2x-autosubscreen", .display_name = "autosubscreen", @@ -585,9 +561,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Automatically show the subscreen overlay when the game starts, if the game has one.", .type = OptionType::Bool, .category = "General Overlay", - }, - { - // spice2x_IOPanelAutoShow + }; + c[launcher::Options::spice2x_IOPanelAutoShow] = { .title = "Auto Show IO Panel", .name = "sp2x-autoiopanel", .display_name = "autoiopanel", @@ -595,9 +570,8 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Automatically show I/O panel window when the game starts.", .type = OptionType::Bool, .category = "General Overlay", - }, - { - // spice2x_KeypadAutoShow + }; + c[launcher::Options::spice2x_KeypadAutoShow] = { .title = "Auto Show Keypad", .name = "sp2x-autokeypad", .display_name = "autokeypad", @@ -611,25 +585,24 @@ static const std::vector OPTION_DEFINITIONS = { {"2", "P2"}, {"3", "P1 and P2"}, }, - }, - { + }; + c[launcher::Options::LoadIIDXModule] = { .title = "Force Load IIDX Module", .name = "iidx", .desc = "Manually enable Beatmania IIDX module.", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::IIDXCameraOrderFlip] = { .title = "IIDX Camera Order Flip", .name = "iidxflipcams", .desc = "Flip the camera order.", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { - // IIDXDisableCameras + }; + c[launcher::Options::IIDXDisableCameras] = { .title = "IIDX Disable Cameras (DEPRECATED - no longer needed)", .name = "iidxdisablecams", .desc = "Disables cameras.", @@ -637,9 +610,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { - // IIDXCabCamAccess + }; + c[launcher::Options::IIDXCabCamAccess] = { .title = "IIDX Official AC Camera Access", .name = "iidxcabcams", .desc = "Controls how the game accesses USB cameras for IIDX 25+.\n\n" @@ -656,9 +628,8 @@ static const std::vector OPTION_DEFINITIONS = { {"legacy", ""}, {"off", ""}, }, - }, - { - // IIDXCamHook + }; + c[launcher::Options::IIDXCamHook] = { .title = "IIDX Cam Hook", .name = "iidxtdjcamhook", .display_name = "iidxcamhook", @@ -667,9 +638,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { - // IIDXCamHookRatio + }; + c[launcher::Options::IIDXCamHookRatio] = { .title = "IIDX Cam Hook Aspect Ratio", .name = "iidxtdjcamhookratio", .display_name = "iidxcamhookratio", @@ -683,9 +653,8 @@ static const std::vector OPTION_DEFINITIONS = { {"43", "4:3"}, {"169", "16:9"}, }, - }, - { - // IIDXCamHookOverride + }; + c[launcher::Options::IIDXCamHookOverride] = { .title = "IIDX Cam Hook Offset Override", .name = "iidxtdjcamhookoffset", .display_name = "iidxcamhookoffset", @@ -697,9 +666,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "0x5817a0,0x6fffbd8,0xbbae40,0xe0,0x30", .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { - // IIDXCamHookTopId + }; + c[launcher::Options::IIDXCamHookTopId] = { .title = "IIDX Cam Hook Top ID", .name = "iidxtdjcamhooktop", .display_name = "iidxcamhooktop", @@ -712,9 +680,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "vid_1234&pid_5678", .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { - // IIDXCamHookFrontId + }; + c[launcher::Options::IIDXCamHookFrontId] = { .title = "IIDX Cam Hook Front ID", .name = "iidxtdjcamhookfront", .display_name = "iidxcamhookfront", @@ -727,8 +694,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "vid_90ab&pid_cdef", .game_name = "Beatmania IIDX", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::IIDXSoundOutputDevice] = { .title = "IIDX Sound Output Device", .name = "iidxsounddevice", .desc = @@ -743,8 +710,8 @@ static const std::vector OPTION_DEFINITIONS = { {"asio", "ASIO"}, }, .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::IIDXAsioDriver] = { .title = "IIDX ASIO Driver", .name = "iidxasio", .desc = "ASIO driver name to use, replacing XONAR SOUND CARD(64). " @@ -755,8 +722,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .picker = OptionPickerType::AsioDriver, .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::IIDXBIO2FW] = { .title = "IIDX BIO2 Firmware Update", .name = "iidxbio2fw", .desc = "Enables BIO2 firmware updates. WARNING - can cause semi-permanent change to your I/O board.", @@ -764,8 +731,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::IIDXTDJMode] = { .title = "IIDX TDJ Mode (Lightning Model)", .name = "iidxtdj", .desc = "Enables TDJ mode (Lightning Model cabinet).", @@ -773,9 +740,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Beatmania IIDX", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // spice2x_IIDXDigitalTTSensitivity + }; + c[launcher::Options::spice2x_IIDXDigitalTTSensitivity] = { .title = "IIDX Digital TT Sensitivity", .name = "sp2x-iidxdigitalttsens", .display_name = "iidxdigitalttsens", @@ -785,9 +751,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(0-255)", .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // IIDXDigitalTTSocd + }; + c[launcher::Options::IIDXDigitalTTSocd] = { .title = "IIDX Digital TT SOCD Cleaner", .name = "iidxsocd", .desc = "SOCD for turntables when using button input; what happens when both directions are pressed.\n\n" @@ -802,9 +767,8 @@ static const std::vector OPTION_DEFINITIONS = { {"first", ""}, {"neutral", ""}, }, - }, - { - // spice2x_IIDXLDJForce720p + }; + c[launcher::Options::spice2x_IIDXLDJForce720p] = { .title = "IIDX LDJ Force 720p (HD)", .name = "sp2x-iidxldjforce720p", .display_name = "iidxldjforce720p", @@ -813,9 +777,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // spice2x_IIDXTDJSubSize + }; + c[launcher::Options::spice2x_IIDXTDJSubSize] = { .title = "IIDX TDJ Subscreen Size", .name = "sp2x-iidxtdjsubsize", .display_name = "iidxtdjsubsize", @@ -830,9 +793,8 @@ static const std::vector OPTION_DEFINITIONS = { {"large", ""}, {"fullscreen", ""}, }, - }, - { - // spice2x_IIDXLEDFontSize + }; + c[launcher::Options::spice2x_IIDXLEDFontSize] = { .title = "IIDX LED Ticker Font Size", .name = "sp2x-iidxledfontsize", .display_name = "iidxledfontsize", @@ -841,9 +803,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .game_name = "Beatmania IIDX", .category = "Game Overlay", - }, - { - // spice2x_IIDXLEDColor + }; + c[launcher::Options::spice2x_IIDXLEDColor] = { .title = "IIDX LED Ticker Color", .name = "sp2x-iidxledcolor", .display_name = "iidxledcolor", @@ -853,9 +814,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Hex, .game_name = "Beatmania IIDX", .category = "Game Overlay", - }, - { - // spice2x_IIDXLEDPos + }; + c[launcher::Options::spice2x_IIDXLEDPos] = { .title = "IIDX LED Ticker Position", .name = "sp2x-iidxledpos", .display_name = "iidxledpos", @@ -872,9 +832,8 @@ static const std::vector OPTION_DEFINITIONS = { {"bottom", ""}, {"bottomright", ""}, }, - }, - { - // IIDXLEDBorderless + }; + c[launcher::Options::IIDXLEDBorderless] = { .title = "IIDX LED Ticker Borderless", .name = "iidxledborderless", .aliases= "iidxledborderless", @@ -882,16 +841,16 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Game Overlay", - }, - { + }; + c[launcher::Options::LoadSoundVoltexModule] = { .title = "Force Load Sound Voltex Module", .name = "sdvx", .desc = "Manually enable Sound Voltex Module.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::SDVXForce720p] = { .title = "SDVX Vivid Wave Force 720p Window (DEPRECATED - use -windowsize instead)", .name = "sdvx720", .desc = "Old & deprecated option for launching Vivid Wave in 720p when using windowed mode.", @@ -899,17 +858,16 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Sound Voltex", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::SDVXPrinterEmulation] = { .title = "SDVX Printer Emulation", .name = "printer", .desc = "Enable Sound Voltex printer emulation.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { - // SDVXPrinterOutputPath + }; + c[launcher::Options::SDVXPrinterOutputPath] = { .title = "SDVX Printer Output Path", .name = "printerpath", .desc = "Path to folder where images will be stored.", @@ -917,25 +875,24 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Cab Peripherals", .picker = OptionPickerType::DirectoryPath, - }, - { + }; + c[launcher::Options::SDVXPrinterOutputClear] = { .title = "SDVX Printer Output Clear", .name = "printerclear", .desc = "Clean up saved images in the output directory on startup.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::SDVXPrinterOutputOverwrite] = { .title = "SDVX Printer Output Overwrite", .name = "printeroverwrite", .desc = "Always overwrite the same file in output directory.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { - // SDVXPrinterOutputFormat + }; + c[launcher::Options::SDVXPrinterOutputFormat] = { .title = "SDVX Printer Output Format", .name = "printerformat", .desc = "File format for printer output.", @@ -943,8 +900,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(png/bmp/tga/jpg)", .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::SDVXPrinterJPGQuality] = { .title = "SDVX Printer JPG Quality", .name = "printerjpgquality", .desc = "Quality setting in percent if JPG format is used.", @@ -952,8 +909,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(0-100)", .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::SDVXDisableCameras] = { .title = "SDVX Disable Cameras (DEPRECATED - no longer needed)", .name = "sdvxdisablecams", .desc = "This option does nothing.\n\n" @@ -963,8 +920,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Sound Voltex", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::SDVXNativeTouch] = { .title = "SDVX Native Touch (DEPRECATED - no longer needed)", .name = "sdvxnativetouch", .desc = "This option does nothing.\n\n" @@ -973,9 +930,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Sound Voltex", .category = "Advanced Game Options", - }, - { - // spice2x_SDVXSubRedraw + }; + c[launcher::Options::spice2x_SDVXSubRedraw] = { .title = "SDVX FS Subscreen Force Redraw", .name = "sp2x-sdvxsubredraw", .display_name = "sdvxsubredraw", @@ -985,9 +941,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Advanced Game Options", - }, - { - // spice2x_SDVXDigitalKnobSensitivity + }; + c[launcher::Options::spice2x_SDVXDigitalKnobSensitivity] = { .title = "SDVX Digital Knob Sensitivity", .name = "sp2x-sdvxdigitalknobsens", .display_name = "sdvxdigitalknobsens", @@ -997,9 +952,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(0-255)", .game_name = "Sound Voltex", .category = "Advanced Game Options", - }, - { - // SDVXDigitalKnobSocd + }; + c[launcher::Options::SDVXDigitalKnobSocd] = { .title = "SDVX Digital Knob SOCD Cleaner", .name = "sdvxsocd", .desc = "SOCD for knobs when using button input; what happens when both directions are pressed.\n\n" @@ -1014,9 +968,8 @@ static const std::vector OPTION_DEFINITIONS = { {"first", ""}, {"neutral", ""}, }, - }, - { - // spice2x_SDVXAsioDriver + }; + c[launcher::Options::spice2x_SDVXAsioDriver] = { .title = "SDVX ASIO driver", .name = "sp2x-sdvxasio", .display_name = "sdvxasio", @@ -1029,9 +982,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .picker = OptionPickerType::AsioDriver, .quick_setting_category = "Game", - }, - { - // SDVXAsioTwoChannel + }; + c[launcher::Options::SDVXAsioTwoChannel] = { .title = "SDVX ASIO Two Channel Audio", .name = "sdvxasio2ch", .desc = "Force the game to use two channels for ASIO output.", @@ -1039,9 +991,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // SDVXDisableLive2D + }; + c[launcher::Options::SDVXDisableLive2D] = { .title = "SDVX Disable Live2D (EXPERIMENTAL)", .name = "sdvxnolive2d", .desc = "Skip rendering the SDVX Live2D graphics to save GPU.\n\n" @@ -1057,9 +1008,8 @@ static const std::vector OPTION_DEFINITIONS = { {"ingame", "Hide during songs"}, }, .quick_setting_category = "Game", - }, - { - // spice2x_SDVXSubPos + }; + c[launcher::Options::spice2x_SDVXSubPos] = { .title = "SDVX Subscreen Overlay Position", .name = "sp2x-sdvxsubpos", .display_name = "sdvxsubpos", @@ -1075,9 +1025,8 @@ static const std::vector OPTION_DEFINITIONS = { {"bottomleft", "for landscape"}, {"bottomright", "for landscape"}, }, - }, - { - // SDVXSubMonitorOverride + }; + c[launcher::Options::SDVXSubMonitorOverride] = { .title = "SDVX Subscreen Monitor Override", .name = "sdvxsubmonitor", .desc = "If you have three or more monitors, this option can be set to tell the game which monitor is the subscreen.", @@ -1086,41 +1035,40 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Full Screen Settings", .picker = OptionPickerType::Monitor, - }, - { + }; + c[launcher::Options::LoadDDRModule] = { .title = "Force Load DDR Module", .name = "ddr", .desc = "Manually enable Dance Dance Revolution module.", .type = OptionType::Bool, .game_name = "Dance Dance Revolution", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::DDR43Mode] = { .title = "DDR 4:3 Mode", .name = "ddrsd/o", .desc = "Enable DDR 4:3 (SD) mode.", .type = OptionType::Bool, .game_name = "Dance Dance Revolution", .category = "Game Options", - }, - { - // DDRSkipCodecRegisteration + }; + c[launcher::Options::DDRSkipCodecRegisteration] = { .title = "DDR Skip Codec Registration", .name = "ddrnocodec", .desc = "Prevent automatic registration of codecs in the com folder.", .type = OptionType::Bool, .game_name = "Dance Dance Revolution", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadPopnMusicModule] = { .title = "Force Load Pop'n Music Module", .name = "pnm", .desc = "Manually enable Pop'n Music module.", .type = OptionType::Bool, .game_name = "Pop'n Music", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::PopnMusicForceHDMode] = { .title = "Pop'n Music Force HD Mode", .name = "pnmhd", .desc = "Force enable Pop'n Music HD mode.", @@ -1128,8 +1076,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Pop'n Music", .category = "Game Options", .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::PopnMusicForceSDMode] = { .title = "Pop'n Music Force SD Mode", .name = "pnmsd", .desc = "Force enable Pop'n Music SD mode.", @@ -1137,9 +1085,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Pop'n Music", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // PopnNoSub + }; + c[launcher::Options::PopnNoSub] = { .title = "Pop'n Music PikaPika Subscreen Disable", .name = "popnnosub", .desc = "Prevents PikaPika model subscreen from launching a separate window.", @@ -1147,9 +1094,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Pop'n Music", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // PopnSubMonitorOverride + }; + c[launcher::Options::PopnSubMonitorOverride] = { .title = "Pop'n Music PikaPika Subscreen Monitor Override", .name = "popnsubmonitor", .desc = "If you have three or more monitors, this option can be set to tell the game which monitor is the subscreen.", @@ -1158,9 +1104,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Pop'n Music", .category = "Full Screen Settings", .picker = OptionPickerType::Monitor, - }, - { - // PopnNativeTouch + }; + c[launcher::Options::PopnNativeTouch] = { .title = "Pop'n Music Native Touch (DEPRECATED - no longer needed)", .name = "popnnativetouch", .desc = "This option does nothing.\n\n" @@ -1169,9 +1114,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Pop'n Music", .category = "Advanced Game Options", - }, - { - // PopnSubRedraw + }; + c[launcher::Options::PopnSubRedraw] = { .title = "Pop'n Music PikaPika Subscreen Force Redraw", .name = "popnsubredraw", .desc = "Check if submonitor in fullscreen mode appears stuck; " @@ -1180,24 +1124,24 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Pop'n Music", .category = "Advanced Game Options", .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::LoadHelloPopnMusicModule] = { .title = "Force Load HELLO! Pop'n Music Module", .name = "hpm", .desc = "Manually enable HELLO! Pop'n Music module.", .type = OptionType::Bool, .game_name = "HELLO! Pop'n Music", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadGitaDoraModule] = { .title = "Force Load GitaDora Module", .name = "gd", .desc = "Manually enable GitaDora module.", .type = OptionType::Bool, .game_name = "GitaDora", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::GitaDoraTwoChannelAudio] = { .title = "GitaDora Two Channel Audio", .name = "2ch", .desc = "Attempt to reduce audio channels down to just two channels.", @@ -1205,8 +1149,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Options", .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::GitaDoraDisableFrameLimiter] = { .title = "GitaDora Disable Frame Limiter (EXPERIMENTAL)", .name = "gdnoframelimiter", .desc = "Only for GITADORA to GALAXY WAVE; not for XG series or Arena Model.\n\n" @@ -1218,8 +1162,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Options", .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::GitaDoraCabinetType] = { .title = "GitaDora Cabinet Type", .name = "gdcabtype", .desc = "Select cabinet type. DX has more input and lights. Pick SD2 for single player guitar mode on white cab.", @@ -1228,9 +1172,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .elements = {{"1", "DX"}, {"2", "SD"}, {"3", "SD2 - white cab"}}, .quick_setting_category = "Game", - }, - { - // GitaDoraLefty + }; + c[launcher::Options::GitaDoraLefty] = { .title = "GitaDora Lefty Guitar (for Digital Wailing)", .name = "gdlefty", .desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n" @@ -1248,9 +1191,8 @@ static const std::vector OPTION_DEFINITIONS = { {"both", "both lefty"}, }, .quick_setting_category = "Game", - }, - { - // GitaDoraWailHold + }; + c[launcher::Options::GitaDoraWailHold] = { .title = "GitaDora Digital Wail Hold", .name = "gdwailhold", .desc = "For digital wail input, how long (in milliseconds) to hold the wail state. Practically, this controls " @@ -1261,9 +1203,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .game_name = "GitaDora", .category = "Game Options", - }, - { - // GitaDoraPickAlgo + }; + c[launcher::Options::GitaDoraPickAlgo] = { .title = "GitaDora Picking Algorithm", .name = "gdpickalgo", .desc = "Select picking algorithm for guitar.\n\n" @@ -1280,9 +1221,8 @@ static const std::vector OPTION_DEFINITIONS = { {"neutral", ""}, {"raw", ""} }, - }, - { - // GitaDoraSubOverlaySize + }; + c[launcher::Options::GitaDoraSubOverlaySize] = { .title = "GitaDora Subscreen Overlay Size", .name = "gdsubsize", .desc = "Default size of the subscreen overlay. Default: medium.", @@ -1294,9 +1234,8 @@ static const std::vector OPTION_DEFINITIONS = { {"medium", ""}, {"large", ""} }, - }, - { - // GitaDoraArenaSingleWindow + }; + c[launcher::Options::GitaDoraArenaSingleWindow] = { .title = "GitaDora Arena Disable Subscreens (DEPRECATED - use -gdalayout)", .name = "gdaonewindow", .desc = "DEPRECATED - use -gdalayout.\n\n" @@ -1308,9 +1247,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "GitaDora", .category = "Game Options", - }, - { - // GitaDoraArenaWindowLayout + }; + c[launcher::Options::GitaDoraArenaWindowLayout] = { .title = "GitaDora Arena Layout (EXPERIMENTAL)", .name = "gdalayout", .desc = "For Arena Model: select how many windows to create.\n\n" @@ -1329,9 +1267,8 @@ static const std::vector OPTION_DEFINITIONS = { {"4", "4 windows"} }, .quick_setting_category = "Game", - }, - { - // GitaDoraArenaSubLayout + }; + c[launcher::Options::GitaDoraArenaSubLayout] = { .title = "GitaDora Arena Subscreen Layout (EXPERIMENTAL)", .name = "gdasublayout", .desc = "For Arena Model full screen two-window mode: select the image layout on the second monitor.\n\n" @@ -1348,9 +1285,8 @@ static const std::vector OPTION_DEFINITIONS = { {"combine", ""} }, .quick_setting_category = "Game", - }, - { - // GitaDoraArenaAsioDriver + }; + c[launcher::Options::GitaDoraArenaAsioDriver] = { .title = "GitaDora Arena ASIO driver", .name = "gdaasio", .desc = "For Arena Model: ASIO driver name to use in place of XONAR. " @@ -1363,9 +1299,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Game Options", .picker = OptionPickerType::AsioDriver, .quick_setting_category = "Game", - }, - { - // GitaDoraArenaRealtekAccess + }; + c[launcher::Options::GitaDoraArenaRealtekAccess] = { .title = "GitaDora Arena ASIO Allow Headphones", .name = "gdarealtek", .desc = "For Arena Model: allow the game to access the Realtek audio for headphone output.\n\n" @@ -1375,237 +1310,232 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "GitaDora", .category = "Game Options", - }, - { + }; + c[launcher::Options::LoadJubeatModule] = { .title = "Force Load Jubeat Module", .name = "jb", .desc = "Manually enable Jubeat module.", .type = OptionType::Bool, .game_name = "Jubeat", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadReflecBeatModule] = { .title = "Force Load Reflec Beat Module", .name = "rb", .desc = "Manually enable Reflec Beat module.", .type = OptionType::Bool, .game_name = "Reflec Beat", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadShogikaiModule] = { .title = "Force Load Tenkaichi Shogikai Module", .name = "shogikai", .desc = "Manually enable Tenkaichi Shogikai module.", .type = OptionType::Bool, .game_name = "Tenkaichi Shogikai", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadBeatstreamModule] = { .title = "Force Load Beatstream Module", .name = "bs", .desc = "Manually enable Beatstream module.", .type = OptionType::Bool, .game_name = "Beatstream", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadNostalgiaModule] = { .title = "Force Load Nostalgia Module", .name = "nostalgia", .desc = "Manually enable Nostalgia module.", .type = OptionType::Bool, .game_name = "Nostalgia", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadDanceEvolutionModule] = { .title = "Force Load Dance Evolution Module", .name = "dea", .desc = "Manually enable Dance Evolution module.", .type = OptionType::Bool, .game_name = "Dance Evolution", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadFutureTomTomModule] = { .title = "Force Load FutureTomTom Module", .name = "ftt", .desc = "Manually enable FutureTomTom module.", .type = OptionType::Bool, .game_name = "FutureTomTom", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadBBCModule] = { .title = "Force Load BBC Module", .name = "bbc", .desc = "Manually enable Bishi Bashi Channel module.", .type = OptionType::Bool, .game_name = "Bishi Bashi Channel", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadMetalGearArcadeModule] = { .title = "Force Load Metal Gear Arcade Module", .name = "mga", .desc = "Manually enable Metal Gear Arcade module.", .type = OptionType::Bool, .game_name = "Metal Gear", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadQuizMagicAcademyModule] = { .title = "Force Load Quiz Magic Academy Module", .name = "qma", .desc = "Manually enable Quiz Magic Academy module.", .type = OptionType::Bool, .game_name = "Quiz Magic Academy", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadRoadFighters3DModule] = { .title = "Force Load Road Fighters 3D Module", .name = "rf3d", .desc = "Manually enable Road Fighters 3D module.", .type = OptionType::Bool, .game_name = "Road Fighters 3D", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadSteelChronicleModule] = { .title = "Force Load Steel Chronicle Module", .name = "sc", .desc = "Manually enable Steel Chronicle module.", .type = OptionType::Bool, .game_name = "Steel Chronicle", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadMahjongFightClubModule] = { .title = "Force Load Mahjong Fight Club Module", .name = "mfc", .desc = "Manually enable Mahjong Fight Club module.", .type = OptionType::Bool, .game_name = "Mahjong Fight Club", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadScottoModule] = { .title = "Force Load Scotto Module", .name = "scotto", .desc = "Manually enable Scotto module.", .type = OptionType::Bool, .game_name = "Scotto", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadDanceRushModule] = { .title = "Force Load Dance Rush Module", .name = "dr", .desc = "Manually enable Dance Rush module.", .type = OptionType::Bool, .game_name = "DANCERUSH", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadWinningElevenModule] = { .title = "Force Load Winning Eleven Module", .name = "we", .desc = "Manually enable Winning Eleven module.", .type = OptionType::Bool, .game_name = "Winning Eleven", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadOtocaModule] = { .title = "Force Load Otoca D'or Module", .name = "otoca", .desc = "Manually enable Otoca D'or module.", .type = OptionType::Bool, .game_name = "Otoca D'or", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadLovePlusModule] = { .title = "Force Load LovePlus Module", .name = "loveplus", .desc = "Manually enable LovePlus module.", .type = OptionType::Bool, .game_name = "LovePlus", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadChargeMachineModule] = { .title = "Force Load Charge Machine Module", .name = "pcm", .desc = "Manually enable Charge Machine module.", .type = OptionType::Bool, .game_name = "Charge Machine", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadOngakuParadiseModule] = { .title = "Force Load Ongaku Paradise Module", .name = "onpara", .desc = "Manually enable Ongaku Paradise module.", .type = OptionType::Bool, .game_name = "Ongaku Paradise", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::LoadBusouShinkiModule] = { .title = "Force Load Busou Shinki Module", .name = "busou", .desc = "Manually enable Busou Shinki module.", .type = OptionType::Bool, .game_name = "Busou Shinki: Armored Princess Battle Conductor", .category = "Advanced Game Options", - }, - { - // LoadCCJModule + }; + c[launcher::Options::LoadCCJModule] = { .title = "Force Load Chase Chase Jokers Module", .name = "ccj", .desc = "Manually enable Chase Chase Jokers module.", .type = OptionType::Bool, .game_name = "Chase Chase Jokers", .category = "Advanced Game Options", - }, - { - // LoadQKSModule + }; + c[launcher::Options::LoadQKSModule] = { .title = "Force Load QuizKnock STADIUM Module", .name = "qks", .desc = "Manually enable QuizKnock STADIUM module.", .type = OptionType::Bool, .game_name = "QuizKnock STADIUM", .category = "Advanced Game Options", - }, - { - // LoadMFGModule + }; + c[launcher::Options::LoadMFGModule] = { .title = "Force Load Mahjong Fight Girl Module", .name = "mfg", .desc = "Manually enable Mahjong Fight Girl module.", .type = OptionType::Bool, .game_name = "Mahjong Fight Girl", .category = "Advanced Game Options", - }, - { - // LoadPCModule + }; + c[launcher::Options::LoadPCModule] = { .title = "Force Load Polaris Chord Module", .name = "pc", .desc = "Manually enable Polaris Chord module.", .type = OptionType::Bool, .game_name = "Polaris Chord", .category = "Advanced Game Options", - }, - { - // LoadMusecaModule + }; + c[launcher::Options::LoadMusecaModule] = { .title = "Force Load Museca Module", .name = "museca", .desc = "Manually enable Museca module.", .type = OptionType::Bool, .game_name = "Museca", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::PathToModules] = { .title = "Modules Folder Override", .name = "modules", .desc = "Sets a custom path to the modules folder.", .type = OptionType::Text, .category = "Path Overrides", // intentionally not setting a folder picker here to discourage people setting this without a good reason - }, - { + }; + c[launcher::Options::ScreenshotFolder] = { .title = "Screenshot Folder Override", .name = "screenshotpath", .desc = "Sets a custom path to the screenshots folder.", .type = OptionType::Text, .category = "Path Overrides", .picker = OptionPickerType::DirectoryPath, - }, - { + }; + c[launcher::Options::ConfigurationPath] = { .title = "Configuration Path Override", .name = "cfgpath", .desc = "Sets a custom file path for config file. Must be passed via the command line. " @@ -1614,9 +1544,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(default)", .category = "Path Overrides", .disabled = true, - }, - { - // ScreenResizeConfigPath + }; + c[launcher::Options::ScreenResizeConfigPath] = { .title = "Screen Resize Config Path Override", .name = "resizecfgpath", .desc = "Sets a custom file path for screen resize config file. " @@ -1625,9 +1554,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "JSON", - }, - { - // PatchManagerConfigPath + }; + c[launcher::Options::PatchManagerConfigPath] = { .title = "Patch Manager Config Path Override", .name = "patchcfgpath", .desc = "Sets a custom file path for patch manager config file. Can be used to manage 'profiles' for auto-patches. " @@ -1636,16 +1564,16 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "JSON", - }, - { + }; + c[launcher::Options::IntelSDEFolder] = { .title = "Intel SDE", .name = "sde", .desc = "Path to Intel SDE kit path for automatic attaching.", .type = OptionType::Text, .category = "Development", .picker = OptionPickerType::DirectoryPath - }, - { + }; + c[launcher::Options::PathToEa3Config] = { .title = "ea3-config.xml Override", .name = "e", .desc = "Sets a custom path to ea3-config.xml.", @@ -1653,8 +1581,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "XML", - }, - { + }; + c[launcher::Options::PathToAppConfig] = { .title = "app-config.xml Override", .name = "a", .desc = "Sets a custom path to app-config.xml.", @@ -1662,8 +1590,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "XML", - }, - { + }; + c[launcher::Options::PathToAvsConfig] = { .title = "avs-config.xml Override", .name = "v", .desc = "Sets a custom path to avs-config.xml.", @@ -1671,8 +1599,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "XML", - }, - { + }; + c[launcher::Options::PathToBootstrap] = { .title = "bootstrap.xml Override", .name = "b", .desc = "Sets a custom path to bootstrap.xml.", @@ -1680,8 +1608,8 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "XML", - }, - { + }; + c[launcher::Options::PathToLog] = { .title = "log.txt Override", .name = "y", .desc = "Sets a custom path to log.txt.", @@ -1689,59 +1617,58 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Path Overrides", .picker = OptionPickerType::FilePath, .file_extension = "TXT", - }, - { + }; + c[launcher::Options::APITCPPort] = { .title = "API TCP Port", .name = "api", .desc = "Port the API should be listening on.", .type = OptionType::Integer, .category = "Companion & API", - }, - { + }; + c[launcher::Options::APIPassword] = { .title = "API Password", .name = "apipass", .desc = "Set the custom user password needed to use the API.", .type = OptionType::Text, .category = "Companion & API", .sensitive = true, - }, - { + }; + c[launcher::Options::APIVerboseLogging] = { .title = "API Verbose Logging", .name = "apilogging", .desc = "Verbose logging of API activity.", .type = OptionType::Bool, .category = "API Dev", - }, - { + }; + c[launcher::Options::APISerialPort] = { .title = "API Serial Port", .name = "apiserial", .desc = "Serial port the API should be listening on.", .type = OptionType::Text, .category = "Serial API", - }, - { + }; + c[launcher::Options::APISerialBaud] = { .title = "API Serial Baud", .name = "apiserialbaud", .desc = "Baud rate for the serial port.", .type = OptionType::Integer, .category = "Serial API", - }, - { + }; + c[launcher::Options::APIPretty] = { .title = "API Pretty", .name = "apipretty", .desc = "Slower, but pretty API output.", .type = OptionType::Bool, .category = "API Dev", - }, - { + }; + c[launcher::Options::APIDebugMode] = { .title = "API Debug Mode", .name = "apidebug", .desc = "Enables API debugging mode.", .type = OptionType::Bool, .category = "API Dev", - }, - { - // APIScreenMirrorQuality + }; + c[launcher::Options::APIScreenMirrorQuality] = { .title = "API Screen Mirror Quality Override", .name = "apiscreenq", .desc = "JPEG compression level of mirrored images, overriding any client request. " @@ -1749,9 +1676,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "(0-100)", .category = "API Dev", - }, - { - // APIScreenMirrorDivide + }; + c[launcher::Options::APIScreenMirrorDivide] = { .title = "API Screen Mirror Divide Override", .name = "apiscreendiv", .desc = "Divide value of mirrored images, overriding any client request. " @@ -1761,9 +1687,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "1", .category = "API Dev", - }, - { - // APIStreamEnable + }; + c[launcher::Options::APIStreamEnable] = { .title = "API Video Stream Server Enable (EXPERIMENTAL)", .name = "apistream", .desc = "Allows companion apps to receive compressed video streams over the API. " @@ -1773,64 +1698,64 @@ static const std::vector OPTION_DEFINITIONS = { "Developers: see the wiki page for more details.", .type = OptionType::Bool, .category = "Companion & API", - }, - { + }; + c[launcher::Options::EnableAllIOModules] = { .title = "Enable All IO Modules", .name = "io", .desc = "Manually enable ALL IO emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableACIOModule] = { .title = "Enable ACIO Module", .name = "acio", .desc = "Manually enable ACIO emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableICCAModule] = { .title = "Enable ICCA Module", .name = "icca", .desc = "Manually enable ICCA emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableDEVICEModule] = { .title = "Enable DEVICE Module", .name = "device", .desc = "Manually enable DEVICE emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableEXTDEVModule] = { .title = "Enable EXTDEV Module", .name = "extdev", .desc = "Manually enable EXTDEV emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableAMI2000Module] = { .title = "Enable AMI2000 Module", .name = "ami2000", .desc = "Manually enable AMI2000 emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableSCIUNITModule] = { .title = "Enable SCIUNIT Module", .name = "sciunit", .desc = "Manually enable SCIUNIT emulation.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::EnableDevicePassthrough] = { .title = "Enable device passthrough", .name = "devicehookdisable", .desc = "Disable I/O and serial device hooks.", .type = OptionType::Bool, .category = "I/O Modules", - }, - { + }; + c[launcher::Options::ForceWinTouch] = { .title = "Touch Compatibility Mode (Disable Raw Input Touch)", .name = "wintouch", .desc = "For touch screen input, disable usage of Raw Input API and instead use " @@ -1838,25 +1763,24 @@ static const std::vector OPTION_DEFINITIONS = { "touch screens, but may result in worse performance and higher latency.", .type = OptionType::Bool, .category = "Touch Parameters", - }, - { + }; + c[launcher::Options::ForceTouchEmulation] = { .title = "Force Legacy Touch Emulation", .name = "touchemuforce", .desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. " "Do not enable this unless you have trouble with using a mouse to emulate touch input.", .type = OptionType::Bool, .category = "Touch Parameters", - }, - { + }; + c[launcher::Options::InvertTouchCoordinates] = { .title = "Invert Touch", .name = "touchinvert", .desc = "Inverts touch coordinates; only works for default raw input handler and IIDX/SDVX native touch handler, " "and not Windows Touch API.", .type = OptionType::Bool, .category = "Touch Parameters", - }, - { - // RawInputTouchAspectRatio + }; + c[launcher::Options::RawInputTouchAspectRatio] = { .title = "Raw Input Touch Fix Aspect Ratio", .name = "rawtouchaspect", .desc = "Compensate for letterboxing/pillarboxing when the game runs at a display mode with a " @@ -1872,18 +1796,16 @@ static const std::vector OPTION_DEFINITIONS = { {"on", ""}, {"off", ""}, }, - }, - { - // DisableTouchCardInsert + }; + c[launcher::Options::DisableTouchCardInsert] = { .title = "Disable Touch Card Insert (DEPRECATED - use -touchcard instead)", .name = "touchnocard", .desc = "Disables touch overlay card insert button.", .type = OptionType::Bool, .hidden = true, .category = "Touch Parameters", - }, - { - // spice2x_TouchCardInsert + }; + c[launcher::Options::spice2x_TouchCardInsert] = { .title = "Show Insert Card button (DEPRECATED)", .name = "sp2x-touchcard", .display_name = "touchcard", @@ -1893,45 +1815,43 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .category = "Touch Parameters", - }, - { - // ICCAReaderPort + }; + c[launcher::Options::ICCAReaderPort] = { .title = "ICCA Reader Port", .name = "reader", .desc = "Connects to and uses a ICCA on a given COM port.", .type = OptionType::Text, .category = "NFC Card Readers", - }, - { + }; + c[launcher::Options::ICCAReaderPortToggle] = { .title = "ICCA Reader Port (with toggle)", .name = "togglereader", .desc = "Connects to and uses a ICCA on a given COM port, and enabled NumLock toggling between P1/P2.", .type = OptionType::Text, .category = "NFC Card Readers", - }, - { + }; + c[launcher::Options::CardIOHIDReaderSupport] = { .title = "CardIO HID Reader Support", .name = "cardio", .desc = "Enables detection and support of cardio HID readers.", .type = OptionType::Bool, .category = "NFC Card Readers", - }, - { + }; + c[launcher::Options::CardIOHIDReaderOrderFlip] = { .title = "CardIO HID Reader Order Flip", .name = "cardioflip", .desc = "Flips the order of detection for P1/P2.", .type = OptionType::Bool, .category = "NFC Card Readers", - }, - { - // CardIOHIDReaderOrderToggle + }; + c[launcher::Options::CardIOHIDReaderOrderToggle] = { .title = "CardIO HID Reader Order Toggle", .name = "cardiotoggle", .desc = "Toggles reader between P1/P2 using the NumLock key state.", .type = OptionType::Bool, .category = "NFC Card Readers", - }, - { + }; + c[launcher::Options::HIDSmartCard] = { .title = "HID SmartCard (ACR122U)", .name = "scard", .desc = "Detects and uses HID smart card readers for card input. Developed for ACR122U reader.", @@ -1939,8 +1859,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = !WITH_SCARD, .category = "NFC Card Readers", .disabled = !WITH_SCARD, - }, - { + }; + c[launcher::Options::HIDSmartCardOrderFlip] = { .title = "HID SmartCard Order Flip", .name = "scardflip", .desc = "Flips the order of detection for P1/P2.", @@ -1948,8 +1868,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = !WITH_SCARD, .category = "NFC Card Readers", .disabled = !WITH_SCARD, - }, - { + }; + c[launcher::Options::HIDSmartCardOrderToggle] = { .title = "HID SmartCard Order Toggle", .name = "scardtoggle", .desc = "Toggles reader between P1/P2 using the NumLock key state.", @@ -1957,9 +1877,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = !WITH_SCARD, .category = "NFC Card Readers", .disabled = !WITH_SCARD, - }, - { - // HIDSmartCardIdConvert + }; + c[launcher::Options::HIDSmartCardIdConvert] = { .title = "HID SmartCard Fix UID", .name = "scardfix", .desc = "Modify behavior of SmartCard UID logic.\n\n" @@ -1978,32 +1897,30 @@ static const std::vector OPTION_DEFINITIONS = { {"all", "Force all cards"}, }, .disabled = !WITH_SCARD, - }, - { - // SextetStreamPort + }; + c[launcher::Options::SextetStreamPort] = { .title = "SextetStream Port", .name = "sextet", .desc = "Use a SextetStream device on a given COM port.", .type = OptionType::Text, .category = "I/O Options", - }, - { + }; + c[launcher::Options::EnableBemaniTools5API] = { .title = "Enable BemaniTools 5 API", .name = "bt5api", .desc = "Enables partial BemaniTools 5 API compatibility layer.", .type = OptionType::Bool, .category = "BT5 API", - }, - { + }; + c[launcher::Options::RealtimeProcessPriority] = { .title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)", .name = "realtime", .desc = "Sets the process priority to realtime; can help with odd lag spikes.", .type = OptionType::Bool, .hidden = true, .category = "Performance", - }, - { - // spice2x_ProcessPriority + }; + c[launcher::Options::spice2x_ProcessPriority] = { .title = "Process Priority", .name = "sp2x-processpriority", .display_name = "processpriority", @@ -2018,9 +1935,8 @@ static const std::vector OPTION_DEFINITIONS = { {"high", ""}, {"realtime", ""} }, - }, - { - // spice2x_ProcessAffinity + }; + c[launcher::Options::spice2x_ProcessAffinity] = { .title = "Process Affinity", .name = "sp2x-processaffinity", .display_name = "processaffinity", @@ -2030,9 +1946,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Hex, .category = "Performance", .picker = OptionPickerType::CpuAffinity, - }, - { - // spice2x_ProcessorEfficiencyClass + }; + c[launcher::Options::spice2x_ProcessorEfficiencyClass] = { .title = "Process Efficiency Class", .name = "sp2x-processefficiency", .display_name = "processefficiency", @@ -2045,8 +1960,8 @@ static const std::vector OPTION_DEFINITIONS = { {"pcores", "Performant cores only"}, {"ecores", "Efficient cores only"}, }, - }, - { + }; + c[launcher::Options::HeapSize] = { .title = "Heap Size", .name = "h", .desc = "Custom heap size in bytes.", @@ -2054,17 +1969,15 @@ static const std::vector OPTION_DEFINITIONS = { .category = "Development", }, // TODO: remove this and create an ignore list - { - // DisableGSyncDetection + c[launcher::Options::DisableGSyncDetection] = { .title = "(REMOVED) Disable G-Sync Detection", .name = "keepgsync", .desc = "Broken feature that was not implemented correctly.", .type = OptionType::Bool, .hidden = true, .category = "Graphics", - }, - { - // spice2x_NvapiProfile + }; + c[launcher::Options::spice2x_NvapiProfile] = { .title = "NVIDIA profile optimization", .name = "sp2x-nvprofile", .display_name = "nvprofile", @@ -2078,9 +1991,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "Graphics", .quick_setting_category = "Display", - }, - { - // DisableAudioHooks + }; + c[launcher::Options::DisableAudioHooks] = { .title = "Disable All Spice Audio Hooks", .name = "audiohookdisable", .desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n" @@ -2089,9 +2001,8 @@ static const std::vector OPTION_DEFINITIONS = { "Check this to allow games to natively access your audio device.", .type = OptionType::Bool, .category = "Audio Hacks", - }, - { - // spice2x_DisableVolumeHook + }; + c[launcher::Options::spice2x_DisableVolumeHook] = { .title = "Disable Audio Volume Hook", .name = "sp2x-volumehookdisable", .display_name = "volumehookdisable", @@ -2101,9 +2012,8 @@ static const std::vector OPTION_DEFINITIONS = { "Check this to allow games to freely change your volume.", .type = OptionType::Bool, .category = "Audio Hacks", - }, - { - // AudioShared + }; + c[launcher::Options::AudioShared] = { .title = "WASAPI Force Shared Mode & Auto-Resample (EXPERIMENTAL)", .name = "wasapishared", .desc = "This option converts all WASAPI exclusive mode requests to shared mode.\n\n" @@ -2113,23 +2023,21 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "Audio", .quick_setting_category = "Audio", - }, - { - // spice2x_LowLatencySharedAudio + }; + c[launcher::Options::spice2x_LowLatencySharedAudio] = { .title = "Low Latency Shared Audio", .name = "sp2x-lowlatencysharedaudio", .display_name = "lowlatencysharedaudio", .aliases= "lowlatencysharedaudio", .desc = "Force the usage of smallest buffer size supported by the device when shared mode audio is used. " - "Works for games using DirectSound or shared WASAPI; no effect for exclusive WASAPI and ASIO. " - "For best results (under 10ms), use the default Windows inbox audio driver instead of manufacturer supplied driver. " + "Works for games using DirectSound or shared WASAPI; no effect for exclusive WASAPI and ASIO. " + "For best results (under 10ms), use the default Windows inbox audio driver instead of manufacturer supplied driver. " "Requires Windows 10 and above.", .type = OptionType::Bool, .category = "Audio", .quick_setting_category = "Audio", - }, - { - // AudioBackend + }; + c[launcher::Options::AudioBackend] = { .title = "Spice Audio Hook Backend (DEPRECATED - use -asioconvert instead)", .name = "audiobackend", .desc = "Selects the audio backend to use when spice audio hook is enabled, overriding exclusive WASAPI. " @@ -2141,18 +2049,16 @@ static const std::vector OPTION_DEFINITIONS = { {"asio", "ASIO"}, {"waveout", "broken, do not use"} }, - }, - { - // AsioDriverId + }; + c[launcher::Options::AsioDriverId] = { .title = "Spice Audio Hook ASIO Driver ID (DEPRECATED - use -asioconvert instead)", .name = "asiodriverid", .desc = "Selects the ASIO driver id to use when Spice Audio Backend is set to ASIO.", .type = OptionType::Integer, .hidden = true, .category = "Audio Conversion", - }, - { - // AsioDriverName + }; + c[launcher::Options::AsioDriverName] = { .title = "WASAPI Exclusive to ASIO Conversion", .name = "asioconvert", .desc = "Converts WASAPI Exclusive audio output to ASIO. Value here should match registry key under HKLM\\SOFTWARE\\ASIO\\\n\n" @@ -2162,8 +2068,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .category = "Audio Conversion", .picker = OptionPickerType::AsioDriver, - }, - { + }; + c[launcher::Options::AudioDummy] = { .title = "WASAPI Dummy Context", .name = "audiodummy", .desc = "Uses a dummy `IAudioClient` context to maintain full audio control. " @@ -2171,9 +2077,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .category = "Audio Hacks", - }, - { - // DownmixAudioToStereo + }; + c[launcher::Options::DownmixAudioToStereo] = { .title = "WASAPI Stereo Downmix", .name = "downmix", .desc = "Downmixes multi-channel (surround) audio output to stereo.\n\n" @@ -2193,9 +2098,8 @@ static const std::vector OPTION_DEFINITIONS = { {"rear", "Rear channels only"}, {"side", "Side channels only"}, }, - }, - { - // VolumeBoost + }; + c[launcher::Options::VolumeBoost] = { .title = "WASAPI/ASIO Boost Audio Volume", .name = "volumeboost", .desc = "Artificially amplifies the hooked audio output by the selected amount, applied " @@ -2214,9 +2118,8 @@ static const std::vector OPTION_DEFINITIONS = { {"25", "+25 dB"}, {"30", "+30 dB"}, }, - }, - { - // AudioResample + }; + c[launcher::Options::AudioResample] = { .title = "WASAPI Exclusive Mode Resampling", .name = "resample", .desc = "Resamples the hooked audio output to a fixed sample rate before it reaches the " @@ -2234,9 +2137,8 @@ static const std::vector OPTION_DEFINITIONS = { {"176400", "176.4 kHz"}, {"192000", "192 kHz"}, }, - }, - { - // AudioExclusiveBuffer + }; + c[launcher::Options::AudioExclusiveBuffer] = { .title = "WASAPI Exclusive Buffer Size", .name = "exclusivebuffer", .desc = "Enlarges the WASAPI exclusive-mode device buffer to the specified duration in milliseconds.\n\n" @@ -2245,9 +2147,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "16", .category = "Audio Conversion", - }, - { - // AsioDownmixToStereo + }; + c[launcher::Options::AsioDownmixToStereo] = { .title = "ASIO 7.1 to Stereo Downmix", .name = "asiodownmix", .desc = "Extracts a single stereo channel pair from a multi-channel ASIO output, " @@ -2265,18 +2166,16 @@ static const std::vector OPTION_DEFINITIONS = { {"rear", "Rear (ch 4/5)"}, {"side", "Side (ch 6/7)"}, }, - }, - { - // DelayBy5Seconds + }; + c[launcher::Options::DelayBy5Seconds] = { .title = "Delay by 5 Seconds (DEPRECATED - use -sleepduration instead)", .name = "sleep", .desc = "Waits five seconds before starting the game.", .type = OptionType::Bool, .hidden = true, // superseded by sp2x-sleep_duration .category = "Miscellaneous", - }, - { - // spice2x_DelayByNSeconds + }; + c[launcher::Options::spice2x_DelayByNSeconds] = { .title = "Delay Game Launch", .name = "sp2x-sleepduration", .display_name = "sleepduration", @@ -2284,15 +2183,15 @@ static const std::vector OPTION_DEFINITIONS = { .desc = "Wait for N seconds before starting the game.", .type = OptionType::Integer, .category = "Miscellaneous", - }, - { + }; + c[launcher::Options::LoadStubs] = { .title = "Load KBT/KLD Stubs", .name = "stubs", .desc = "Enables loading kbt/kld stub files.", .type = OptionType::Bool, .category = "Development", - }, - { + }; + c[launcher::Options::AdjustOrientation] = { .title = "Adjust Display Orientation (DEPRECATED - use -autoorientation instead)", .name = "adjustorientation", .desc = "Automatically adjust the orientation of your display in portrait games. " @@ -2301,9 +2200,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, // superseded by sp2x-autoorientation .category = "Graphics", - }, - { - // spice2x_AutoOrientation + }; + c[launcher::Options::spice2x_AutoOrientation] = { .title = "Rotate Monitor", .name = "sp2x-autoorientation", .display_name = "autoorientation", @@ -2319,68 +2217,66 @@ static const std::vector OPTION_DEFINITIONS = { {"3", "Landscape, Flipped"} }, .quick_setting_category = "Display", - }, - { - // ChangeResolution + }; + c[launcher::Options::ChangeResolution] = { .title = "Change Monitor Resolution", .name = "changeres", .desc = "Changes monitor resolution before booting the game.", .type = OptionType::Text, .setting_name = "1280,720", .category = "Monitor" - }, - { + }; + c[launcher::Options::LogLevel] = { .title = "AVS Log Level", .name = "loglevel", .desc = "Set the level of detail for AVS log messages written to the log. Does not affect logging from spice.", .type = OptionType::Enum, .category = "Debug Log", .elements = {{"fatal", ""}, {"warning", ""}, {"info", ""}, {"misc", ""}, {"all", ""}, {"disable", ""}}, - }, - { + }; + c[launcher::Options::EAAutomap] = { .title = "EA Automap", .name = "automap", .desc = "Enable automap in patch configuration.", .type = OptionType::Bool, .category = "Network Dev", - }, - { + }; + c[launcher::Options::EANetdump] = { .title = "EA Netdump", .name = "netdump", .desc = "Enable automap in network dumping configuration.", .type = OptionType::Bool, .category = "Network Dev", - }, - { + }; + c[launcher::Options::BlockingLogger] = { .title = "Blocking Logger", .name = "logblock", .desc = "Slower but safer logging for debugging.", .type = OptionType::Bool, .category = "Debug Log", - }, - { + }; + c[launcher::Options::DebugCreateFile] = { .title = "Debug CreateFile", .name = "createfiledebug", .desc = "Outputs CreateFile debug prints.", .type = OptionType::Bool, .category = "Debug Log", - }, - { + }; + c[launcher::Options::VerboseGraphicsLogging] = { .title = "Verbose Graphics Logging", .name = "graphicsverbose", .desc = "Enable the verbose logging of graphics hook code.", .type = OptionType::Bool, .category = "Debug Log", - }, - { + }; + c[launcher::Options::VerboseAVSLogging] = { .title = "Verbose AVS Logging", .name = "avsverbose", .desc = "Enable the verbose logging of AVS filesystem functions.", .type = OptionType::Bool, .category = "Debug Log", - }, - { - // AllowEA3Verbose + }; + c[launcher::Options::AllowEA3Verbose] = { .title = "Allow EA3 Verbose Debug Logging", .name = "ea3verbose", .desc = "This option is only useful for network developers; leave this OFF.\n\n" @@ -2388,59 +2284,57 @@ static const std::vector OPTION_DEFINITIONS = { "When this is enabled, ea3/debug/verbose node in the XML will be kept and exposed to the game.", .type = OptionType::Bool, .category = "Debug Log", - }, - { + }; + c[launcher::Options::DisableColoredOutput] = { .title = "Disable Colored Output", .name = "nocolor", .desc = "Disable terminal colors for log outputs to console.", .type = OptionType::Bool, .category = "Debug Log", - }, - { + }; + c[launcher::Options::DisableACPHook] = { .title = "Disable ACP Hook", .name = "acphookdisable", .desc = "Force disable advanced code pages hooks for encoding.", .type = OptionType::Bool, .category = "Development", - }, - { + }; + c[launcher::Options::DisableSignalHandling] = { .title = "Disable Signal Handling", .name = "signaldisable", .desc = "Force disable signal handling.", .type = OptionType::Bool, .category = "Development", - }, - { + }; + c[launcher::Options::DisableDebugHooks] = { .title = "Disable Debug Hooks", .name = "dbghookdisable", .desc = "Disable hooks for debug functions (e.g. OutputDebugString).", .type = OptionType::Bool, .category = "Development", - }, - { + }; + c[launcher::Options::DisableAvsVfsDriveMountRedirection] = { .title = "Disable AVS VFS Drive Mount Redirection", .name = "avs-redirect-disable", .desc = "Disable D:/E:/F: AVS VFS mount redirection.", .type = OptionType::Bool, .category = "Development", - }, - { - // DisableAvsCache + }; + c[launcher::Options::DisableAvsCache] = { .title = "Disable AVS Cache", .name = "avscachedisable", .desc = "Disable optimization used for some games to cache data file info (only for IIDX32+ for now).", .type = OptionType::Bool, .category = "Development", - }, - { + }; + c[launcher::Options::OutputPEB] = { .title = "Output PEB", .name = "pebprint", .desc = "Prints PEB on startup to console.", .type = OptionType::Bool, .category = "Debug Log", - }, - { - // DumpSystemInfo + }; + c[launcher::Options::DumpSystemInfo] = { .title = "Dump System Information", .name = "sysdump", .desc = "Print system information to the log on startup. Default: basic.", @@ -2451,8 +2345,8 @@ static const std::vector OPTION_DEFINITIONS = { {"basic", "OS, CPU, SMBIOS, GPU"}, {"all", "basic + HID devices"}, }, - }, - { + }; + c[launcher::Options::QKSArgs] = { .title = "QKS Arguments Override", .name = "qksargs", .desc = "Command line arguments passed to the game.", @@ -2460,8 +2354,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "QuizKnock STADIUM", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::CCJArgs] = { .title = "CCJ Arguments Override", .name = "ccjargs", .desc = "Command line arguments passed to the game. " @@ -2470,8 +2364,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "Chase Chase Jokers", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::CCJMouseTrackball] = { .title = "CCJ Mouse Trackball", .name = "ccjmousetb", .desc = "Use mouse for trackball input; disables any keyboard / joystick control for the trackball. "\ @@ -2480,16 +2374,16 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Chase Chase Jokers", .category = "Game Options", .quick_setting_category = "Game", - }, - { + }; + c[launcher::Options::CCJMouseTrackballWithToggle] = { .title = "CCJ Mouse Trackball Toggle", .name = "ccjmousetbt", .desc = "Instead of holding RMB, click RMB to toggle trackball input.", .type = OptionType::Bool, .game_name = "Chase Chase Jokers", .category = "Game Options", - }, - { + }; + c[launcher::Options::CCJTrackballSensitivity] = { .title = "CCJ Trackball Sensitivity", .name = "ccjtrackballsens", .desc = "Adjust sensitivity of trackball. Affects both digital and analog input. Default: 10.", @@ -2497,8 +2391,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(0-255)", .game_name = "Chase Chase Jokers", .category = "Game Options", - }, - { + }; + c[launcher::Options::MFGArgs] = { .title = "MFG Arguments Override", .name = "mfgargs", .desc = "Command line arguments passed to the game.", @@ -2506,8 +2400,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "Mahjong Fight Girl", .category = "Advanced Game Options", - }, - { + }; + c[launcher::Options::MFGCabType] = { .title = "MFG Cabinet Type", .name = "mfgcabtype", .desc = "MFG Cabinet Type. Default is HG.", @@ -2521,8 +2415,8 @@ static const std::vector OPTION_DEFINITIONS = { {"C", "C"}, {"UKS", "UKS"}, }, - }, - { + }; + c[launcher::Options::MFGNoIO] = { .title = "MFG Disable IO Emulation", .name = "mfgnoio", .desc = "Disables BI2X hooks for MFG.", @@ -2530,9 +2424,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "Mahjong Fight Girl", .category = "Advanced Game Options" - }, - { - // PCArgs + }; + c[launcher::Options::PCArgs] = { .title = "PC Arguments Override", .name = "pcargs", .desc = "Command line arguments passed to the game.", @@ -2540,9 +2433,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "Polaris Chord", .category = "Advanced Game Options", - }, - { - // PCNoIO + }; + c[launcher::Options::PCNoIO] = { .title = "PC Disable IO Emulation", .name = "pcnoio", .desc = "Disables BI2X hooks for PC.", @@ -2550,18 +2442,16 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "", .game_name = "Polaris Chord", .category = "Advanced Game Options" - }, - { - // PCKnobMode + }; + c[launcher::Options::PCKnobMode] = { .title = "PC Fader Knobs Mode", .name = "pcknobs", .desc = "Allows SDVX knobs and IIDX turntables to be bound as Polaris Chord faders.", .type = OptionType::Bool, .game_name = "Polaris Chord", .category = "Game Options", - }, - { - // spice2x_LightsOverallBrightness + }; + c[launcher::Options::spice2x_LightsOverallBrightness] = { .title = "Lights Brightness Adjustment", .name = "sp2x-lights-brightness", .display_name = "lightsbrightness", @@ -2571,9 +2461,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "(0-100)", .category = "I/O Options", - }, - { - // spice2x_WindowBorder + }; + c[launcher::Options::spice2x_WindowBorder] = { .title = "Window Border Style", .name = "sp2x-windowborder", .display_name = "windowborder", @@ -2589,9 +2478,8 @@ static const std::vector OPTION_DEFINITIONS = { {"1", "borderless"}, {"2", "resizable window"}, }, - }, - { - // spice2x_WindowSize + }; + c[launcher::Options::spice2x_WindowSize] = { .title = "Window Size", .name = "sp2x-windowsize", .display_name = "windowsize", @@ -2601,9 +2489,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "1280,720", .category = "Windowed Settings", - }, - { - // spice2x_WindowPosition + }; + c[launcher::Options::spice2x_WindowPosition] = { .title = "Window Position", .name = "sp2x-windowpos", .display_name = "windowpos", @@ -2613,9 +2500,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .setting_name = "120,240", .category = "Windowed Settings", - }, - { - // spice2x_WindowAlwaysOnTop + }; + c[launcher::Options::spice2x_WindowAlwaysOnTop] = { .title = "Window Always on Top", .name = "sp2x-windowalwaysontop", .display_name = "windowalwaysontop", @@ -2624,26 +2510,23 @@ static const std::vector OPTION_DEFINITIONS = { "Can also be changed in Screen Resize window (default F11).", .type = OptionType::Bool, .category = "Windowed Settings", - }, - { - // WindowForceScaling + }; + c[launcher::Options::WindowForceScaling] = { .title = "Window Forced Render Scaling", .name = "windowscale", .desc = "For windowed mode: forcibly set DX9 back buffer dimensions to match window size. " "Reduces pixelated scaling artifacts. Works great for some games, but can COMPLETELY BREAK other games - YMMV!", .type = OptionType::Bool, .category = "Windowed Settings", - }, - { - // WindowDisableRoundedCorners + }; + c[launcher::Options::WindowDisableRoundedCorners] = { .title = "Disable Round Window Corners", .name = "windownoroundcorners", .desc = "Windows 11 and above only: Disables rounded corners on the game window(s).", .type = OptionType::Bool, .category = "Windowed Settings", - }, - { - // GitaDoraWindowedMainMonitor + }; + c[launcher::Options::GitaDoraWindowedMainMonitor] = { .title = "GitaDora Windowed Main Monitor", .name = "gdwmainmonitor", .desc = "For GITADORA Arena windowed mode: place and size the " @@ -2653,9 +2536,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Windowed Settings", .picker = OptionPickerType::Monitor, - }, - { - // GitaDoraWindowedLeftMonitor + }; + c[launcher::Options::GitaDoraWindowedLeftMonitor] = { .title = "GitaDora Windowed LEFT Monitor", .name = "gdwleftmonitor", .desc = "For GITADORA Arena windowed mode: place and size the " @@ -2665,9 +2547,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Windowed Settings", .picker = OptionPickerType::Monitor, - }, - { - // GitaDoraWindowedRightMonitor + }; + c[launcher::Options::GitaDoraWindowedRightMonitor] = { .title = "GitaDora Windowed RIGHT Monitor", .name = "gdwrightmonitor", .desc = "For GITADORA Arena windowed mode: place and size the " @@ -2677,9 +2558,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Windowed Settings", .picker = OptionPickerType::Monitor, - }, - { - // GitaDoraWindowedSmallMonitor + }; + c[launcher::Options::GitaDoraWindowedSmallMonitor] = { .title = "GitaDora Windowed SMALL Monitor", .name = "gdwsmallmonitor", .desc = "For GITADORA Arena windowed mode: place and size the " @@ -2689,9 +2569,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "GitaDora", .category = "Game Windowed Settings", .picker = OptionPickerType::Monitor, - }, - { - // GitaDoraWindowedSmallSize + }; + c[launcher::Options::GitaDoraWindowedSmallSize] = { .title = "GitaDora Windowed SMALL Size", .name = "gdwsmallsize", .desc = "Size of the GITADORA Arena SMALL touch window. Defaults to (800,1280).", @@ -2699,9 +2578,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "800,1280", .game_name = "GitaDora", .category = "Game Windowed Settings", - }, - { - // GitaDoraWindowedSmallPosition + }; + c[launcher::Options::GitaDoraWindowedSmallPosition] = { .title = "GitaDora Windowed SMALL Position", .name = "gdwsmallpos", .desc = "Initial position of the GITADORA Arena SMALL touch window. Defaults to (0,0).", @@ -2709,9 +2587,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "0,0", .game_name = "GitaDora", .category = "Game Windowed Settings", - }, - { - // spice2x_IIDXWindowedSubscreenSize + }; + c[launcher::Options::spice2x_IIDXWindowedSubscreenSize] = { .title = "IIDX Windowed Subscreen Size", .name = "iidxwsubsize", .desc = "Size of the subscreen window. Defaults to (1280,720).", @@ -2719,9 +2596,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "1280,720", .game_name = "Beatmania IIDX", .category = "Game Windowed Settings", - }, - { - // spice2x_IIDXWindowedSubscreenPosition + }; + c[launcher::Options::spice2x_IIDXWindowedSubscreenPosition] = { .title = "IIDX Windowed Subscreen Position", .name = "iidxwsubpos", .desc = "Initial position of the subscreen window. Defaults to (0,0).", @@ -2729,27 +2605,24 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "0,0", .game_name = "Beatmania IIDX", .category = "Game Windowed Settings", - }, - { - // IIDXWindowedSubscreenBorderless + }; + c[launcher::Options::IIDXWindowedSubscreenBorderless] = { .title = "IIDX Windowed Subscreen Borderless", .name = "iidxwsubborderless", .desc = "Remove window decoration from windowed subscreen.", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Game Windowed Settings", - }, - { - // IIDXWindowedSubscreenAlwaysOnTop + }; + c[launcher::Options::IIDXWindowedSubscreenAlwaysOnTop] = { .title = "IIDX Windowed Subscreen Always On Top", .name = "iidxwsubtop", .desc = "Keep windowed subscreen on top.", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Game Windowed Settings", - }, - { - // spice2x_JubeatLegacyTouch + }; + c[launcher::Options::spice2x_JubeatLegacyTouch] = { .title = "JB Legacy Touch Targets (Deprecated - use -jubeattouchalgo instead)", .name = "sp2x-jubeatlegacytouch", .display_name = "jubeatlegacytouch", @@ -2759,8 +2632,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Jubeat", .category = "Game Options", - }, - { + }; + c[launcher::Options::JubeatTouchAlgo] = { .title = "JB Touch Algorithm", .name = "jubeattouchalgo", .desc = "For touch screen players: choose the touch algorithm to use.\n\n" @@ -2778,9 +2651,8 @@ static const std::vector OPTION_DEFINITIONS = { {"accurate", ""}, }, .quick_setting_category = "Game", - }, - { - // JubeatTouchDebug + }; + c[launcher::Options::JubeatTouchDebug] = { .title = "JB Touch Debug Overlay", .name = "jubeattouchdebug", .desc = "For touch screen players: draw a debug overlay on the main display. " @@ -2799,9 +2671,8 @@ static const std::vector OPTION_DEFINITIONS = { {"all", ""}, }, .quick_setting_category = "Game", - }, - { - // JubeatTouchDebounce + }; + c[launcher::Options::JubeatTouchDebounce] = { .title = "JB Touch Debounce", .name = "jubeattouchdebounce", .desc = "For touch screen players: ignore extremely quick touches by requiring a " @@ -2813,9 +2684,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Jubeat", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // RBTouchDebug + }; + c[launcher::Options::RBTouchDebug] = { .title = "RB Touch Debug Overlay", .name = "rbtouchdebug", .desc = "Draw lines to show IR sensor emulation state.\n\n" @@ -2825,9 +2695,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Reflec Beat", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // spice2x_RBTouchScale + }; + c[launcher::Options::spice2x_RBTouchScale] = { .title = "RB Touch Emulation Scale", .name = "sp2x-rbscaletouch", .display_name = "rbscaletouch", @@ -2837,9 +2706,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .game_name = "Reflec Beat", .category = "Game Options", - }, - { - // RBTouchSize + }; + c[launcher::Options::RBTouchSize] = { .title = "RB Touch Emulation Size (DEPRECATED - no longer has any effect)", .name = "rbtouchsize", .desc = "This option is deprecated and no longer has any effect. " @@ -2851,9 +2719,8 @@ static const std::vector OPTION_DEFINITIONS = { .elements = { {"3", "3x3"}, }, - }, - { - // RBTouchPollRate + }; + c[launcher::Options::RBTouchPollRate] = { .title = "RB Touch Emulation Poll Hz", .name = "rbtouchhz", .desc = "By default, the game polls for touch at ~125Hz. " @@ -2864,9 +2731,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "250", .game_name = "Reflec Beat", .category = "Game Options", - }, - { - // spice2x_AsioForceUnload + }; + c[launcher::Options::spice2x_AsioForceUnload] = { .title = "ASIO Force Unload On Stop", .name = "sp2x-asioforceunload", .display_name = "asioforceunload", @@ -2875,9 +2741,8 @@ static const std::vector OPTION_DEFINITIONS = { "Used for working around ASIO drivers that lock up after force quitting games.", .type = OptionType::Bool, .category = "Audio Hacks", - }, - { - // spice2x_IIDXNoESpec + }; + c[launcher::Options::spice2x_IIDXNoESpec] = { .title = "IIDX Disable E-spec I/O", .name = "sp2x-iidxnoespec", .display_name = "iidxnoespec", @@ -2887,9 +2752,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // spice2x_IIDXWindowedTDJ + }; + c[launcher::Options::spice2x_IIDXWindowedTDJ] = { .title = "IIDX TDJ Windowed Mode (DEPRECATED - just use -iidxtdj and -w together)", .name = "sp2x-iidxtdjw", .display_name = "iidxtdjw", @@ -2900,9 +2764,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Beatmania IIDX", .category = "Game Options", - }, - { - // spice2x_DRSDisableTouch + }; + c[launcher::Options::spice2x_DRSDisableTouch] = { .title = "DRS Disable Touch Input", .name = "sp2x-drsdisabletouch", .display_name = "drsdisabletouch", @@ -2911,9 +2774,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "DANCERUSH", .category = "Game Options", - }, - { - // spice2x_DRSTransposeTouch + }; + c[launcher::Options::spice2x_DRSTransposeTouch] = { .title = "DRS Transpose Touch Input", .name = "sp2x-drstransposetouch", .display_name = "drstransposetouch", @@ -2922,9 +2784,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "DANCERUSH", .category = "Game Options", - }, - { - // DRSRGBCameraHook + }; + c[launcher::Options::DRSRGBCameraHook] = { .title = "DRS RGB Camera Hook", .name = "drsrgbcamhook", .desc = "Hook into the RGB camera detection and attempt to use your own non-official webcam. " @@ -2933,9 +2794,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "DANCERUSH", .category = "Cab Peripherals", - }, - { - // spice2x_IIDXNativeTouch + }; + c[launcher::Options::spice2x_IIDXNativeTouch] = { .title = "IIDX Native Touch (DEPRECATED - no longer needed)", .name = "sp2x-iidxnativetouch", .display_name = "iidxnativetouch", @@ -2946,9 +2806,8 @@ static const std::vector OPTION_DEFINITIONS = { .hidden = true, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // spice2x_IIDXNoSub + }; + c[launcher::Options::spice2x_IIDXNoSub] = { .title = "IIDX TDJ Subscreen Disable", .name = "sp2x-iidxnosub", .display_name = "iidxnosub", @@ -2958,9 +2817,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Beatmania IIDX", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // IIDXSubMonitorOverride + }; + c[launcher::Options::IIDXSubMonitorOverride] = { .title = "IIDX TDJ Subscreen Monitor Override", .name = "iidxsubmonitor", .desc = "If you have three or more monitors, this option can be set to tell the game which monitor is the subscreen.", @@ -2969,9 +2827,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Beatmania IIDX", .category = "Full Screen Settings", .picker = OptionPickerType::Monitor, - }, - { - // spice2x_IIDXEmulateSubscreenKeypadTouch + }; + c[launcher::Options::spice2x_IIDXEmulateSubscreenKeypadTouch] = { .title = "IIDX TDJ Subscreen Keypad Touch Emulation", .name = "sp2x-iidxsubpoke", .display_name = "iidxsubpoke", @@ -2982,9 +2839,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // spice2x_AutoCard + }; + c[launcher::Options::spice2x_AutoCard] = { .title = "Auto Card Insert", .name = "sp2x-autocard", .display_name = "autocard", @@ -3002,9 +2858,8 @@ static const std::vector OPTION_DEFINITIONS = { {"both", ""}, }, .quick_setting_category = "Network", - }, - { - // spice2x_TapeLedAlgorithm + }; + c[launcher::Options::spice2x_TapeLedAlgorithm] = { .title = "Tape LED Avg Algorithm", .name = "sp2x-tapeledalgo", .display_name = "tapeledalgo", @@ -3020,9 +2875,8 @@ static const std::vector OPTION_DEFINITIONS = { {"last", "Last LED"}, {"avg", "Average color"}, }, - }, - { - // spice2x_NoNVAPI + }; + c[launcher::Options::spice2x_NoNVAPI] = { .title = "NVAPI Block", .name = "sp2x-nonvapi", .display_name = "nonvapi", @@ -3032,9 +2886,8 @@ static const std::vector OPTION_DEFINITIONS = { "hex edits to boot the game correctly.", .type = OptionType::Bool, .category = "Graphics", - }, - { - // spice2x_NoD3D9DeviceHook + }; + c[launcher::Options::spice2x_NoD3D9DeviceHook] = { .title = "Disable D3D9 Device Hook (DEPRECATED - no longer needed for TDJ recording)", .name = "sp2x-nod3d9devhook", .display_name = "nod3d9devhook", @@ -3045,9 +2898,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .hidden = true, .category = "Graphics", - }, - { - // spice2x_SDVXNoSub + }; + c[launcher::Options::spice2x_SDVXNoSub] = { .title = "SDVX Subscreen Disable", .name = "sp2x-sdvxnosub", .display_name = "sdvxnosub", @@ -3057,9 +2909,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // SDVXFullscreenLandscape + }; + c[launcher::Options::SDVXFullscreenLandscape] = { .title = "SDVX Full Screen Landscape Mode (SDVX5+, EXPERIMENTAL)", .name = "sdvxlandscape", .desc = @@ -3071,9 +2922,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Sound Voltex", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // spice2x_EnableSMXStage + }; + c[launcher::Options::spice2x_EnableSMXStage] = { .title = "StepManiaX Stage Lighting Support", .name = "sp2x-smx-stage", .display_name = "smxstage", @@ -3082,9 +2932,8 @@ static const std::vector OPTION_DEFINITIONS = { "For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.", .type = OptionType::Bool, .category = "I/O Options", - }, - { - // spice2x_EnableSMXDedicab + }; + c[launcher::Options::spice2x_EnableSMXDedicab] = { .title = "StepManiaX Dedicated Cabinet Lighting Support", .name = "sp2x-smx-dedicab", .display_name = "smxdedicab", @@ -3093,9 +2942,8 @@ static const std::vector OPTION_DEFINITIONS = { "For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.", .type = OptionType::Bool, .category = "I/O Options" - }, - { - // IIDXRecQuality + }; + c[launcher::Options::IIDXRecQuality] = { .title = "IIDX Recording Quality", .name = "iidxreccqp", .desc = "WARNING: double check if your network allows this & your hardware supports it.\n\n" @@ -3104,18 +2952,16 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // IIDXRecDisable + }; + c[launcher::Options::IIDXRecDisable] = { .title = "IIDX Recording Force Disable", .name = "iidxnorec", .desc = "When enabled, this prevents the play record feature from being used.", .type = OptionType::Bool, .game_name = "Beatmania IIDX", .category = "Advanced Game Options", - }, - { - // MidiAlgoVer + }; + c[launcher::Options::MidiAlgoVer] = { .title = "MIDI Note Input Algorithm", .name = "midialgo", .desc = @@ -3133,9 +2979,8 @@ static const std::vector OPTION_DEFINITIONS = { {"v2_drum", ""}, {"legacy", ""}, }, - }, - { - // MidiNoteSustain + }; + c[launcher::Options::MidiNoteSustain] = { .title = "MIDI Note Min. Sustain", .name = "midisustain", .desc = @@ -3147,9 +2992,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Integer, .setting_name = "20", .category = "I/O Options", - }, - { - // DDRP4IOBufferMode + }; + c[launcher::Options::DDRP4IOBufferMode] = { .title = "DDR P4IO Buffer Algorithm", .name = "ddrp4iobuffer", .desc = @@ -3167,9 +3011,8 @@ static const std::vector OPTION_DEFINITIONS = { {"thread", ""}, {"backfill", ""}, }, - }, - { - // InputRequiresFocus + }; + c[launcher::Options::InputRequiresFocus] = { .title = "Input Requires Focus", .name = "inputfocus", .desc = @@ -3184,9 +3027,8 @@ static const std::vector OPTION_DEFINITIONS = { {"naive", ""}, {"always", ""}, }, - }, - { - // NostalgiaPoke + }; + c[launcher::Options::NostalgiaPoke] = { .title = "Nost Screen Poke", .name = "nostpoke", .desc = @@ -3198,9 +3040,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Nostalgia", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // NostalgiaTouchMode + }; + c[launcher::Options::NostalgiaTouchMode] = { .title = "Nostalgia Touch Piano", .name = "nosttouch", .desc = @@ -3212,9 +3053,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Nostalgia", .category = "Game Options", .quick_setting_category = "Game", - }, - { - // ForceBackBufferCount + }; + c[launcher::Options::ForceBackBufferCount] = { .title = "V-Sync Buffering", .name = "vsyncbuffer", .desc = "Using triple buffering may improve cases of micro-stuttering but increases input latency.", @@ -3224,9 +3064,8 @@ static const std::vector OPTION_DEFINITIONS = { {"2", "double buffer"}, {"3", "triple buffer"}, }, - }, - { - // SDVXWindowedSubscreenSize + }; + c[launcher::Options::SDVXWindowedSubscreenSize] = { .title = "SDVX Windowed Subscreen Size", .name = "sdvxwsubsize", .desc = "Size of the subscreen window. Defaults to (1920,1080).", @@ -3234,9 +3073,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "1920,1080", .game_name = "Sound Voltex", .category = "Game Windowed Settings", - }, - { - // SDVXWindowedSubscreenPosition + }; + c[launcher::Options::SDVXWindowedSubscreenPosition] = { .title = "SDVX Windowed Subscreen Position", .name = "sdvxwsubpos", .desc = "Initial position of the subscreen window. Defaults to (0,0).", @@ -3244,36 +3082,32 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "0,0", .game_name = "Sound Voltex", .category = "Game Windowed Settings", - }, - { - // SDVXWindowedSubscreenBorderless + }; + c[launcher::Options::SDVXWindowedSubscreenBorderless] = { .title = "SDVX Windowed Subscreen Borderless", .name = "sdvxwsubborderless", .desc = "Remove window decoration from windowed subscreen.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Game Windowed Settings", - }, - { - // SDVXWindowedSubscreenAlwaysOnTop + }; + c[launcher::Options::SDVXWindowedSubscreenAlwaysOnTop] = { .title = "SDVX Windowed Subscreen Always On Top", .name = "sdvxwsubtop", .desc = "Keep windowed subscreen on top.", .type = OptionType::Bool, .game_name = "Sound Voltex", .category = "Game Windowed Settings", - }, - { - // LovePlusCamEnable + }; + c[launcher::Options::LovePlusCamEnable] = { .title = "LovePlus Camera Enable", .name = "lovepluscam", .desc = "Allow game to access camera; camera must be compatible with game.", .type = OptionType::Bool, .game_name = "LovePlus", .category = "Cab Peripherals", - }, - { - // LovePlusPrinterOutputPath + }; + c[launcher::Options::LovePlusPrinterOutputPath] = { .title = "LovePlus Printer Output Path", .name = "lpprinterpath", .desc = "Path to folder where images will be stored.", @@ -3281,25 +3115,24 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "LovePlus", .category = "Cab Peripherals", .picker = OptionPickerType::DirectoryPath, - }, - { + }; + c[launcher::Options::LovePlusPrinterOutputClear] = { .title = "LovePlus Printer Output Clear", .name = "lpprinterclear", .desc = "Clean up saved images in the output directory on startup.", .type = OptionType::Bool, .game_name = "LovePlus", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::LovePlusPrinterOutputOverwrite] = { .title = "LovePlus Printer Output Overwrite", .name = "lpprinteroverwrite", .desc = "Always overwrite the same file in output directory.", .type = OptionType::Bool, .game_name = "LovePlus", .category = "Cab Peripherals", - }, - { - // LovePlusPrinterOutputFormat + }; + c[launcher::Options::LovePlusPrinterOutputFormat] = { .title = "LovePlus Printer Output Format", .name = "lpprinterformat", .desc = "File format for printer output.", @@ -3307,8 +3140,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(png/bmp/tga/jpg)", .game_name = "LovePlus", .category = "Cab Peripherals", - }, - { + }; + c[launcher::Options::LovePlusPrinterJPGQuality] = { .title = "LovePlus Printer JPG Quality", .name = "lpprinterjpgquality", .desc = "Quality setting in percent if JPG format is used.", @@ -3316,9 +3149,8 @@ static const std::vector OPTION_DEFINITIONS = { .setting_name = "(0-100)", .game_name = "LovePlus", .category = "Cab Peripherals", - }, - { - // OptionConflictResolution + }; + c[launcher::Options::OptionConflictResolution] = { .title = "Command Line Args Override", .name = "cmdoverride", .desc = "By default, option values in spicecfg take precedence over command-line args, for legacy compat.\n" @@ -3326,9 +3158,8 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Bool, .category = "Development", .disabled = true, - }, - { - // OtocaCamHook + }; + c[launcher::Options::OtocaCamHook] = { .title = "Otoca Camera Check Bypass", .name = "otocacamhook", .desc = @@ -3338,9 +3169,8 @@ static const std::vector OPTION_DEFINITIONS = { .game_name = "Otoca D'or", .category = "Cab Peripherals", .quick_setting_category = "Game", - }, - { - // DisableHighResTimer + }; + c[launcher::Options::DisableHighResTimer] = { .title = "Use Legacy Timers", .name = "notimerhacks", .desc = "Disables high resolution timers, reverting to legacy behavior. " @@ -3348,18 +3178,16 @@ static const std::vector OPTION_DEFINITIONS = { "unless you're on a resource-constrained system (e.g., old cabinet PC)", .type = OptionType::Bool, .category = "Performance" - }, - { - // EnableICMPHook + }; + c[launcher::Options::EnableICMPHook] = { .title = "Enable ICMP Emulation", .name = "icmphook", .desc = "Emulate keepalive ping replies in user mode, so the game does not need to " "open privileged raw ICMP sockets.", .type = OptionType::Bool, .category = "Network Dev", - }, - { - // AutoElevate + }; + c[launcher::Options::AutoElevate] = { .title = "Run as", .name = "runas", .desc = "Controls whether spice will automatically re-launch with administrator privileges at startup.\n\n" @@ -3373,44 +3201,39 @@ static const std::vector OPTION_DEFINITIONS = { {"admin", ""}, {"user", ""}, }, - }, - { - // CfgForceSoftwareRender + }; + c[launcher::Options::CfgForceSoftwareRender] = { .title = "Configurator Force Software Rendering", .name = "forcesoftware", .desc = "Forces the use of software rendering instead of hardware acceleration for " "the configurator.", .type = OptionType::Bool, .category = "Development", - }, - { - // OBSWebSocketEnabled + }; + c[launcher::Options::OBSWebSocketEnabled] = { .title = "OBS WebSocket Enable", .name = "obsenable", .desc = "Enables the in-game OBS Control overlay and its connection to the OBS Studio " "obs-websocket (v5) server.", .type = OptionType::Bool, .category = "OBS Control", - }, - { - // OBSWebSocketHost + }; + c[launcher::Options::OBSWebSocketHost] = { .title = "OBS WebSocket Host", .name = "obshost", .desc = "Host name or IP address of the OBS Studio obs-websocket (v5) server used by " "the in-game OBS Control overlay. Defaults to 127.0.0.1 when left empty.", .type = OptionType::Text, .category = "OBS Control", - }, - { - // OBSWebSocketPort + }; + c[launcher::Options::OBSWebSocketPort] = { .title = "OBS WebSocket Port", .name = "obsport", .desc = "Port of the OBS Studio obs-websocket (v5) server. Defaults to 4455 when left empty.", .type = OptionType::Integer, .category = "OBS Control", - }, - { - // OBSWebSocketPassword + }; + c[launcher::Options::OBSWebSocketPassword] = { .title = "OBS WebSocket Password", .name = "obspass", .desc = "Password for the OBS Studio obs-websocket (v5) server. Leave empty if " @@ -3418,33 +3241,32 @@ static const std::vector OPTION_DEFINITIONS = { .type = OptionType::Text, .category = "OBS Control", .sensitive = true, - }, - { - // OBSWebSocketDebug + }; + c[launcher::Options::OBSWebSocketDebug] = { .title = "OBS WebSocket Debug", .name = "obsdebug", .desc = "Writes the OBS WebSocket client's internal connection diagnostics to the log. " "Only enable this when troubleshooting connection problems.", .type = OptionType::Bool, .category = "OBS Control", - }, - { - // ScreenshotIncludeOverlay + }; + c[launcher::Options::ScreenshotIncludeOverlay] = { .title = "Include Overlay in Screenshots", .name = "screenshotoverlay", .desc = "Includes Spice overlay in screenshots.", .type = OptionType::Bool, .category = "General Overlay", - }, - { - // ScreenshotSubscreens + }; + c[launcher::Options::ScreenshotSubscreens] = { .title = "Include Subscreens in Screenshots", .name = "screenshotsub", .desc = "Saves each subscreen as a separate PNG alongside the primary screenshot.", .type = OptionType::Bool, .category = "General Overlay", - }, -}; + }; + + return c; +}); const std::vector &launcher::get_categories(Options::OptionsCategory category) { switch (category) { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index 89dde71..e93c56a 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -325,6 +325,8 @@ namespace launcher { OBSWebSocketDebug, ScreenshotIncludeOverlay, ScreenshotSubscreens, + + _Count, }; enum class OptionsCategory { From f11b9d0189245ddfafae311aa14a3c34a1ddaeb4 Mon Sep 17 00:00:00 2001 From: CHO-HSUN-TE <44806508+SugiuraAyano@users.noreply.github.com> Date: Wed, 9 Sep 2026 05:20:15 +0800 Subject: [PATCH 09/19] 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 --- src/spice2x/external/scard/scard.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/spice2x/external/scard/scard.cpp b/src/spice2x/external/scard/scard.cpp index ac5c599..5ac7386 100644 --- a/src/spice2x/external/scard/scard.cpp +++ b/src/spice2x/external/scard/scard.cpp @@ -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; From 531e7a54b30a85ae4c7884734fd574641b30b6e0 Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Thu, 10 Sep 2026 20:00:07 +0000 Subject: [PATCH 10/19] 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. --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/hooks/nicspoof.cpp | 546 ++++++++++++++++++++++++++++++ src/spice2x/hooks/nicspoof.h | 3 + src/spice2x/launcher/launcher.cpp | 10 + src/spice2x/launcher/options.cpp | 9 + src/spice2x/launcher/options.h | 1 + 6 files changed, 570 insertions(+) create mode 100644 src/spice2x/hooks/nicspoof.cpp create mode 100644 src/spice2x/hooks/nicspoof.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index 9a9f09d..d194d82 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -616,6 +616,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/networkhook.cpp hooks/icmphook_net.cpp hooks/icmphook_iphlpapi.cpp + hooks/nicspoof.cpp hooks/powrprof.cpp #hooks/rom.cpp hooks/setupapihook.cpp diff --git a/src/spice2x/hooks/nicspoof.cpp b/src/spice2x/hooks/nicspoof.cpp new file mode 100644 index 0000000..3ba787c --- /dev/null +++ b/src/spice2x/hooks/nicspoof.cpp @@ -0,0 +1,546 @@ +#define _WIN32_WINNT 0x0601 + +#include "nicspoof.h" + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "util/detour.h" +#include "util/logging.h" + +namespace { +constexpr uint32_t k_local_ip = 0x0A390302u; /* 10.57.3.2 */ +constexpr uint32_t k_gateway = 0x0A390301u; /* 10.57.3.1 */ +constexpr uint32_t k_dns = 0x0A390301u; /* 10.57.3.1 */ +constexpr uint32_t k_dhcp = 0x0A390301u; /* 10.57.3.1 */ +constexpr uint32_t k_mask = 0xFFFFFF00u; /* 255.255.255.0 */ +constexpr uint32_t k_subnet = 0x0A390300u; +constexpr int k_prefix_len = 24; +constexpr DWORD k_ifindex = 77; +constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37}; +constexpr char k_hostname[] = "N1CSP00F"; +constexpr char k_domain[] = "local"; +constexpr char k_hostname_fqdn[] = "N1CSP00F.local"; +constexpr wchar_t k_domain_w[] = L"local"; +constexpr char k_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"; + +#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(k_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(k_ifindex)); + g_log_adapters = false; + } + return ERROR_SUCCESS; +} + +DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) { + ULONG need = static_cast(sizeof(FIXED_INFO)); + char dns[16]; + + 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_dns, dns, sizeof(dns)); + strcpy(p->DnsServerList.IpAddress.String, dns); + p->DnsServerList.IpMask.String[0] = 0; + p->DnsServerList.Context = 0; + p->DnsServerList.Next = nullptr; + p->CurrentDnsServer = &p->DnsServerList; + p->NodeType = 1; + p->EnableDns = 1; + *s = need; + if (g_log_params) { + log_misc("network", "NIC spoof GetNetworkParams host={} domain={} dns={}", + k_hostname, k_domain, dns); + 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(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) + + sizeof(IP_ADAPTER_GATEWAY_ADDRESS) + + sizeof(sockaddr_in) * 4 + 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; + sockaddr_in *sa = nullptr; + sockaddr_in *sm = nullptr; + sockaddr_in *sg = nullptr; + sockaddr_in *ds = 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(a + 1); + + a->Length = sizeof(IP_ADAPTER_ADDRESSES); + a->IfIndex = k_ifindex; + a->AdapterName = reinterpret_cast(blob); + strcpy(reinterpret_cast(blob), k_adapter_name); + blob += STR_SLOT; + a->FriendlyName = reinterpret_cast(blob); + wcscpy(reinterpret_cast(blob), k_friendly_name); + blob += STR_SLOT; + a->Description = reinterpret_cast(blob); + wcscpy(reinterpret_cast(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(blob); + wcsncpy(reinterpret_cast(blob), k_domain_w, + (STR_SLOT / sizeof(wchar_t)) - 1); + reinterpret_cast(blob)[(STR_SLOT / sizeof(wchar_t)) - 1] = 0; + blob += STR_SLOT; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + u = reinterpret_cast(blob); + blob += sizeof(*u); + memset(u, 0, sizeof(*u)); + u->Length = sizeof(*u); + u->DadState = IpDadStatePreferred; + u->OnLinkPrefixLength = static_cast(k_prefix_len); + sa = reinterpret_cast(blob); + blob += sizeof(*sa); + memset(sa, 0, sizeof(*sa)); + sa->sin_family = AF_INET; + sa->sin_addr.s_addr = htonl(k_local_ip); + u->Address.lpSockaddr = reinterpret_cast(sa); + u->Address.iSockaddrLength = sizeof(*sa); + a->FirstUnicastAddress = u; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + pref = reinterpret_cast(blob); + blob += sizeof(*pref); + memset(pref, 0, sizeof(*pref)); + pref->Length = sizeof(*pref); + pref->PrefixLength = static_cast(k_prefix_len); + sm = reinterpret_cast(blob); + blob += sizeof(*sm); + memset(sm, 0, sizeof(*sm)); + sm->sin_family = AF_INET; + sm->sin_addr.s_addr = htonl(k_subnet); + pref->Address.lpSockaddr = reinterpret_cast(sm); + pref->Address.iSockaddrLength = sizeof(*sm); + a->FirstPrefix = pref; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + gw = reinterpret_cast(blob); + blob += sizeof(*gw); + memset(gw, 0, sizeof(*gw)); + gw->Length = sizeof(*gw); + sg = reinterpret_cast(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(sg); + gw->Address.iSockaddrLength = sizeof(*sg); + a->FirstGatewayAddress = gw; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + dns = reinterpret_cast(blob); + blob += sizeof(*dns); + memset(dns, 0, sizeof(*dns)); + ds = reinterpret_cast(blob); + blob += sizeof(*ds); + memset(ds, 0, sizeof(*ds)); + ds->sin_family = AF_INET; + ds->sin_addr.s_addr = htonl(k_dns); + dns->Address.lpSockaddr = reinterpret_cast(ds); + dns->Address.iSockaddrLength = sizeof(*ds); + dns->Next = nullptr; + 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( + reinterpret_cast(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(ai); + BYTE *addr = nullptr; + if (!ai || !ai->ai_addr) { + return false; + } + addr = reinterpret_cast(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 = k_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 = k_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(calloc(1, sizeof(*hdr))); + sa = static_cast(calloc(1, sizeof(*sa))); + canon = static_cast(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(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(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(k_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(k_fake_mac[0]), + static_cast(k_fake_mac[1]), + static_cast(k_fake_mac[2]), + static_cast(k_fake_mac[3]), + static_cast(k_fake_mac[4]), + static_cast(k_fake_mac[5]), + k_hostname, static_cast(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); + + /* FreeAddrInfoA is often the same address; only hook if distinct. */ + { + HMODULE ws = GetModuleHandleA("ws2_32.dll"); + void *faa = nullptr; + void *fai = nullptr; + if (ws) { + faa = reinterpret_cast(GetProcAddress(ws, "FreeAddrInfoA")); + fai = reinterpret_cast(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)"); + } +} + +} // namespace + +void nicspoof_init() { + install_nicspoof_hooks(); +} diff --git a/src/spice2x/hooks/nicspoof.h b/src/spice2x/hooks/nicspoof.h new file mode 100644 index 0000000..6f00bad --- /dev/null +++ b/src/spice2x/hooks/nicspoof.h @@ -0,0 +1,3 @@ +#pragma once + +void nicspoof_init(); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index d1e0cf7..09804cd 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -84,6 +84,7 @@ #include "hooks/lang.h" #include "hooks/networkhook.h" #include "hooks/icmphook_net.h" +#include "hooks/nicspoof.h" #include "hooks/unisintrhook.h" #include "launcher/launcher.h" #include "launcher/logger.h" @@ -257,6 +258,7 @@ int main_implementation(int argc, char *argv[]) { bool load_stubs = false; bool netfix_disable = false; bool icmphook_enable = false; + bool nicspoof_enable = false; bool lang_disable = false; std::string process_priority_str = "high"; bool cardio_enabled = false; @@ -799,6 +801,9 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::EnableICMPHook].value_bool()) { icmphook_enable = true; } + if (options[launcher::Options::EnableNICSpoof].value_bool()) { + nicspoof_enable = true; + } if (options[launcher::Options::DisableACPHook].value_bool()) { lang_disable = true; } @@ -2558,6 +2563,11 @@ int main_implementation(int argc, char *argv[]) { icmphook_net_init(); } + // NIC spoof (opt-in; fake Ethernet with no real NIC) + if (nicspoof_enable) { + nicspoof_init(); + } + // net fix if (!netfix_disable) { networkhook_init(); diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index c6de7bd..632258b 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -3187,6 +3187,15 @@ static const std::vector OPTION_DEFINITIONS = std::invoke([]() .type = OptionType::Bool, .category = "Network Dev", }; + c[launcher::Options::EnableNICSpoof] = { + .title = "Enable NIC Spoof", + .name = "nicspoof", + .desc = "Report a fake Ethernet IPv4 adapter (10.57.3.2) so the game can " + "boot when no real NIC is present. DNS names are mapped to that " + "address. Does not create a real adapter.", + .type = OptionType::Bool, + .category = "Network Dev", + }; c[launcher::Options::AutoElevate] = { .title = "Run as", .name = "runas", diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index e93c56a..adc42de 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -316,6 +316,7 @@ namespace launcher { OtocaCamHook, DisableHighResTimer, EnableICMPHook, + EnableNICSpoof, AutoElevate, CfgForceSoftwareRender, OBSWebSocketEnabled, From 5e4afecd901d6b9d662796c1069e952b7ab28bb5 Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Fri, 11 Sep 2026 07:37:45 +0000 Subject: [PATCH 11/19] 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 --- src/spice2x/CMakeLists.txt | 1 + src/spice2x/hooks/icmphook_net.cpp | 135 +- src/spice2x/hooks/icmphook_net.h | 21 +- src/spice2x/hooks/nicspoof.cpp | 163 +- src/spice2x/hooks/nicspoof.h | 25 + src/spice2x/hooks/nicspoof_tunnel.cpp | 2055 +++++++++++++++++++++++++ src/spice2x/hooks/nicspoof_tunnel.h | 5 + src/spice2x/launcher/launcher.cpp | 57 +- src/spice2x/launcher/options.cpp | 55 +- src/spice2x/launcher/options.h | 3 + 10 files changed, 2447 insertions(+), 73 deletions(-) create mode 100644 src/spice2x/hooks/nicspoof_tunnel.cpp create mode 100644 src/spice2x/hooks/nicspoof_tunnel.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index d194d82..b495b53 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -617,6 +617,7 @@ set(SOURCE_FILES ${SOURCE_FILES} hooks/icmphook_net.cpp hooks/icmphook_iphlpapi.cpp hooks/nicspoof.cpp + hooks/nicspoof_tunnel.cpp hooks/powrprof.cpp #hooks/rom.cpp hooks/setupapihook.cpp diff --git a/src/spice2x/hooks/icmphook_net.cpp b/src/spice2x/hooks/icmphook_net.cpp index b655822..ddc9606 100644 --- a/src/spice2x/hooks/icmphook_net.cpp +++ b/src/spice2x/hooks/icmphook_net.cpp @@ -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 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(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(); diff --git a/src/spice2x/hooks/icmphook_net.h b/src/spice2x/hooks/icmphook_net.h index 1081366..cd998aa 100644 --- a/src/spice2x/hooks/icmphook_net.h +++ b/src/spice2x/hooks/icmphook_net.h @@ -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(); diff --git a/src/spice2x/hooks/nicspoof.cpp b/src/spice2x/hooks/nicspoof.cpp index 3ba787c..4613246 100644 --- a/src/spice2x/hooks/nicspoof.cpp +++ b/src/spice2x/hooks/nicspoof.cpp @@ -14,26 +14,32 @@ #include "util/detour.h" #include "util/logging.h" +#include "hooks/nicspoof_tunnel.h" -namespace { -constexpr uint32_t k_local_ip = 0x0A390302u; /* 10.57.3.2 */ -constexpr uint32_t k_gateway = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_dns = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_dhcp = 0x0A390301u; /* 10.57.3.1 */ -constexpr uint32_t k_mask = 0xFFFFFF00u; /* 255.255.255.0 */ -constexpr uint32_t k_subnet = 0x0A390300u; -constexpr int k_prefix_len = 24; +namespace nicspoof_detail { +NicSpoofConfig g_cfg; + +constexpr uint32_t k_default_local_ip = 0x0A64640Au; /* 10.100.100.10 */ +constexpr uint32_t k_gateway = 0x0A646401u; /* 10.100.100.1 */ +constexpr uint32_t k_dns1 = 0x0A02010Au; /* 10.2.1.10 */ +constexpr uint32_t k_dns2 = 0x0A02011Eu; /* 10.2.1.30 */ +constexpr uint32_t k_dhcp = 0xC0A80001u; /* 192.168.0.1 */ +constexpr uint32_t k_mask = 0xFF000000u; /* 255.0.0.0 */ +constexpr int k_prefix_len = 8; constexpr DWORD k_ifindex = 77; constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37}; -constexpr char k_hostname[] = "N1CSP00F"; -constexpr char k_domain[] = "local"; -constexpr char k_hostname_fqdn[] = "N1CSP00F.local"; -constexpr wchar_t k_domain_w[] = L"local"; +constexpr char k_hostname[] = "N1C5P00F"; +constexpr char k_domain[] = "sp2x"; +constexpr char k_hostname_fqdn[] = "N1C5P00F.sp2x"; +constexpr wchar_t k_domain_w[] = L"sp2x"; constexpr char k_adapter_name[] = "{NICSPOOF-0880-0001-4250-4E6963537066}"; constexpr char k_adapter_desc[] = "NicSpoof Virtual Ethernet"; constexpr wchar_t k_friendly_name[] = L"NicSpoof"; constexpr wchar_t k_adapter_desc_w[] = L"NicSpoof Virtual Ethernet"; +uint32_t g_local_ip = k_default_local_ip; +uint32_t g_subnet = k_default_local_ip & k_mask; + #define ALIGN_UP_PTR(p, a) \ ((BYTE *)(((ULONG_PTR)(p) + ((ULONG_PTR)(a) - 1)) & ~((ULONG_PTR)(a) - 1))) @@ -125,7 +131,7 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) { memcpy(p->Address, k_fake_mac, 6); p->Type = MIB_IF_TYPE_ETHERNET; p->DhcpEnabled = 1; - ip_to_str(k_local_ip, ip, sizeof(ip)); + ip_to_str(g_local_ip, ip, sizeof(ip)); ip_to_str(k_gateway, gw, sizeof(gw)); ip_to_str(k_dhcp, dhcp, sizeof(dhcp)); strcpy(p->IpAddressList.IpAddress.String, ip); @@ -151,8 +157,11 @@ DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) { } DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) { - ULONG need = static_cast(sizeof(FIXED_INFO)); - char dns[16]; + /* FIXED_INFO embeds the first IP_ADDR_STRING; second follows in the buffer. */ + ULONG need = static_cast(sizeof(FIXED_INFO) + sizeof(IP_ADDR_STRING)); + char dns1[16]; + char dns2[16]; + PIP_ADDR_STRING dns_second = nullptr; if (!s) { return ERROR_INVALID_PARAMETER; @@ -164,18 +173,27 @@ DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) { memset(p, 0, need); strncpy(p->HostName, k_hostname, sizeof(p->HostName) - 1); strncpy(p->DomainName, k_domain, sizeof(p->DomainName) - 1); - ip_to_str(k_dns, dns, sizeof(dns)); - strcpy(p->DnsServerList.IpAddress.String, dns); + ip_to_str(k_dns1, dns1, sizeof(dns1)); + ip_to_str(k_dns2, dns2, sizeof(dns2)); + strcpy(p->DnsServerList.IpAddress.String, dns1); p->DnsServerList.IpMask.String[0] = 0; p->DnsServerList.Context = 0; - p->DnsServerList.Next = nullptr; + dns_second = reinterpret_cast( + reinterpret_cast(p) + sizeof(FIXED_INFO)); + memset(dns_second, 0, sizeof(*dns_second)); + strcpy(dns_second->IpAddress.String, dns2); + dns_second->IpMask.String[0] = 0; + dns_second->Context = 0; + dns_second->Next = nullptr; + p->DnsServerList.Next = dns_second; p->CurrentDnsServer = &p->DnsServerList; p->NodeType = 1; p->EnableDns = 1; *s = need; if (g_log_params) { - log_misc("network", "NIC spoof GetNetworkParams host={} domain={} dns={}", - k_hostname, k_domain, dns); + log_misc("network", + "NIC spoof GetNetworkParams host={} domain={} dns={} dns2={}", + k_hostname, k_domain, dns1, dns2); g_log_params = false; } return ERROR_SUCCESS; @@ -197,19 +215,21 @@ ULONG WINAPI GetAdaptersAddresses_hook( sizeof(void *) * 8 + sizeof(IP_ADAPTER_UNICAST_ADDRESS) + sizeof(IP_ADAPTER_PREFIX) + - sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) + + sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) * 2 + sizeof(IP_ADAPTER_GATEWAY_ADDRESS) + - sizeof(sockaddr_in) * 4 + 64); + sizeof(sockaddr_in) * 5 + 64); BYTE *blob = nullptr; PIP_ADAPTER_ADDRESSES a = nullptr; PIP_ADAPTER_UNICAST_ADDRESS u = nullptr; PIP_ADAPTER_PREFIX pref = nullptr; PIP_ADAPTER_GATEWAY_ADDRESS gw = nullptr; PIP_ADAPTER_DNS_SERVER_ADDRESS dns = nullptr; + PIP_ADAPTER_DNS_SERVER_ADDRESS dns2 = nullptr; sockaddr_in *sa = nullptr; sockaddr_in *sm = nullptr; sockaddr_in *sg = nullptr; sockaddr_in *ds = nullptr; + sockaddr_in *ds2 = nullptr; (void)Reserved; (void)Flags; @@ -267,7 +287,7 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*sa); memset(sa, 0, sizeof(*sa)); sa->sin_family = AF_INET; - sa->sin_addr.s_addr = htonl(k_local_ip); + sa->sin_addr.s_addr = htonl(g_local_ip); u->Address.lpSockaddr = reinterpret_cast(sa); u->Address.iSockaddrLength = sizeof(*sa); a->FirstUnicastAddress = u; @@ -282,7 +302,7 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*sm); memset(sm, 0, sizeof(*sm)); sm->sin_family = AF_INET; - sm->sin_addr.s_addr = htonl(k_subnet); + sm->sin_addr.s_addr = htonl(g_subnet); pref->Address.lpSockaddr = reinterpret_cast(sm); pref->Address.iSockaddrLength = sizeof(*sm); a->FirstPrefix = pref; @@ -309,10 +329,23 @@ ULONG WINAPI GetAdaptersAddresses_hook( blob += sizeof(*ds); memset(ds, 0, sizeof(*ds)); ds->sin_family = AF_INET; - ds->sin_addr.s_addr = htonl(k_dns); + ds->sin_addr.s_addr = htonl(k_dns1); dns->Address.lpSockaddr = reinterpret_cast(ds); dns->Address.iSockaddrLength = sizeof(*ds); - dns->Next = nullptr; + + blob = ALIGN_UP_PTR(blob, sizeof(void *)); + dns2 = reinterpret_cast(blob); + blob += sizeof(*dns2); + memset(dns2, 0, sizeof(*dns2)); + ds2 = reinterpret_cast(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(ds2); + dns2->Address.iSockaddrLength = sizeof(*ds2); + dns2->Next = nullptr; + dns->Next = dns2; a->FirstDnsServerAddress = dns; a->Next = nullptr; @@ -404,7 +437,7 @@ INT WSAAPI getaddrinfo_hook( if (!pNodeName || !pNodeName[0] || name_is_local(pNodeName)) { if (pNodeName && (_stricmp(pNodeName, k_hostname) == 0 || _stricmp(pNodeName, k_hostname_fqdn) == 0)) { - ip = k_local_ip; + ip = g_local_ip; } else { ip = 0x7F000001u; } @@ -412,7 +445,7 @@ INT WSAAPI getaddrinfo_hook( ip = parse_ipv4(pNodeName); } else { /* offline=1: map external names to local_ip */ - ip = k_local_ip; + ip = g_local_ip; } if (pServiceName && pServiceName[0]) { @@ -471,7 +504,7 @@ void install_nicspoof_hooks() { char ipstr[16]; char maskstr[16]; char gwstr[16]; - ip_to_str(k_local_ip, ipstr, sizeof(ipstr)); + ip_to_str(g_local_ip, ipstr, sizeof(ipstr)); ip_to_str(k_mask, maskstr, sizeof(maskstr)); ip_to_str(k_gateway, gwstr, sizeof(gwstr)); log_info("network", @@ -508,7 +541,6 @@ void install_nicspoof_hooks() { (void *) freeaddrinfo_hook, (void **) &freeaddrinfo_orig); - /* FreeAddrInfoA is often the same address; only hook if distinct. */ { HMODULE ws = GetModuleHandleA("ws2_32.dll"); void *faa = nullptr; @@ -539,8 +571,75 @@ void install_nicspoof_hooks() { } } -} // namespace +bool ip_in_overlay_range(uint32_t ip) { + const uint32_t net = k_gateway & k_mask; + const uint32_t bcast = net | ~k_mask; + if ((ip & k_mask) != net) { + return false; + } + if (ip == net || ip == bcast || ip == k_gateway || + ip == k_dns1 || ip == k_dns2) { + return false; + } + return true; +} + +void init_impl() { + if (g_cfg.mode == NicSpoofMode::Off) { + return; + } + + g_local_ip = g_cfg.local_ip ? g_cfg.local_ip : k_default_local_ip; + if (!ip_in_overlay_range(g_local_ip)) { + char bad[16]; + ip_to_str(g_local_ip, bad, sizeof(bad)); + log_warning("network", + "NIC spoof: IP {} is outside 10.0.0.0/8 usable range; " + "using 10.100.100.10", + bad); + g_local_ip = k_default_local_ip; + } + g_subnet = g_local_ip & k_mask; + + const char *mode_str = "offline"; + if (g_cfg.mode == NicSpoofMode::TunnelHost) { + mode_str = "tunnelhost"; + } else if (g_cfg.mode == NicSpoofMode::TunnelClient) { + mode_str = "tunnelclient"; + } + log_info("network", "NIC spoof mode={}", mode_str); + + install_nicspoof_hooks(); + + if (g_cfg.mode == NicSpoofMode::TunnelHost || + g_cfg.mode == NicSpoofMode::TunnelClient) { + nicspoof_tunnel_init(g_cfg); + } +} + +} // namespace nicspoof_detail + +void nicspoof_configure(const NicSpoofConfig &cfg) { + nicspoof_detail::g_cfg = cfg; +} + +bool nicspoof_tunnel_enabled() { + return nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelHost || + nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelClient; +} + +uint32_t nicspoof_local_ip() { + return nicspoof_detail::g_local_ip; +} + +uint32_t nicspoof_mask() { + return nicspoof_detail::k_mask; +} + +uint32_t nicspoof_subnet() { + return nicspoof_detail::g_subnet; +} void nicspoof_init() { - install_nicspoof_hooks(); + nicspoof_detail::init_impl(); } diff --git a/src/spice2x/hooks/nicspoof.h b/src/spice2x/hooks/nicspoof.h index 6f00bad..63a32ad 100644 --- a/src/spice2x/hooks/nicspoof.h +++ b/src/spice2x/hooks/nicspoof.h @@ -1,3 +1,28 @@ #pragma once +#include +#include + +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(); diff --git a/src/spice2x/hooks/nicspoof_tunnel.cpp b/src/spice2x/hooks/nicspoof_tunnel.cpp new file mode 100644 index 0000000..c73fadc --- /dev/null +++ b/src/spice2x/hooks/nicspoof_tunnel.cpp @@ -0,0 +1,2055 @@ +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef _WINSOCK_DEPRECATED_NO_WARNINGS +#define _WINSOCK_DEPRECATED_NO_WARNINGS +#endif +#ifndef _WIN32_WINNT +#define _WIN32_WINNT 0x0601 +#endif + +#include "nicspoof_tunnel.h" + +#include +#include +#include + +#include +#include +#include +#include + +#include "hooks/icmphook_net.h" +#include "util/detour.h" +#include "util/logging.h" + +namespace nicspoof_tunnel_detail { + +constexpr uint8_t MAGIC0 = 'B'; +constexpr uint8_t MAGIC1 = 'P'; +constexpr uint8_t MAGIC2 = 'L'; +constexpr uint8_t MAGIC3 = '1'; +constexpr uint8_t MSG_REGISTER = 1; +constexpr uint8_t MSG_REGISTER_ACK = 2; +constexpr uint8_t MSG_KEEPALIVE = 3; +constexpr uint8_t MSG_DATA = 4; +constexpr uint8_t ACK_OK = 0; +constexpr uint8_t KIND_UDP = 1; +constexpr uint8_t KIND_TCP_SYN = 2; +constexpr uint8_t KIND_TCP_ACCEPTED = 3; +constexpr uint8_t KIND_TCP_DATA = 4; +constexpr uint8_t KIND_TCP_FIN = 5; + +constexpr int MAX_UDP_Q = 64; +constexpr int MAX_UDP_PAYLOAD = 8192; +constexpr int MAX_TCP = 32; +constexpr int MAX_LISTEN = 8; +constexpr int MAX_ACCEPT_Q = 8; +constexpr int MAX_UDP_SOCK = 32; +constexpr int KEEP_ALIVE_SEC = 5; + +bool g_is_hub = false; +uint16_t g_tunnel_port = 51820; +char g_hub[256] = {}; +uint32_t g_local_ip = 0; +uint32_t g_peer_ip = 0; +uint32_t g_subnet = 0; +uint32_t g_mask = 0; + +SOCKET g_tun_sock = INVALID_SOCKET; +sockaddr_in g_peer_addr = {}; +int g_peer_valid = 0; +volatile LONG g_registered = 0; +volatile LONG g_shutdown = 0; +HANDLE g_tunnel_th = nullptr; +HANDLE g_keepalive_th = nullptr; +CRITICAL_SECTION g_peer_cs; + +bool g_logged_overlay_listen = false; + +decltype(sendto) *sendto_orig = nullptr; +decltype(recvfrom) *recvfrom_orig = nullptr; +decltype(bind) *bind_orig = nullptr; +decltype(connect) *connect_orig = nullptr; +decltype(WSAConnect) *WSAConnect_orig = nullptr; +decltype(listen) *listen_orig = nullptr; +decltype(accept) *accept_orig = nullptr; +decltype(closesocket) *closesocket_orig = nullptr; +decltype(ioctlsocket) *ioctlsocket_orig = nullptr; +decltype(send) *send_orig = nullptr; +decltype(recv) *recv_orig = nullptr; +decltype(getsockname) *getsockname_orig = nullptr; +decltype(select) *select_orig = nullptr; +decltype(WSASendTo) *WSASendTo_orig = nullptr; +decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr; + +struct UdpDgram { + uint32_t from_ip; + uint16_t from_port; + int32_t len; + uint8_t data[MAX_UDP_PAYLOAD]; +}; + +struct UdpSock { + SOCKET s; + int32_t in_use; + int32_t closing; + int32_t bound_port; + volatile LONG refs; + CRITICAL_SECTION cs; + HANDLE event; + int32_t q_head; + int32_t q_tail; + int32_t q_count; + UdpDgram q[MAX_UDP_Q]; +}; + +UdpSock g_udp[MAX_UDP_SOCK]; +CRITICAL_SECTION g_udp_cs; + +struct TcpConn { + int32_t in_use; + uint32_t conn_id; + SOCKET game_sock; + SOCKET relay_sock; + HANDLE thread; + volatile LONG alive; +}; + +struct AcceptItem { + SOCKET client_sock; + sockaddr_in peer; +}; + +struct ListenState { + SOCKET listen_sock; + int32_t in_use; + int32_t closing; + uint16_t port; + int32_t nonblock; + volatile LONG refs; + CRITICAL_SECTION cs; + HANDLE event; + int32_t q_count; + AcceptItem q[MAX_ACCEPT_Q]; +}; + +TcpConn g_tcp[MAX_TCP]; +ListenState g_listen[MAX_LISTEN]; +CRITICAL_SECTION g_tcp_cs; +CRITICAL_SECTION g_conn_wait_cs; + +struct ConnWait { + uint32_t id; + HANDLE ev; + int32_t used; +}; + +ConnWait g_conn_wait[MAX_TCP]; +volatile LONG g_next_conn_id = 1; + +constexpr int MAX_NB = 64; + +struct NbState { + SOCKET s; + int32_t in_use; + int32_t nonblock; +}; + +NbState g_nb[MAX_NB]; +CRITICAL_SECTION g_nb_cs; + +volatile LONG g_tunnel_up = 0; +bool g_wsa_started = false; + +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; +} + +bool in_overlay(uint32_t ip) { + return (ip & g_mask) == (g_subnet & g_mask); +} + +bool is_bcast(uint32_t ip) { + return ip == 0xFFFFFFFFu || ip == (g_subnet | ~g_mask); +} + +uint16_t rd16(const uint8_t *p) { + return static_cast((p[0] << 8) | p[1]); +} + +uint32_t rd32(const uint8_t *p) { + return (static_cast(p[0]) << 24) | + (static_cast(p[1]) << 16) | + (static_cast(p[2]) << 8) | + static_cast(p[3]); +} + +void wr16(uint8_t *p, uint16_t v) { + p[0] = static_cast(v >> 8); + p[1] = static_cast(v); +} + +void wr32(uint8_t *p, uint32_t v) { + p[0] = static_cast(v >> 24); + p[1] = static_cast(v >> 16); + p[2] = static_cast(v >> 8); + p[3] = static_cast(v); +} + +void peer_set(const sockaddr_in *addr, int32_t valid, uint32_t peer_ip) { + EnterCriticalSection(&g_peer_cs); + if (addr) { + g_peer_addr = *addr; + } + g_peer_valid = valid; + if (peer_ip) { + g_peer_ip = peer_ip; + } + LeaveCriticalSection(&g_peer_cs); +} + +int32_t peer_get(sockaddr_in *addr, uint32_t *peer_ip) { + int32_t v; + EnterCriticalSection(&g_peer_cs); + v = g_peer_valid; + if (addr) { + *addr = g_peer_addr; + } + if (peer_ip) { + *peer_ip = g_peer_ip; + } + LeaveCriticalSection(&g_peer_cs); + return v; +} + +UdpSock *udp_find(SOCKET s) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (g_udp[i].in_use && !g_udp[i].closing && g_udp[i].s == s) { + return &g_udp[i]; + } + } + return nullptr; +} + +void udp_destroy_unlocked(UdpSock *st) { + if (st->event) { + CloseHandle(st->event); + st->event = nullptr; + } + DeleteCriticalSection(&st->cs); + memset(st, 0, sizeof(*st)); +} + +UdpSock *udp_acquire(SOCKET s) { + UdpSock *st; + + EnterCriticalSection(&g_udp_cs); + st = udp_find(s); + if (st) { + InterlockedIncrement(&st->refs); + } + LeaveCriticalSection(&g_udp_cs); + return st; +} + +void udp_release(UdpSock *st) { + if (!st) { + return; + } + EnterCriticalSection(&g_udp_cs); + if (InterlockedDecrement(&st->refs) == 0) { + udp_destroy_unlocked(st); + } + LeaveCriticalSection(&g_udp_cs); +} + +UdpSock *udp_add(SOCKET s) { + UdpSock *st; + HANDLE ev; + + EnterCriticalSection(&g_udp_cs); + st = udp_find(s); + if (!st) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (!g_udp[i].in_use) { + memset(&g_udp[i], 0, sizeof(g_udp[i])); + ev = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!ev) { + LeaveCriticalSection(&g_udp_cs); + return nullptr; + } + g_udp[i].s = s; + g_udp[i].in_use = 1; + g_udp[i].refs = 1; + InitializeCriticalSection(&g_udp[i].cs); + g_udp[i].event = ev; + st = &g_udp[i]; + break; + } + } + } + LeaveCriticalSection(&g_udp_cs); + return st; +} + +void udp_remove(SOCKET s) { + UdpSock *st = nullptr; + + EnterCriticalSection(&g_udp_cs); + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + if (g_udp[i].in_use && g_udp[i].s == s) { + st = &g_udp[i]; + break; + } + } + if (st && !st->closing) { + st->closing = 1; + if (st->event) { + SetEvent(st->event); + } + } else { + st = nullptr; + } + LeaveCriticalSection(&g_udp_cs); + if (st) { + udp_release(st); + } +} + +void sock_signal_read(HANDLE ev) { + if (ev) { + SetEvent(ev); + } +} + +void udp_enqueue_sock(UdpSock *st, uint32_t from_ip, uint16_t from_port, + const uint8_t *data, int32_t len) { + if (!st || st->closing || len < 0 || len > MAX_UDP_PAYLOAD) { + return; + } + EnterCriticalSection(&st->cs); + if (st->q_count < MAX_UDP_Q) { + UdpDgram *d = &st->q[st->q_tail]; + d->from_ip = from_ip; + d->from_port = from_port; + d->len = len; + memcpy(d->data, data, static_cast(len)); + st->q_tail = (st->q_tail + 1) % MAX_UDP_Q; + st->q_count++; + sock_signal_read(st->event); + } + LeaveCriticalSection(&st->cs); +} + +void udp_enqueue(uint16_t dport, uint32_t from_ip, uint16_t from_port, + const uint8_t *data, int32_t len) { + int32_t matched = 0; + + if (len < 0 || len > MAX_UDP_PAYLOAD) { + return; + } + EnterCriticalSection(&g_udp_cs); + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + UdpSock *st = &g_udp[i]; + if (!st->in_use || st->closing) { + continue; + } + if (st->bound_port == static_cast(dport)) { + udp_enqueue_sock(st, from_ip, from_port, data, len); + matched = 1; + } + } + if (!matched) { + for (int32_t i = 0; i < MAX_UDP_SOCK; i++) { + UdpSock *st = &g_udp[i]; + if (!st->in_use || st->closing) { + continue; + } + if (!st->bound_port) { + udp_enqueue_sock(st, from_ip, from_port, data, len); + matched = 1; + } + } + } + LeaveCriticalSection(&g_udp_cs); + (void) matched; +} + +ListenState *listen_find(SOCKET s) { + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && !g_listen[i].closing && + g_listen[i].listen_sock == s) { + return &g_listen[i]; + } + } + return nullptr; +} + +ListenState *listen_find_port(uint16_t port) { + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && !g_listen[i].closing && + g_listen[i].port == port) { + return &g_listen[i]; + } + } + return nullptr; +} + +void listen_destroy_unlocked(ListenState *ls) { + if (ls->event) { + CloseHandle(ls->event); + ls->event = nullptr; + } + DeleteCriticalSection(&ls->cs); + memset(ls, 0, sizeof(*ls)); +} + +ListenState *listen_acquire(SOCKET s) { + ListenState *ls; + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + InterlockedIncrement(&ls->refs); + } + LeaveCriticalSection(&g_tcp_cs); + return ls; +} + +ListenState *listen_acquire_port(uint16_t port) { + ListenState *ls; + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find_port(port); + if (ls) { + InterlockedIncrement(&ls->refs); + } + LeaveCriticalSection(&g_tcp_cs); + return ls; +} + +void listen_release(ListenState *ls) { + if (!ls) { + return; + } + EnterCriticalSection(&g_tcp_cs); + if (InterlockedDecrement(&ls->refs) == 0) { + listen_destroy_unlocked(ls); + } + LeaveCriticalSection(&g_tcp_cs); +} + +void nb_set(SOCKET s, int32_t nonblock) { + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + g_nb[i].nonblock = nonblock; + LeaveCriticalSection(&g_nb_cs); + return; + } + } + for (int32_t i = 0; i < MAX_NB; i++) { + if (!g_nb[i].in_use) { + g_nb[i].in_use = 1; + g_nb[i].s = s; + g_nb[i].nonblock = nonblock; + break; + } + } + LeaveCriticalSection(&g_nb_cs); +} + +int32_t nb_get(SOCKET s) { + int32_t v = 0; + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + v = g_nb[i].nonblock; + break; + } + } + LeaveCriticalSection(&g_nb_cs); + return v; +} + +void nb_clear(SOCKET s) { + EnterCriticalSection(&g_nb_cs); + for (int32_t i = 0; i < MAX_NB; i++) { + if (g_nb[i].in_use && g_nb[i].s == s) { + g_nb[i].in_use = 0; + } + } + LeaveCriticalSection(&g_nb_cs); +} + +int32_t sock_overlay_readable(SOCKET s) { + UdpSock *us; + ListenState *ls; + int32_t n = 0; + + EnterCriticalSection(&g_udp_cs); + us = udp_find(s); + if (us) { + EnterCriticalSection(&us->cs); + n = us->q_count; + LeaveCriticalSection(&us->cs); + LeaveCriticalSection(&g_udp_cs); + return n > 0; + } + LeaveCriticalSection(&g_udp_cs); + + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + EnterCriticalSection(&ls->cs); + n = ls->q_count; + LeaveCriticalSection(&ls->cs); + LeaveCriticalSection(&g_tcp_cs); + return n > 0; + } + LeaveCriticalSection(&g_tcp_cs); + return 0; +} + +int32_t fd_set_has_overlay(const fd_set *set) { + if (!set) { + return 0; + } + for (u_int i = 0; i < set->fd_count; i++) { + if (sock_overlay_readable(set->fd_array[i])) { + return 1; + } + } + return 0; +} + +int32_t fd_set_contains_tracked(const fd_set *set) { + if (!set) { + return 0; + } + EnterCriticalSection(&g_udp_cs); + EnterCriticalSection(&g_tcp_cs); + for (u_int i = 0; i < set->fd_count; i++) { + SOCKET s = set->fd_array[i]; + if (udp_find(s) || listen_find(s)) { + LeaveCriticalSection(&g_tcp_cs); + LeaveCriticalSection(&g_udp_cs); + return 1; + } + } + LeaveCriticalSection(&g_tcp_cs); + LeaveCriticalSection(&g_udp_cs); + return 0; +} + +int32_t merge_overlay_into_fdset(fd_set *set, const fd_set *orig) { + int32_t added = 0; + + if (!set || !orig) { + return 0; + } + for (u_int i = 0; i < orig->fd_count; i++) { + SOCKET s = orig->fd_array[i]; + if (sock_overlay_readable(s) && !FD_ISSET(s, set)) { + if (set->fd_count >= FD_SETSIZE) { + break; + } + FD_SET(s, set); + added++; + } + } + return added; +} + +int32_t tun_sendto_raw(SOCKET s, const char *buf, int32_t n, + const sockaddr_in *to) { + if (sendto_orig) { + return sendto_orig(s, buf, n, 0, + reinterpret_cast(to), sizeof(*to)); + } + return sendto(s, buf, n, 0, + reinterpret_cast(to), sizeof(*to)); +} + +int32_t tun_recvfrom_raw(SOCKET s, char *buf, int32_t n, sockaddr_in *from, + int32_t *flen) { + if (recvfrom_orig) { + return recvfrom_orig(s, buf, n, 0, + reinterpret_cast(from), flen); + } + return recvfrom(s, buf, n, 0, reinterpret_cast(from), flen); +} + +int32_t tun_bind_raw(SOCKET s, const sockaddr *name, int32_t namelen) { + if (bind_orig) { + return bind_orig(s, name, namelen); + } + return bind(s, name, namelen); +} + +int32_t tun_send(uint8_t type, const uint8_t *payload, int32_t plen) { + uint8_t buf[65536]; + int32_t n; + sockaddr_in peer; + int32_t valid; + + if (g_tun_sock == INVALID_SOCKET) { + return -1; + } + valid = peer_get(&peer, nullptr); + if (!valid) { + return -1; + } + if (5 + plen > static_cast(sizeof(buf))) { + return -1; + } + buf[0] = MAGIC0; + buf[1] = MAGIC1; + buf[2] = MAGIC2; + buf[3] = MAGIC3; + buf[4] = type; + if (plen > 0) { + memcpy(buf + 5, payload, static_cast(plen)); + } + n = 5 + plen; + return tun_sendto_raw(g_tun_sock, reinterpret_cast(buf), n, &peer); +} + +int32_t tun_send_udp(uint16_t sport, uint16_t dport, const uint8_t *data, + int32_t len) { + uint8_t buf[MAX_UDP_PAYLOAD + 8]; + int32_t r; + + if (len < 0 || len > MAX_UDP_PAYLOAD) { + return -2; + } + buf[0] = KIND_UDP; + wr16(buf + 1, sport); + wr16(buf + 3, dport); + memcpy(buf + 5, data, static_cast(len)); + r = tun_send(MSG_DATA, buf, 5 + len); + if (r < 0) { + return -1; + } + return len; +} + +void tun_send_tcp_hdr(uint8_t kind, uint32_t conn_id, const uint8_t *extra, + int32_t elen) { + uint8_t buf[MAX_UDP_PAYLOAD + 16]; + int32_t n = 5; + + buf[0] = kind; + wr32(buf + 1, conn_id); + if (extra && elen > 0) { + if (elen > MAX_UDP_PAYLOAD) { + elen = MAX_UDP_PAYLOAD; + } + memcpy(buf + 5, extra, static_cast(elen)); + n = 5 + elen; + } + tun_send(MSG_DATA, buf, n); +} + +void tun_send_tcp_syn(uint32_t conn_id, uint16_t dport, uint16_t sport) { + uint8_t buf[9]; + + buf[0] = KIND_TCP_SYN; + wr32(buf + 1, conn_id); + wr16(buf + 5, dport); + wr16(buf + 7, sport); + tun_send(MSG_DATA, buf, 9); +} + +int32_t conn_wait_reg(uint32_t id, HANDLE ev) { + int32_t ok = 0; + + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (!g_conn_wait[i].used) { + g_conn_wait[i].used = 1; + g_conn_wait[i].id = id; + g_conn_wait[i].ev = ev; + ok = 1; + break; + } + } + LeaveCriticalSection(&g_conn_wait_cs); + return ok; +} + +void conn_wait_clear(uint32_t id) { + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_conn_wait[i].used && g_conn_wait[i].id == id) { + g_conn_wait[i].used = 0; + } + } + LeaveCriticalSection(&g_conn_wait_cs); +} + +void conn_wait_signal(uint32_t id) { + EnterCriticalSection(&g_conn_wait_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_conn_wait[i].used && g_conn_wait[i].id == id) { + SetEvent(g_conn_wait[i].ev); + } + } + LeaveCriticalSection(&g_conn_wait_cs); +} + +DWORD WINAPI tcp_relay_thread(LPVOID param) { + TcpConn *c = static_cast(param); + char buf[4096]; + int32_t n; + SOCKET rs = c->relay_sock; + + while (!g_shutdown && c->alive && rs != INVALID_SOCKET) { + fd_set rfds; + timeval tv; + FD_ZERO(&rfds); + FD_SET(rs, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 200000; + n = select_orig(0, &rfds, nullptr, nullptr, &tv); + if (n > 0 && FD_ISSET(rs, &rfds)) { + n = recv_orig(rs, buf, sizeof(buf), 0); + if (n <= 0) { + break; + } + tun_send_tcp_hdr(KIND_TCP_DATA, c->conn_id, + reinterpret_cast(buf), n); + } + } + tun_send_tcp_hdr(KIND_TCP_FIN, c->conn_id, nullptr, 0); + InterlockedExchange(&c->alive, 0); + return 0; +} + +int32_t make_loopback_pair(SOCKET *out_game, SOCKET *out_relay) { + SOCKET lst = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SOCKET a = INVALID_SOCKET; + SOCKET b = INVALID_SOCKET; + sockaddr_in addr; + int32_t alen = sizeof(addr); + u_long nb = 1; + + if (lst == INVALID_SOCKET) { + return -1; + } + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (bind_orig(lst, reinterpret_cast(&addr), sizeof(addr)) != 0) { + goto fail; + } + if (getsockname_orig(lst, reinterpret_cast(&addr), &alen) != 0) { + goto fail; + } + if (listen_orig(lst, 1) != 0) { + goto fail; + } + a = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (a == INVALID_SOCKET) { + goto fail; + } + if (connect_orig(a, reinterpret_cast(&addr), sizeof(addr)) != 0) { + goto fail; + } + b = accept_orig(lst, nullptr, nullptr); + if (b == INVALID_SOCKET) { + goto fail; + } + closesocket_orig(lst); + if (ioctlsocket_orig) { + ioctlsocket_orig(a, FIONBIO, &nb); + ioctlsocket_orig(b, FIONBIO, &nb); + } else { + ioctlsocket(a, FIONBIO, &nb); + ioctlsocket(b, FIONBIO, &nb); + } + *out_game = a; + *out_relay = b; + return 0; + +fail: + if (lst != INVALID_SOCKET) { + closesocket_orig(lst); + } + if (a != INVALID_SOCKET) { + closesocket_orig(a); + } + if (b != INVALID_SOCKET) { + closesocket_orig(b); + } + return -1; +} + +TcpConn *tcp_alloc(uint32_t conn_id, SOCKET game, SOCKET relay) { + HANDLE th; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (!g_tcp[i].in_use) { + memset(&g_tcp[i], 0, sizeof(g_tcp[i])); + g_tcp[i].in_use = 1; + g_tcp[i].conn_id = conn_id; + g_tcp[i].game_sock = game; + g_tcp[i].relay_sock = relay; + g_tcp[i].alive = 1; + th = CreateThread(nullptr, 0, tcp_relay_thread, &g_tcp[i], 0, nullptr); + if (!th) { + g_tcp[i].in_use = 0; + LeaveCriticalSection(&g_tcp_cs); + return nullptr; + } + g_tcp[i].thread = th; + LeaveCriticalSection(&g_tcp_cs); + return &g_tcp[i]; + } + } + LeaveCriticalSection(&g_tcp_cs); + return nullptr; +} + +void tcp_free_by_index(int32_t i) { + HANDLE th; + SOCKET rs; + + EnterCriticalSection(&g_tcp_cs); + if (!g_tcp[i].in_use) { + LeaveCriticalSection(&g_tcp_cs); + return; + } + InterlockedExchange(&g_tcp[i].alive, 0); + rs = g_tcp[i].relay_sock; + th = g_tcp[i].thread; + g_tcp[i].relay_sock = INVALID_SOCKET; + g_tcp[i].thread = nullptr; + LeaveCriticalSection(&g_tcp_cs); + if (rs != INVALID_SOCKET) { + closesocket_orig(rs); + } + if (th) { + WaitForSingleObject(th, INFINITE); + CloseHandle(th); + } + EnterCriticalSection(&g_tcp_cs); + g_tcp[i].in_use = 0; + LeaveCriticalSection(&g_tcp_cs); +} + +void tcp_free_sock(SOCKET s) { + int32_t hit = -1; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && + (g_tcp[i].game_sock == s || g_tcp[i].relay_sock == s)) { + hit = i; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + if (hit >= 0) { + tcp_free_by_index(hit); + } +} + +void tcp_free_id(uint32_t id) { + int32_t hit = -1; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && g_tcp[i].conn_id == id) { + hit = i; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + if (hit >= 0) { + tcp_free_by_index(hit); + } +} + +SOCKET tcp_relay_for_id(uint32_t id) { + SOCKET rs = INVALID_SOCKET; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_TCP; i++) { + if (g_tcp[i].in_use && g_tcp[i].conn_id == id) { + rs = g_tcp[i].relay_sock; + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + return rs; +} + +void on_tcp_syn(uint32_t conn_id, uint16_t dport, uint16_t sport) { + ListenState *ls; + SOCKET game_side; + SOCKET relay_side; + AcceptItem item; + uint32_t peer_ip; + int32_t queued = 0; + + ls = listen_acquire_port(dport); + if (!ls) { + log_warning("network", "NIC tunnel: TCP_SYN unbound port {}", dport); + return; + } + if (make_loopback_pair(&game_side, &relay_side) != 0) { + log_warning("network", "NIC tunnel: loopback pair failed"); + listen_release(ls); + return; + } + if (!tcp_alloc(conn_id, game_side, relay_side)) { + closesocket_orig(game_side); + closesocket_orig(relay_side); + listen_release(ls); + return; + } + peer_get(nullptr, &peer_ip); + memset(&item, 0, sizeof(item)); + item.client_sock = game_side; + item.peer.sin_family = AF_INET; + item.peer.sin_addr.s_addr = htonl(peer_ip); + item.peer.sin_port = htons(sport); + EnterCriticalSection(&ls->cs); + if (!ls->closing && ls->q_count < MAX_ACCEPT_Q) { + ls->q[ls->q_count++] = item; + sock_signal_read(ls->event); + queued = 1; + } + LeaveCriticalSection(&ls->cs); + if (!queued) { + log_warning("network", + "NIC tunnel: TCP accept queue full dport={}", dport); + closesocket_orig(game_side); + tcp_free_id(conn_id); + tun_send_tcp_hdr(KIND_TCP_FIN, conn_id, nullptr, 0); + listen_release(ls); + return; + } + tun_send_tcp_hdr(KIND_TCP_ACCEPTED, conn_id, nullptr, 0); + log_info("network", "NIC tunnel: TCP accepted conn={} dport={}", + conn_id, dport); + listen_release(ls); +} + +void on_data_payload(const uint8_t *p, int32_t n) { + uint8_t kind; + uint32_t peer_ip; + + if (n < 1) { + return; + } + kind = p[0]; + peer_get(nullptr, &peer_ip); + if (!peer_ip) { + return; + } + if (kind == KIND_UDP && n >= 5) { + udp_enqueue(rd16(p + 3), peer_ip, rd16(p + 1), p + 5, n - 5); + return; + } + if (kind == KIND_TCP_SYN && n >= 9) { + on_tcp_syn(rd32(p + 1), rd16(p + 5), rd16(p + 7)); + return; + } + if (kind == KIND_TCP_ACCEPTED && n >= 5) { + conn_wait_signal(rd32(p + 1)); + return; + } + if (kind == KIND_TCP_DATA && n >= 5) { + SOCKET rs = tcp_relay_for_id(rd32(p + 1)); + if (rs != INVALID_SOCKET && send_orig) { + send_orig(rs, reinterpret_cast(p + 5), n - 5, 0); + } + return; + } + if (kind == KIND_TCP_FIN && n >= 5) { + tcp_free_id(rd32(p + 1)); + } +} + +DWORD WINAPI keepalive_thread(LPVOID) { + uint8_t ipb[4]; + + wr32(ipb, g_local_ip); + while (!g_shutdown) { + for (int32_t i = 0; i < KEEP_ALIVE_SEC * 10 && !g_shutdown; i++) { + Sleep(100); + } + if (g_shutdown || g_tun_sock == INVALID_SOCKET) { + continue; + } + if (!g_is_hub && peer_get(nullptr, nullptr) && + InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + uint8_t reg[4]; + uint8_t pkt[9]; + sockaddr_in peer; + + wr32(reg, g_local_ip); + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER; + memcpy(pkt + 5, reg, 4); + if (peer_get(&peer, nullptr)) { + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 9, + &peer); + } + } + tun_send(MSG_KEEPALIVE, ipb, 4); + } + return 0; +} + +DWORD WINAPI tunnel_thread(LPVOID) { + uint8_t buf[65536]; + + while (!g_shutdown) { + sockaddr_in from; + int32_t flen = sizeof(from); + int32_t n; + uint8_t type; + uint8_t *payload; + int32_t plen; + + if (g_tun_sock == INVALID_SOCKET) { + Sleep(50); + continue; + } + n = tun_recvfrom_raw(g_tun_sock, reinterpret_cast(buf), + sizeof(buf), &from, &flen); + if (n < 5) { + if (g_shutdown) { + break; + } + continue; + } + if (buf[0] != MAGIC0 || buf[1] != MAGIC1 || buf[2] != MAGIC2 || + buf[3] != MAGIC3) { + continue; + } + type = buf[4]; + payload = buf + 5; + plen = n - 5; + if (type == MSG_REGISTER && g_is_hub && plen >= 4) { + peer_set(&from, 1, rd32(payload)); + { + uint8_t pkt[10]; + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER_ACK; + pkt[5] = ACK_OK; + wr32(pkt + 6, g_local_ip); + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 10, + &from); + } + InterlockedExchange(&g_registered, 1); + { + char peer_str[16]; + ip_to_str(rd32(payload), peer_str, sizeof(peer_str)); + log_info("network", + "NIC tunnel: hub peer registered peer_ip={}", peer_str); + } + continue; + } + if (type == MSG_REGISTER_ACK && plen >= 1) { + if (payload[0] == ACK_OK) { + InterlockedExchange(&g_registered, 1); + if (plen >= 5) { + peer_set(&from, 1, rd32(payload + 1)); + char hub_str[16]; + ip_to_str(rd32(payload + 1), hub_str, sizeof(hub_str)); + log_info("network", + "NIC tunnel: registered ok hub_peer_ip={}", hub_str); + } else { + log_info("network", "NIC tunnel: registered ok"); + } + } else { + log_warning("network", "NIC tunnel: register status={}", + payload[0]); + } + continue; + } + if (type == MSG_KEEPALIVE) { + /* Hub ignores keepalive until REGISTER establishes the peer. */ + if (g_is_hub && + InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + continue; + } + if (plen >= 4) { + peer_set(&from, 1, rd32(payload)); + } else if (g_is_hub) { + peer_set(&from, 1, 0); + } + continue; + } + if (type == MSG_DATA && plen > 0) { + uint32_t peer_ip = 0; + + if (InterlockedCompareExchange(&g_registered, 0, 0) == 0) { + continue; + } + if (g_is_hub) { + peer_set(&from, 1, 0); + } + peer_get(nullptr, &peer_ip); + if (!peer_ip) { + continue; + } + on_data_payload(payload, plen); + } + } + return 0; +} + +int32_t start_tunnel() { + WSADATA wsa; + sockaddr_in bind_addr; + + if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) { + log_warning("network", "NIC tunnel: WSAStartup failed"); + return -1; + } + g_wsa_started = true; + g_tun_sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (g_tun_sock == INVALID_SOCKET) { + log_warning("network", "NIC tunnel: socket create failed"); + goto fail; + } + memset(&bind_addr, 0, sizeof(bind_addr)); + bind_addr.sin_family = AF_INET; + bind_addr.sin_addr.s_addr = htonl(INADDR_ANY); + if (g_is_hub) { + bind_addr.sin_port = htons(g_tunnel_port); + if (tun_bind_raw(g_tun_sock, reinterpret_cast(&bind_addr), + sizeof(bind_addr)) != 0) { + log_warning("network", "NIC tunnel: hub bind fail err={}", + WSAGetLastError()); + goto fail; + } + log_info("network", "NIC tunnel: hub listen UDP {}", g_tunnel_port); + } else { + sockaddr_in peer; + char host[128]; + char *colon; + int32_t port = g_tunnel_port; + size_t hl; + + bind_addr.sin_port = 0; + if (tun_bind_raw(g_tun_sock, reinterpret_cast(&bind_addr), + sizeof(bind_addr)) != 0) { + log_warning("network", "NIC tunnel: client bind fail err={}", + WSAGetLastError()); + goto fail; + } + memset(&peer, 0, sizeof(peer)); + peer.sin_family = AF_INET; + colon = strchr(g_hub, ':'); + if (colon) { + hl = static_cast(colon - g_hub); + if (hl >= sizeof(host)) { + hl = sizeof(host) - 1; + } + memcpy(host, g_hub, hl); + host[hl] = 0; + port = atoi(colon + 1); + } else { + strncpy(host, g_hub, sizeof(host) - 1); + host[sizeof(host) - 1] = 0; + } + peer.sin_port = htons(static_cast(port)); + peer.sin_addr.s_addr = inet_addr(host); + if (peer.sin_addr.s_addr == INADDR_NONE) { + hostent *he = gethostbyname(host); + if (!he) { + log_warning("network", + "NIC tunnel: failed to resolve hub host"); + goto fail; + } + memcpy(&peer.sin_addr, he->h_addr, 4); + } + peer_set(&peer, 1, 0); + { + uint8_t reg[4]; + uint8_t pkt[9]; + + wr32(reg, g_local_ip); + pkt[0] = MAGIC0; + pkt[1] = MAGIC1; + pkt[2] = MAGIC2; + pkt[3] = MAGIC3; + pkt[4] = MSG_REGISTER; + memcpy(pkt + 5, reg, 4); + tun_sendto_raw(g_tun_sock, reinterpret_cast(pkt), 9, &peer); + log_info("network", "NIC tunnel: client register -> {}", g_hub); + } + } + g_tunnel_th = CreateThread(nullptr, 0, tunnel_thread, nullptr, 0, nullptr); + g_keepalive_th = CreateThread(nullptr, 0, keepalive_thread, nullptr, 0, + nullptr); + if (!g_tunnel_th || !g_keepalive_th) { + log_warning("network", "NIC tunnel: thread create failed"); + goto fail; + } + return 0; + +fail: + if (g_tunnel_th) { + InterlockedExchange(&g_shutdown, 1); + WaitForSingleObject(g_tunnel_th, 2000); + CloseHandle(g_tunnel_th); + g_tunnel_th = nullptr; + } + if (g_keepalive_th) { + InterlockedExchange(&g_shutdown, 1); + WaitForSingleObject(g_keepalive_th, 2000); + CloseHandle(g_keepalive_th); + g_keepalive_th = nullptr; + } + InterlockedExchange(&g_shutdown, 0); + if (g_tun_sock != INVALID_SOCKET) { + closesocket(g_tun_sock); + g_tun_sock = INVALID_SOCKET; + } + if (g_wsa_started) { + WSACleanup(); + g_wsa_started = false; + } + return -1; +} + +uint16_t sock_local_port(SOCKET s) { + sockaddr_in a; + int32_t alen = sizeof(a); + + memset(&a, 0, sizeof(a)); + if (getsockname_orig && + getsockname_orig(s, reinterpret_cast(&a), &alen) == 0) { + return ntohs(a.sin_port); + } + return 0; +} + +int WSAAPI bind_hook(SOCKET s, const sockaddr *name, int namelen) { + int32_t r; + UdpSock *us; + int32_t type = 0; + int32_t tlen = sizeof(type); + int icmp_r = 0; + + if (icmphook_try_bind(s, name, namelen, &icmp_r)) { + return icmp_r; + } + + r = bind_orig(s, name, namelen); + if (r != 0 || !g_tunnel_up) { + return r; + } + getsockopt(s, SOL_SOCKET, SO_TYPE, reinterpret_cast(&type), &tlen); + if (type == SOCK_DGRAM) { + us = udp_add(s); + if (us) { + uint16_t p = sock_local_port(s); + us->bound_port = static_cast(p); + } + } + (void) namelen; + return r; +} + +int WSAAPI sendto_hook(SOCKET s, const char *buf, int len, int flags, + const sockaddr *to, int tolen) { + int icmp_r = 0; + + if (icmphook_try_sendto(s, buf, len, flags, to, tolen, &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock || !g_tunnel_up) { + return sendto_orig(s, buf, len, flags, to, tolen); + } + if (to && to->sa_family == AF_INET && len >= 0) { + const sockaddr_in *in = reinterpret_cast(to); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (is_bcast(dip) || in_overlay(dip)) { + uint16_t sport = sock_local_port(s); + uint16_t dport = ntohs(in->sin_port); + UdpSock *us = udp_add(s); + int32_t to_self = (dip == g_local_ip); + + if (us && !us->bound_port) { + us->bound_port = static_cast(sport); + } + if (to_self) { + udp_enqueue(dport, g_local_ip, sport, + reinterpret_cast(buf), len); + return len; + } + { + int32_t tr = tun_send_udp(sport, dport, + reinterpret_cast(buf), len); + if (tr == -2) { + WSASetLastError(WSAEMSGSIZE); + return SOCKET_ERROR; + } + if (tr < 0) { + WSASetLastError(WSAENETUNREACH); + return SOCKET_ERROR; + } + return len; + } + } + } + return sendto_orig(s, buf, len, flags, to, tolen); +} + +int WSAAPI WSASendTo_hook(SOCKET s, LPWSABUF b, DWORD n, LPDWORD sent, + DWORD flags, const sockaddr *to, int tolen, LPWSAOVERLAPPED ov, + LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { + int icmp_r = 0; + + if (icmphook_try_WSASendTo(s, b, n, sent, flags, to, tolen, ov, cr, + &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return WSASendTo_orig(s, b, n, sent, flags, to, tolen, ov, cr); + } + if (!ov && !cr && n == 1 && b && to && to->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(to); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (is_bcast(dip) || in_overlay(dip)) { + int32_t r = sendto_hook(s, reinterpret_cast(b[0].buf), + static_cast(b[0].len), 0, to, tolen); + if (sent) { + *sent = static_cast(r < 0 ? 0 : r); + } + return r < 0 ? SOCKET_ERROR : 0; + } + } + return WSASendTo_orig(s, b, n, sent, flags, to, tolen, ov, cr); +} + +int32_t pop_udp(UdpSock *us, char *buf, int32_t len, sockaddr *from, + int32_t *fromlen, int32_t peek) { + UdpDgram d; + + if (!us || len < 0) { + return -1; + } + EnterCriticalSection(&us->cs); + if (us->closing || us->q_count <= 0) { + LeaveCriticalSection(&us->cs); + return -1; + } + d = us->q[us->q_head]; + if (!peek) { + us->q_head = (us->q_head + 1) % MAX_UDP_Q; + us->q_count--; + } + LeaveCriticalSection(&us->cs); + if (len > d.len) { + len = d.len; + } + memcpy(buf, d.data, static_cast(len)); + if (from && fromlen && *fromlen >= static_cast(sizeof(sockaddr_in))) { + sockaddr_in *in = reinterpret_cast(from); + memset(in, 0, sizeof(*in)); + in->sin_family = AF_INET; + in->sin_addr.s_addr = htonl(d.from_ip); + in->sin_port = htons(d.from_port); + *fromlen = sizeof(*in); + } + return len; +} + +int WSAAPI recvfrom_hook(SOCKET s, char *buf, int len, int flags, + sockaddr *from, int *fromlen) { + UdpSock *us; + int32_t r; + int icmp_r = 0; + const int32_t peek = (flags & MSG_PEEK) ? 1 : 0; + + if (icmphook_try_recvfrom(s, buf, len, flags, from, fromlen, &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + if (!g_tunnel_up) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + if (len < 0) { + WSASetLastError(WSAEINVAL); + return SOCKET_ERROR; + } + + us = udp_acquire(s); + if (!us) { + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + + for (;;) { + r = pop_udp(us, buf, len, from, fromlen, peek); + if (r >= 0) { + udp_release(us); + return r; + } + if (us->closing) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + + { + fd_set rfds; + timeval tv; + FD_ZERO(&rfds); + FD_SET(s, &rfds); + tv.tv_sec = 0; + tv.tv_usec = 0; + if (select_orig && select_orig(0, &rfds, nullptr, nullptr, &tv) > 0) { + udp_release(us); + return recvfrom_orig(s, buf, len, flags, from, fromlen); + } + } + + if (nb_get(s)) { + udp_release(us); + WSASetLastError(WSAEWOULDBLOCK); + return SOCKET_ERROR; + } + + if (!us->event) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + if (WaitForSingleObject(us->event, 500) == WAIT_FAILED) { + udp_release(us); + WSASetLastError(WSAENOTSOCK); + return SOCKET_ERROR; + } + } +} + +int WSAAPI WSARecvFrom_hook(SOCKET s, LPWSABUF b, DWORD n, LPDWORD recvd, + LPDWORD flags, sockaddr *from, LPINT fromlen, LPWSAOVERLAPPED ov, + LPWSAOVERLAPPED_COMPLETION_ROUTINE cr) { + int icmp_r = 0; + + if (icmphook_try_WSARecvFrom(s, b, n, recvd, flags, from, fromlen, ov, cr, + &icmp_r)) { + return icmp_r; + } + if (s == g_tun_sock) { + return WSARecvFrom_orig(s, b, n, recvd, flags, from, fromlen, ov, cr); + } + if (!ov && !cr && n == 1 && b) { + int32_t fl = fromlen ? *fromlen : 0; + int32_t r = recvfrom_hook(s, reinterpret_cast(b[0].buf), + static_cast(b[0].len), flags ? *flags : 0, from, + fromlen ? &fl : nullptr); + if (fromlen) { + *fromlen = fl; + } + if (r == SOCKET_ERROR) { + return SOCKET_ERROR; + } + if (recvd) { + *recvd = static_cast(r); + } + return 0; + } + return WSARecvFrom_orig(s, b, n, recvd, flags, from, fromlen, ov, cr); +} + +int WSAAPI select_hook(int nfds, fd_set *readfds, fd_set *writefds, + fd_set *exceptfds, const timeval *timeout) { + fd_set orig_r; + int32_t tracked = 0; + DWORD start; + DWORD budget_ms; + + if (readfds) { + orig_r = *readfds; + tracked = fd_set_contains_tracked(&orig_r); + } + if (!tracked) { + return select_orig(nfds, readfds, writefds, exceptfds, timeout); + } + + start = GetTickCount(); + if (!timeout) { + budget_ms = INFINITE; + } else { + budget_ms = static_cast( + timeout->tv_sec * 1000 + timeout->tv_usec / 1000); + if (timeout->tv_sec == 0 && timeout->tv_usec == 0) { + budget_ms = 0; + } + } + + for (;;) { + fd_set r; + fd_set w; + fd_set e; + fd_set *pr = nullptr; + fd_set *pw = nullptr; + fd_set *pe = nullptr; + timeval tv; + int32_t n; + int32_t added; + DWORD elapsed; + DWORD remain; + int32_t slice_ms; + + if (budget_ms == INFINITE) { + slice_ms = 50; + } else { + elapsed = GetTickCount() - start; + if (elapsed >= budget_ms) { + if (readfds) { + FD_ZERO(readfds); + } + if (writefds) { + FD_ZERO(writefds); + } + if (exceptfds) { + FD_ZERO(exceptfds); + } + if (fd_set_has_overlay(&orig_r)) { + int32_t ready = 0; + if (readfds) { + FD_ZERO(readfds); + merge_overlay_into_fdset(readfds, &orig_r); + ready = static_cast(readfds->fd_count); + } + return ready; + } + return 0; + } + remain = budget_ms - elapsed; + slice_ms = static_cast(remain > 50 ? 50 : remain); + } + + if (fd_set_has_overlay(&orig_r)) { + slice_ms = 0; + } + tv.tv_sec = slice_ms / 1000; + tv.tv_usec = (slice_ms % 1000) * 1000; + + if (readfds) { + r = orig_r; + pr = &r; + } + if (writefds) { + w = *writefds; + pw = &w; + } + if (exceptfds) { + e = *exceptfds; + pe = &e; + } + + n = select_orig(nfds, pr, pw, pe, &tv); + if (n == SOCKET_ERROR) { + return SOCKET_ERROR; + } + added = 0; + if (pr) { + added = merge_overlay_into_fdset(pr, &orig_r); + } + if (n + added > 0) { + if (readfds) { + *readfds = r; + } + if (writefds) { + *writefds = w; + } + if (exceptfds) { + *exceptfds = e; + } + return n + added; + } + if (budget_ms == 0) { + return 0; + } + } +} + +int WSAAPI listen_hook(SOCKET s, int backlog) { + int32_t r; + uint16_t port; + HANDLE ev; + + r = listen_orig(s, backlog); + port = sock_local_port(s); + if (r == 0 && port && g_tunnel_up) { + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (!g_listen[i].in_use) { + memset(&g_listen[i], 0, sizeof(g_listen[i])); + ev = CreateEvent(nullptr, FALSE, FALSE, nullptr); + if (!ev) { + break; + } + g_listen[i].in_use = 1; + g_listen[i].listen_sock = s; + g_listen[i].port = port; + g_listen[i].nonblock = nb_get(s); + g_listen[i].refs = 1; + InitializeCriticalSection(&g_listen[i].cs); + g_listen[i].event = ev; + if (!g_logged_overlay_listen) { + log_misc("network", + "NIC tunnel: overlay listen port={} nb={}", + port, g_listen[i].nonblock); + g_logged_overlay_listen = true; + } + break; + } + } + LeaveCriticalSection(&g_tcp_cs); + } + return r; +} + +int WSAAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) { + int icmp_r = 0; + int32_t r; + + if (icmphook_try_ioctlsocket(s, cmd, argp, &icmp_r)) { + return icmp_r; + } + r = ioctlsocket_orig(s, cmd, argp); + + if (r == 0 && static_cast(cmd) == FIONBIO && argp) { + ListenState *ls; + nb_set(s, (*argp) ? 1 : 0); + EnterCriticalSection(&g_tcp_cs); + ls = listen_find(s); + if (ls) { + ls->nonblock = (*argp) ? 1 : 0; + } + LeaveCriticalSection(&g_tcp_cs); + } + return r; +} + +SOCKET WSAAPI accept_hook(SOCKET s, sockaddr *addr, int *addrlen) { + ListenState *ls; + + ls = listen_acquire(s); + if (!ls) { + return accept_orig(s, addr, addrlen); + } + for (;;) { + EnterCriticalSection(&ls->cs); + if (ls->closing) { + LeaveCriticalSection(&ls->cs); + listen_release(ls); + WSASetLastError(WSAENOTSOCK); + return INVALID_SOCKET; + } + if (ls->q_count > 0) { + AcceptItem it = ls->q[0]; + for (int32_t i = 1; i < ls->q_count; i++) { + ls->q[i - 1] = ls->q[i]; + } + ls->q_count--; + LeaveCriticalSection(&ls->cs); + if (addr && addrlen && + *addrlen >= static_cast(sizeof(it.peer))) { + memcpy(addr, &it.peer, sizeof(it.peer)); + *addrlen = sizeof(it.peer); + } + listen_release(ls); + return it.client_sock; + } + LeaveCriticalSection(&ls->cs); + if (ls->nonblock || nb_get(s)) { + listen_release(ls); + WSASetLastError(WSAEWOULDBLOCK); + return INVALID_SOCKET; + } + if (!ls->event) { + listen_release(ls); + WSASetLastError(WSAENOTSOCK); + return INVALID_SOCKET; + } + WaitForSingleObject(ls->event, 500); + } +} + +int WSAAPI connect_hook(SOCKET s, const sockaddr *name, int namelen) { + if (!g_tunnel_up) { + return connect_orig(s, name, namelen); + } + if (name && name->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(name); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (in_overlay(dip) && dip != g_local_ip) { + uint32_t cid = static_cast( + InterlockedIncrement(&g_next_conn_id)); + uint16_t dport = ntohs(in->sin_port); + uint16_t sport; + HANDLE ev; + sockaddr_in la; + int32_t was_nb = nb_get(s); + u_long zero = 0; + u_long one = 1; + char dip_str[16]; + + sport = sock_local_port(s); + if (!sport) { + memset(&la, 0, sizeof(la)); + la.sin_family = AF_INET; + la.sin_addr.s_addr = htonl(INADDR_ANY); + la.sin_port = 0; + bind_orig(s, reinterpret_cast(&la), sizeof(la)); + sport = sock_local_port(s); + } + ev = CreateEvent(nullptr, TRUE, FALSE, nullptr); + if (!ev) { + WSASetLastError(WSAENOBUFS); + return SOCKET_ERROR; + } + if (!conn_wait_reg(cid, ev)) { + CloseHandle(ev); + WSASetLastError(WSAENOBUFS); + return SOCKET_ERROR; + } + tun_send_tcp_syn(cid, dport, sport); + ip_to_str(dip, dip_str, sizeof(dip_str)); + log_info("network", + "NIC tunnel: TCP connect -> {}:{} conn={}", + dip_str, dport, cid); + if (WaitForSingleObject(ev, 10000) != WAIT_OBJECT_0) { + conn_wait_clear(cid); + CloseHandle(ev); + WSASetLastError(WSAETIMEDOUT); + return SOCKET_ERROR; + } + conn_wait_clear(cid); + CloseHandle(ev); + { + SOCKET lst = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SOCKET rel; + sockaddr_in addr; + int32_t alen = sizeof(addr); + int32_t cr; + int32_t err; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &zero); + } else { + ioctlsocket(s, FIONBIO, &zero); + } + bind_orig(lst, reinterpret_cast(&addr), sizeof(addr)); + getsockname_orig(lst, reinterpret_cast(&addr), &alen); + listen_orig(lst, 1); + cr = connect_orig(s, reinterpret_cast(&addr), + sizeof(addr)); + err = (cr != 0) ? WSAGetLastError() : 0; + if (cr != 0 && err == WSAEWOULDBLOCK) { + fd_set wfds; + timeval tv; + FD_ZERO(&wfds); + FD_SET(s, &wfds); + tv.tv_sec = 2; + tv.tv_usec = 0; + if (select_orig(0, nullptr, &wfds, nullptr, &tv) > 0) { + cr = 0; + err = 0; + } + } + if (cr != 0) { + log_warning("network", + "NIC tunnel: TCP loopback connect fail " + "conn={} err={}", + cid, err); + closesocket_orig(lst); + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + } + WSASetLastError(err ? err : WSAECONNREFUSED); + return SOCKET_ERROR; + } + rel = accept_orig(lst, nullptr, nullptr); + closesocket_orig(lst); + if (rel == INVALID_SOCKET) { + log_warning("network", + "NIC tunnel: TCP loopback accept fail " + "conn={} err={}", + cid, WSAGetLastError()); + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + } + return SOCKET_ERROR; + } + { + u_long nbr = 1; + if (ioctlsocket_orig) { + ioctlsocket_orig(rel, FIONBIO, &nbr); + } else { + ioctlsocket(rel, FIONBIO, &nbr); + } + } + if (was_nb) { + if (ioctlsocket_orig) { + ioctlsocket_orig(s, FIONBIO, &one); + } else { + ioctlsocket(s, FIONBIO, &one); + } + nb_set(s, 1); + } else { + nb_set(s, 0); + } + if (!tcp_alloc(cid, s, rel)) { + closesocket_orig(rel); + return SOCKET_ERROR; + } + log_info("network", + "NIC tunnel: TCP connected conn={}", cid); + return 0; + } + } + } + return connect_orig(s, name, namelen); +} + +int WSAAPI WSAConnect_hook(SOCKET s, const sockaddr *name, int namelen, + LPWSABUF caller, LPWSABUF callee, LPQOS sqos, LPQOS gqos) { + if (name && name->sa_family == AF_INET) { + const sockaddr_in *in = reinterpret_cast(name); + uint32_t dip = ntohl(in->sin_addr.s_addr); + + if (in_overlay(dip) && dip != g_local_ip) { + return connect_hook(s, name, namelen); + } + } + if (WSAConnect_orig) { + return WSAConnect_orig(s, name, namelen, caller, callee, sqos, gqos); + } + return connect_orig(s, name, namelen); +} + +void listen_close(SOCKET s) { + ListenState *ls = nullptr; + AcceptItem drain[MAX_ACCEPT_Q]; + int32_t dn = 0; + + EnterCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < MAX_LISTEN; i++) { + if (g_listen[i].in_use && g_listen[i].listen_sock == s) { + ls = &g_listen[i]; + break; + } + } + if (ls && !ls->closing) { + ls->closing = 1; + if (ls->event) { + SetEvent(ls->event); + } + EnterCriticalSection(&ls->cs); + dn = ls->q_count; + for (int32_t i = 0; i < dn; i++) { + drain[i] = ls->q[i]; + } + ls->q_count = 0; + LeaveCriticalSection(&ls->cs); + InterlockedIncrement(&ls->refs); + } else { + ls = nullptr; + } + LeaveCriticalSection(&g_tcp_cs); + for (int32_t i = 0; i < dn; i++) { + tcp_free_sock(drain[i].client_sock); + closesocket_orig(drain[i].client_sock); + } + if (ls) { + /* Drop close-path ref and the listen_hook base ref. */ + listen_release(ls); + listen_release(ls); + } +} + +int WSAAPI closesocket_hook(SOCKET s) { + int icmp_r = 0; + + if (icmphook_try_closesocket(s, &icmp_r)) { + return icmp_r; + } + udp_remove(s); + tcp_free_sock(s); + nb_clear(s); + listen_close(s); + return closesocket_orig(s); +} + +void install_hooks() { + bool ok = true; + + ok &= detour::trampoline_try("ws2_32.dll", "bind", + bind_hook, &bind_orig); + ok &= detour::trampoline_try("ws2_32.dll", "sendto", + sendto_hook, &sendto_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSASendTo", + WSASendTo_hook, &WSASendTo_orig); + ok &= detour::trampoline_try("ws2_32.dll", "recvfrom", + recvfrom_hook, &recvfrom_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSARecvFrom", + WSARecvFrom_hook, &WSARecvFrom_orig); + ok &= detour::trampoline_try("ws2_32.dll", "select", + select_hook, &select_orig); + ok &= detour::trampoline_try("ws2_32.dll", "listen", + listen_hook, &listen_orig); + ok &= detour::trampoline_try("ws2_32.dll", "accept", + accept_hook, &accept_orig); + ok &= detour::trampoline_try("ws2_32.dll", "connect", + connect_hook, &connect_orig); + ok &= detour::trampoline_try("ws2_32.dll", "WSAConnect", + WSAConnect_hook, &WSAConnect_orig); + ok &= detour::trampoline_try("ws2_32.dll", "ioctlsocket", + ioctlsocket_hook, &ioctlsocket_orig); + ok &= detour::trampoline_try("ws2_32.dll", "closesocket", + closesocket_hook, &closesocket_orig); + + { + HMODULE ws = GetModuleHandleA("ws2_32.dll"); + if (!ws) { + ws = LoadLibraryA("ws2_32.dll"); + } + if (ws) { + send_orig = reinterpret_cast( + GetProcAddress(ws, "send")); + recv_orig = reinterpret_cast( + GetProcAddress(ws, "recv")); + getsockname_orig = reinterpret_cast( + GetProcAddress(ws, "getsockname")); + } + } + + if (!send_orig || !recv_orig || !getsockname_orig || !select_orig || + !sendto_orig || !recvfrom_orig || !bind_orig || !closesocket_orig) { + ok = false; + } + + if (!ok) { + log_warning("network", + "NIC tunnel: one or more divert hooks failed to install"); + } else { + log_info("network", + "NIC tunnel: divert hooks installed " + "(bind/sendto/recvfrom/select/listen/accept/connect/...)"); + } +} + +bool install_hooks_ok() { + return send_orig && recv_orig && getsockname_orig && select_orig && + sendto_orig && recvfrom_orig && bind_orig && closesocket_orig && + listen_orig && accept_orig && connect_orig && ioctlsocket_orig; +} + +} // namespace nicspoof_tunnel_detail + +void nicspoof_tunnel_init(const NicSpoofConfig &cfg) { + using namespace nicspoof_tunnel_detail; + + static bool done = false; + char ipstr[16]; + + if (done) { + return; + } + + if (cfg.mode != NicSpoofMode::TunnelHost && + cfg.mode != NicSpoofMode::TunnelClient) { + done = true; + return; + } + + g_is_hub = (cfg.mode == NicSpoofMode::TunnelHost); + g_tunnel_port = cfg.tunnel_port ? cfg.tunnel_port : 51820; + + if (!g_is_hub && cfg.hub_host.empty()) { + log_warning("network", + "NIC tunnel: TunnelClient requires hub host; not starting"); + done = true; + return; + } + + g_local_ip = nicspoof_local_ip(); + g_mask = nicspoof_mask(); + g_subnet = nicspoof_subnet(); + g_peer_ip = 0; + + memset(g_hub, 0, sizeof(g_hub)); + if (!cfg.hub_host.empty()) { + strncpy(g_hub, cfg.hub_host.c_str(), sizeof(g_hub) - 1); + } + + InitializeCriticalSection(&g_peer_cs); + InitializeCriticalSection(&g_udp_cs); + InitializeCriticalSection(&g_tcp_cs); + InitializeCriticalSection(&g_conn_wait_cs); + InitializeCriticalSection(&g_nb_cs); + + ip_to_str(g_local_ip, ipstr, sizeof(ipstr)); + if (g_is_hub) { + log_info("network", + "NIC tunnel init role=host local={} port={}", + ipstr, g_tunnel_port); + } else { + log_info("network", + "NIC tunnel init role=client local={} port={} hub={}", + ipstr, g_tunnel_port, g_hub); + } + + /* Install hooks first with g_tunnel_up=0 so divert stays inert until ready. */ + install_hooks(); + if (!install_hooks_ok()) { + log_warning("network", + "NIC tunnel: aborting; required Winsock trampolines missing"); + done = true; + return; + } + if (start_tunnel() != 0) { + log_warning("network", "NIC tunnel: start failed"); + done = true; + return; + } + InterlockedExchange(&g_tunnel_up, 1); + done = true; +} diff --git a/src/spice2x/hooks/nicspoof_tunnel.h b/src/spice2x/hooks/nicspoof_tunnel.h new file mode 100644 index 0000000..32a062e --- /dev/null +++ b/src/spice2x/hooks/nicspoof_tunnel.h @@ -0,0 +1,5 @@ +#pragma once + +#include "nicspoof.h" + +void nicspoof_tunnel_init(const NicSpoofConfig &cfg); diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 09804cd..2ec4007 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -258,7 +259,7 @@ int main_implementation(int argc, char *argv[]) { bool load_stubs = false; bool netfix_disable = false; bool icmphook_enable = false; - bool nicspoof_enable = false; + NicSpoofConfig nicspoof_cfg; bool lang_disable = false; std::string process_priority_str = "high"; bool cardio_enabled = false; @@ -801,8 +802,43 @@ int main_implementation(int argc, char *argv[]) { if (options[launcher::Options::EnableICMPHook].value_bool()) { icmphook_enable = true; } - if (options[launcher::Options::EnableNICSpoof].value_bool()) { - nicspoof_enable = true; + if (options[launcher::Options::EnableNICSpoof].is_active()) { + const auto &mode = options[launcher::Options::EnableNICSpoof].value_text(); + if (mode == "offline" || mode == "/ENABLED") { + nicspoof_cfg.mode = NicSpoofMode::Offline; + } else if (mode == "tunnelhost") { + nicspoof_cfg.mode = NicSpoofMode::TunnelHost; + } else if (mode == "tunnelclient") { + nicspoof_cfg.mode = NicSpoofMode::TunnelClient; + } else { + log_warning("launcher", "unknown -nicspoof value '{}', ignoring", mode); + } + } + if (nicspoof_cfg.mode != NicSpoofMode::Off) { + if (options[launcher::Options::NICSpoofIP].is_active()) { + unsigned a = 0, b = 0, c = 0, d = 0; + char trail = 0; + const auto &ip = options[launcher::Options::NICSpoofIP].value_text(); + if (sscanf(ip.c_str(), "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) == 4 && + a <= 255 && b <= 255 && c <= 255 && d <= 255) { + nicspoof_cfg.local_ip = (a << 24) | (b << 16) | (c << 8) | d; + } else { + log_warning("launcher", "invalid -nicspoofip '{}'", ip); + } + } + if (options[launcher::Options::NICSpoofHostRealIP].is_active()) { + nicspoof_cfg.hub_host = + options[launcher::Options::NICSpoofHostRealIP].value_text(); + } + if (options[launcher::Options::NICSpoofPort].is_active()) { + const uint32_t p = + options[launcher::Options::NICSpoofPort].value_uint32(); + if (p == 0 || p > 65535) { + log_warning("launcher", "invalid -nicspoofport {}", p); + } else { + nicspoof_cfg.tunnel_port = static_cast(p); + } + } } if (options[launcher::Options::DisableACPHook].value_bool()) { lang_disable = true; @@ -2558,14 +2594,17 @@ int main_implementation(int argc, char *argv[]) { avs::core::load_dll(); avs::ea3::load_dll(); - // ICMP emulation (opt-in; before games open raw ICMP sockets) - if (icmphook_enable) { - icmphook_net_init(); + // NIC spoof / matching tunnel first so divert owns overlapping ws2_32 + // MinHook slots (bind/sendto/recvfrom/...). ICMP then installs only the + // non-overlapping socket-creation hooks and is reached via icmphook_try_*. + if (nicspoof_cfg.mode != NicSpoofMode::Off) { + nicspoof_configure(nicspoof_cfg); + nicspoof_init(); } - // NIC spoof (opt-in; fake Ethernet with no real NIC) - if (nicspoof_enable) { - nicspoof_init(); + // ICMP emulation (opt-in; after tunnel so hooks do not collide) + if (icmphook_enable) { + icmphook_net_init(); } // net fix diff --git a/src/spice2x/launcher/options.cpp b/src/spice2x/launcher/options.cpp index 632258b..a492227 100644 --- a/src/spice2x/launcher/options.cpp +++ b/src/spice2x/launcher/options.cpp @@ -3188,12 +3188,57 @@ static const std::vector OPTION_DEFINITIONS = std::invoke([]() .category = "Network Dev", }; c[launcher::Options::EnableNICSpoof] = { - .title = "Enable NIC Spoof", + .title = "NIC Spoof", .name = "nicspoof", - .desc = "Report a fake Ethernet IPv4 adapter (10.57.3.2) so the game can " - "boot when no real NIC is present. DNS names are mapped to that " - "address. Does not create a real adapter.", - .type = OptionType::Bool, + .desc = + "Use with -icmphook.\n\n" + "offline: report a fake Ethernet IPv4 adapter and map DNS names to " + "that address so the game can boot when no real NIC is present. " + "Does not create a real adapter.\n\n" + "tunnelhost: same fake NIC, plus listen for a matching tunnel " + "from a peer.\n\n" + "tunnelclient: same fake NIC, plus connect to a tunnel host. " + "Set -nicspoofhostrealip.", + .type = OptionType::Enum, + .category = "Network Dev", + .elements = { + {"offline", ""}, + {"tunnelhost", ""}, + {"tunnelclient", ""}, + }, + }; + c[launcher::Options::NICSpoofIP] = { + .title = "NIC Spoof IP", + .name = "nicspoofip", + .desc = + "Overlay IPv4 for the fake adapter. Used when NIC Spoof is not Default. " + "If unset, 10.100.100.10 is used.\n\n" + "Must be on the same subnet as gateway 10.100.100.1 with mask " + "255.0.0.0 (10.0.0.0/8). Examples: 10.100.100.10, 10.0.0.10. " + "Do not use 10.0.0.0, 10.255.255.255, the gateway 10.100.100.1, " + "or DNS 10.2.1.10 / 10.2.1.30. Host and client must use different IPs.", + .type = OptionType::Text, + .setting_name = "10.100.100.10", + .category = "Network Dev", + }; + c[launcher::Options::NICSpoofHostRealIP] = { + .title = "NIC Spoof Tunnel Host", + .name = "nicspoofhostrealip", + .desc = + "Real IP address or hostname of the tunnel host. Required for " + "tunnel client mode. Not used for offline or tunnel host.", + .type = OptionType::Text, + .setting_name = "192.168.1.10", + .category = "Network Dev", + }; + c[launcher::Options::NICSpoofPort] = { + .title = "NIC Spoof Tunnel Port", + .name = "nicspoofport", + .desc = + "UDP port for the matching tunnel. Host listens on this port; " + "client connects to it. If unset, 51820 is used.", + .type = OptionType::Integer, + .setting_name = "51820", .category = "Network Dev", }; c[launcher::Options::AutoElevate] = { diff --git a/src/spice2x/launcher/options.h b/src/spice2x/launcher/options.h index adc42de..c37dccc 100644 --- a/src/spice2x/launcher/options.h +++ b/src/spice2x/launcher/options.h @@ -317,6 +317,9 @@ namespace launcher { DisableHighResTimer, EnableICMPHook, EnableNICSpoof, + NICSpoofIP, + NICSpoofHostRealIP, + NICSpoofPort, AutoElevate, CfgForceSoftwareRender, OBSWebSocketEnabled, From 603fbfc69d61e66804de906bd571bcec24ac60eb Mon Sep 17 00:00:00 2001 From: drmext <71258889+drmext@users.noreply.github.com> Date: Fri, 11 Sep 2026 09:20:21 +0000 Subject: [PATCH 12/19] network: heap-allocate NIC spoof tunnel UDP queue (#913) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- src/spice2x/hooks/nicspoof_tunnel.cpp | 93 ++++++++++++++++++++------- 1 file changed, 71 insertions(+), 22 deletions(-) diff --git a/src/spice2x/hooks/nicspoof_tunnel.cpp b/src/spice2x/hooks/nicspoof_tunnel.cpp index c73fadc..495ad8a 100644 --- a/src/spice2x/hooks/nicspoof_tunnel.cpp +++ b/src/spice2x/hooks/nicspoof_tunnel.cpp @@ -87,7 +87,7 @@ struct UdpDgram { uint32_t from_ip; uint16_t from_port; int32_t len; - uint8_t data[MAX_UDP_PAYLOAD]; + uint8_t *data; }; struct UdpSock { @@ -237,7 +237,23 @@ UdpSock *udp_find(SOCKET s) { return nullptr; } +void udp_queue_clear_unlocked(UdpSock *st) { + while (st->q_count > 0) { + UdpDgram *d = &st->q[st->q_head]; + free(d->data); + d->data = nullptr; + d->len = 0; + st->q_head = (st->q_head + 1) % MAX_UDP_Q; + st->q_count--; + } + st->q_head = 0; + st->q_tail = 0; +} + void udp_destroy_unlocked(UdpSock *st) { + EnterCriticalSection(&st->cs); + udp_queue_clear_unlocked(st); + LeaveCriticalSection(&st->cs); if (st->event) { CloseHandle(st->event); st->event = nullptr; @@ -330,16 +346,34 @@ void sock_signal_read(HANDLE ev) { void udp_enqueue_sock(UdpSock *st, uint32_t from_ip, uint16_t from_port, const uint8_t *data, int32_t len) { + uint8_t *copy = nullptr; + if (!st || st->closing || len < 0 || len > MAX_UDP_PAYLOAD) { return; } + if (len > 0) { + if (!data) { + return; + } + copy = static_cast(malloc(static_cast(len))); + if (!copy) { + return; + } + memcpy(copy, data, static_cast(len)); + } + EnterCriticalSection(&st->cs); - if (st->q_count < MAX_UDP_Q) { + if (st->closing || st->q_count >= MAX_UDP_Q) { + LeaveCriticalSection(&st->cs); + free(copy); + return; + } + { UdpDgram *d = &st->q[st->q_tail]; d->from_ip = from_ip; d->from_port = from_port; d->len = len; - memcpy(d->data, data, static_cast(len)); + d->data = copy; st->q_tail = (st->q_tail + 1) % MAX_UDP_Q; st->q_count++; sock_signal_read(st->event); @@ -1349,9 +1383,10 @@ int WSAAPI WSASendTo_hook(SOCKET s, LPWSABUF b, DWORD n, LPDWORD sent, int32_t pop_udp(UdpSock *us, char *buf, int32_t len, sockaddr *from, int32_t *fromlen, int32_t peek) { - UdpDgram d; + uint8_t *owned = nullptr; + int32_t out_len = -1; - if (!us || len < 0) { + if (!us || len < 0 || !buf) { return -1; } EnterCriticalSection(&us->cs); @@ -1359,25 +1394,39 @@ int32_t pop_udp(UdpSock *us, char *buf, int32_t len, sockaddr *from, LeaveCriticalSection(&us->cs); return -1; } - d = us->q[us->q_head]; - if (!peek) { - us->q_head = (us->q_head + 1) % MAX_UDP_Q; - us->q_count--; + { + UdpDgram *d = &us->q[us->q_head]; + const int32_t dlen = d->len; + const uint8_t *src = d->data; + + if (dlen < 0 || (dlen > 0 && !src)) { + LeaveCriticalSection(&us->cs); + return -1; + } + out_len = (len > dlen) ? dlen : len; + if (out_len > 0) { + memcpy(buf, src, static_cast(out_len)); + } + if (from && fromlen && + *fromlen >= static_cast(sizeof(sockaddr_in))) { + sockaddr_in *in = reinterpret_cast(from); + memset(in, 0, sizeof(*in)); + in->sin_family = AF_INET; + in->sin_addr.s_addr = htonl(d->from_ip); + in->sin_port = htons(d->from_port); + *fromlen = sizeof(*in); + } + if (!peek) { + owned = d->data; + d->data = nullptr; + d->len = 0; + us->q_head = (us->q_head + 1) % MAX_UDP_Q; + us->q_count--; + } } LeaveCriticalSection(&us->cs); - if (len > d.len) { - len = d.len; - } - memcpy(buf, d.data, static_cast(len)); - if (from && fromlen && *fromlen >= static_cast(sizeof(sockaddr_in))) { - sockaddr_in *in = reinterpret_cast(from); - memset(in, 0, sizeof(*in)); - in->sin_family = AF_INET; - in->sin_addr.s_addr = htonl(d.from_ip); - in->sin_port = htons(d.from_port); - *fromlen = sizeof(*in); - } - return len; + free(owned); + return out_len; } int WSAAPI recvfrom_hook(SOCKET s, char *buf, int len, int flags, From 4c77a3f1487404c1d8b25f30efe87537058697b4 Mon Sep 17 00:00:00 2001 From: bicarus <202771338+bicarus-dev@users.noreply.github.com> Date: Fri, 11 Sep 2026 02:49:07 -0700 Subject: [PATCH 13/19] 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. --- src/spice2x/CMakeLists.txt | 12 +- .../graphics/backends/d3d9/d3d9_backend.cpp | 3 + .../graphics/backends/d3d9/d3d9_device.cpp | 19 +- .../graphics/backends/d3d9/d3d9_gfdm.cpp | 4 + .../graphics/backends/d3d9/d3d9_swapchain.cpp | 4 + src/spice2x/hooks/graphics/graphics.cpp | 8 +- src/spice2x/launcher/launcher.cpp | 2 +- src/spice2x/sdk/d3d9.cpp | 403 ++++++++++++++++++ src/spice2x/sdk/d3d9.h | 17 + src/spice2x/sdk/include/spicesdk.h | 95 +++++ src/spice2x/sdk/modules.cpp | 161 +++++++ src/spice2x/sdk/modules.h | 24 ++ src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp | 63 ++- .../sdk/sample/v0/cpp/v0_cpp_imgui.cpp | 218 ++++++++++ src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.h | 10 + src/spice2x/sdk/sdk.cpp | 65 ++- src/spice2x/sdk/sdk.h | 2 +- 17 files changed, 1095 insertions(+), 15 deletions(-) create mode 100644 src/spice2x/sdk/d3d9.cpp create mode 100644 src/spice2x/sdk/d3d9.h create mode 100644 src/spice2x/sdk/modules.cpp create mode 100644 src/spice2x/sdk/modules.h create mode 100644 src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.cpp create mode 100644 src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.h diff --git a/src/spice2x/CMakeLists.txt b/src/spice2x/CMakeLists.txt index b495b53..4126589 100644 --- a/src/spice2x/CMakeLists.txt +++ b/src/spice2x/CMakeLists.txt @@ -719,6 +719,8 @@ set(SOURCE_FILES ${SOURCE_FILES} reader/crypt.cpp # sdk + sdk/d3d9.cpp + sdk/modules.cpp sdk/sdk.cpp # stubs @@ -1047,8 +1049,16 @@ if(NOT MSVC) endif() # sdk_sample_v0_cpp.dll (64 bit) -set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp) +set(SOURCE_FILES + sdk/sample/v0/cpp/v0_cpp.cpp + sdk/sample/v0/cpp/v0_cpp_imgui.cpp + external/imgui/imgui.cpp + external/imgui/imgui_draw.cpp + external/imgui/imgui_tables.cpp + external/imgui/imgui_widgets.cpp + external/imgui/backends/imgui_impl_dx9.cpp) add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def) +target_link_libraries(spicetools_sdk_sample_v0_cpp_64 PRIVATE imm32) set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "") set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp") diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp index 719f561..fb545c1 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_backend.cpp @@ -22,6 +22,7 @@ #include "launcher/shutdown.h" #include "misc/wintouchemu.h" #include "overlay/overlay.h" +#include "sdk/d3d9.h" #include "util/detour.h" #include "util/deferlog.h" #include "util/flags_helper.h" @@ -1511,6 +1512,8 @@ void graphics_d3d9_on_present( device->EndScene(); } + sdk::d3d9::draw(hFocusWindow, device); + // after the overlay render so the screenshot includes toasts / menus if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) { graphics_d3d9_process_screenshot(device, wrapped_device); diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp index d03b9c8..ac43601 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_device.cpp @@ -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(pReal)->PresentEx( - pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags)); + const HRESULT result = static_cast(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) diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp index c00246d..e27c6e1 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_gfdm.cpp @@ -7,6 +7,7 @@ #include "games/gitadora/gitadora.h" #include "hooks/graphics/graphics.h" +#include "sdk/d3d9.h" #include "util/logging.h" #include "d3d9_device.h" @@ -558,6 +559,7 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode( temporary_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate; temporary_modes[1] = alternate_small; + sdk::d3d9::invalidate(device); HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes); const bool temporary_settled = temporary_result == D3D_OK @@ -574,6 +576,8 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode( restore_result == D3D_OK && gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]); + sdk::d3d9::reset_complete(device, SUCCEEDED(restore_result)); + if (temporary_result != D3D_OK) { return temporary_result; } diff --git a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp index dacb126..deedb4c 100644 --- a/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp +++ b/src/spice2x/hooks/graphics/backends/d3d9/d3d9_swapchain.cpp @@ -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) { diff --git a/src/spice2x/hooks/graphics/graphics.cpp b/src/spice2x/hooks/graphics/graphics.cpp index e3fb016..6ad6733 100644 --- a/src/spice2x/hooks/graphics/graphics.cpp +++ b/src/spice2x/hooks/graphics/graphics.cpp @@ -7,6 +7,7 @@ #include #include +#include #include #include #include @@ -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; } diff --git a/src/spice2x/launcher/launcher.cpp b/src/spice2x/launcher/launcher.cpp index 2ec4007..f259010 100644 --- a/src/spice2x/launcher/launcher.cpp +++ b/src/spice2x/launcher/launcher.cpp @@ -2908,7 +2908,7 @@ int main_implementation(int argc, char *argv[]) { bt5api_dispose(); } - sdk::fini_sdk_modules(); + sdk::fini_sdk_modules(true); // stop raw input hotkeys::disable_raw_input(); diff --git a/src/spice2x/sdk/d3d9.cpp b/src/spice2x/sdk/d3d9.cpp new file mode 100644 index 0000000..479e9f8 --- /dev/null +++ b/src/spice2x/sdk/d3d9.cpp @@ -0,0 +1,403 @@ +#include "d3d9.h" + +#include +#include +#include +#include +#include +#include + +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> 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 +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 block; + std::array, 4> targets; + ComReference depth; + D3DVIEWPORT9 viewport{}; + std::array transforms{}; + std::array 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(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> 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 &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(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{callback, userdata}); + if (!GetModuleHandleExW( + GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN, + reinterpret_cast(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 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; + }); +} + +} \ No newline at end of file diff --git a/src/spice2x/sdk/d3d9.h b/src/spice2x/sdk/d3d9.h new file mode 100644 index 0000000..1b702be --- /dev/null +++ b/src/spice2x/sdk/d3d9.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include "modules.h" + +namespace sdk::d3d9 { + +SPICE_SDK_STATUS_CODE register_d3d9(const std::vector &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); + +} \ No newline at end of file diff --git a/src/spice2x/sdk/include/spicesdk.h b/src/spice2x/sdk/include/spicesdk.h index 2a52603..767ea5c 100644 --- a/src/spice2x/sdk/include/spicesdk.h +++ b/src/spice2x/sdk/include/spicesdk.h @@ -4,6 +4,7 @@ #include #include +#include #ifdef __cplusplus #define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl @@ -247,6 +248,95 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)( uint8_t amount ); +typedef struct SPICE_SDK_MODULE_INFO { + uint32_t size; // initialize to sizeof(SPICE_SDK_MODULE_INFO) + uintptr_t base; // loaded address, not the preferred PE image base + uint32_t image_size; + uint32_t timestamp; + uint32_t entry_point; // RVA, not an absolute address + uint16_t machine; // PE IMAGE_FILE_MACHINE_* value +} SPICE_SDK_MODULE_INFO; + +// get_module_info (v0.4 and up) +// gets PE info about an already loaded module +// +// does not load or permanently pin the module; the caller must keep it loaded +// while using the returned base address +// returns GENERIC_ERROR if not found or unreadable, NOT_SUPPORTED for non-PE32/PE32+ +// +// module_name: null-terminated UTF-16 module name or full path +// info: receives module info; initialize size to sizeof(SPICE_SDK_MODULE_INFO) + +typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_module_info_func)( + const wchar_t *module_name, + SPICE_SDK_MODULE_INFO *info +); + +// get_plugin_directory (v0.4 and up) +// gets the directory containing a registered SDK DLL +// +// available during entry-point initialization and until destroy callbacks finish +// returns INVALID_ARGUMENT_1 for addresses outside registered SDK DLLs +// +// plugin_address: address of a function or static object in the plugin DLL +// used only to identify the DLL; the pointed-to object is not read +// address of any global variable in your DLL will work +// buffer: caller-owned UTF-16 output, or NULL to query the required size +// size: input capacity and output required wchar_t count, including the terminator +// NULL buffer or insufficient capacity returns TOO_SMALL and sets the +// required size without partial output + +typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_plugin_directory_func)( + const void *plugin_address, + wchar_t *buffer, + uint32_t *size +); + +typedef enum SPICE_SDK_D3D9_EVENT { + SPICE_SDK_D3D9_READY = 0, + SPICE_SDK_D3D9_DRAW = 1, + SPICE_SDK_D3D9_INVALIDATE = 2, + SPICE_SDK_D3D9_DESTROY = 3, +} SPICE_SDK_D3D9_EVENT; + +typedef struct SPICE_SDK_D3D9_FRAME { + uint32_t size; + void *device; // borrowed IDirect3DDevice9*, not Spice's wrapper + void *window; // HWND + uint32_t width; + uint32_t height; +} SPICE_SDK_D3D9_FRAME; + +typedef void (__cdecl spice_sdk_d3d9_callback_func)( + SPICE_SDK_D3D9_EVENT event, + const SPICE_SDK_D3D9_FRAME *frame, + void *userdata +); + +// register_d3d9 (v0.4 and up) +// registers a process-lifetime renderer for the primary D3D9 presentation target +// +// READY precedes drawing on a usable device and follows each successful reset +// DRAW runs on top of all Spice overlays, even when the overlay is closed; the host +// binds the backbuffer, brackets the scene, and restores graphics state +// INVALIDATE precedes reset; release default-pool resources, even if reset fails +// DESTROY releases all device references before device destruction or SDK shutdown +// callbacks are serialized; DRAW/READY run on the presentation thread; teardown +// runs at a graphics-thread boundary, or after the game stops rendering +// do not throw, block, reset/present the +// device, or shut down Spice from a callback; frame is valid only during the call +// no callbacks run after the plugin's SDK destroy callback begins +// registration from inside a render callback is not supported +// input capture and non-D3D9 backends are not provided +// +// callback: function in a registered plugin DLL; the DLL is retained until exit +// userdata: opaque plugin state passed unchanged to each callback + +typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_register_d3d9_func)( + spice_sdk_d3d9_callback_func *callback, + void *userdata +); + typedef struct SPICE_SDK_V0 { uint32_t size; @@ -274,6 +364,11 @@ typedef struct SPICE_SDK_V0 { spice_sdk_insert_coin_func *insert_coin; + spice_sdk_get_module_info_func *get_module_info; + spice_sdk_get_plugin_directory_func *get_plugin_directory; + + spice_sdk_register_d3d9_func *register_d3d9; + } SPICE_SDK_V0; typedef void (__cdecl spice_sdk_destroy_callback_func)( diff --git a/src/spice2x/sdk/modules.cpp b/src/spice2x/sdk/modules.cpp new file mode 100644 index 0000000..ce82c37 --- /dev/null +++ b/src/spice2x/sdk/modules.cpp @@ -0,0 +1,161 @@ +#include "modules.h" + +#include +#include +#include +#include + +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 +bool read_module_value(uintptr_t base, size_t offset, Value &value) { + if (offset > std::numeric_limits::max() - base || + sizeof(Value) > std::numeric_limits::max() - (base + offset)) { + return false; + } + + SIZE_T copied = 0; + return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast(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(module.handle); + IMAGE_DOS_HEADER dos{}; + if (!read_module_value(base, 0, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE || + dos.e_lfanew < static_cast(sizeof(dos))) { + return SPICE_SDK_STATUS_GENERIC_ERROR; + } + + const auto nt_offset = static_cast(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 ®istered_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(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 filename{}; + const auto length = GetModuleFileNameW(module.handle, filename.data(), static_cast(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(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; + } +} + +} \ No newline at end of file diff --git a/src/spice2x/sdk/modules.h b/src/spice2x/sdk/modules.h new file mode 100644 index 0000000..ce3b995 --- /dev/null +++ b/src/spice2x/sdk/modules.h @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +#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 ®istered_modules, + const void *plugin_address, wchar_t *buffer, uint32_t *size); + +} + +} \ No newline at end of file diff --git a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp index fe7b2e1..ccdd356 100644 --- a/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp +++ b/src/spice2x/sdk/sample/v0/cpp/v0_cpp.cpp @@ -1,8 +1,12 @@ #include #include #include +#include +#include +#include #include +#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 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 directory{}; + uint32_t size = static_cast(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(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); diff --git a/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.cpp b/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.cpp new file mode 100644 index 0000000..afa0a71 --- /dev/null +++ b/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.cpp @@ -0,0 +1,218 @@ +#include "v0_cpp_imgui.h" + +#include +#include +#include +#include +#include + +#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(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(position.x) * frame.width / client.right, + static_cast(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(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(frame.width), static_cast(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(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(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); +} + +} \ No newline at end of file diff --git a/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.h b/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.h new file mode 100644 index 0000000..8ee32d7 --- /dev/null +++ b/src/spice2x/sdk/sample/v0/cpp/v0_cpp_imgui.h @@ -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(); + +} \ No newline at end of file diff --git a/src/spice2x/sdk/sdk.cpp b/src/spice2x/sdk/sdk.cpp index 5677ded..ba90acb 100644 --- a/src/spice2x/sdk/sdk.cpp +++ b/src/spice2x/sdk/sdk.cpp @@ -1,8 +1,11 @@ #include #include #include +#include #include "sdk.h" +#include "modules.h" +#include "d3d9.h" #include "avs/game.h" #include "games/io.h" #include "launcher/launcher.h" @@ -32,11 +35,9 @@ static spice_sdk_insert_card_func sdk_insert_card; static spice_sdk_set_keypad_func sdk_set_keypad; static spice_sdk_add_toast_func sdk_add_toast; static spice_sdk_insert_coin_func sdk_insert_coin; - -struct SdkModule { - std::string dll; - HINSTANCE module; -}; +static spice_sdk_get_module_info_func sdk_get_module_info; +static spice_sdk_get_plugin_directory_func sdk_get_plugin_directory; +static spice_sdk_register_d3d9_func sdk_register_d3d9; // DLLs static int sdk_modules_count = 0; @@ -46,6 +47,7 @@ static std::shared_mutex sdk_global_mutex; // internal static bool sdk_initialized = false; static bool sdk_shutting_down = false; +static bool sdk_finalizing = false; static std::vector