Compare commits

..

7 Commits

Author SHA1 Message Date
bicarus 2b930ad95f gitadora: warn about 2ch audio option not working on arena model (#702)
shrug
2026-05-25 17:00:49 -07:00
bicarus 3c60f3966b rb: more touch options (#699)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
Add two options:

1. option to adjust poll rate
2. option to change emulation size

Also, the default configuration is changing from:

* ~1000Hz ish to 120Hz (default)
* 3x3 point to 1x1 point

In theory this does not make a meaningful change to how the game plays.
But if people want the old behavior they can change it.

## Testing
Tested on Volzza2 and Reflesia.
2026-05-25 16:02:17 -07:00
Biduleman c59d399ab8 Automated PIN Macro after auto-inserting a card (#698)
## Description of change
Adds a way to automatically use the PIN Macro for P1, P2 or both when
Auto Card Insert is used.

### Launcher changes: 
 - Added a checkbox to enable/disable the feature
 - Added a textbox for each player to set what to look for in the logs.

### How it works:
The feature requires Auto Card Insert and PIN Macro to be enabled and
set.
It uses the Auto Card Insert value to enable the feature for P1, P2 or
both, with a "master" checkbox to completely disable the feature. It
then relies on the current PIN Macro code to send the PIN to the games.

The games logs send a message when a new scene is loaded with the name
of the scene. By hooking on the logs, we can see when a PIN is required
and send the relevant macro. Games have different scene names, and
sometimes one for each player, so the field is configurable per game per
player. These could be hard-coded in a lookup table but I didn't want to
recompile the project every time I wanted to test, and I don't have all
the games to get this info from.

The PIN Macro thread is used to hook to the logs and check if the
defined string appears, which triggers the PIN macro just like a button
press would.

Setting nothing for the log trigger disables the feature for that
player. It's useful if someone wants to have Auto Card Insert enabled
for both players but only Auto Pin on 1 player, but is mainly there so
the check won't happen if nothing is set.

## Testing
I used SDVX and DDR to have a single and a two player test with the
feature both enabled and disabled. I also tested multiple combinations
of PIN Macro settings and Auto Card Insert settings with no apparent
issues. The PIN is automatically entered as soon as the game requests
it.

## Discussion

I know that hooking to the logs might not be the best solution. I'm very
open to discussions about this, and understand that this might be a deal
breaker for this feature. I made this to use with my setup and thought
I'd try opening this PR to see if the feature would be useful for
others.

I haven't found another way to make sure we're on the correct screen to
launch the PIN Macro, and sending the macro every time the Auto Card
Insert finishes had sync issues where the end of the PIN would actually
be registered by the game.

It would also be much more convenient for the users to hardcode a lookup
table for every games' log-trigger but I don't have the games currently
so I can't check the logs to see what would be best.

---------

Co-authored-by: Maxime St-Pierre <maxime.stpierre@ticketmaster.com>
2026-05-25 16:00:06 -07:00
bicarus f69e40fa26 gitadora: (arena model) booting fullscreen with one monitor (#696)
## Link to GitHub Issue or related Pull Request, if one exists
#477 

## Description of change
By default the game needs 4 monitors.

With the new option to disable subscreens, you can now boot fullscreen
with a single 4K monitor, with subscreen overlay enabled. Side monitors
are not shown in the overlay; only the touch screen is emulated.

## Future work

- [ ] custom resolution (-forceres) doesn't work properly (hooking
ResetEx does allow the game to boot but will render incorrectly;
probably needs hex edits)
- [x] enable this automatically if the user has fewer than 4 monitors
- [ ] ability to boot with a 4k main screen + small subscreen (with no
side screens which do nothing gameplay wise), but this whole display
emulation is a nightmare to work with & test thoroughly so it'll have to
be in the future.


## Testing
Checklist:

- [x] fs on 1 monitor with option set
- [x] fs on 2 monitor with option set
- [x] windowed - default (4 windows, no overlay)
- [x] windowed - with option set (1 window, overlay)
- [x] fs with 4 monitors?
- [x] check popn hc for regression
2026-05-18 15:28:12 -07:00
bicarus e9dcc5717e launcher: option to run as user without elevation (#695)
## Link to GitHub Issue or related Pull Request, if one exists
#694 

## Description of change
Before this change, we have it set in the executable manifest to always
ask for UAC elevation.

This change removes that and instead attempts to prompt for elevation
when we launch, depending on the new developer option that is being
added here to run as current user instead of elevating.

The default behavior continues to run as admin. This is still the safest
in terms of regression risk.

A bit of history on this: while spicecfg never technically needed to
elevate, we continue to do so to avoid difference in behavior when the
game is launched. For example, if the user is using an input remapper
running without elevation, it would inject input into spicecfg running
as user but fail to do so if the game runs elevated.

WinXP builds are unaffected (there is no UAC).

Some features will straight up fail without elevation so this will
remain as a debug option. To name a few (not a complete list):

* reboot/shutdown of PC
* running at realtime process priority
* `-nvprofile` option
* ...

## Testing
2026-05-16 16:27:59 -07:00
drmext 5bec3d5db7 network: icmp emulation (#694)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Add `-icmphook` option under spicecfg Development/Network for emulating
keepalive ping replies in user mode, so games do not need to open
privileged raw ICMP sockets. This makes connecting to networks possible
on macOS under Whisky, and on Windows with ICMP firewalled or spice
running without Administrator privileges.

## Testing
Tested DDR and SDVX on Windows 10 and macOS Tahoe. Network check status
in test menu shows `CONNECTED` when -icmphook is enabled, and `NOT
CONNECTED` when it is disabled. Actual network functionality works as
expected (under previously impossible conditions).
2026-05-14 00:44:14 -07:00
bicarus-dev 6ec2b47345 fix option text 2026-05-13 17:54:25 -07:00
28 changed files with 1565 additions and 144 deletions
+2
View File
@@ -523,6 +523,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/lang.cpp
hooks/libraryhook.cpp
hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
+1 -1
View File
@@ -23,7 +23,7 @@
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
+5 -5
View File
@@ -20,10 +20,10 @@
</dependentAssembly>
</dependency>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="requireAdministrator" uiAccess="false"/>
</requestedPrivileges>
</security>
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>
+332 -24
View File
@@ -8,11 +8,14 @@
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "util/cpuutils.h"
#include "util/deferlog.h"
#include "util/detour.h"
#include "util/libutils.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "util/socd_cleaner.h"
#include "util/sysutils.h"
#include "util/utils.h"
#include "hooks/setupapihook.h"
namespace games::gitadora {
@@ -20,7 +23,6 @@ namespace games::gitadora {
// settings
bool TWOCHANNEL = false;
std::optional<unsigned int> CAB_TYPE = std::nullopt;
bool ARENA_SINGLE_WINDOW = false;
bool P1_LEFTY = false;
bool P2_LEFTY = false;
std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
@@ -223,8 +225,7 @@ namespace games::gitadora {
// arena model launches a tiny window yet backbuffer at 4k, resulting in unusable overlay
// force scaling to make things usable
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() &&
GRAPHICS_WINDOWED && !cfg::CONFIGURATOR_STANDALONE) {
if (!overlay::UI_SCALE_PERCENT.has_value() && is_arena_model() && !cfg::CONFIGURATOR_STANDALONE) {
overlay::UI_SCALE_PERCENT = 250;
}
@@ -237,9 +238,292 @@ namespace games::gitadora {
} else {
log_info("gitadora", "guitar pick SOCD algorithm: legacy");
}
#if SPICE64 && !SPICE_XP
if (is_arena_model() && !GRAPHICS_WINDOWED && !GRAPHICS_FORCE_SINGLE_ADAPTER) {
const auto &monitors = sysutils::enumerate_monitors();
const size_t active_count = monitors.size();
log_info("gitadora", "arena model: detected {} active monitor(s)", active_count);
if (active_count < 4) {
log_info("gitadora", "arena model: enable single monitor mode due to insufficient monitors");
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
}
}
#endif
}
}
#if SPICE64 && !SPICE_XP
static decltype(GetDisplayConfigBufferSizes) *GetDisplayConfigBufferSizes_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
static decltype(DisplayConfigGetDeviceInfo) *DisplayConfigGetDeviceInfo_orig = nullptr;
// cached primary real monitor: its path + source/target mode entries.
// modeInfoIdx values on the path are renumbered to 0 / 1 so the cache is self-contained.
static DISPLAYCONFIG_PATH_INFO real_primary_path = {};
static DISPLAYCONFIG_MODE_INFO real_primary_modes[2] = {}; // [0]=source, [1]=target
// fake monitors appended after the real ones. the game classifies monitors
// by outputTechnology + connectorInstance:
// HDMI -> main 4k monitor (real primary)
// DP connInstance 0 -> left
// DP connInstance 1 -> right
// DP connInstance 2 -> small (sub/touch)
// ids are negated on the fake monitor headers so they can be distinguished
// from real ones in DisplayConfigGetDeviceInfo.
struct FakeMonitor {
LONG id;
int width;
int height;
int offset_x;
int offset_y;
UINT32 connector_instance;
};
// ORDERING MATTERS: the d3d9 wrapper (FAKE_SUBSCREEN_ADAPTER) hands out
// adapters as "\\.\DISPLAY_SPICE_FAKE_{N}" for N=1,2,3. The game maps each
// adapter to a swap chain role via its DisplayConfig connector instance,
// so the entries here must be listed in the same order the wrapper enumerates
// them: id=1 -> adapter 1 -> left, id=2 -> adapter 2 -> right, id=3 -> adapter 3 -> small.
static constexpr FakeMonitor FAKE_MONITORS[] = {
{ 1, 1080, 1920, -100000, -100000, 0 }, // left (DP connector instance 0)
{ 2, 1080, 1920, -200000, -200000, 1 }, // right (DP connector instance 1)
{ 3, 800, 1280, -300000, -300000, 2 }, // small (DP connector instance 2, touch)
};
static constexpr UINT32 FAKE_MONITOR_COUNT = static_cast<UINT32>(std::size(FAKE_MONITORS));
// call QueryDisplayConfig once, keep only the primary monitor's path and its
// two referenced modes (source + target). modeInfoIdx values are rewritten to
// 0 and 1 so the cache is self-consistent.
static void cache_primary_monitor_info() {
UINT32 path_count = 0;
UINT32 mode_count = 0;
if (GetDisplayConfigBufferSizes_orig(
QDC_ONLY_ACTIVE_PATHS, &path_count, &mode_count) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: GetDisplayConfigBufferSizes failed");
}
std::vector<DISPLAYCONFIG_PATH_INFO> all_paths(path_count);
std::vector<DISPLAYCONFIG_MODE_INFO> all_modes(mode_count);
if (QueryDisplayConfig_orig(
QDC_ONLY_ACTIVE_PATHS,
&path_count, all_paths.data(),
&mode_count, all_modes.data(),
nullptr) != ERROR_SUCCESS) {
log_fatal("gitadora", "cache_primary_monitor_info: QueryDisplayConfig failed");
}
all_paths.resize(path_count);
all_modes.resize(mode_count);
// pick the primary monitor: source mode at (0, 0)
auto primary = std::find_if(all_paths.begin(), all_paths.end(),
[&](const auto &p) {
const auto idx = p.sourceInfo.modeInfoIdx;
return idx < all_modes.size() &&
all_modes[idx].infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE &&
all_modes[idx].sourceMode.position.x == 0 &&
all_modes[idx].sourceMode.position.y == 0;
});
if (primary == all_paths.end()) {
log_fatal("gitadora", "cache_primary_monitor_info: no primary monitor found");
}
real_primary_modes[0] = all_modes[primary->sourceInfo.modeInfoIdx];
real_primary_modes[1] = all_modes[primary->targetInfo.modeInfoIdx];
real_primary_path = *primary;
real_primary_path.sourceInfo.modeInfoIdx = 0;
real_primary_path.targetInfo.modeInfoIdx = 1;
log_info("gitadora", "cache_primary_monitor_info: cached primary monitor");
}
static
LONG
WINAPI
GetDisplayConfigBufferSizes_hook(
UINT32 Flags,
UINT32 *pNumPathArrayElements,
UINT32 *pNumModeInfoArrayElements)
{
// populate cached primary real monitor on the first call
static std::once_flag populate_once;
std::call_once(populate_once, cache_primary_monitor_info);
// always report exactly 1 real + N fake monitors
*pNumPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*pNumModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
log_info(
"gitadora",
"GetDisplayConfigBufferSizes: 1 real path + {} fake monitor(s)",
FAKE_MONITOR_COUNT);
return ERROR_SUCCESS;
}
// write fake monitor i into the caller's path/mode arrays. layout (single-monitor
// assumption): index 0 in both arrays holds the cached primary real monitor, so
// fake i occupies path slot (1 + i) and mode slots (2 + i*2) / (2 + i*2 + 1).
static void insert_fake_monitor(
DISPLAYCONFIG_PATH_INFO *paths,
DISPLAYCONFIG_MODE_INFO *modes,
UINT32 i)
{
const FakeMonitor &m = FAKE_MONITORS[i];
const UINT32 src_idx = 2 + i * 2;
const UINT32 tgt_idx = src_idx + 1;
const LUID adapter_id { .LowPart = static_cast<DWORD>(-m.id), .HighPart = -m.id };
const UINT32 uid = static_cast<UINT32>(-m.id);
paths[1 + i] = {
.sourceInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = src_idx,
.statusFlags = DISPLAYCONFIG_SOURCE_IN_USE,
},
.targetInfo = {
.adapterId = adapter_id,
.id = uid,
.modeInfoIdx = tgt_idx,
.outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL,
.rotation = DISPLAYCONFIG_ROTATION_IDENTITY,
.scaling = DISPLAYCONFIG_SCALING_IDENTITY,
.refreshRate = { .Numerator = 60000, .Denominator = 1000 },
.scanLineOrdering = DISPLAYCONFIG_SCANLINE_ORDERING_PROGRESSIVE,
.targetAvailable = TRUE,
.statusFlags = DISPLAYCONFIG_TARGET_IN_USE,
},
.flags = DISPLAYCONFIG_PATH_ACTIVE,
};
modes[src_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
.id = uid,
.adapterId = adapter_id,
.sourceMode = {
.width = static_cast<UINT32>(m.width),
.height = static_cast<UINT32>(m.height),
.pixelFormat = DISPLAYCONFIG_PIXELFORMAT_32BPP,
.position = { .x = m.offset_x, .y = m.offset_y },
},
};
modes[tgt_idx] = {
.infoType = DISPLAYCONFIG_MODE_INFO_TYPE_TARGET,
.id = uid,
.adapterId = adapter_id,
.targetMode = {},
};
log_misc(
"gitadora",
"inserted fake monitor: id={}, width={}, height={}, offset_x={}, offset_y={}",
m.id, m.width, m.height, m.offset_x, m.offset_y);
}
static
LONG
WINAPI
QueryDisplayConfig_hook(
UINT32 flags,
UINT32* numPathArrayElements,
DISPLAYCONFIG_PATH_INFO* pathArray,
UINT32* numModeInfoArrayElements,
DISPLAYCONFIG_MODE_INFO* modeInfoArray,
DISPLAYCONFIG_TOPOLOGY_ID* currentTopologyId)
{
// copy cached primary real monitor into caller buffers at index 0
pathArray[0] = real_primary_path;
modeInfoArray[0] = real_primary_modes[0];
modeInfoArray[1] = real_primary_modes[1];
*numPathArrayElements = 1 + FAKE_MONITOR_COUNT;
*numModeInfoArrayElements = 2 + FAKE_MONITOR_COUNT * 2;
if (currentTopologyId != nullptr) {
*currentTopologyId = DISPLAYCONFIG_TOPOLOGY_EXTEND;
}
log_misc("gitadora", "QueryDisplayConfig returning fake monitor paths and modes");
// append fake monitors after the real one
for (UINT32 i = 0; i < FAKE_MONITOR_COUNT; i++) {
insert_fake_monitor(pathArray, modeInfoArray, i);
}
return ERROR_SUCCESS;
}
static
LONG
WINAPI
DisplayConfigGetDeviceInfo_hook(DISPLAYCONFIG_DEVICE_INFO_HEADER* requestPacket)
{
if (requestPacket == nullptr) {
return DisplayConfigGetDeviceInfo_orig(requestPacket);
}
// handle fake monitors (negative id) directly without calling orig
const auto id = static_cast<int>(requestPacket->id);
if (id < 0) {
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto sourceName = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// name must match WrappedIDirect3D9::GetAdapterIdentifier
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", -id);
wcscpy(sourceName->viewGdiDeviceName, s2ws(adapter_name).c_str());
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake source id={} name={}", id, adapter_name);
return ERROR_SUCCESS;
}
if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const LONG fake_id = -id;
UINT32 conn_inst = 0xff;
for (const auto& f : FAKE_MONITORS) {
if (f.id == fake_id) {
conn_inst = f.connector_instance;
break;
}
}
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_DISPLAYPORT_EXTERNAL;
targetName->connectorInstance = conn_inst;
wcscpy(targetName->monitorFriendlyDeviceName, L"Spice Fake Monitor");
wcscpy(targetName->monitorDevicePath, L"\\\\?\\SpiceFakeMonitor");
log_misc("gitadora",
"DisplayConfigGetDeviceInfo: fake target id={} -> DP connInst {}",
id, targetName->connectorInstance);
return ERROR_SUCCESS;
}
}
const auto ret = DisplayConfigGetDeviceInfo_orig(requestPacket);
if (ret != ERROR_SUCCESS ||
requestPacket->type != DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME) {
return ret;
}
// override the cached primary real monitor target info to look like HDMI/0
const auto targetName = reinterpret_cast<DISPLAYCONFIG_TARGET_DEVICE_NAME*>(requestPacket);
const auto& target = real_primary_path.targetInfo;
if (target.id == targetName->header.id &&
target.adapterId.HighPart == targetName->header.adapterId.HighPart &&
target.adapterId.LowPart == targetName->header.adapterId.LowPart)
{
targetName->outputTechnology = DISPLAYCONFIG_OUTPUT_TECHNOLOGY_HDMI;
targetName->connectorInstance = 0;
log_info("gitadora",
"overriding primary monitor (id={}) to pretend to be HDMI",
targetName->header.id);
}
return ret;
}
#endif
void GitaDoraGame::attach() {
Game::attach();
@@ -296,38 +580,62 @@ namespace games::gitadora {
// volume change prevention
hooks::audio::mme::init(avs::game::DLL_INSTANCE);
// touch hook
if (ARENA_SINGLE_WINDOW) {
// monitor/touch hooks (windowed or full screen)
if (GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW) {
// enable touch hook for subscreen overlay
wintouchemu::FORCE = true;
wintouchemu::INJECT_MOUSE_AS_WM_TOUCH = true;
wintouchemu::hook("GITADORA", avs::game::DLL_INSTANCE);
}
return;
}
HMODULE gdme_module = libutils::try_module("libgdme.dll");
#if !SPICE_XP
if (!GRAPHICS_WINDOWED) {
// monitor hook: always pretend to have 1 primary real monitor + 3 fake monitors
// (LEFT / RIGHT / SMALL) so the game accepts the arena cab topology
GetDisplayConfigBufferSizes_orig =
detour::iat_try("GetDisplayConfigBufferSizes",
GetDisplayConfigBufferSizes_hook, avs::game::DLL_INSTANCE);
QueryDisplayConfig_orig =
detour::iat_try("QueryDisplayConfig",
QueryDisplayConfig_hook, avs::game::DLL_INSTANCE);
DisplayConfigGetDeviceInfo_orig =
detour::iat_try("DisplayConfigGetDeviceInfo",
DisplayConfigGetDeviceInfo_hook, avs::game::DLL_INSTANCE);
}
#endif
}
}
// window patch
if (GRAPHICS_WINDOWED && !replace_pattern(
gdme_module,
"754185ED753D8B4118BF0000CB02",
"9090????9090??????????????12", 0, 0))
{
log_warning("gitadora", "windowed mode failed");
if (!is_arena_model()) {
HMODULE gdme_module = libutils::try_module("libgdme.dll");
if (GRAPHICS_WINDOWED && !replace_pattern(
gdme_module,
"754185ED753D8B4118BF0000CB02",
"9090????9090??????????????12", 0, 0)) {
log_warning("gitadora", "windowed mode failed");
}
}
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
// two channel mod
if (TWOCHANNEL) {
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (is_arena_model()) {
log_warning("gitadora", "two channel audio (-2ch) is not supported on Arena Model - use a patch instead");
deferredlogs::defer_error_messages({
"two channel audio (-2ch) is not supported on Arena Model - use a patch instead",
});
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0)))
{
log_warning("gitadora", "two channel mode failed");
} else {
HMODULE bmsd_engine_module = libutils::try_module("libbmsd-engine.dll");
HMODULE bmsd_module = libutils::try_module("libbmsd.dll");
bmsd2_boot_orig = detour::iat_try("bmsd2_boot", bmsd2_boot_hook, bmsd_module);
if (!(replace_pattern(bmsd_engine_module, "33000000488D", "03??????????", 0, 0) ||
replace_pattern(bmsd_engine_module, "330000000F10", "03??????????", 0, 0))) {
log_warning("gitadora", "two channel mode failed");
}
}
}
-1
View File
@@ -11,7 +11,6 @@ namespace games::gitadora {
// settings
extern bool TWOCHANNEL;
extern std::optional<unsigned int> CAB_TYPE;
extern bool ARENA_SINGLE_WINDOW;
extern bool P1_LEFTY;
extern bool P2_LEFTY;
extern std::optional<std::string> SUBSCREEN_OVERLAY_SIZE;
+1 -1
View File
@@ -261,7 +261,7 @@ namespace games::popn {
} else if (requestPacket->type == DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME) {
const auto source = reinterpret_cast<DISPLAYCONFIG_SOURCE_DEVICE_NAME*>(requestPacket);
// value must match WrappedIDirect3D9::GetAdapterIdentifier
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE");
wcscpy(source->viewGdiDeviceName, L"\\\\.\\DISPLAY_SPICE_FAKE_1");
} else {
log_fatal(
"popn",
+20 -6
View File
@@ -6,27 +6,32 @@
#include "util/detour.h"
#include "util/precise_timer.h"
#include "util/logging.h"
#include "cfg/configurator.h"
#include "touch.h"
namespace games::rb {
uint16_t TOUCH_POLL_RATE = 0;
}
static decltype(SleepEx) *SleepEx_orig;
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAltertable) {
static DWORD WINAPI SleepEx_hook(DWORD dwMilliseconds, BOOL bAlertable) {
// increase touch poll from ~110 FPS to ~500 FPS (Sleep) or ~1000 FPS (Win10 high-res timer)
// increase touch poll rate
if (dwMilliseconds == 8) {
dwMilliseconds = 1;
dwMilliseconds = 1000 / games::rb::TOUCH_POLL_RATE;
}
// most calls from rb are actually non-alertable
if (!bAltertable) {
if (!bAlertable) {
static thread_local timeutils::PreciseSleepTimer timer;
timer.sleep(dwMilliseconds);
return 0;
}
// call original
return SleepEx_orig(dwMilliseconds, bAltertable);
return SleepEx_orig(dwMilliseconds, bAlertable);
}
games::rb::RBGame::RBGame() : Game("Reflec Beat") {
@@ -35,6 +40,10 @@ games::rb::RBGame::RBGame() : Game("Reflec Beat") {
void games::rb::RBGame::attach() {
Game::attach();
if (1000 < games::rb::TOUCH_POLL_RATE) {
log_fatal("rb", "-rbtouchsize is set too high; cannot exceed 1000");
}
// init stuff
devicehook_init();
@@ -47,7 +56,12 @@ void games::rb::RBGame::attach() {
}
// hooks
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
if (0 < TOUCH_POLL_RATE) {
log_info("rb", "force increasing touch poll rate by hooking SleepEx ({}Hz)", TOUCH_POLL_RATE);
SleepEx_orig = detour::iat_try("SleepEx", SleepEx_hook, avs::game::DLL_INSTANCE);
} else {
log_info("rb", "not changing touch poll rate (120Hz by default)");
}
}
void games::rb::RBGame::detach() {
+2
View File
@@ -7,6 +7,8 @@
namespace games::rb {
extern uint16_t TOUCH_SCALING;
extern uint8_t TOUCH_SIZE;
extern uint16_t TOUCH_POLL_RATE;
class RBGame : public games::Game {
public:
+14 -9
View File
@@ -13,6 +13,7 @@ static std::string WINDOW_TITLE = "REFLEC BEAT";
namespace games::rb {
uint16_t TOUCH_SCALING = 1000;
uint8_t TOUCH_SIZE = 1;
}
games::rb::ReflecBeatTouchDeviceHandle::ReflecBeatTouchDeviceHandle(bool log_fps) {
@@ -160,16 +161,20 @@ int games::rb::ReflecBeatTouchDeviceHandle::read(LPVOID lpBuffer, DWORD nNumberO
const auto point_x = (int) ((x - window_width / 2.0) * 48.0 / 54.0 + window_width / 2.0);
const auto point_y = (int) (y - window_height / 76);
// insert 9 times with offset to double the precision
// center
grid_insert(data, point_x, point_y);
grid_insert(data, point_x - offset_x, point_y);
grid_insert(data, point_x - offset_x, point_y - offset_y);
grid_insert(data, point_x - offset_x, point_y + offset_y);
grid_insert(data, point_x + offset_x, point_y);
grid_insert(data, point_x + offset_x, point_y + offset_y);
grid_insert(data, point_x + offset_x, point_y - offset_y);
grid_insert(data, point_x, point_y - offset_y);
grid_insert(data, point_x, point_y + offset_y);
// surrounding points
if (games::rb::TOUCH_SIZE == 3) {
grid_insert(data, point_x - offset_x, point_y); // west
grid_insert(data, point_x - offset_x, point_y - offset_y); // northwest
grid_insert(data, point_x - offset_x, point_y + offset_y); // southwest
grid_insert(data, point_x + offset_x, point_y); // east
grid_insert(data, point_x + offset_x, point_y + offset_y); // southeast
grid_insert(data, point_x + offset_x, point_y - offset_y); // northeast
grid_insert(data, point_x, point_y - offset_y); // north
grid_insert(data, point_x, point_y + offset_y); // south
}
}
// copy data to buffer
@@ -465,15 +465,21 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::RegisterSoftwareDevice(void *pIniti
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterCount() {
UINT result = pReal->GetAdapterCount();
if (!FAKE_SUBSCREEN_ADAPTER && games::popn::is_pikapika_model() && result == 1) {
FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned 1, popn needs 2 adapters - enabliing fake submonitor mode");
}
if (!FAKE_SUBSCREEN_ADAPTER) {
if (games::popn::is_pikapika_model() && result == 1) {
FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned {}, popn needs 2 adapters - enabling fake submonitor mode", result);
return 2;
if (FAKE_SUBSCREEN_ADAPTER) {
return 2;
} else if (games::gitadora::is_arena_model() && !GRAPHICS_WINDOWED && result < 4) {
FAKE_SUBSCREEN_ADAPTER = true;
log_info(
"graphics::d3d9",
"GetAdapterCount returned {}, gfdm needs 4 adapters - enabling fake submonitor mode", result);
return 4;
}
}
return result;
@@ -485,12 +491,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
D3DADAPTER_IDENTIFIER9 *pIdentifier)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1 && pIdentifier) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0 && pIdentifier) {
*pIdentifier = {};
strcpy(pIdentifier->DeviceName, "\\\\.\\DISPLAY_SPICE_FAKE");
const std::string adapter_name = fmt::format("\\\\.\\DISPLAY_SPICE_FAKE_{}", Adapter);
strcpy(pIdentifier->DeviceName, adapter_name.c_str());
log_misc(
"graphics::d3d9",
"GetAdapterIdentifier called for fake subscreen adapter 1: {}",
"GetAdapterIdentifier called for fake subscreen adapter {}: {}",
Adapter,
pIdentifier->DeviceName);
return S_OK;
}
@@ -499,8 +507,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterIdentifier(
}
UINT STDMETHODCALLTYPE WrappedIDirect3D9::GetAdapterModeCount(UINT Adapter, D3DFORMAT Format) {
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter");
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "GetAdapterModeCount called for fake subscreen adapter {}", Adapter);
return 1;
}
@@ -513,8 +521,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::EnumAdapterModes(
UINT Mode,
D3DDISPLAYMODE *pMode)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter");
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "EnumAdapterModes called for fake subscreen adapter {}", Adapter);
if (Mode == 0 && pMode) {
pMode->Width = 1280;
pMode->Height = 800;
@@ -622,8 +630,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CheckDeviceFormat(
D3DRESOURCETYPE RType,
D3DFORMAT CheckFormat)
{
if (FAKE_SUBSCREEN_ADAPTER && Adapter == 1) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter");
if (FAKE_SUBSCREEN_ADAPTER && Adapter > 0) {
log_misc("graphics::d3d9", "CheckDeviceFormat called for fake subscreen adapter {}", Adapter);
return D3D_OK;
}
@@ -1143,10 +1151,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(hFocusWindow, *ppReturnedDeviceInterface);
// initialize sub screen if IIDX/SDVX requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39"}) &&
// initialize sub screen if the game requested a multi-head context
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[1]);
UINT i = 1;
if (games::gitadora::is_arena_model()) {
i = 2;
}
graphics_d3d9_ldj_init_sub_screen(*ppReturnedDeviceInterface, &pPresentationParameters[i]);
}
}
@@ -1196,7 +1209,13 @@ static void graphics_d3d9_ldj_init_sub_screen(IDirect3DDevice9Ex *device, D3DPRE
log_info("graphics::d3d9", "created additional swap chain for windowed mode");
}
} else {
hr = device->GetSwapChain(1, &SUB_SWAP_CHAIN);
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 2;
}
hr = device->GetSwapChain(swapchain, &SUB_SWAP_CHAIN);
if (FAILED(hr)) {
log_warning("graphics::d3d9", "failed to acquire fullscreen sub swap chain, hr={}", FMT_HRESULT(hr));
} else {
@@ -1233,7 +1252,7 @@ static void graphics_d3d9_ldj_on_present(IDirect3DDevice9 *wrapped_device) {
// iidx/sdvx
int swapchain = 1;
if (games::gitadora::is_arena_model()) {
swapchain = 3;
swapchain = 2;
}
if (!ATTEMPTED_SUB_SWAP_CHAIN_ACQUIRE && SUB_SWAP_CHAIN == nullptr) {
@@ -1452,8 +1471,9 @@ void graphics_d3d9_on_present(
// for IIDX TDJ / SDVX UFC, handle subscreen
const bool is_vm = games::sdvx::is_valkyrie_model();
const bool is_tdj = avs::game::is_model("LDJ") && games::iidx::TDJ_MODE;
const bool is_gfdm_arena =
games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW;
const bool is_gfdm_arena = games::gitadora::is_arena_model() &&
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW);
const bool is_pika = games::popn::is_pikapika_model();
if (is_vm || is_tdj || is_gfdm_arena || is_pika) {
graphics_d3d9_ldj_on_present(wrapped_device);
@@ -129,13 +129,17 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
this->main_swapchain->Release();
this->main_swapchain = nullptr;
}
if (this->sub_swapchain) {
this->sub_swapchain->Release();
this->sub_swapchain = nullptr;
for (auto &sc : this->sub_swapchain) {
if (sc) {
sc->Release();
sc = nullptr;
}
}
if (this->fake_sub_swapchain) {
this->fake_sub_swapchain->Release();
this->fake_sub_swapchain = nullptr;
for (auto &sc : this->fake_sub_swapchain) {
if (sc) {
sc->Release();
sc = nullptr;
}
}
if (overlay::ENABLED) {
@@ -250,27 +254,47 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::CreateAdditionalSwapChain(
WRAP_VERBOSE;
HRESULT hr = pReal->CreateAdditionalSwapChain(pPresentationParameters, ppSwapChain);
int index = 0;
bool create_swap_chain = false;
if (avs::game::is_model({"LDJ", "KFC", "M39"})) {
create_swap_chain = true;
} else if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
pPresentationParameters->BackBufferWidth == 800) {
create_swap_chain = true;
(GRAPHICS_FORCE_SINGLE_ADAPTER || GRAPHICS_PREVENT_SECONDARY_WINDOW)) {
if (pPresentationParameters->BackBufferWidth == 800) {
// SMALL (subscreen)
create_swap_chain = true;
index = 0;
} else if (pPresentationParameters->BackBufferWidth == 1080) {
// LEFT/RIGHT
create_swap_chain = true;
index = 1;
if (sub_swapchain[index] || fake_sub_swapchain[index]) {
index = 2;
}
} else {
log_warning("graphics::d3d9", "unknown swap chain detected in CreateAdditionalSwapChain");
}
}
if (create_swap_chain) {
if (SUCCEEDED(hr) && !sub_swapchain) {
sub_swapchain = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain->should_run_hooks = false;
} else if (FAILED(hr) && !fake_sub_swapchain) {
log_misc(
"graphics::d3d9",
"CreateAdditionalSwapChain called for swap chain {}, creating swap chain",
index);
if (SUCCEEDED(hr) && !sub_swapchain[index]) {
sub_swapchain[index] = new WrappedIDirect3DSwapChain9(this, *ppSwapChain);
sub_swapchain[index]->should_run_hooks = false;
} else if (FAILED(hr) && !fake_sub_swapchain[index]) {
log_warning("graphics::d3d9",
"failed to create sub swap chain, hr={}, using fake swap chain",
FMT_HRESULT(hr));
fake_sub_swapchain = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
fake_sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain);
fake_sub_swapchain[index] = new FakeIDirect3DSwapChain9(this, pPresentationParameters, false);
fake_sub_swapchain[index]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[index]);
return D3D_OK;
}
@@ -308,22 +332,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetSwapChain(
if (iSwapChain == 1 && avs::game::is_model({"LDJ", "KFC", "M39"})) {
swap = true;
}
if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
iSwapChain == 3) {
if (games::gitadora::is_arena_model() && iSwapChain == 2) {
swap = true;
}
if (swap) {
if (sub_swapchain) {
sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain);
if (sub_swapchain[0]) {
sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(sub_swapchain[0]);
graphics_screens_register(iSwapChain);
return D3D_OK;
} else if (fake_sub_swapchain) {
fake_sub_swapchain->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain);
} else if (fake_sub_swapchain[0]) {
fake_sub_swapchain[0]->AddRef();
*ppSwapChain = static_cast<IDirect3DSwapChain9 *>(fake_sub_swapchain[0]);
graphics_screens_register(iSwapChain);
return D3D_OK;
@@ -344,7 +366,7 @@ UINT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetNumberOfSwapChains() {
UINT n = pReal->GetNumberOfSwapChains();
if (sub_swapchain && avs::game::is_model({"LDJ", "KFC", "M39"})) {
if (sub_swapchain[0] && avs::game::is_model({"LDJ", "KFC", "M39"})) {
n += 1;
}
@@ -204,7 +204,8 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain = nullptr;
FakeIDirect3DSwapChain9 *fake_sub_swapchain = nullptr;
WrappedIDirect3DSwapChain9 *sub_swapchain[3] = { nullptr, nullptr, nullptr };
FakeIDirect3DSwapChain9 *fake_sub_swapchain[3] = { nullptr, nullptr, nullptr };
IDirect3DVertexShader9 *vertex_shader = nullptr;
};
+2 -2
View File
@@ -421,7 +421,7 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
// only hook touch window if multiple windows are allowed
if (is_gfdm_sub_window && !games::gitadora::ARENA_SINGLE_WINDOW) {
if (is_gfdm_sub_window && GRAPHICS_WINDOWED && !GRAPHICS_PREVENT_SECONDARY_WINDOW) {
GFDM_SUBSCREEN_WINDOW = result;
graphics_hook_subscreen_window(GFDM_SUBSCREEN_WINDOW);
}
@@ -702,7 +702,7 @@ static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
static BOOL WINAPI ShowWindow_hook(HWND hWnd, int nCmdShow) {
if (games::gitadora::is_arena_model() &&
games::gitadora::ARENA_SINGLE_WINDOW &&
GRAPHICS_PREVENT_SECONDARY_WINDOW &&
hWnd != GRAPHICS_HOOKED_WINDOW) {
log_info("graphics", "ShowWindow_hook - hiding sub window {}", fmt::ptr(hWnd));
return true;
+94
View File
@@ -0,0 +1,94 @@
#include <winsock2.h>
#include <windows.h>
#include <iptypes.h>
#include <iphlpapi.h>
#include <icmpapi.h>
#include <algorithm>
#include <cstddef>
#include <cstring>
#include "util/detour.h"
#include "util/logging.h"
namespace {
#pragma pack(push, 1)
struct IcmpHdr {
uint8_t type;
uint8_t code;
uint16_t checksum_be;
};
#pragma pack(pop)
decltype(IcmpSendEcho) *IcmpSendEcho_orig = nullptr;
DWORD WINAPI IcmpSendEcho_hook(
HANDLE IcmpHandle,
IPAddr DestinationAddress,
LPVOID RequestData,
WORD RequestSize,
PIP_OPTION_INFORMATION RequestOptions,
LPVOID ReplyBuffer,
DWORD ReplySize,
DWORD Timeout) {
if (!IcmpSendEcho_orig) {
SetLastError(ERROR_INVALID_FUNCTION);
return 0;
}
DWORD n = IcmpSendEcho_orig(
IcmpHandle,
DestinationAddress,
RequestData,
RequestSize,
RequestOptions,
ReplyBuffer,
ReplySize,
Timeout);
if (n != 0) {
return n;
}
if (ReplyBuffer == nullptr || ReplySize < sizeof(ICMP_ECHO_REPLY) || RequestData == nullptr) {
return 0;
}
if (RequestSize < sizeof(IcmpHdr)) {
return 0;
}
const auto *req = reinterpret_cast<const uint8_t *>(RequestData);
const auto *ih = reinterpret_cast<const IcmpHdr *>(req);
if (ih->type != 8 || ih->code != 0) {
return 0;
}
memset(ReplyBuffer, 0, ReplySize);
auto *reply = reinterpret_cast<ICMP_ECHO_REPLY *>(ReplyBuffer);
const size_t data_off = offsetof(ICMP_ECHO_REPLY, Data);
const size_t avail = ReplySize > data_off ? ReplySize - data_off : 0;
const size_t copy_len = (std::min)(static_cast<size_t>(RequestSize), avail);
reply->Address = DestinationAddress;
reply->Status = IP_SUCCESS;
reply->RoundTripTime = 1;
reply->DataSize = static_cast<USHORT>(copy_len);
if (copy_len > 0) {
memcpy(reinterpret_cast<uint8_t *>(ReplyBuffer) + data_off, RequestData, copy_len);
}
SetLastError(ERROR_SUCCESS);
return 1;
}
} // namespace
void icmphook_iphlpapi_install() {
static bool done = false;
if (done) {
return;
}
done = true;
if (!detour::trampoline_try(
"iphlpapi.dll",
"IcmpSendEcho",
(void *) IcmpSendEcho_hook,
(void **) &IcmpSendEcho_orig)) {
log_warning("network", "ICMP emulation: IcmpSendEcho hook was not installed");
}
}
+692
View File
@@ -0,0 +1,692 @@
#include "icmphook_net.h"
#include "util/detour.h"
#include "util/logging.h"
#include <iphlpapi.h>
#include <mutex>
#include <deque>
#include <vector>
#include <chrono>
#include <cstring>
#include <atomic>
#include <condition_variable>
#include <unordered_map>
void icmphook_iphlpapi_install();
#ifndef SPICE64
using socket_uint = unsigned int;
#else
using socket_uint = unsigned long long;
#endif
namespace icmphook_internal {
constexpr uint32_t k_loopback_host = 0x7f000001u;
#pragma pack(push, 1)
struct IcmpHeaderView {
uint8_t type;
uint8_t code;
uint16_t checksum_be;
};
#pragma pack(pop)
bool icmp_packet_valid(const uint8_t *icmp, size_t len) {
if (len < sizeof(IcmpHeaderView)) {
return false;
}
unsigned sum = 0;
for (size_t i = 0; i + 1 < len; i += 2) {
sum += (static_cast<unsigned>(icmp[i]) << 8) | icmp[i + 1];
sum = (sum >> 16) + (sum & 0xFFFF);
}
if (len & 1) {
sum += static_cast<unsigned>(icmp[len - 1]) << 8;
sum = (sum >> 16) + (sum & 0xFFFF);
}
sum = (sum >> 16) + (sum & 0xFFFF);
return sum == 0xFFFFu || sum == 0;
}
bool icmp_build_echo_reply(std::vector<uint8_t> &out, const uint8_t *req, size_t req_len) {
if (req_len < sizeof(IcmpHeaderView)) {
return false;
}
out.resize(req_len);
memcpy(out.data(), req, req_len);
auto *hdr = reinterpret_cast<IcmpHeaderView *>(out.data());
hdr->type = 0;
hdr->code = 0;
hdr->checksum_be = 0;
unsigned sum = 0;
for (size_t i = 0; i + 1 < out.size(); i += 2) {
sum += (static_cast<unsigned>(out[i]) << 8) | out[i + 1];
sum = (sum >> 16) + (sum & 0xFFFF);
}
if (out.size() & 1) {
sum += static_cast<unsigned>(out[out.size() - 1]) << 8;
sum = (sum >> 16) + (sum & 0xFFFF);
}
sum = (sum >> 16) + (sum & 0xFFFF);
uint16_t csum = static_cast<uint16_t>(~sum);
hdr->checksum_be = htons(csum);
return true;
}
bool icmp_fix_pointer(
const uint8_t *base,
size_t total_len,
const uint8_t **icmp_start,
size_t *icmp_len) {
if (total_len >= 20 && (base[0] >> 4) == 4) {
size_t ihl = (base[0] & 0x0Fu) * 4;
if (ihl < 20 || ihl > total_len || base[9] != IPPROTO_ICMP) {
return false;
}
*icmp_start = base + ihl;
*icmp_len = total_len - ihl;
return true;
}
*icmp_start = base;
*icmp_len = total_len;
return true;
}
bool ipv4_encode_datagram(
std::vector<uint8_t> &out,
uint32_t src_host_order,
uint32_t dst_host_order,
const uint8_t *payload,
size_t payload_len) {
const size_t hdr_len = 20;
if (payload_len > 0xFFFFu - hdr_len) {
return false;
}
const auto total_len = static_cast<uint16_t>(hdr_len + payload_len);
out.resize(hdr_len + payload_len);
uint8_t *p = out.data();
memset(p, 0, hdr_len);
p[0] = 0x45;
p[1] = 0;
p[2] = static_cast<uint8_t>(total_len >> 8);
p[3] = static_cast<uint8_t>(total_len & 0xFF);
p[6] = 0x40;
p[8] = 1;
p[9] = IPPROTO_ICMP;
uint32_t src_wire = htonl(src_host_order);
uint32_t dst_wire = htonl(dst_host_order);
memcpy(p + 12, &src_wire, 4);
memcpy(p + 16, &dst_wire, 4);
unsigned sum = 0;
for (size_t i = 0; i < hdr_len; i += 2) {
sum += (static_cast<unsigned>(p[i]) << 8) | p[i + 1];
while (sum >> 16) {
sum = (sum & 0xFFFF) + (sum >> 16);
}
}
uint16_t hdr_csum = static_cast<uint16_t>(~sum);
p[10] = static_cast<uint8_t>(hdr_csum >> 8);
p[11] = static_cast<uint8_t>(hdr_csum & 0xFF);
memcpy(p + hdr_len, payload, payload_len);
return true;
}
// Cached adapter list. Populated once per process: adapter changes after the
// first ICMP send are intentionally not picked up, in exchange for avoiding
// repeated IP Helper calls on every emulated send.
std::once_flag g_adapters_cache_init;
std::vector<uint8_t> g_adapters_cache;
void fill_adapters_cache() {
std::call_once(g_adapters_cache_init, []() {
ULONG buf_len = 0;
DWORD r = GetAdaptersInfo(nullptr, &buf_len);
if (r != ERROR_BUFFER_OVERFLOW && r != ERROR_SUCCESS) {
return;
}
if (buf_len == 0) {
return;
}
g_adapters_cache.resize(buf_len);
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
if (GetAdaptersInfo(info, &buf_len) != ERROR_SUCCESS) {
g_adapters_cache.clear();
}
});
}
uint32_t local_ipv4_for_peer(
uint32_t peer_host_order,
uint32_t bind_pref_host_order,
bool has_bind) {
if (peer_host_order == k_loopback_host) {
return k_loopback_host;
}
if (has_bind && bind_pref_host_order != 0) {
return bind_pref_host_order;
}
fill_adapters_cache();
if (g_adapters_cache.empty()) {
return peer_host_order;
}
auto *info = reinterpret_cast<PIP_ADAPTER_INFO>(g_adapters_cache.data());
for (PIP_ADAPTER_INFO a = info; a != nullptr; a = a->Next) {
uint32_t ip = ntohl(inet_addr(a->IpAddressList.IpAddress.String));
uint32_t mask = ntohl(inet_addr(a->IpAddressList.IpMask.String));
if (mask == 0) {
continue;
}
if ((peer_host_order & mask) == (ip & mask)) {
return ip;
}
}
return peer_host_order;
}
struct EmuSock {
std::deque<std::vector<uint8_t>> queue;
std::condition_variable_any cv;
DWORD rx_timeout_ms = INFINITE;
bool nonblocking = false;
bool has_bind = false;
uint32_t bind_ip_host = 0;
};
std::recursive_mutex g_mu;
std::unordered_map<SOCKET, std::unique_ptr<EmuSock>> g_socks;
std::atomic<uint32_t> g_seq{1};
// Fast-path flag so the bind() hook in networkhook.cpp can short-circuit
// without taking g_mu when the ICMP feature was never enabled.
std::atomic<bool> g_installed{false};
decltype(socket) *socket_orig = nullptr;
decltype(WSASocketW) *WSASocketW_orig = nullptr;
decltype(WSASocketA) *WSASocketA_orig = nullptr;
decltype(closesocket) *closesocket_orig = nullptr;
decltype(bind) *bind_trampoline_orig = nullptr;
decltype(sendto) *sendto_orig = nullptr;
decltype(recvfrom) *recvfrom_orig = nullptr;
decltype(WSASendTo) *WSASendTo_orig = nullptr;
decltype(WSARecvFrom) *WSARecvFrom_orig = nullptr;
decltype(ioctlsocket) *ioctlsocket_orig = nullptr;
decltype(setsockopt) *setsockopt_orig = nullptr;
EmuSock *lookup(SOCKET s) {
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
return nullptr;
}
return it->second.get();
}
SOCKET alloc_icmp_socket() {
std::lock_guard<std::recursive_mutex> lock(g_mu);
for (;;) {
uint32_t x = g_seq.fetch_add(1, std::memory_order_relaxed);
SOCKET s = (SOCKET)(socket_uint)(0xE0000000u | (x & 0x0FFFFFFFu));
if (s == INVALID_SOCKET) {
continue;
}
if (g_socks.find(s) == g_socks.end()) {
g_socks[s] = std::make_unique<EmuSock>();
return s;
}
}
}
void release_icmp_socket(SOCKET s) {
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
it->second->cv.notify_all();
g_socks.erase(it);
}
}
bool process_icmp_send(SOCKET s, const uint8_t *buf, int len, const sockaddr *to, int tolen) {
if (len <= 0 || tolen < (int) sizeof(sockaddr_in) || !buf || !to) {
return true;
}
auto *sin = reinterpret_cast<const sockaddr_in *>(to);
if (sin->sin_family != AF_INET) {
return true;
}
uint32_t peer_host = ntohl(sin->sin_addr.s_addr);
const uint8_t *icmp_ptr = nullptr;
size_t icmp_len = 0;
const auto *raw = reinterpret_cast<const uint8_t *>(buf);
if (!icmp_fix_pointer(raw, static_cast<size_t>(len), &icmp_ptr, &icmp_len)) {
return true;
}
if (!icmp_packet_valid(icmp_ptr, icmp_len)) {
return true;
}
if (icmp_len < sizeof(IcmpHeaderView)) {
return true;
}
const auto *ih = reinterpret_cast<const IcmpHeaderView *>(icmp_ptr);
if (ih->type != 8 || ih->code != 0) {
return true;
}
std::vector<uint8_t> icmp_reply;
if (!icmp_build_echo_reply(icmp_reply, icmp_ptr, icmp_len)) {
return true;
}
std::vector<uint8_t> packet;
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
return true;
}
EmuSock *es = it->second.get();
uint32_t local_host = local_ipv4_for_peer(peer_host, es->bind_ip_host, es->has_bind);
if (!ipv4_encode_datagram(
packet, peer_host, local_host, icmp_reply.data(), icmp_reply.size())) {
return true;
}
es->queue.push_back(std::move(packet));
es->cv.notify_all();
}
return true;
}
SOCKET WINAPI socket_hook(int af, int type, int protocol) {
if (af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw socket {}", (socket_uint) s);
return s;
}
return socket_orig(af, type, protocol);
}
SOCKET WINAPI WSASocketW_hook(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOW lpProtocolInfo,
GROUP g,
DWORD dwFlags) {
const bool is_raw_icmp =
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
if (lpProtocolInfo == nullptr && is_raw_icmp) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw WSASocketW {}", (socket_uint) s);
return s;
}
return WSASocketW_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
}
SOCKET WINAPI WSASocketA_hook(
int af,
int type,
int protocol,
LPWSAPROTOCOL_INFOA lpProtocolInfo,
GROUP g,
DWORD dwFlags) {
const bool is_raw_icmp =
af == AF_INET && type == SOCK_RAW && protocol == IPPROTO_ICMP;
if (lpProtocolInfo == nullptr && is_raw_icmp) {
SOCKET s = alloc_icmp_socket();
log_info("network", "ICMP emulation: allocated raw WSASocketA {}", (socket_uint) s);
return s;
}
return WSASocketA_orig(af, type, protocol, lpProtocolInfo, g, dwFlags);
}
int WINAPI closesocket_hook(SOCKET s) {
if (lookup(s)) {
release_icmp_socket(s);
return 0;
}
return closesocket_orig(s);
}
int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) {
auto *in = reinterpret_cast<const sockaddr_in *>(name);
if (in->sin_family != AF_INET) {
WSASetLastError(WSAEAFNOSUPPORT);
return SOCKET_ERROR;
}
it->second->has_bind = true;
it->second->bind_ip_host = ntohl(in->sin_addr.s_addr);
return 0;
}
}
return bind_trampoline_orig(s, name, namelen);
}
int WINAPI sendto_hook(
SOCKET s, const char *buf, int len, int flags, const sockaddr *to, int tolen) {
EmuSock *es = lookup(s);
if (!es) {
return sendto_orig(s, buf, len, flags, to, tolen);
}
if (!buf || !to) {
WSASetLastError(WSAEFAULT);
return SOCKET_ERROR;
}
process_icmp_send(s, reinterpret_cast<const uint8_t *>(buf), len, to, tolen);
return len;
}
int WINAPI WSASendTo_hook(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
DWORD dwFlags,
const sockaddr *lpTo,
int iTolen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
EmuSock *es = lookup(s);
if (!es) {
return WSASendTo_orig(
s,
lpBuffers,
dwBufferCount,
lpNumberOfBytesSent,
dwFlags,
lpTo,
iTolen,
lpOverlapped,
lpCompletionRoutine);
}
if (lpOverlapped != nullptr) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesSent) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
int len = (int) lpBuffers[0].len;
process_icmp_send(
s,
reinterpret_cast<const uint8_t *>(lpBuffers[0].buf),
len,
lpTo,
iTolen);
*lpNumberOfBytesSent = static_cast<DWORD>(len);
return 0;
}
// Dequeue one datagram for an emulated ICMP socket into the caller-provided
// buffer. If out_full_dgram_bytes is non-null, it receives the full packet
// size (before truncation to len) so callers can signal WSAEMSGSIZE.
int emu_recv_dequeue(
SOCKET s, char *buf, int len, sockaddr *from, int *fromlen,
size_t *out_full_dgram_bytes) {
if (!buf || len <= 0) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
std::vector<uint8_t> pkt;
{
std::unique_lock<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it == g_socks.end()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
auto wait_pred = [&] {
auto j = g_socks.find(s);
return j == g_socks.end() || !j->second->queue.empty();
};
EmuSock *es = it->second.get();
if (es->nonblocking) {
if (es->queue.empty()) {
WSASetLastError(WSAEWOULDBLOCK);
return SOCKET_ERROR;
}
} else if (es->rx_timeout_ms == INFINITE || es->rx_timeout_ms == 0) {
it->second->cv.wait(lock, wait_pred);
} else {
const auto timeout = std::chrono::milliseconds(es->rx_timeout_ms);
if (!it->second->cv.wait_for(lock, timeout, wait_pred)) {
WSASetLastError(WSAETIMEDOUT);
return SOCKET_ERROR;
}
}
it = g_socks.find(s);
if (it == g_socks.end()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
es = it->second.get();
if (es->queue.empty()) {
WSASetLastError(WSAENOTSOCK);
return SOCKET_ERROR;
}
pkt = std::move(es->queue.front());
es->queue.pop_front();
}
if (out_full_dgram_bytes) {
*out_full_dgram_bytes = pkt.size();
}
const int take = (len < (int) pkt.size()) ? len : (int) pkt.size();
memcpy(buf, pkt.data(), static_cast<size_t>(take));
if (from && fromlen && *fromlen >= (int) sizeof(sockaddr_in)) {
auto *out = reinterpret_cast<sockaddr_in *>(from);
memset(out, 0, sizeof(*out));
out->sin_family = AF_INET;
if (pkt.size() >= 20) {
memcpy(&out->sin_addr, pkt.data() + 12, 4);
} else {
out->sin_addr.s_addr = htonl(k_loopback_host);
}
*fromlen = sizeof(sockaddr_in);
}
return take;
}
int WINAPI recvfrom_hook(
SOCKET s, char *buf, int len, int flags, sockaddr *from, int *fromlen) {
if (!lookup(s)) {
return recvfrom_orig(s, buf, len, flags, from, fromlen);
}
return emu_recv_dequeue(s, buf, len, from, fromlen, nullptr);
}
int WINAPI WSARecvFrom_hook(
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd,
LPDWORD lpFlags,
sockaddr *lpFrom,
LPINT lpFromlen,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine) {
if (!lookup(s)) {
return WSARecvFrom_orig(
s,
lpBuffers,
dwBufferCount,
lpNumberOfBytesRecvd,
lpFlags,
lpFrom,
lpFromlen,
lpOverlapped,
lpCompletionRoutine);
}
if (lpOverlapped != nullptr) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (dwBufferCount != 1 || !lpBuffers || !lpNumberOfBytesRecvd) {
WSASetLastError(WSAEINVAL);
return SOCKET_ERROR;
}
sockaddr_in from_tmp{};
int fromlen_tmp = sizeof(from_tmp);
size_t full_bytes = 0;
int r = emu_recv_dequeue(
s,
lpBuffers[0].buf,
(int) lpBuffers[0].len,
reinterpret_cast<sockaddr *>(&from_tmp),
&fromlen_tmp,
&full_bytes);
if (r == SOCKET_ERROR) {
return SOCKET_ERROR;
}
if (full_bytes > lpBuffers[0].len) {
WSASetLastError(WSAEMSGSIZE);
return SOCKET_ERROR;
}
*lpNumberOfBytesRecvd = static_cast<DWORD>(r);
if (lpFlags) {
*lpFlags = 0;
}
if (lpFrom && lpFromlen && *lpFromlen >= (int) sizeof(sockaddr_in)) {
*reinterpret_cast<sockaddr_in *>(lpFrom) = from_tmp;
*lpFromlen = sizeof(sockaddr_in);
}
return 0;
}
int WINAPI ioctlsocket_hook(SOCKET s, long cmd, u_long *argp) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
EmuSock *es = it->second.get();
if (argp && static_cast<u_long>(cmd) == FIONBIO) {
es->nonblocking = (*argp != 0);
return 0;
}
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
}
return ioctlsocket_orig(s, cmd, argp);
}
int WINAPI setsockopt_hook(
SOCKET s, int level, int optname, const char *optval, int optlen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end()) {
const bool deref_optval = level == SOL_SOCKET && optlen >= (int) sizeof(DWORD);
if (deref_optval && optval) {
EmuSock *es = it->second.get();
auto v = reinterpret_cast<const DWORD *>(optval);
if (optname == SO_RCVTIMEO) {
DWORD ms = *v;
es->rx_timeout_ms = (ms == 0) ? INFINITE : ms;
return 0;
}
if (optname == SO_SNDTIMEO) {
return 0;
}
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
if (!deref_optval) {
WSASetLastError(WSAEOPNOTSUPP);
return SOCKET_ERROR;
}
}
}
return setsockopt_orig(s, level, optname, optval, optlen);
}
void install_icmphook_hooks() {
static bool done = false;
if (done) {
return;
}
done = true;
bool ok = true;
ok &= detour::trampoline_try(
"ws2_32.dll", "socket",
(void *) socket_hook, (void **) &socket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASocketW",
(void *) WSASocketW_hook, (void **) &WSASocketW_orig);
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) {
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)");
}
g_installed.store(true, std::memory_order_release);
}
} // namespace icmphook_internal
bool icmphook_is_emulated_socket(SOCKET s) {
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
return false;
}
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
return icmphook_internal::g_socks.find(s) != icmphook_internal::g_socks.end();
}
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result) {
if (!icmphook_internal::g_installed.load(std::memory_order_acquire)) {
return false;
}
std::lock_guard<std::recursive_mutex> lock(icmphook_internal::g_mu);
if (icmphook_internal::g_socks.find(s) == icmphook_internal::g_socks.end()) {
return false;
}
*out_result = icmphook_internal::bind_hook_ws2(s, name, namelen);
return true;
}
void icmphook_net_init() {
icmphook_internal::install_icmphook_hooks();
icmphook_iphlpapi_install();
}
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <winsock2.h>
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).
*/
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result);
void icmphook_net_init();
+6
View File
@@ -14,6 +14,7 @@
#include "util/fileutils.h"
#include "util/libutils.h"
#include "util/deferlog.h"
#include "hooks/icmphook_net.h"
// hooking related stuff
static decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
@@ -189,6 +190,11 @@ static int WINAPI bind_hook(SOCKET s, const struct sockaddr *name, int namelen)
#pragma ide diagnostic ignored "OCDFAInspection"
#endif
int icmp_bind_result = 0;
if (icmphook_try_bind(s, name, namelen, &icmp_bind_result)) {
return icmp_bind_result;
}
// cast to sockaddr_in
struct sockaddr_in *in_name = (struct sockaddr_in *) name;
+53 -2
View File
@@ -5,6 +5,7 @@
#include <assert.h>
#include <shlwapi.h>
#include <windows.h>
#include <cfg/configurator.h>
#include "api/modules/capture.h"
@@ -76,6 +77,7 @@
#include "hooks/graphics/graphics.h"
#include "hooks/lang.h"
#include "hooks/networkhook.h"
#include "hooks/icmphook_net.h"
#include "hooks/unisintrhook.h"
#include "launcher/launcher.h"
#include "launcher/logger.h"
@@ -244,6 +246,7 @@ int main_implementation(int argc, char *argv[]) {
bool easrv_smart = false;
bool load_stubs = false;
bool netfix_disable = false;
bool icmphook_enable = false;
bool lang_disable = false;
std::string process_priority_str = "high";
bool cardio_enabled = false;
@@ -299,6 +302,26 @@ int main_implementation(int argc, char *argv[]) {
}
auto &options = *options_ptr;
#if !SPICE_XP
{
// skip elevation only if AutoElevate is explicitly set to "user"
const bool skip_elevation = options[launcher::Options::AutoElevate].is_active() &&
options[launcher::Options::AutoElevate].value_text() == "user";
if (!skip_elevation && !sysutils::is_running_as_admin()) {
log_info("launcher", "relaunching with administrator privileges");
if (sysutils::relaunch_as_admin()) {
exit(0);
} else {
// elevation failed or was denied by the user
log_fatal("launcher", "failed to launch with administrator privileges");
exit(1);
}
}
}
#endif // !SPICE_XP
// check options
// TODO: get rid of some booleans here and make use of the options directly
if (options[launcher::Options::OpenConfigurator].value_bool()) {
@@ -621,8 +644,11 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::GitaDoraCabinetType].is_active()) {
games::gitadora::CAB_TYPE = options[launcher::Options::GitaDoraCabinetType].value_uint32();
}
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool() && GRAPHICS_WINDOWED) {
games::gitadora::ARENA_SINGLE_WINDOW = true;
if (options[launcher::Options::GitaDoraArenaSingleWindow].value_bool()) {
// for full screen
GRAPHICS_FORCE_SINGLE_ADAPTER = true;
// for windowed
GRAPHICS_PREVENT_SECONDARY_WINDOW = true;
}
if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
@@ -732,6 +758,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::DisableNetworkFixes].value_bool()) {
netfix_disable = true;
}
if (options[launcher::Options::EnableICMPHook].value_bool()) {
icmphook_enable = true;
}
if (options[launcher::Options::DisableACPHook].value_bool()) {
lang_disable = true;
}
@@ -889,6 +918,12 @@ int main_implementation(int argc, char *argv[]) {
PIN_MACRO_ENABLED = true;
PIN_MACRO_VALUES[1] = options[launcher::Options::Player2PinMacro].value_text();
}
if (options[launcher::Options::AutoPinMacroTrigger0].is_active()) {
AUTO_PIN_MACRO_TRIGGER[0] = options[launcher::Options::AutoPinMacroTrigger0].value_text();
}
if (options[launcher::Options::AutoPinMacroTrigger1].is_active()) {
AUTO_PIN_MACRO_TRIGGER[1] = options[launcher::Options::AutoPinMacroTrigger1].value_text();
}
for (auto &reader : options[launcher::Options::ICCAReaderPort].values_text()) {
static int reader_id = 0;
@@ -1170,9 +1205,20 @@ int main_implementation(int argc, char *argv[]) {
}
}
// reflec beat touch emulation
if (options[launcher::Options::spice2x_RBTouchScale].is_active()) {
games::rb::TOUCH_SCALING = options[launcher::Options::spice2x_RBTouchScale].value_uint32();
}
if (options[launcher::Options::RBTouchSize].is_active()) {
const auto text = options[launcher::Options::RBTouchSize].value_text();
if (text == "3") {
games::rb::TOUCH_SIZE = 3;
}
}
if (options[launcher::Options::RBTouchPollRate].is_active()) {
games::rb::TOUCH_POLL_RATE = options[launcher::Options::RBTouchPollRate].value_uint32();
}
if (options[launcher::Options::spice2x_AsioForceUnload].value_bool()) {
hooks::audio::ASIO_FORCE_UNLOAD_ON_STOP = true;
}
@@ -2259,6 +2305,11 @@ 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();
}
// net fix
if (!netfix_disable) {
networkhook_init();
+107 -33
View File
@@ -203,7 +203,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Show Cursor & Touch Emulation Enable",
.name = "s",
.desc = "Shows the cursor in the game window; also turns on touch emulation. Do not use if you use a real touch screen.",
.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 = "Common",
},
@@ -265,7 +265,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Full Screen Orientation Swap (EXPERIMENTAL)",
.name = "forceresswap",
.desc =
"Allows you to play portrait games in in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
"Allows you to play portrait games in landscape (and vice versa) by transposing resolution and applying image scaling.\n\n"
"Works great for some games, but can COMPLETELY BREAK other games - YMMV!\n\n"
"Strongly consider combining this with -forceres option to render at monitor native resolution\n\n"
"WARNING: for SDVX, this messes with camera angle for note lanes, causing it to be zoomed out, and causes performance issues "
@@ -312,7 +312,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-dx9on12",
.display_name = "dx9on12",
.aliases= "dx9on12",
.desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect games on that don't use DX9. Can cause some games to crash.\n\n"
.desc = "Use D3D9On12 wrapper library, requires Windows 10. Has no effect on games that don't use DX9. Can cause some games to crash.\n\n"
"Default: auto (use DX9 for most games, but turn on 9on12 for games that require it on non-NVIDIA GPUs).",
.type = OptionType::Enum,
.category = "Graphics (Common)",
@@ -1038,9 +1038,10 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.category = "Game Options (Advanced)",
},
{
.title = "GitaDora Two Channel Audio",
.title = "GitaDora Two Channel Audio (DX/SD only)",
.name = "2ch",
.desc = "Attempt to reduce audio channels down to just two channels.",
.desc = "Attempt to reduce audio channels down to just two channels. "
"This option does nothing for Arena Model (GW Delta and above); need a patch for that.",
.type = OptionType::Bool,
.game_name = "GitaDora",
.category = "Game Options",
@@ -1056,9 +1057,12 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// GitaDoraArenaSingleWindow
.title = "GitaDora Arena Single Window (EXPERIMENTAL)",
.title = "GitaDora Arena Disable Subscreens (EXPERIMENTAL)",
.name = "gdaonewindow",
.desc = "For Arena Model, when combined with windowed mode, only show the main game window; enables subscreen overlay.",
.desc = "For Arena Model:\n\n"
"Windowed mode: instead of 4 windows, create 1 window.\n\n"
"Fullscreen mode: instead of requiring 4 monitors, use only the primary monitor. WARNING: requires 4K monitor.\n\n"
"To access the subscreen, use the subscreen overlay.",
.type = OptionType::Bool,
.game_name = "GitaDora",
.category = "Game Options",
@@ -1069,8 +1073,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "gdlefty",
.desc = "Enables lefty mode, flipping motion sensor directions. Default: off.\n\n"
"Without this option, enabling LEFT in the game option will continuously trigger UP wail.\n\n"
"Has no effect if you are using analog bindings for X/Y axis; expectation is that your controller "
"handles this correctly (most do not, however).\n\n"
"Has no effect if you are using analog bindings for X/Y axis; most controllers do not handle this correctly.\n\n"
"As always, remember to restart the game after changing options. If you need to change in the game, "
"use the I/O panel overlay window.",
.type = OptionType::Enum,
@@ -1268,7 +1271,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Force Load LovePlus Module",
.name = "loveplus",
.desc = "manually enable LovePlus module.",
.desc = "Manually enable LovePlus module.",
.type = OptionType::Bool,
.game_name = "LovePlus",
.category = "Game Options (Advanced)",
@@ -1276,7 +1279,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Force Load Charge Machine Module",
.name = "pcm",
.desc = "manually enable Charge Machine module.",
.desc = "Manually enable Charge Machine module.",
.type = OptionType::Bool,
.game_name = "Charge Machine",
.category = "Game Options (Advanced)",
@@ -1284,7 +1287,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Force Load Ongaku Paradise Module",
.name = "onpara",
.desc = "manually enable Ongaku Paradise module.",
.desc = "Manually enable Ongaku Paradise module.",
.type = OptionType::Bool,
.game_name = "Ongaku Paradise",
.category = "Game Options (Advanced)",
@@ -1292,7 +1295,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Force Load Busou Shinki Module",
.name = "busou",
.desc = "manually enable Busou Shinki module.",
.desc = "Manually enable Busou Shinki module.",
.type = OptionType::Bool,
.game_name = "Busou Shinki: Armored Princess Battle Conductor",
.category = "Game Options (Advanced)",
@@ -1301,7 +1304,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadCCJModule
.title = "Force Load Chase Chase Jokers Module",
.name = "ccj",
.desc = "manually enable Chase Chase Jokers module.",
.desc = "Manually enable Chase Chase Jokers module.",
.type = OptionType::Bool,
.game_name = "Chase Chase Jokers",
.category = "Game Options (Advanced)",
@@ -1310,7 +1313,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadQKSModule
.title = "Force Load QuizKnock STADIUM Module",
.name = "qks",
.desc = "manually enable QuizKnock STADIUM module",
.desc = "Manually enable QuizKnock STADIUM module.",
.type = OptionType::Bool,
.game_name = "QuizKnock STADIUM",
.category = "Game Options (Advanced)",
@@ -1319,7 +1322,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadMFGModule
.title = "Force Load Mahjong Fight Girl Module",
.name = "mfg",
.desc = "manually enable Mahjong Fight Girl module.",
.desc = "Manually enable Mahjong Fight Girl module.",
.type = OptionType::Bool,
.game_name = "Mahjong Fight Girl",
.category = "Game Options (Advanced)",
@@ -1328,7 +1331,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadPCModule
.title = "Force Load Polaris Chord Module",
.name = "pc",
.desc = "manually enable Polaris Chord module.",
.desc = "Manually enable Polaris Chord module.",
.type = OptionType::Bool,
.game_name = "Polaris Chord",
.category = "Game Options (Advanced)",
@@ -1337,7 +1340,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
// LoadMusecaModule
.title = "Force Load Museca Module",
.name = "museca",
.desc = "manually enable Museca module.",
.desc = "Manually enable Museca module.",
.type = OptionType::Bool,
.game_name = "Museca",
.category = "Game Options (Advanced)",
@@ -1461,7 +1464,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "API Verbose Logging",
.name = "apilogging",
.desc = "verbose logging of API activity.",
.desc = "Verbose logging of API activity.",
.type = OptionType::Bool,
.category = "API (Dev)",
},
@@ -1584,8 +1587,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.title = "Force Touch Emulation",
.name = "touchemuforce",
.desc = "Force enable hook for GetTouchInputInfo API and inject WM_TOUCH events. "
"This is automatically turned on when needed, so it is not typically required to "
"turn it on manually. Do not enable this unless you have trouble.",
"This is automatically enabled when needed and is typically not required. Do not enable this unless you have trouble.",
.type = OptionType::Bool,
.category = "Touch Parameters",
},
@@ -1688,7 +1690,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"legacy: Preserve buggy old behavior (Default)\n\n"
"fix: Add E00401 to non-FeliCa cards, scan FeliCa cards as-is (Recommended for most users)\n\n"
"all: Add E00401 to all cards, including FeliCa cards (For really old games only)\n\n"
"The algorithm is simple; the prefix is applied, the scanned card number is appended upto 8 bytes, and remaining digits are discarded. "
"The algorithm is simple; the prefix is applied, the scanned card number is appended up to 8 bytes, and remaining digits are discarded. "
"For example, abcd56780123 is converted to e00401abcd567801 on card in. This results in different card numbers so "
"be careful when connecting to servers!",
.type = OptionType::Enum,
@@ -1719,7 +1721,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Realtime Process Priority (DEPRECATED - use -processpriority instead)",
.name = "realtime",
.desc = "Sets the process priority to realtime, can help with odd lag spikes.",
.desc = "Sets the process priority to realtime; can help with odd lag spikes.",
.type = OptionType::Bool,
.hidden = true,
.category = "Performance",
@@ -1730,7 +1732,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-processpriority",
.display_name = "processpriority",
.aliases= "processpriority",
.desc = "Sets the process priority, can help with odd lag spikes. Default: high.",
.desc = "Sets the process priority, which can help with odd lag spikes. Default: high.",
.type = OptionType::Enum,
.category = "Performance",
.elements = {
@@ -1807,7 +1809,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.desc = "Disables all audio hooks, including device initialization and volume hooks.\n\n"
"Default: off (allow Spice to hook IMMDeviceEnumerator for and optionally let "
"you use a different backend instead of exclusive WASAPI).\n\n"
"Check this if you want games to natively access your audio device.",
"Check this to allow games to natively access your audio device.",
.type = OptionType::Bool,
.category = "Audio (Hacks)",
},
@@ -1819,7 +1821,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.aliases= "volumehookdisable",
.desc = "Disables audio volume hook.\n\n"
"Default: off (prevent games from changing audio volume by hooking IAudioEndpointVolume).\n\n"
"Check this if you want games to freely change your volume.",
"Check this to allow games to freely change your volume.",
.type = OptionType::Bool,
.category = "Audio (Hacks)",
},
@@ -1961,7 +1963,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "Blocking Logger",
.name = "logblock",
.desc = "Slower but safer logging used for debugging.",
.desc = "Slower but safer logging for debugging.",
.type = OptionType::Bool,
.category = "Debug Log",
},
@@ -2301,7 +2303,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{
.title = "JB Touch Algorithm",
.name = "jubeattouchalgo",
.desc = "For touch screen players - choose the touch algorithm to use.\n\n"
.desc = "For touch screen players: choose the touch algorithm to use.\n\n"
"legacy - evenly divide the grid into 16 squares; old spicetools behavior, slightly inaccurate in gaps\n\n"
"improved (default) - squares register as-is, gaps will trigger the closest square\n\n"
"accurate - only touches within squares will trigger; gaps do nothing (for AC size touch screens)",
@@ -2316,7 +2318,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
},
{
// spice2x_RBTouchScale
.title = "RB Scale Touch Input",
.title = "RB Touch Emulation Scale",
.name = "sp2x-rbscaletouch",
.display_name = "rbscaletouch",
.aliases= "rbscaletouch",
@@ -2326,6 +2328,31 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.game_name = "Reflec Beat",
.category = "Game Options",
},
{
// RBTouchSize
.title = "RB Touch Emulation Size",
.name = "rbtouchsize",
.desc = "Size of the touch area; how many IR sensors a single finger activates. Default: 1 (1x1).",
.type = OptionType::Enum,
.game_name = "Reflec Beat",
.category = "Game Options",
.elements = {
{"1", "1x1"},
{"3", "3x3"},
},
},
{
// RBTouchPollRate
.title = "RB Touch Emulation Poll Hz",
.name = "rbtouchhz",
.desc = "By default, the game polls for touch at 120Hz. "
"This option overrides that rate; enter a number betwen 1 and 1000.\n\n"
"It should be noted that higher poll does not necessarily improve accuracy or performance.",
.type = OptionType::Integer,
.setting_name = "250",
.game_name = "Reflec Beat",
.category = "Game Options",
},
{
// spice2x_AsioForceUnload
.title = "ASIO Force Unload On Stop",
@@ -2463,6 +2490,28 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
{"both", ""},
},
},
{
// AutoPinMacroTrigger0
.title = "Auto PIN Macro Trigger Log String (P1)",
.name = "autopinmacrotrigger0",
.desc =
"Substring matched against game log output to detect when Player 1's PIN entry "
"screen is ready. When the substring appears in any log line, the PIN macro is "
"typed for Player 1 only. Leave blank to disable auto-trigger for P1.",
.type = OptionType::Text,
.category = "Network (Advanced)",
},
{
// AutoPinMacroTrigger1
.title = "Auto PIN Macro Trigger Log String (P2)",
.name = "autopinmacrotrigger1",
.desc =
"Substring matched against game log output to detect when Player 2's PIN entry "
"screen is ready. When the substring appears in any log line, the PIN macro is "
"typed for Player 2 only. Leave blank to disable auto-trigger for P2.",
.type = OptionType::Text,
.category = "Network (Advanced)",
},
{
// spice2x_LowLatencySharedAudio
.title = "Low Latency Shared Audio",
@@ -2502,7 +2551,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.aliases= "nonvapi",
.desc = "Completely block the game from accessing NVAPI. Can be used if NVAPI is returning undesirable "
"results to the game (e.g., wrong values from NvDisplayConfig for SDVX). You may need to apply additional "
"hex edits in order to boot the game correctly.",
"hex edits to boot the game correctly.",
.type = OptionType::Bool,
.category = "Graphics (Common)",
},
@@ -2550,7 +2599,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-smx-stage",
.display_name = "smxstage",
.aliases= "smxstage",
.desc = "StepmaniaX stage will show up as a device that can recieve lighting output. "
.desc = "StepmaniaX stage will show up as a device that can receive lighting output. "
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
.type = OptionType::Bool,
.category = "I/O Options",
@@ -2561,7 +2610,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.name = "sp2x-smx-dedicab",
.display_name = "smxdedicab",
.aliases = "smxdedicab",
.desc = "StepManiaX Dedicated Cabinet will show up as a device that can recieve lighting output. "
.desc = "StepManiaX Dedicated Cabinet will show up as a device that can receive lighting output. "
"For configurator, restart spicecfg.exe after enabling this to have the device show up for binding.",
.type = OptionType::Bool,
.category = "I/O Options"
@@ -2665,7 +2714,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
"Enables emulation of touch gestures to make it easier to navigate the game without a touchscreen.\n\n"
"This enables two things:\n"
" 1. Swipe/Touch bindings in Buttons tab (Swipe Next Page, etc)\n"
" 2. PIN pad bindings (make sure Unscramble Keypad patch is applied)).",
" 2. PIN pad bindings (make sure Unscramble Keypad patch is applied).",
.type = OptionType::Bool,
.game_name = "Nostalgia",
.category = "Game Options",
@@ -2805,6 +2854,31 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
.type = OptionType::Bool,
.category = "Development"
},
{
// 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 (Development)",
},
{
// AutoElevate
.title = "Run as",
.name = "runas",
.desc = "Controls whether spice will automatically re-launch with administrator privileges at startup.\n\n"
"admin (default): automatically re-launch with administrator privileges, "
"needed for most games to function properly\n\n"
"user: skip elevation, run with current user privileges; game may fail to launch, "
"crash, or critical features may silently fail; use at your own risk.",
.type = OptionType::Enum,
.category = "Development",
.elements = {
{"admin", ""},
{"user", ""},
},
},
};
const std::vector<std::string> &launcher::get_categories(Options::OptionsCategory category) {
+6
View File
@@ -242,6 +242,8 @@ namespace launcher {
spice2x_JubeatLegacyTouch,
JubeatTouchAlgo,
spice2x_RBTouchScale,
RBTouchSize,
RBTouchPollRate,
spice2x_AsioForceUnload,
spice2x_IIDXNoESpec,
spice2x_IIDXWindowedTDJ,
@@ -253,6 +255,8 @@ namespace launcher {
IIDXSubMonitorOverride,
spice2x_IIDXEmulateSubscreenKeypadTouch,
spice2x_AutoCard,
AutoPinMacroTrigger0,
AutoPinMacroTrigger1,
spice2x_LowLatencySharedAudio,
spice2x_TapeLedAlgorithm,
spice2x_NoNVAPI,
@@ -282,6 +286,8 @@ namespace launcher {
OptionConflictResolution,
OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
AutoElevate,
};
enum class OptionsCategory {
+46 -3
View File
@@ -1,5 +1,6 @@
#include "eamuse.h"
#include <atomic>
#include <fstream>
#include <thread>
@@ -44,6 +45,9 @@ static uint8_t AUTO_INSERT_CARD_CACHED_DATA[2][8];
// pin macro
bool PIN_MACRO_ENABLED = false;
std::string PIN_MACRO_VALUES[2] = {"", ""};
std::string AUTO_PIN_MACRO_TRIGGER[2];
static std::atomic_bool AUTO_PIN_MACRO_REQUEST[2] {false, false};
static bool AUTO_PIN_MACRO_PLAYER_ACTIVE[2] = {false, false};
static std::thread *PIN_MACRO_THREAD = nullptr;
static bool PIN_MACRO_THREAD_ACTIVE = false;
static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
@@ -51,6 +55,18 @@ static uint16_t PIN_MACRO_TRIGGER_KEYS[2] = {
games::OverlayButtons::TriggerPinMacroP2
};
static bool pin_macro_log_hook(void *, const std::string &data, logger::Style, std::string &) {
for (int unit = 0; unit < 2; unit++) {
if (!AUTO_PIN_MACRO_PLAYER_ACTIVE[unit]) {
continue;
}
if (data.find(AUTO_PIN_MACRO_TRIGGER[unit]) != std::string::npos) {
AUTO_PIN_MACRO_REQUEST[unit].store(true);
}
}
return false;
}
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
// get unit index
@@ -324,6 +340,25 @@ void eamuse_pin_macro_start_thread() {
// set active
PIN_MACRO_THREAD_ACTIVE = true;
// a unit is eligible for auto-trigger only if all the static prerequisites are
// satisfied at startup; precomputing this lets the log hook skip per-line checks
// on values that never change at runtime
for (int unit = 0; unit < 2; unit++) {
AUTO_PIN_MACRO_PLAYER_ACTIVE[unit] =
!AUTO_PIN_MACRO_TRIGGER[unit].empty() &&
!PIN_MACRO_VALUES[unit].empty();
if(!AUTO_PIN_MACRO_TRIGGER[unit].empty() && PIN_MACRO_VALUES[unit].empty()) {
log_warning("eamuse", "Configuration error; Pin Macro empty for P{}.", unit+1);
}
}
// register scene log hook so the macro fires on the per-game trigger string,
// but only if at least one player is eligible
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
logger::hook_add(pin_macro_log_hook, nullptr);
log_info("eamuse", "AUTO_PIN_MACRO enabled");
}
// create thread
PIN_MACRO_THREAD = new std::thread([]() {
uint16_t keypad_overrides[] = {
@@ -345,15 +380,20 @@ void eamuse_pin_macro_start_thread() {
timeutils::PreciseSleepTimer timer;
while (PIN_MACRO_THREAD_ACTIVE) {
// wait for key press
// wait for key press or auto-trigger
if (!active_unit.has_value()) {
for (int unit = 0; unit < 2; unit++) {
if (PIN_MACRO_VALUES[unit].empty()) {
continue;
}
if (overlay_buttons &&
bool key_press = overlay_buttons &&
(!overlay::OVERLAY || overlay::OVERLAY->hotkeys_triggered()) &&
GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]))) {
GameAPI::Buttons::getState(RI_MGR, overlay_buttons->at(PIN_MACRO_TRIGGER_KEYS[unit]));
bool auto_request = AUTO_PIN_MACRO_REQUEST[unit].exchange(false);
if (auto_request) {
log_info("eamuse", "AUTO_PIN_MACRO_REQUEST detected for P{}", unit+1);
}
if (key_press || auto_request) {
active_unit = unit;
// Reset key index
pin_index[unit] = 0;
@@ -405,6 +445,9 @@ void eamuse_pin_macro_stop_thread() {
delete PIN_MACRO_THREAD;
PIN_MACRO_THREAD = nullptr;
}
if (AUTO_PIN_MACRO_PLAYER_ACTIVE[0] || AUTO_PIN_MACRO_PLAYER_ACTIVE[1]) {
logger::hook_remove(pin_macro_log_hook, nullptr);
}
}
void eamuse_set_keypad_overrides(size_t unit, uint16_t keypad_state) {
+1
View File
@@ -38,6 +38,7 @@ extern float AUTO_INSERT_CARD_COOLDOWN;
extern bool PIN_MACRO_ENABLED;
extern std::string PIN_MACRO_VALUES[2];
extern std::string AUTO_PIN_MACRO_TRIGGER[2];
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
+1 -1
View File
@@ -416,7 +416,7 @@ namespace wintouchemu {
// same as iidx case above
log_info("wintouchemu", "use mouse cursor API for popn overlay subscreen");
USE_MOUSE = true;
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
} else if (games::gitadora::is_arena_model() && GRAPHICS_PREVENT_SECONDARY_WINDOW) {
log_info("wintouchemu", "use mouse cursor API for gitadora overlay subscreen");
USE_MOUSE = true;
} else {
+1 -1
View File
@@ -415,7 +415,7 @@ void overlay::SpiceOverlay::init() {
window_sub = new overlay::windows::DRSDanceFloorDisplay(this);
} else if (avs::game::is_model("KFC")) {
window_sub = new overlay::windows::SDVXSubScreen(this);
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
} else if (games::gitadora::is_arena_model()) {
window_sub = new overlay::windows::GitaDoraSubScreen(this);
} else if (games::popn::is_pikapika_model()) {
window_sub = new overlay::windows::PopnSubScreen(this);
+1 -1
View File
@@ -71,7 +71,7 @@ namespace overlay::windows {
sub = "Show Valkyrie Subscreen";
} else if (avs::game::is_model("REC")) {
sub = "Show DRS Dance Floor";
} else if (games::gitadora::is_arena_model() && games::gitadora::ARENA_SINGLE_WINDOW) {
} else if (games::gitadora::is_arena_model()) {
sub = "Show GITADORA Subscreen";
} else if (games::popn::is_pikapika_model()) {
sub = "Show Pop'n Subscreen";
+6
View File
@@ -11,6 +11,12 @@ namespace overlay::windows {
if (!games::gitadora::is_arena_model()) {
this->disabled_message = "Requires Arena Model!";
} else if (!GRAPHICS_PREVENT_SECONDARY_WINDOW) {
if (GRAPHICS_WINDOWED) {
this->disabled_message = "Use the dedicated subscreen window!";
} else {
this->disabled_message = "Subscreen overlay disabled by user; enable single window mode for overlay.";
}
}
this->resize_callback = keep_10_by_16;
+55
View File
@@ -10,6 +10,9 @@
#include <windows.h>
#undef WIN32_NO_STATUS
#include <shellapi.h>
#include <shlwapi.h>
#include "hooks/graphics/graphics.h"
#include "avs/game.h"
#include "util/detour.h"
@@ -585,4 +588,56 @@ namespace sysutils {
(void*)EnumDisplayDevicesA_hook,
(void**)&EnumDisplayDevicesA_orig);
}
#if !SPICE_XP
bool is_running_as_admin() {
HANDLE token_handle = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token_handle)) {
return false;
}
TOKEN_ELEVATION elevation = {};
DWORD size = sizeof(elevation);
const BOOL ok = GetTokenInformation(
token_handle, TokenElevation, &elevation, size, &size);
CloseHandle(token_handle);
if (!ok) {
return false;
}
return elevation.TokenIsElevated != 0;
}
bool relaunch_as_admin() {
// get the current executable path
wchar_t executable_path[MAX_PATH];
if (!GetModuleFileNameW(nullptr, executable_path, MAX_PATH)) {
return false;
}
// get the current working directory to pass to the elevated process
wchar_t working_dir[MAX_PATH];
if (!GetCurrentDirectoryW(MAX_PATH, working_dir)) {
return false;
}
// skip past argv[0] in the original command line
const wchar_t *args = PathGetArgsW(GetCommandLineW());
// use ShellExecuteW with "runas" verb to elevate
HINSTANCE result = ShellExecuteW(
nullptr,
L"runas",
executable_path,
args,
working_dir,
SW_SHOW
);
// ShellExecute returns > 32 on success
return (intptr_t)result > 32;
}
#endif // !SPICE_XP
}
+9 -2
View File
@@ -21,5 +21,12 @@ namespace sysutils {
const std::vector<MonitorEntry> &enumerate_monitors();
extern std::string SECOND_MONITOR_OVERRIDE;
void hook_EnumDisplayDevicesA();
}
void hook_EnumDisplayDevicesA();
#if !SPICE_XP
bool is_running_as_admin();
bool relaunch_as_admin();
#endif // !SPICE_XP
}