Compare commits

...

30 Commits

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

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

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

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

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

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


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

- Tested full game play, user profile saving with Asphyxia

Assisted by AI.

---------

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

## Description of change

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

## Testing
- Validated by compiling spice2x successfully
- Tested with latest action compiled spice 32bit executable, card insert
work instantly
2026-09-12 09:50:36 -07:00
bicarus 4c77a3f148 sdk: add get_module_info, get_plugin_directory, d3d9 callbacks (#908)
## Link to GitHub Issue or related Pull Request, if one exists
n/a

## Description of change
To make plugin development easier, add `get_module_info` and
`get_plugin_directory` to deal with common tasks for hooking DLLs and
reading INI files.

Also, add the ability to register for d3d9 present callbacks via
`register_d3d9`, which makes it significantly easier for plugins to draw
things on screen.

## Testing
See sample code.
2026-09-11 02:49:07 -07:00
drmext 603fbfc69d network: heap-allocate NIC spoof tunnel UDP queue (#913)
## Link to GitHub Issue or related Pull Request, if one exists
#912

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Fell out of the changes in #905

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

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

---------

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

## Description of change

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

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

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

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

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

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

## Testing
2p card was recognized in test mode. No other testing performed.
2026-09-05 00:00:41 -07:00
bicarus 0169fe1e31 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
2026-08-30 22:35:12 -07:00
bicarus 357e6c86f1 gitadora: allow a landscape monitor to display subscreen (#899)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Arena Model's SMALL touch subscreen is a portrait (800x1280) panel -
however, it's common for people to set up a touch screen in landscape
orientation since IIDX/SDVX/popn all use landscape.

Add a new option, `-gdsublandscape`, which forces the subscreen to
launch at 1080p landscape. The image is rendered with black bars on the
sides. Touches will be unaligned initially; user needs to go into test
mode and recalibrate. In reality this option is just a short hand for
`-forceressub 1920,1080`.

Wire up `-forceressub` which previously didn't work for gitadora, so
that a resolution other than 1080p can be used in either orientation. In
other games `-forceressub` just forces the rendering resolution and
IIDX/SDVX/popn's game engine automatically stretches to fill... however
for gitadora it works differently since we want to keep the aspect ratio
of the original screen; it instead hijacks the back buffer and makes the
game draw to an intermediate buffer.

## Testing
2026-08-29 19:09:56 -07:00
bicarus 67963389ef drs: hook down movement, adjust touch hooks (#898)
When Down Motion button is bound, install hooks into the game that
detects down movement via patches.

When Disable Touch option is enabled, don't install touch hooks.
2026-08-29 17:21:21 -07:00
bicarus 4009bb8a84 sc: bone eater preliminary support (#895)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #114

## Description of change
Initial support for Silent Scope: Bone Eater, enough to be playable;
doesn't include QoS work needed to make this a smoother experience.

* Game detection
* Fix DX11 overlay to work with non-Unity games
* Implements sub monitor i/o (only really needed to get past I/O check)
* Try to make the game reasonably playable with a mouse
* Ignore the game's attempt to move windows, position them in a
reasonable place
* Fix language hook

## Testing
2026-08-28 03:04:43 -07:00
bicarus 8fb6d94000 Include AI-Use Policy in README
Added AI-Use Policy section to README.md
2026-08-28 02:47:15 -07:00
bicarus 8d3685415d popn: fix LED handling (#894)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix the issue with popn lights flickering.

`SetTapeLedDataPart()` updates BI3A’s internal LED buffer without
immediately sending it to hardware. High Cheers briefly clears this
buffer before writing the complete set of lights.

Stage pin-0 (button light) updates in the hook and publish only the
latest frame during `GetDeviceStatus()`, matching real BI3A behavior.
This prevents temporary clear frames from reaching HID controllers.

## Testing
Tested with my iidx controller with HID lights.
2026-08-28 01:42:56 -07:00
James Liu 50b38b856f gitadora: fix 32bit gfdm coin button not working (#893)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Fix Add Coin/COIN button not actuated after pressing

## Testing
*XG2*
<img width="1594" height="458" alt="XG2"
src="https://github.com/user-attachments/assets/0d90ffaf-b0a0-421f-8986-5d87040ace93"
/>

*XG3*
<img width="1594" height="458" alt="XG3"
src="https://github.com/user-attachments/assets/e326fd91-3477-44ed-854b-4160cf0f103a"
/>
2026-08-27 09:05:36 -07:00
Jiongjia Lu dad88caf01 api: add card lookup endpoint (#891)
## Link to GitHub Issue or related Pull Request, if one exists

N/A

## Description of change

I am developing [spice.nimabe.net](https://spice.nimabe.net), a static
frontend for SpiceAPI. It already supports generating and inserting new
card IDs, but SpiceAPI does not provide a way to discover cards already
configured on the game machine.

This makes the frontend inconvenient for existing users who generated
and saved their card IDs locally. They currently have to open each card
file on the game machine and manually copy its ID into the frontend.

This change adds a `card.get_cards` API function that enumerates the
current cards for readers supported by the running game. Each entry
contains:

- reader index
- canonical 16-character card ID
- `source`, set to `file` or `override`
- `file_name` for file-backed cards

File-backed cards expose only the save-file basename. Active `-card0`
and `-card1` values are returned with `source: "override"` and no
`file_name`, instead of representing an override as a file. spicefe
derives the useful default import names `card0` and `card1` from the
reader index. Reader enumeration uses `eamuse_get_game_keypads()` so
games with only one reader do not expose a second card.

This allows spicefe and other API clients to offer one-click import for
existing cards, without requiring users to manually find and copy their
card IDs.

Because card IDs are sensitive, `card.get_cards` can only be used when
the operator has configured an API password. No full filesystem path is
exposed. Missing, unreadable, and invalid cards are omitted.

The existing `card.insert` function remains unchanged. To avoid
requiring a password for the whole card module, this change adds support
for password requirements on individual API functions while preserving
existing module-wide password behavior.

Access to configured card paths and runtime card overrides is
synchronized because API requests may read them concurrently with
configuration updates.

The Python and Dart API wrappers and README documentation have been
updated for the new function and source metadata.

## Testing

- Ran the complete `src/spice2x/build_docker.sh` build successfully.
- Built all required 32-bit and 64-bit targets.
- Built the Windows XP-compatible 32-bit targets.
- Passed static import checks.
- Passed Windows 7 and Windows XP compatibility checks.
- Completed release packaging successfully.
- Verified both updated Dart wrappers are correctly formatted.
- Ran all 106 spicefe tests successfully.
- `git diff --check` passes.

*This implementation was prepared with assistance from OpenAI Codex.*
2026-08-25 00:49:31 -07:00
bicarus 699659d4bf cfg: update video options (#890)
Move around legacy video options.
2026-08-23 15:24:13 -07:00
bicarus 21e7d24ed3 graphics: take stream captures off the game's present thread to avoid game perf hit (#889)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
Capturing a frame for the API stream made the game wait for
`GetRenderTargetData` in the middle of its present, roughly 1270us per
frame at 1080p. A 120Hz cab visibly lost frames for as long as a viewer
was connected.

The present thread now only issues a `StretchRect` into a render target
we own, which is queued rather than waited on, and a pool thread does
the readback and the pixel conversion. That takes the present thread
cost to 1-4us. Each snapshot is read on the request after the one that
took it, so the blit and its transfer have a full frame to land and the
read does not stall on the GPU either, at the cost of one frame of
stream latency.

Only streaming takes this path, and only on a device created with
`D3DCREATE_MULTITHREADED`. Screenshots, `capture.get_jpg` and the
`THREAD_BAN` models keep the existing inline readback unchanged.

Also raises the x264 encoder from `i_threads = 1` to 4, which was
holding a 1080p60 stream to 41fps and making a keyframe cost 12.7ms
against 6.6ms for an ordinary frame. Capped rather than automatic
because this encodes on the same machine it is capturing.

## Testing
tested against iidx33, which was the most sensitive to frame drops
2026-08-23 12:24:41 -07:00
bicarus 1cdb7be810 overlay: fix hotkey init race (#888)
## Link to GitHub Issue or related Pull Request, if one exists
Regressed by #864

## Description of change
The hotkey sampler thread starts touching `games::io` the moment
`enable_raw_input()` is called, and that call sat right after `RI_MGR`
was constructed, a few lines before the main thread's own first
`games::io` calls. Both threads then hit the unsynchronized lazy
`initialize()` at once, crashing at startup with an access violation in
`memcmp`.

Moved the call down next to `enable_input()`, after the bindings are
built.

## Testing
2026-08-23 00:34:08 -07:00
bicarus-dev 09314f1fd3 fix bad merge 2026-08-22 21:53:03 -07:00
bicarus e7b90271ca api: serve the websocket with wslay instead of headsocket (#887)
`HeadSocket` library we are currently using is full of bugs and no
longer maintained. Swap it out with `wslay`.
2026-08-22 21:47:26 -07:00
bicarus 95f10ed733 api: get_streams (#886)
## Link to GitHub Issue or related Pull Request, if one exists
#0

## Description of change
MSE method of streaming video needs the dimensions up front. This new
api delivers that. Also provides an easier way to determine which
screens are available and a way to discover port number.

## Testing
See pending changes in substream project
2026-08-22 19:38:48 -07:00
116 changed files with 12184 additions and 3535 deletions
+30 -1
View File
@@ -29,4 +29,33 @@ jobs:
with:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
if-no-files-found: error
if-no-files-found: error
msvc-ci:
name: Build (MSVC ${{ matrix.arch }}-bit)
# windows-latest ships Visual Studio 2026; pin to the VS 2022 image, which is
# the toolset spice2x is actually developed against
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- arch: 32
platform: Win32
target: spicetools_spice spicetools_cfg
- arch: 64
platform: x64
target: spicetools_spice64
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
- name: Configure
run: >
cmake -S . -B cmake-build-msvc-${{ matrix.arch }}
-A ${{ matrix.platform }}
- name: Compile
run: >
cmake --build cmake-build-msvc-${{ matrix.arch }}
--config Release --target ${{ matrix.target }}
+1 -1
View File
@@ -105,5 +105,5 @@ if (conditional)
* We don't use GSL.
* Don't throw exceptions.
* Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`.
* Writing in C is also completely acceptable.
* Writing in C is also completely acceptable but prefer to use RAII patterns.
* Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying.
+4
View File
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
### AI-Use Policy
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
## Additional information
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
+26 -7
View File
@@ -254,6 +254,7 @@ add_subdirectory(external/discord-rpc EXCLUDE_FROM_ALL)
add_subdirectory(external/hash-library EXCLUDE_FROM_ALL)
add_subdirectory(external/imgui EXCLUDE_FROM_ALL)
add_subdirectory(external/minhook EXCLUDE_FROM_ALL)
add_subdirectory(external/wslay EXCLUDE_FROM_ALL)
add_subdirectory(external/cpu_features EXCLUDE_FROM_ALL)
# libjpeg-turbo, prebuilt into the deps image. The WinXP toolchains have their own
@@ -519,6 +520,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/scotto/io.cpp
games/drs/drs.cpp
games/drs/io.cpp
games/drs/motion_cam.cpp
games/drs/rgb_cam.cpp
games/we/we.cpp
games/we/io.cpp
@@ -530,6 +532,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/otoca/p4io.cpp
games/silentscope/silentscope.cpp
games/silentscope/io.cpp
games/silentscope/projector.cpp
games/pcm/pcm.cpp
games/pcm/io.cpp
games/onpara/onpara.cpp
@@ -551,6 +554,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/pc/pc.cpp
games/pc/io.cpp
games/pc/bi2x_hook.cpp
games/udn/udn.cpp
games/udn/io.cpp
games/udn/bi2x_hook.cpp
# hooks
hooks/audio/acm.cpp
@@ -602,6 +608,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/graphics/backends/d3d9/d3d9_texture.cpp
hooks/graphics/backends/d3d11/d3d11_backend.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain.cpp
hooks/graphics/backends/d3d11/d3d11_swapchain_udn.cpp
hooks/graphics/backends/d3d11/d3d11_factory.cpp
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
@@ -613,6 +620,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
hooks/networkhook.cpp
hooks/icmphook_net.cpp
hooks/icmphook_iphlpapi.cpp
hooks/nicspoof.cpp
hooks/nicspoof_tunnel.cpp
hooks/powrprof.cpp
#hooks/rom.cpp
hooks/setupapihook.cpp
@@ -714,6 +723,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
reader/crypt.cpp
# sdk
sdk/d3d9.cpp
sdk/modules.cpp
sdk/sdk.cpp
# stubs
@@ -797,7 +808,7 @@ endfunction()
add_library(spicetools_spice_objs OBJECT ${SOURCE_FILES})
target_link_libraries(spicetools_spice_objs
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
target_link_libraries(spicetools_spice_objs PUBLIC winscard)
if(NOT MSVC)
@@ -837,7 +848,7 @@ set(RESOURCE_FILES build/manifest.manifest build/manifest.rc build/icon.rc cfg/W
add_executable(spicetools_spice_linux ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_spice_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
set_target_properties(spicetools_spice_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice_linux PROPERTIES OUTPUT_NAME "spice_linux")
target_compile_definitions(spicetools_spice_linux PRIVATE NO_SCARD=1 PRIVATE SPICE_LINUX=1)
@@ -855,7 +866,7 @@ add_executable(spicetools_spice64 ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
target_link_libraries(spicetools_spice64 PUBLIC winscard)
set_target_properties(spicetools_spice64 PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64 PROPERTIES OUTPUT_NAME "spice64")
@@ -878,7 +889,7 @@ add_executable(spicetools_spice64_linux ${SOURCE_FILES} ${RESOURCE_FILES})
# do NOT link against: mf, mfplat, mfreadwrite; otherwise unity games will break
target_link_libraries(spicetools_spice64_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp mfuuid strmiids dxva2
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features spice_jpeg spice_x264)
set_target_properties(spicetools_spice64_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_spice64_linux PROPERTIES OUTPUT_NAME "spice64_linux")
target_compile_definitions(spicetools_spice64_linux PRIVATE SPICE64=1)
@@ -900,7 +911,7 @@ add_executable(spicetools_cfg WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
# the configurator serves neither the API nor the video stream, so it needs no codecs
target_link_libraries(spicetools_cfg
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
target_link_libraries(spicetools_cfg PUBLIC winscard)
set_target_properties(spicetools_cfg PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg PROPERTIES OUTPUT_NAME "spicecfg")
@@ -918,7 +929,7 @@ set(RESOURCE_FILES cfg/manifest.manifest cfg/manifest.rc cfg/icon.rc cfg/Win32D.
add_executable(spicetools_cfg_linux WIN32 ${SOURCE_FILES} ${RESOURCE_FILES})
target_link_libraries(spicetools_cfg_linux
PUBLIC d3d9 ws2_32 version comctl32 shlwapi iphlpapi hid secur32 setupapi psapi winmm winhttp strmiids
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook imm32 dwmapi CpuFeatures::cpu_features)
PRIVATE fmt::fmt-header-only discord-rpc imgui hash-library minhook wslay imm32 dwmapi CpuFeatures::cpu_features)
set_target_properties(spicetools_cfg_linux PROPERTIES PREFIX "")
set_target_properties(spicetools_cfg_linux PROPERTIES OUTPUT_NAME "spicecfg_linux")
target_compile_definitions(spicetools_cfg_linux PRIVATE SPICETOOLS_SPICECFG_STANDALONE=1)
@@ -1042,8 +1053,16 @@ if(NOT MSVC)
endif()
# sdk_sample_v0_cpp.dll (64 bit)
set(SOURCE_FILES sdk/sample/v0/cpp/v0_cpp.cpp)
set(SOURCE_FILES
sdk/sample/v0/cpp/v0_cpp.cpp
sdk/sample/v0/cpp/v0_cpp_imgui.cpp
external/imgui/imgui.cpp
external/imgui/imgui_draw.cpp
external/imgui/imgui_tables.cpp
external/imgui/imgui_widgets.cpp
external/imgui/backends/imgui_impl_dx9.cpp)
add_library(spicetools_sdk_sample_v0_cpp_64 SHARED ${SOURCE_FILES} ${RESOURCE_FILES} sdk/sample/v0/cpp/v0_cpp.def)
target_link_libraries(spicetools_sdk_sample_v0_cpp_64 PRIVATE imm32)
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES PREFIX "")
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
+31 -3
View File
@@ -127,7 +127,30 @@ doesn't matter since the TCP protocol doesn't allow for out of order data,
however this may change when/if support for UDP is being introduced. The only
restriction is that the ID has to be a valid 64-bit unsigned integer.
#### Capture
- get_screens()
- returns the screen numbers the game has registered for capture
- get_jpg(screen: uint, quality: uint, divide: uint)
- returns the timestamp, width, height and base64 encoded JPEG of one screen
- all parameters are optional and default to screen 0, quality 70, divide 1
- divide shrinks the image by that factor before encoding
- get_streams()
- returns a dict describing the HTTP video stream, or no data at all when
`-apistream` is not enabled and there is nothing to describe
- `port` is the stream server port
- `formats` lists the wire formats this build serves, each with a `name`
(`h264` or `mjpeg`) and the `path` to request them on
- `screens` lists every capturable screen with its `width`, `height`, and
`busy`
#### Card
- get_cards()
- returns the current card ID and source for each active card reader
- each entry contains `index`, `card_id`, and `source`
- `source` is `file` or `override`
- file entries also contain `file_name`, without the full configured path
- `-card0` and `-card1` overrides do not contain `file_name`
- this function only works when an API password is configured
- insert(index: uint, card_id: hex)
- inserts a card which gets read by the emulated card readers for the game
- index has to be either 0 (for P1) or 1 (for P2)
@@ -298,6 +321,10 @@ stream over plain HTTP. Enable it with `-apistream`. It listens on the API port
plus two, in the same way the WebSocket server uses the API port plus one, so
`-api 1337` puts the stream on 1339. This means `-api` has to be enabled too.
Rather than working the port out, clients should ask the JSON API for it with
`capture.get_streams()`, which also reports which of the formats below this
build serves, the size of each screen and whether one is already taken.
Two formats are served:
http://host:1339/stream.mjpg JPEG frames, multipart/x-mixed-replace
@@ -323,9 +350,10 @@ The stream is view only. Touch and other input still go through the JSON API,
so a companion app needs both. There is no authentication on the stream port -
anyone who can reach it can watch the screen.
WinXP builds have no video stream. Neither encoder is compiled in, so every
endpoint returns 404, and the JSON API's JPEG screen capture is unavailable for
the same reason.
WinXP builds have no video stream. Neither encoder is compiled in, so nothing
listens on the stream port even with `-apistream`, `capture.get_streams()`
returns no data, and the JSON API's JPEG screen capture is unavailable for the
same reason.
## Native wrapper libraries
Spicetools provides wrapper libraries in: Arduino, C++, Dart, and Python.
+32 -6
View File
@@ -1,5 +1,7 @@
#include "bmpu.h"
#include <algorithm>
#include "acio/icca/icca.h"
#include "avs/game.h"
#include "cfg/api.h"
@@ -9,6 +11,7 @@
#include "games/ftt/io.h"
#include "games/museca/io.h"
#include "games/silentscope/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
STATUS_BUFFER[4] |= 0x20;
}
// joy stick raw input
// gun position, sent big endian; the game keeps the top 12 bits of each pair
auto &analogs = games::silentscope::get_analogs();
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
unsigned short joy_x = 0x7FFF;
unsigned short joy_y = 0x7FFF;
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
}
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
if (gun_x.isSet() || gun_y.isSet()) {
if (gun_x.isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
}
if (gun_y.isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
}
} else {
// the gun reports where it is aimed, which a relative pointer cannot express - the
// calibration screen asks for the screen centre and two corners, so read the cursor
POINT cursor {};
RECT client {};
POINT origin {};
if (NDD_MAIN_WINDOW != nullptr &&
GetCursorPos(&cursor) &&
GetClientRect(NDD_MAIN_WINDOW, &client) &&
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
client.right > 1 && client.bottom > 1)
{
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
}
}
// invert X axis
+2 -2
View File
@@ -118,11 +118,11 @@ static inline void update_card(int unit_id) {
const bool card_presented = eamuse_card_insert_consume(icca_get_active_count(), unit_id);
const bool key_pressed = (kb_insert_press && !kb_insert_press_old[unit_id]);
// beatstream and nostalgia have logic that requires ac_io_icca_get_uid_felica to return the
// beatstream, nostalgia and Dance Evolution have logic that requires ac_io_icca_get_uid_felica to return the
// exact same card number multiple times in a row in order for the card number to be read...
// for whatever reason setting this to 3-4 doesn't work the very first time the game boots up
// so we use 10 just to be safe
const bool need_felica_retries = avs::game::is_model({"NBT", "PAN"});
const bool need_felica_retries = avs::game::is_model({"KDM", "NBT", "PAN"});
if (need_felica_retries && (card_presented || key_pressed)) {
unit->felica_retries = 10;
}
+13 -4
View File
@@ -8,6 +8,7 @@
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/error/en.h"
#include "util/crypt.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -92,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// bind socket to address
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -102,6 +104,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// set socket to listen
if (listen(this->server, server_backlog) == -1) {
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -312,6 +315,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
// check for parse error
if (document.HasParseError()) {
log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)",
GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size);
// return empty response and close connection
out->push_back(0);
@@ -337,9 +342,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
if (module->name == request.module) {
module_found = true;
// check password force
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
Value err("Module requires the password to be set.");
// check password requirement
if (module->requires_password(request.function)
&& this->password.empty()
&& request.function != "session_refresh") {
Value err("Function requires the password to be set.");
response.add_error(err);
break;
}
@@ -441,7 +448,9 @@ void Controller::free_socket() {
this->server = INVALID_SOCKET;
}
this->websocket->free_socket();
if (this->websocket) {
this->websocket->free_socket();
}
for (auto &s : this->serial) {
s->free_port();
+3 -3
View File
@@ -41,21 +41,21 @@ namespace api {
bool pretty;
// server
WebSocketController *websocket;
WebSocketController *websocket = nullptr;
std::vector<SerialController *> serial;
std::vector<std::thread> server_workers;
std::vector<std::thread> server_handlers;
std::mutex server_handlers_m;
std::vector<api::ClientState *> client_states;
std::mutex client_states_m;
SOCKET server;
SOCKET server = INVALID_SOCKET;
void server_worker();
void connection_handler(ClientState client_state);
public:
// state
bool server_running;
bool server_running = false;
// constructor / destructor
Controller(unsigned short port, std::string password, bool pretty);
+7 -1
View File
@@ -118,7 +118,13 @@ namespace api {
param.i_height = height;
param.i_fps_num = this->fps;
param.i_fps_den = 1;
param.i_threads = 1;
// sliced threading, which zerolatency already selected, so a frame is split
// across workers rather than held back to be reordered. deliberately not the
// automatic count: this shares a machine with the game it is capturing, and
// taking every core to encode would win back frames at the game's expense
param.i_threads = 4;
param.b_annexb = 1;
// SPS/PPS ahead of every IDR, so a client can start decoding cold
param.b_repeat_headers = 1;
+8
View File
@@ -16,6 +16,14 @@ namespace api {
this->password_force = password_force;
}
void Module::require_password(const std::string &function) {
this->password_force_functions.emplace(function);
}
bool Module::requires_password(const std::string &function) const {
return this->password_force || this->password_force_functions.contains(function);
}
void Module::handle(Request &req, Response &res) {
// log module access
+10
View File
@@ -4,6 +4,7 @@
#include <map>
#include <string>
#include <sstream>
#include <unordered_set>
#include <external/robin_hood.h>
#include "response.h"
@@ -26,6 +27,13 @@ namespace api {
// default constructor
explicit Module(std::string name, bool password_force=false);
void require_password(const std::string &function);
private:
// functions which expose sensitive data or actions
std::unordered_set<std::string> password_force_functions;
public:
// virtual deconstructor
@@ -35,6 +43,8 @@ namespace api {
std::string name;
bool password_force;
bool requires_password(const std::string &function) const;
// the magic
void handle(Request &req, Response &res);
+98
View File
@@ -1,8 +1,11 @@
#include "capture.h"
#include <atomic>
#include <functional>
#include <mutex>
#include <unordered_map>
#include "api/capture_pump.h"
#include "api/stream_format.h"
#include "api/stream_server.h"
#include "external/rapidjson/document.h"
#include "hooks/graphics/graphics.h"
#include "hooks/graphics/jpeg_encoder.h"
@@ -71,6 +74,7 @@ namespace api::modules {
Capture::Capture() : Module("capture") {
functions["get_screens"] = std::bind(&Capture::get_screens, this, _1, _2);
functions["get_jpg"] = std::bind(&Capture::get_jpg, this, _1, _2);
functions["get_streams"] = std::bind(&Capture::get_streams, this, _1, _2);
}
/**
@@ -141,4 +145,98 @@ namespace api::modules {
CAPTURE_BUFFER.clear();
try_cached_response(screen, res);
}
/**
* get_streams()
*/
void Capture::get_streams(Request &req, Response &res) {
auto &alloc = res.doc()->GetAllocator();
// nothing is listening without -apistream, so there is no stream to describe
const unsigned short port = stream_server_port();
if (port == 0) {
return;
}
Value formats(kArrayType);
for (const auto &[name, path] : stream_formats()) {
Value entry(kObjectType);
entry.AddMember("name", Value(name.c_str(), alloc), alloc);
entry.AddMember("path", Value(path.c_str(), alloc), alloc);
formats.PushBack(entry, alloc);
}
std::vector<int> screen_numbers;
graphics_screens_get(screen_numbers);
// measuring a screen nobody has captured yet waits for the game to present, which can
// take as long as the whole request is allowed, so only one screen is measured per
// call and the rest are reported null until a later one settles them. which screen
// gets the attempt rotates, otherwise one that never presents would take every
// request and the screens behind it would stay unmeasured forever
int probe_screen = -1;
{
std::vector<int> unmeasured;
for (const auto screen : screen_numbers) {
if (screen < static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)
&& !graphics_capture_last_size(screen, nullptr, nullptr)
&& !capture_pump::screen_claimed(screen)) {
unmeasured.push_back(screen);
}
}
if (!unmeasured.empty()) {
static std::atomic<unsigned> probe_cursor { 0 };
probe_screen = unmeasured[probe_cursor.fetch_add(1) % unmeasured.size()];
}
}
Value screens(kArrayType);
for (const auto screen : screen_numbers) {
if (screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
continue;
}
int width = 0;
int height = 0;
bool known = graphics_capture_last_size(screen, &width, &height);
// a probe holds the screen for as long as it waits, so a second caller arriving
// during one would queue behind it and then take a wait of its own; let it report
// the screen as unmeasured instead and pick the size up once the first is done
static std::atomic<bool> probe_running { false };
if (!known && screen == probe_screen && !probe_running.exchange(true)) {
std::shared_ptr<uint8_t[]> pixels;
known = capture_pump::capture_direct(
screen, pixels, 1, nullptr, &width, &height);
probe_running = false;
}
// a screen of unknown size cannot be described, and a client told about it could
// not size its decoder anyway; leaving it out until it has been measured beats
// handing over an entry that has to be treated as absent
if (!known) {
continue;
}
Value entry(kObjectType);
entry.AddMember("screen", screen, alloc);
entry.AddMember("width", width, alloc);
entry.AddMember("height", height, alloc);
// a screen carries one viewer at a time, so this is what decides whether a client
// can connect at all; still racy by the time it does, only more honest than not
entry.AddMember("busy", capture_pump::screen_claimed(screen), alloc);
screens.PushBack(entry, alloc);
}
Value info(kObjectType);
info.AddMember("port", port, alloc);
info.AddMember("formats", formats, alloc);
info.AddMember("screens", screens, alloc);
res.add_data(info);
}
}
+1
View File
@@ -19,5 +19,6 @@ namespace api::modules {
// function definitions
void get_screens(Request &req, Response &res);
void get_jpg(Request &req, Response &res);
void get_streams(Request &req, Response &res);
};
}
+64
View File
@@ -1,4 +1,5 @@
#include "card.h"
#include <fstream>
#include <functional>
#include "external/rapidjson/document.h"
#include "util/logging.h"
@@ -11,8 +12,71 @@ using namespace rapidjson;
namespace api::modules {
static bool normalize_card_id(const std::string &value, std::string &card_id) {
if (value.size() != 16) {
return false;
}
uint8_t card_bin[8] {};
if (!hex2bin(value.c_str(), card_bin)) {
return false;
}
card_id = bin2hex(card_bin, std::size(card_bin));
return true;
}
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
std::ifstream file(path);
char buffer[16] {};
if (!file.read(buffer, std::size(buffer))) {
return false;
}
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
}
Card::Card() : Module("card") {
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
require_password("get_cards");
}
/**
* get_cards()
*/
void Card::get_cards(Request &req, Response &res) {
auto &alloc = res.doc()->GetAllocator();
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
std::string card_id;
std::string filename;
const auto card_override = eamuse_get_card_override(index);
const bool has_override = !card_override.empty();
if (has_override) {
if (!normalize_card_id(card_override, card_id)) {
continue;
}
} else {
const auto path = eamuse_get_card_path(index);
if (!read_card_id(path, card_id)) {
continue;
}
const auto filename_u8 = path.filename().u8string();
filename.assign(filename_u8.begin(), filename_u8.end());
}
Value card(kObjectType);
card.AddMember("index", index, alloc);
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
if (!has_override) {
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
}
res.add_data(card);
}
}
/**
+1
View File
@@ -12,6 +12,7 @@ namespace api::modules {
private:
// function definitions
void get_cards(Request &req, Response &res);
void insert(Request &req, Response &res);
};
}
+1 -1
View File
@@ -61,7 +61,7 @@ namespace api::modules {
return error_type(res, "amount", "int");
// add to coin stock
eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt()));
eamuse_coin_add(std::max(0, req.params[0].GetInt()));
}
/*
@@ -11,6 +11,7 @@ part "src/exceptions.dart";
part "src/rc4.dart";
part "src/wrappers/analogs.dart";
part "src/wrappers/buttons.dart";
part "src/wrappers/capture.dart";
part "src/wrappers/card.dart";
part "src/wrappers/coin.dart";
part "src/wrappers/control.dart";
@@ -4,8 +4,8 @@ part of spiceapi;
class Connection {
// settings
static const _TIMEOUT = Duration(seconds: 2);
static const _BUFFER_SIZE = 1024 * 8;
static const _TIMEOUT = Duration(seconds: 3);
static const _BUFFER_SIZE = 1024 * 1024 * 8;
// state
final String host, pass;
@@ -0,0 +1,47 @@
part of spiceapi;
class CaptureData {
int timestamp;
int width, height;
Uint8List data;
}
var _base64DecoderInstance = Base64Decoder();
Future<List> captureGetScreens(Connection con) {
var req = Request("capture", "get_screens");
return con.request(req).then((res) {
return res.getData();
});
}
Future<CaptureData> captureGetJPG(Connection con, {
int screen = 0,
int quality = 70,
int divide = 1,
}) {
var req = Request("capture", "get_jpg");
req.addParam(screen);
req.addParam(quality);
req.addParam(divide);
return con.request(req).then((res) {
var captureData = CaptureData();
var data = res.getData();
if (data.length > 0) captureData.timestamp = data[0];
if (data.length > 1) captureData.width = data[1];
if (data.length > 2) captureData.height = data[2];
if (data.length > 3) {
captureData.data = _base64DecoderInstance.convert(data[3]);
}
return captureData;
});
}
/// Describes the HTTP video stream, or null when this spice2x serves none.
Future<Map> captureGetStreams(Connection con) {
var req = Request("capture", "get_streams");
return con.request(req).then((res) {
var data = res.getData();
return data.length > 0 ? data[0] : null;
});
}
@@ -1,5 +1,32 @@
part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
@@ -17,7 +17,7 @@ Future<List> captureGetScreens(Connection con) {
Future<CaptureData> captureGetJPG(Connection con, {
int screen = 0,
int quality = 60,
int quality = 70,
int divide = 1,
}) {
var req = Request("capture", "get_jpg");
@@ -36,3 +36,12 @@ Future<CaptureData> captureGetJPG(Connection con, {
return captureData;
});
}
/// Describes the HTTP video stream, or null when this spice2x serves none.
Future<Map> captureGetStreams(Connection con) {
var req = Request("capture", "get_streams");
return con.request(req).then((res) {
var data = res.getData();
return data.length > 0 ? data[0] : null;
});
}
@@ -1,5 +1,32 @@
part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
@@ -2,6 +2,7 @@ from .connection import Connection
from .request import Request
from .analogs import *
from .buttons import *
from .capture import *
from .card import *
from .coin import *
from .control import *
@@ -0,0 +1,33 @@
import base64
from .connection import Connection
from .request import Request
def capture_get_screens(con: Connection):
res = con.request(Request("capture", "get_screens"))
return res.get_data()
def capture_get_jpg(con: Connection, screen: int = 0, quality: int = 70, divide: int = 1):
req = Request("capture", "get_jpg")
req.add_param(screen)
req.add_param(quality)
req.add_param(divide)
data = con.request(req).get_data()
if len(data) < 4:
return None
return {
"timestamp": data[0],
"width": data[1],
"height": data[2],
"data": base64.b64decode(data[3]),
}
def capture_get_streams(con: Connection):
"""Describes the HTTP video stream, or None when this spice2x serves none."""
data = con.request(Request("capture", "get_streams")).get_data()
return data[0] if data else None
@@ -2,6 +2,10 @@ from .connection import Connection
from .request import Request
def card_get_cards(con: Connection):
return con.request(Request("card", "get_cards")).get_data()
def card_insert(con: Connection, unit: int, card_id: str):
req = Request("card", "insert")
req.add_param(unit)
+14
View File
@@ -67,4 +67,18 @@ namespace api {
return nullptr;
}
std::vector<std::pair<std::string, std::string>> stream_formats() {
std::vector<std::pair<std::string, std::string>> formats;
#ifdef SPICE_JPEG
formats.emplace_back("mjpeg", "/stream.mjpg");
#endif
#ifdef SPICE_H264
formats.emplace_back("h264", "/stream.h264");
#endif
return formats;
}
}
+5
View File
@@ -4,6 +4,8 @@
#include <functional>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include "capture_pump.h"
@@ -35,4 +37,7 @@ namespace api {
// null when the path does not name a format this build supports
std::unique_ptr<StreamWriter> make_stream_writer(
const std::string &path, int quality, int fps);
// name and path of every format compiled into this build, for clients to pick from
std::vector<std::pair<std::string, std::string>> stream_formats();
}
+17
View File
@@ -195,11 +195,25 @@ namespace api {
"\r\n";
send_all(socket, response);
}
std::atomic<unsigned short> LISTENING_PORT { 0 };
}
unsigned short stream_server_port() {
return LISTENING_PORT.load();
}
StreamServer::StreamServer(unsigned short port)
: port(port)
{
// WinXP builds compile in neither encoder, so there would be nothing to serve and
// every request would 404; taking the port instead only invites confused clients
if (stream_formats().empty()) {
log_warning("api::stream",
"this build has no video encoders, the video stream is unavailable");
return;
}
if (!this->open_listener()) {
// the stream was asked for explicitly, so say plainly that it is not there
log_warning("api::stream",
@@ -212,6 +226,8 @@ namespace api {
this->accept_worker();
});
LISTENING_PORT = this->port;
// deliberately not logging a full URL; local IPs would leak into shared logs
log_info("api::stream", "video stream is listening on port: {}", this->port);
log_warning("api::stream",
@@ -269,6 +285,7 @@ namespace api {
StreamServer::~StreamServer() {
this->running = false;
LISTENING_PORT = 0;
if (this->listener != INVALID_SOCKET) {
closesocket(this->listener);
+3
View File
@@ -11,6 +11,9 @@
namespace api {
// 0 while no stream server is listening, so the API can tell clients not to look for one
unsigned short stream_server_port();
class StreamServer {
public:
+547 -184
View File
@@ -1,216 +1,579 @@
#define HEADSOCKET_IMPLEMENTATION
#include "external/headsocket.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include "websocket.h"
#include "util/utils.h"
#include "util/rc4.h"
#include "util/logging.h"
#include "overlay/notifications.h"
#include "controller.h"
using namespace headsocket;
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <chrono>
#include <cstring>
#include <mutex>
#include <string>
#include <thread>
#include <vector>
#include "external/wslay/msvc_compat.h"
#include <wslay/wslay.h>
#include "controller.h"
#include "external/hash-library/sha1.h"
#include "overlay/notifications.h"
#include "util/crypt.h"
#include "util/logging.h"
namespace api {
namespace {
// how long a single handshake read may stall before the connection is dropped;
// headsocket reads the request a byte at a time, so this is an idle timeout between
// bytes rather than a deadline for the whole handshake
constexpr int handshake_timeout_ms = 5000;
constexpr int server_backlog = 4;
constexpr size_t client_limit = 8;
void set_recv_timeout(connection &conn, int milliseconds) {
DWORD timeout = static_cast<DWORD>(milliseconds);
setsockopt(conn.impl()->socket, SOL_SOCKET, SO_RCVTIMEO,
// a peer that connects and then says nothing must not hold a slot forever
constexpr int handshake_timeout_ms = 5000;
constexpr size_t request_size_limit = 8 * 1024;
constexpr uint64_t message_size_limit = 64 * 1024;
// how long a quiet connection waits before the loop rechecks whether we are stopping
constexpr int idle_poll_ms = 500;
// RFC 6455 appends this to the client key before hashing
constexpr const char *websocket_guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
constexpr double notification_throttle_seconds = 0.5;
std::string trim(const std::string &text) {
const auto begin = text.find_first_not_of(" \t");
if (begin == std::string::npos) {
return "";
}
return text.substr(begin, text.find_last_not_of(" \t") - begin + 1);
}
std::string to_lower(std::string text) {
std::transform(text.begin(), text.end(), text.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return text;
}
bool contains_token(const std::string &value, const std::string &expected) {
size_t pos = 0;
while (pos < value.size()) {
const size_t end = value.find(',', pos);
if (to_lower(trim(value.substr(pos, end - pos))) == expected) {
return true;
}
if (end == std::string::npos) {
break;
}
pos = end + 1;
}
return false;
}
bool valid_websocket_key(const std::string &key) {
if (key.size() != 24 || key[22] != '=' || key[23] != '=') {
return false;
}
const std::string alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
for (size_t i = 0; i < 22; i++) {
if (alphabet.find(key[i]) == std::string::npos) {
return false;
}
}
return true;
}
bool send_all(SOCKET socket, const std::string &text) {
size_t remaining = text.size();
const char *cursor = text.data();
while (remaining > 0) {
const int sent = send(socket, cursor, static_cast<int>(remaining), 0);
if (sent <= 0) {
return false;
}
cursor += sent;
remaining -= static_cast<size_t>(sent);
}
return true;
}
void set_recv_timeout(SOCKET socket, int milliseconds) {
const DWORD timeout = static_cast<DWORD>(milliseconds);
setsockopt(socket, SOL_SOCKET, SO_RCVTIMEO,
reinterpret_cast<const char *>(&timeout), sizeof(timeout));
}
std::string accept_key(const std::string &client_key) {
SHA1 sha1;
sha1.add(client_key.data(), client_key.size());
sha1.add(websocket_guid, strlen(websocket_guid));
unsigned char digest[SHA1::HashBytes] {};
sha1.getHash(digest);
return crypt::base64_encode(digest, sizeof(digest));
}
bool handshake(SOCKET socket) {
std::string request;
char byte = 0;
bool complete = false;
while (request.size() < request_size_limit) {
const int read = recv(socket, &byte, 1, 0);
if (read <= 0) {
return false;
}
request.push_back(byte);
if (request.size() >= 4
&& request.compare(request.size() - 4, 4, "\r\n\r\n") == 0) {
complete = true;
break;
}
}
std::string key;
std::string version;
bool connection_upgrade = false;
bool upgrade = false;
const size_t request_line_end = request.find("\r\n");
const std::string request_line = request.substr(0, request_line_end);
const size_t target_end = request_line.find(' ', 4);
const bool valid_request_line = request_line_end != std::string::npos
&& request_line.compare(0, 4, "GET ") == 0
&& target_end != std::string::npos
&& target_end > 4
&& target_end == request_line.rfind(' ')
&& request_line.substr(target_end + 1) == "HTTP/1.1";
size_t pos = request_line_end;
while (pos != std::string::npos) {
const size_t end = request.find("\r\n", pos + 2);
if (end == std::string::npos || end == pos + 2) {
break;
}
const std::string line = request.substr(pos + 2, end - pos - 2);
const size_t colon = line.find(':');
if (colon != std::string::npos) {
const std::string name = to_lower(trim(line.substr(0, colon)));
const std::string value = trim(line.substr(colon + 1));
if (name == "sec-websocket-key") {
key = value;
} else if (name == "sec-websocket-version") {
version = value;
} else if (name == "connection") {
connection_upgrade = connection_upgrade
|| contains_token(value, "upgrade");
} else if (name == "upgrade") {
upgrade = upgrade || contains_token(value, "websocket");
}
}
pos = end;
}
if (!complete || !valid_request_line || !connection_upgrade || !upgrade
|| version != "13" || !valid_websocket_key(key)) {
send_all(socket,
"HTTP/1.1 400 Bad Request\r\n"
"Connection: close\r\n"
"Content-Length: 0\r\n"
"\r\n");
return false;
}
// deliberately echoes back no extension or subprotocol: naming one the client did
// not offer is a handshake failure, and none of them are wanted here
return send_all(socket,
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: " + accept_key(key) + "\r\n"
"\r\n");
}
// everything one connection needs; wslay hands this back to the callbacks
struct Session {
SOCKET socket = INVALID_SOCKET;
Controller *controller = nullptr;
ClientState *state = nullptr;
bool failed = false;
};
ssize_t recv_callback(wslay_event_context_ptr ctx, uint8_t *buffer, size_t length,
int flags, void *user_data) {
(void) flags;
auto *session = static_cast<Session *>(user_data);
const int read = recv(session->socket, reinterpret_cast<char *>(buffer),
static_cast<int>(length), 0);
if (read > 0) {
return read;
}
// the socket is non-blocking, so an empty one has to read as "nothing yet"
// rather than as a dead peer
if (read < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
return -1;
}
wslay_event_set_error(ctx,
read == 0 ? WSLAY_ERR_NO_MORE_MSG : WSLAY_ERR_CALLBACK_FAILURE);
return -1;
}
ssize_t send_callback(wslay_event_context_ptr ctx, const uint8_t *data, size_t length,
int flags, void *user_data) {
(void) flags;
auto *session = static_cast<Session *>(user_data);
const int sent = send(session->socket, reinterpret_cast<const char *>(data),
static_cast<int>(length), 0);
if (sent > 0) {
return sent;
}
if (sent < 0 && WSAGetLastError() == WSAEWOULDBLOCK) {
wslay_event_set_error(ctx, WSLAY_ERR_WOULDBLOCK);
return -1;
}
wslay_event_set_error(ctx, WSLAY_ERR_CALLBACK_FAILURE);
return -1;
}
void on_msg_recv(wslay_event_context_ptr ctx,
const struct wslay_event_on_msg_recv_arg *arg, void *user_data) {
auto *session = static_cast<Session *>(user_data);
// pings and closes are wslay's business, it answers them itself
if (wslay_is_ctrl_frame(arg->opcode)) {
return;
}
if (arg->opcode != WSLAY_BINARY_FRAME) {
log_warning("api::websocket", "ignoring a non-binary message");
return;
}
std::vector<char> in(arg->msg, arg->msg + arg->msg_length);
std::vector<char> out;
if (session->state->cipher) {
session->state->cipher->crypt(
reinterpret_cast<uint8_t *>(in.data()), in.size());
}
session->controller->process_request(session->state, &in, &out);
if (session->state->cipher) {
session->state->cipher->crypt(
reinterpret_cast<uint8_t *>(out.data()), out.size());
}
wslay_event_msg reply {};
reply.opcode = WSLAY_BINARY_FRAME;
reply.msg = reinterpret_cast<const uint8_t *>(out.data());
reply.msg_length = out.size();
if (wslay_event_queue_msg(ctx, &reply) != 0) {
session->failed = true;
return;
}
Controller::process_password_change(session->state);
}
}
/*
* Client class declaration
*/
class WebSocketClient : public web_socket_client {
// required class header
HEADSOCKET_CLIENT(WebSocketClient, web_socket_client);
private:
ClientState *state = nullptr;
// headsocket doesn't expose the peer address on its own client API, but the
// sockaddr_in captured at accept time is sitting right there in the impl
std::string remote_address() const {
char address_data[INET_ADDRSTRLEN] {};
inet_ntop(AF_INET, &this->_p->conn.impl()->from.sin_addr,
address_data, INET_ADDRSTRLEN);
return std::string(address_data);
}
protected:
bool async_received_data(const data_block &db, uint8_t *ptr, size_t length) override;
void on_accept() override;
void on_disconnect() override;
};
/*
* Server class declaration
*/
class WebSocketServer : public web_socket_server<WebSocketClient> {
HEADSOCKET_SERVER(WebSocketServer, web_socket_server);
public:
WebSocketController *websocket;
protected:
bool handshake(connection &conn) override {
// headsocket runs the handshake on its single accept thread with a blocking
// recv, so a peer that connects and then says nothing would park that thread and
// leave every later connection sitting unaccepted in the backlog
set_recv_timeout(conn, handshake_timeout_ms);
const bool accepted = base_t::handshake(conn);
// from here the client thread owns the socket and wants to block on reads
set_recv_timeout(conn, 0);
return accepted;
}
};
void api::WebSocketServer::init() {}
/*
* Controller state so we don't have to import headsocket stuff in our header
*/
struct WebSocketControllerState {
std::shared_ptr<WebSocketServer> server;
struct Client {
std::thread thread;
SOCKET socket = INVALID_SOCKET;
bool active = false;
};
Controller *controller = nullptr;
unsigned short port = 0;
SOCKET listener = INVALID_SOCKET;
bool wsa_started = false;
std::atomic_bool running { false };
std::thread acceptor;
std::mutex clients_m;
std::array<Client, client_limit> clients;
bool open_listener();
void accept_worker();
void client_worker(int slot, SOCKET socket, std::string address);
void stop();
};
bool WebSocketControllerState::open_listener() {
WSADATA wsa_data;
const int error = WSAStartup(MAKEWORD(2, 2), &wsa_data);
if (error != 0) {
log_warning("api::websocket", "WSAStartup() returned {}", error);
return false;
}
this->wsa_started = true;
this->listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (this->listener == INVALID_SOCKET) {
log_warning("api::websocket", "socket() returned {}", WSAGetLastError());
return false;
}
sockaddr_in address {};
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY;
address.sin_port = htons(this->port);
if (bind(this->listener, reinterpret_cast<sockaddr *>(&address), sizeof(address))
== SOCKET_ERROR) {
log_warning("api::websocket", "bind() returned {}", WSAGetLastError());
return false;
}
if (listen(this->listener, server_backlog) == SOCKET_ERROR) {
log_warning("api::websocket", "listen() returned {}", WSAGetLastError());
return false;
}
return true;
}
void WebSocketControllerState::accept_worker() {
while (this->running) {
sockaddr_in client_address {};
int client_address_size = sizeof(sockaddr_in);
const SOCKET client = accept(
this->listener, reinterpret_cast<sockaddr *>(&client_address),
&client_address_size);
if (client == INVALID_SOCKET) {
// on shutdown the listener is closed under us; otherwise do not spin
if (this->running) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
continue;
}
if (!this->running) {
closesocket(client);
break;
}
// formatted by hand rather than with inet_ntop, which needs a newer Windows than
// the XP toolchain targets, or inet_ntoa, which answers from a shared buffer
const uint32_t raw = ntohl(client_address.sin_addr.s_addr);
const std::string address = fmt::format("{}.{}.{}.{}",
(raw >> 24) & 0xff, (raw >> 16) & 0xff, (raw >> 8) & 0xff, raw & 0xff);
int slot = -1;
{
std::lock_guard<std::mutex> lock(this->clients_m);
for (size_t i = 0; i < this->clients.size(); i++) {
if (!this->clients[i].active) {
this->clients[i].active = true;
this->clients[i].socket = client;
slot = static_cast<int>(i);
break;
}
}
}
if (slot < 0) {
log_warning("api::websocket", "client limit of {} hit", client_limit);
overlay::notifications::add_throttled(
overlay::notifications::Severity::Warning,
"api::websocket.client_limit",
notification_throttle_seconds,
fmt::format("API websocket refused: client limit reached ({})", address));
closesocket(client);
continue;
}
// this thread is the only one that touches the thread objects, so the slot's
// previous occupant gets reaped here rather than being detached
if (this->clients[slot].thread.joinable()) {
this->clients[slot].thread.join();
}
// the handshake runs on the client thread on purpose: doing it here would put
// every later connection behind whatever this one is waiting for
this->clients[slot].thread = std::thread([this, slot, client, address] {
this->client_worker(slot, client, address);
});
}
}
void WebSocketControllerState::client_worker(int slot, SOCKET socket, std::string address) {
// wslay writes a frame header and its payload as separate sends, so leaving Nagle on
// holds the payload back until the peer acknowledges the header, costing a delayed
// ack per message; requests here are small and latency sensitive
int nodelay = 1;
setsockopt(socket, IPPROTO_TCP, TCP_NODELAY,
reinterpret_cast<const char *>(&nodelay), sizeof(nodelay));
set_recv_timeout(socket, handshake_timeout_ms);
if (handshake(socket)) {
// wslay reads until the socket would block, so leaving it blocking would make
// every reply wait out a receive timeout before the send got a turn
u_long non_blocking = 1;
ioctlsocket(socket, FIONBIO, &non_blocking);
Session session;
session.socket = socket;
session.controller = this->controller;
session.state = new ClientState();
this->controller->init_state(session.state);
log_info("api::websocket", "client connected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("API websocket client connected ({})", address));
wslay_event_callbacks callbacks {};
callbacks.recv_callback = recv_callback;
callbacks.send_callback = send_callback;
callbacks.on_msg_recv_callback = on_msg_recv;
wslay_event_context_ptr ctx = nullptr;
if (wslay_event_context_server_init(&ctx, &callbacks, &session) == 0) {
wslay_event_config_set_max_recv_msg_length(ctx, message_size_limit);
while (this->running && !session.failed
&& (wslay_event_want_read(ctx) || wslay_event_want_write(ctx))) {
fd_set read_set;
fd_set write_set;
FD_ZERO(&read_set);
FD_ZERO(&write_set);
if (wslay_event_want_read(ctx)) {
FD_SET(socket, &read_set);
}
if (wslay_event_want_write(ctx)) {
FD_SET(socket, &write_set);
}
// bounded so a silent connection still notices us shutting down
timeval timeout {};
timeout.tv_usec = idle_poll_ms * 1000;
if (select(0, &read_set, &write_set, nullptr, &timeout) < 0) {
break;
}
if (FD_ISSET(socket, &read_set) && wslay_event_recv(ctx) != 0) {
break;
}
// unconditional, so a reply queued by the read above goes out now
if (wslay_event_send(ctx) != 0) {
break;
}
}
wslay_event_context_free(ctx);
}
Controller::free_state(session.state);
delete session.state;
log_info("api::websocket", "client disconnected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("API websocket client disconnected ({})", address));
}
closesocket(socket);
std::lock_guard<std::mutex> lock(this->clients_m);
this->clients[slot].socket = INVALID_SOCKET;
this->clients[slot].active = false;
}
void WebSocketControllerState::stop() {
this->running = false;
if (this->listener != INVALID_SOCKET) {
closesocket(this->listener);
this->listener = INVALID_SOCKET;
}
// drops the client threads out of their blocking send/recv
{
std::lock_guard<std::mutex> lock(this->clients_m);
for (auto &client : this->clients) {
if (client.socket != INVALID_SOCKET) {
::shutdown(client.socket, SD_BOTH);
}
}
}
if (this->acceptor.joinable()) {
this->acceptor.join();
}
// joining is what guarantees no client thread outlives this object
for (auto &client : this->clients) {
if (client.thread.joinable()) {
client.thread.join();
}
}
if (this->wsa_started) {
WSACleanup();
this->wsa_started = false;
}
}
WebSocketController::WebSocketController(Controller *controller, uint16_t port) {
this->controller = controller;
// create state
this->state = new WebSocketControllerState();
this->state->controller = controller;
this->state->port = port;
// start server
this->state->server = WebSocketServer::create(port);
this->state->server->websocket = this;
if (this->state->server->is_running()) {
log_info("api::websocket", "server listening on port: {}", port);
} else {
if (!this->state->open_listener()) {
log_warning("api::websocket", "server failed to listen on port: {}", port);
return;
}
this->state->running = true;
this->state->acceptor = std::thread([this] {
this->state->accept_worker();
});
log_info("api::websocket", "server listening on port: {}", port);
}
WebSocketController::~WebSocketController() {
// stop server
this->state->server->stop();
// delete state
this->state->stop();
delete this->state;
this->state = nullptr;
}
void WebSocketController::free_socket() {
this->state->server->stop();
}
void WebSocketClient::on_accept() {
web_socket_client::on_accept();
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "on_accept has no server");
}
// check for init
state = new ClientState();
srv->websocket->controller->init_state(state);
// log connection
const auto address = this->remote_address();
log_info("api::websocket", "client connected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Success,
fmt::format("API websocket client connected ({})", address));
}
void WebSocketClient::on_disconnect() {
// log disconnection
const auto address = this->remote_address();
log_info("api::websocket", "client disconnected: {}", address);
overlay::notifications::add(
overlay::notifications::Severity::Info,
fmt::format("API websocket client disconnected ({})", address));
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "on_disconnect has no server");
}
// clean up state
srv->websocket->controller->free_state(state);
delete state;
state = nullptr;
// call super
web_socket_client::on_disconnect();
}
/*
* This is where business actually happens, gets called on every datablock receive
*/
bool WebSocketClient::async_received_data(const data_block &db, uint8_t *ptr, size_t length) {
// get pointer to server
auto srv = reinterpret_cast<WebSocketServer *>(server().get());
if (!srv || !srv->websocket) {
log_fatal("api::websocket", "received datablock without server");
}
// check state
if (!state) {
log_fatal("api::websocket", "client with no state received datablock");
}
// check datablock type
switch (db.op) {
case opcode::binary: {
// allocate buffers
std::vector<char> in(ptr, ptr + length);
std::vector<char> out;
// crypt in-data
if (state->cipher) {
state->cipher->crypt(reinterpret_cast<uint8_t *>(in.data()), in.size());
}
// process request
srv->websocket->controller->process_request(state, &in, &out);
// crypt out-data
if (state->cipher) {
state->cipher->crypt(reinterpret_cast<uint8_t *>(out.data()), out.size());
}
// send answer
push(out.data(), out.size());
// check for password change
srv->websocket->controller->process_password_change(state);
break;
}
default:
log_warning("api::websocket", "datablock received with non-binary type");
break;
}
// always consume the datablock, nomnom
return true;
this->state->stop();
}
}
+7
View File
@@ -573,6 +573,13 @@ static inline void easrv_init_messages() {
"\x5b\x99\xa1\x15\x78\x16\x84\xe0\x67\x2c\x75\xf7\xc0\x38\xd5\x20\xf7\x81\xae\x1f\x77\x02\xdf\x0c"
"\x60\x68\x2e\x26\xe5\xbb\xb7\xb4\x41\x72\xc4\xa4\x43\x2a\xc8\x81\xd3\x8a\x1f\xe8\x94\x39\x7e\xf6"
"\x69\x56\x9e\xaf\x06\x10\xfd\x47\x69\xb8\x14\xe6\x48\x36\xd6\x25\x18\x0c\x2b\x3d\x69");
if (avs::game::is_model("UDN")) {
// Only UDN needs an ASCII location/name for the Unity score-screen marshaler.
// These 12 encoded bytes replace the name with "SPICE2 LOCAL";
// its terminator, the other fields and the response length stay unchanged.
EA_FACILITY_GET.replace(311, 12,
"\xf2\x20\xcc\x9a\xe3\x32\x78\x80\x9a\xba\xcd\xc3");
}
EA_PCBEVENT_PUT = std::string(
"\x17\x7e\x7c\x0e\x1a\x15\x41\x0b\xae\xd5\xa0\xf5\x2c\xa1\xb9\xd2\x53\xca\xd2\x0d\x81\x34\xdb\x94\x79\xd0"
"\xc8\x9d\xb5\xfb\xef\x97");
-2195
View File
File diff suppressed because it is too large Load Diff
+7 -3
View File
@@ -50,6 +50,7 @@ static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u };
enum scard_atr_protocol {
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C,
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
SCARD_ATR_PROTOCOL_FELICA_424K = 0x12,
};
@@ -121,11 +122,14 @@ void scard_update(SCARDCONTEXT hContext, LPCTSTR readerName, uint8_t unit_no) {
BYTE cardProtocol = atr[12];
BOOL shouldReverseUid = false;
bool is_felica = false;
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
log_info("scard", "card protocol: ISO14443_PART3");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
log_info("scard", "card protocol: ISO15693_PART3");
shouldReverseUid = true;
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
log_info("scard", "card protocol: ISO14443_PART3");
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART4) {
log_info("scard", "card protocol: ISO15693_PART4");
shouldReverseUid = true;
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
log_info("scard", "card protocol: FELICA_212K");
is_felica = true;
+33
View File
@@ -0,0 +1,33 @@
cmake_minimum_required(VERSION 3.12)
project(wslay C)
set(WSLAY_HEADERS
msvc_compat.h
wslay_event.h
wslay_frame.h
wslay_macro.h
wslay_net.h
wslay_queue.h
includes/wslay/wslay.h
includes/wslay/wslayver.h
)
set(WSLAY_SOURCES
wslay_event.c
wslay_frame.c
wslay_net.c
wslay_queue.c
)
add_library(wslay STATIC ${WSLAY_HEADERS} ${WSLAY_SOURCES})
# config.h here is checked in rather than generated, see the note in it
target_compile_definitions(wslay PRIVATE HAVE_CONFIG_H)
target_include_directories(wslay PRIVATE ${PROJECT_SOURCE_DIR})
target_include_directories(wslay PUBLIC ${PROJECT_SOURCE_DIR}/includes)
if(MSVC)
target_compile_options(wslay PRIVATE "/FI${PROJECT_SOURCE_DIR}/msvc_compat.h")
else()
# upstream code, and not ours to keep warning clean
target_compile_options(wslay PRIVATE -w)
endif()
+22
View File
@@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+9
View File
@@ -0,0 +1,9 @@
/* Hand written stand-in for the autotools/CMake generated config.h. Spice only
builds this for Windows on x86, so the probes have single known answers. */
#ifndef CONFIG_H
#define CONFIG_H
#define HAVE_WINSOCK2_H
/* x86 and x86_64 are little endian, so WORDS_BIGENDIAN stays undefined */
#endif /* CONFIG_H */
+841
View File
@@ -0,0 +1,841 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_H
#define WSLAY_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include <stdlib.h>
#include <sys/types.h>
/*
* wslay/wslayver.h is generated from wslay/wslayver.h.in by
* configure. The projects which do not use autotools can set
* WSLAY_VERSION macro from outside to avoid to generating wslayver.h
*/
#ifndef WSLAY_VERSION
# include <wslay/wslayver.h>
#endif /* WSLAY_VERSION */
enum wslay_error {
WSLAY_ERR_WANT_READ = -100,
WSLAY_ERR_WANT_WRITE = -101,
WSLAY_ERR_PROTO = -200,
WSLAY_ERR_INVALID_ARGUMENT = -300,
WSLAY_ERR_INVALID_CALLBACK = -301,
WSLAY_ERR_NO_MORE_MSG = -302,
WSLAY_ERR_CALLBACK_FAILURE = -400,
WSLAY_ERR_WOULDBLOCK = -401,
WSLAY_ERR_NOMEM = -500
};
/*
* Status codes defined in RFC6455
*/
enum wslay_status_code {
WSLAY_CODE_NORMAL_CLOSURE = 1000,
WSLAY_CODE_GOING_AWAY = 1001,
WSLAY_CODE_PROTOCOL_ERROR = 1002,
WSLAY_CODE_UNSUPPORTED_DATA = 1003,
WSLAY_CODE_NO_STATUS_RCVD = 1005,
WSLAY_CODE_ABNORMAL_CLOSURE = 1006,
WSLAY_CODE_INVALID_FRAME_PAYLOAD_DATA = 1007,
WSLAY_CODE_POLICY_VIOLATION = 1008,
WSLAY_CODE_MESSAGE_TOO_BIG = 1009,
WSLAY_CODE_MANDATORY_EXT = 1010,
WSLAY_CODE_INTERNAL_SERVER_ERROR = 1011,
WSLAY_CODE_TLS_HANDSHAKE = 1015
};
enum wslay_io_flags {
/*
* There is more data to send.
*/
WSLAY_MSG_MORE = 1
};
/*
* Callback function used by wslay_frame_send() function when it needs
* to send data. The implementation of this function must send at most
* len bytes of data in data. flags is the bitwise OR of zero or more
* of the following flag:
*
* WSLAY_MSG_MORE
* There is more data to send
*
* It provides some hints to tune performance and behaviour. user_data
* is one given in wslay_frame_context_init() function. The
* implementation of this function must return the number of bytes
* sent. If there is an error, return -1. The return value 0 is also
* treated an error by the library.
*/
typedef ssize_t (*wslay_frame_send_callback)(const uint8_t *data, size_t len,
int flags, void *user_data);
/*
* Callback function used by wslay_frame_recv() function when it needs
* more data. The implementation of this function must fill at most
* len bytes of data into buf. The memory area of buf is allocated by
* library and not be freed by the application code. flags is always 0
* in this version. user_data is one given in
* wslay_frame_context_init() function. The implementation of this
* function must return the number of bytes filled. If there is an
* error, return -1. The return value 0 is also treated an error by
* the library.
*/
typedef ssize_t (*wslay_frame_recv_callback)(uint8_t *buf, size_t len,
int flags, void *user_data);
/*
* Callback function used by wslay_frame_send() function when it needs
* new mask key. The implementation of this function must write
* exactly len bytes of mask key to buf. user_data is one given in
* wslay_frame_context_init() function. The implementation of this
* function return 0 on success. If there is an error, return -1.
*/
typedef int (*wslay_frame_genmask_callback)(uint8_t *buf, size_t len,
void *user_data);
struct wslay_frame_callbacks {
wslay_frame_send_callback send_callback;
wslay_frame_recv_callback recv_callback;
wslay_frame_genmask_callback genmask_callback;
};
/*
* The opcode defined in RFC6455.
*/
enum wslay_opcode {
WSLAY_CONTINUATION_FRAME = 0x0u,
WSLAY_TEXT_FRAME = 0x1u,
WSLAY_BINARY_FRAME = 0x2u,
WSLAY_CONNECTION_CLOSE = 0x8u,
WSLAY_PING = 0x9u,
WSLAY_PONG = 0xau
};
/*
* Macro that returns 1 if opcode is control frame opcode, otherwise
* returns 0.
*/
#define wslay_is_ctrl_frame(opcode) ((opcode >> 3) & 1)
/*
* Macros that represent and return reserved bits: RSV1, RSV2, RSV3.
* These macros assume that rsv is constructed by ((RSV1 << 2) |
* (RSV2 << 1) | RSV3)
*/
#define WSLAY_RSV_NONE ((uint8_t)0)
#define WSLAY_RSV1_BIT (((uint8_t)1) << 2)
#define WSLAY_RSV2_BIT (((uint8_t)1) << 1)
#define WSLAY_RSV3_BIT (((uint8_t)1) << 0)
#define wslay_get_rsv1(rsv) ((rsv >> 2) & 1)
#define wslay_get_rsv2(rsv) ((rsv >> 1) & 1)
#define wslay_get_rsv3(rsv) (rsv & 1)
struct wslay_frame_iocb {
/* 1 for fragmented final frame, 0 for otherwise */
uint8_t fin;
/*
* reserved 3 bits. rsv = ((RSV1 << 2) | (RSV << 1) | RSV3).
* RFC6455 requires 0 unless extensions are negotiated.
*/
uint8_t rsv;
/* 4 bit opcode */
uint8_t opcode;
/* payload length [0, 2**63-1] */
uint64_t payload_length;
/* 1 for masked frame, 0 for unmasked */
uint8_t mask;
/* part of payload data */
const uint8_t *data;
/* bytes of data defined above */
size_t data_length;
};
struct wslay_frame_context;
typedef struct wslay_frame_context *wslay_frame_context_ptr;
/*
* Initializes ctx using given callbacks and user_data. This function
* allocates memory for struct wslay_frame_context and stores the
* result to *ctx. The callback functions specified in callbacks are
* copied to ctx. user_data is stored in ctx and it will be passed to
* callback functions. When the user code finished using ctx, it must
* call wslay_frame_context_free to deallocate memory.
*/
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
const struct wslay_frame_callbacks *callbacks,
void *user_data);
/*
* Deallocates memory pointed by ctx.
*/
void wslay_frame_context_free(wslay_frame_context_ptr ctx);
/*
* Send WebSocket frame specified in iocb. ctx must be initialized
* using wslay_frame_context_init() function. iocb->fin must be 1 if
* this is a fin frame, otherwise 0. iocb->rsv is reserved bits.
* iocb->opcode must be the opcode of this frame. iocb->mask must be
* 1 if this is masked frame, otherwise 0. iocb->payload_length is
* the payload_length of this frame. iocb->data must point to the
* payload data to be sent. iocb->data_length must be the length of
* the data. This function calls send_callback function if it needs
* to send bytes. This function calls gen_mask_callback function if
* it needs new mask key. This function returns the number of payload
* bytes sent. Please note that it does not include any number of
* header bytes. If it cannot send any single bytes of payload, it
* returns WSLAY_ERR_WANT_WRITE. If the library detects error in iocb,
* this function returns WSLAY_ERR_INVALID_ARGUMENT. If callback
* functions report a failure, this function returns
* WSLAY_ERR_INVALID_CALLBACK. This function does not always send all
* given data in iocb. If there are remaining data to be sent, adjust
* data and data_length in iocb accordingly and call this function
* again.
*/
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb);
/*
* Write WebSocket frame specified in iocb to buf of length
* buflen. ctx must be initialized using wslay_frame_context_init()
* function. iocb->fin must be 1 if this is a fin frame, otherwise 0.
* iocb->rsv is reserved bits. iocb->opcode must be the opcode of
* this frame. iocb->mask must be 1 if this is masked frame,
* otherwise 0. iocb->payload_length is the payload_length of this
* frame. iocb->data must point to the payload data to be
* sent. iocb->data_length must be the length of the data. Unlike
* wslay_frame_send, this function does not call send_callback
* function. This function calls gen_mask_callback function if it
* needs new mask key. This function returns the number of bytes
* written to a buffer. Unlike wslay_frame_send, it includes the
* number of header bytes. Instead, the number of payload bytes
* written is assigned to *pwpayloadlen if this function succeeds. If
* there is not enough space left in a buffer, it returns 0. If the
* library detects error in iocb, this function returns
* WSLAY_ERR_INVALID_ARGUMENT. If callback functions report a
* failure, this function returns WSLAY_ERR_INVALID_CALLBACK. This
* function does not always send all given data in iocb. If there are
* remaining data to be sent, adjust data and data_length in iocb
* accordingly and call this function again.
*/
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb, uint8_t *buf,
size_t buflen, size_t *pwpayloadlen);
/*
* Receives WebSocket frame and stores it in iocb. This function
* returns the number of payload bytes received. This does not
* include header bytes. In this case, iocb will be populated as
* follows: iocb->fin is 1 if received frame is fin frame, otherwise
* 0. iocb->rsv is reserved bits of received frame. iocb->opcode is
* opcode of received frame. iocb->mask is 1 if received frame is
* masked, otherwise 0. iocb->payload_length is the payload length of
* received frame. iocb->data is pointed to the buffer containing
* received payload data. This buffer is allocated by the library and
* must be read-only. iocb->data_length is the number of payload
* bytes recieved. This function calls recv_callback if it needs to
* receive additional bytes. If it cannot receive any single bytes of
* payload, it returns WSLAY_ERR_WANT_READ. If the library detects
* protocol violation in a received frame, this function returns
* WSLAY_ERR_PROTO. If callback functions report a failure, this
* function returns WSLAY_ERR_INVALID_CALLBACK. This function does
* not always receive whole frame in a single call. If there are
* remaining data to be received, call this function again. This
* function ensures frame alignment.
*/
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb);
struct wslay_event_context;
/* Pointer to the event-based API context */
typedef struct wslay_event_context *wslay_event_context_ptr;
struct wslay_event_on_msg_recv_arg {
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
uint8_t rsv;
/* opcode */
uint8_t opcode;
/* received message */
const uint8_t *msg;
/* message length */
size_t msg_length;
/*
* Status code iff opcode == WSLAY_CONNECTION_CLOSE. If no status
* code is included in the close control frame, it is set to 0.
*/
uint16_t status_code;
};
/*
* Callback function invoked by wslay_event_recv() when a message is
* completely received.
*/
typedef void (*wslay_event_on_msg_recv_callback)(
wslay_event_context_ptr ctx, const struct wslay_event_on_msg_recv_arg *arg,
void *user_data);
struct wslay_event_on_frame_recv_start_arg {
/* fin bit; 1 for final frame, or 0. */
uint8_t fin;
/* reserved bits: rsv = (RSV1 << 2) | (RSV2 << 1) | RSV3 */
uint8_t rsv;
/* opcode of the frame */
uint8_t opcode;
/* payload length of ths frame */
uint64_t payload_length;
};
/*
* Callback function invoked by wslay_event_recv() when a new frame
* starts to be received. This callback function is only invoked once
* for each frame.
*/
typedef void (*wslay_event_on_frame_recv_start_callback)(
wslay_event_context_ptr ctx,
const struct wslay_event_on_frame_recv_start_arg *arg, void *user_data);
struct wslay_event_on_frame_recv_chunk_arg {
/* chunk of payload data */
const uint8_t *data;
/* length of data */
size_t data_length;
};
/*
* Callback function invoked by wslay_event_recv() when a chunk of
* frame payload is received.
*/
typedef void (*wslay_event_on_frame_recv_chunk_callback)(
wslay_event_context_ptr ctx,
const struct wslay_event_on_frame_recv_chunk_arg *arg, void *user_data);
/*
* Callback function invoked by wslay_event_recv() when a frame is
* completely received.
*/
typedef void (*wslay_event_on_frame_recv_end_callback)(
wslay_event_context_ptr ctx, void *user_data);
/*
* Callback function invoked by wslay_event_recv() when it wants to
* receive more data from peer. The implementation of this callback
* function must read data at most len bytes from peer and store them
* in buf and return the number of bytes read. flags is always 0 in
* this version.
*
* If there is an error, return -1 and set error code
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
* event-based API on the whole assumes non-blocking I/O. If the cause
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
* instead. This is important because it tells wslay_event_recv() to
* stop receiving further data and return.
*/
typedef ssize_t (*wslay_event_recv_callback)(wslay_event_context_ptr ctx,
uint8_t *buf, size_t len,
int flags, void *user_data);
/*
* Callback function invoked by wslay_event_send() when it wants to
* send more data to peer. The implementation of this callback
* function must send data at most len bytes to peer and return the
* number of bytes sent. flags is the bitwise OR of zero or more of
* the following flag:
*
* WSLAY_MSG_MORE
* There is more data to send
*
* It provides some hints to tune performance and behaviour.
*
* If there is an error, return -1 and set error code
* WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error(). Wslay
* event-based API on the whole assumes non-blocking I/O. If the cause
* of error is EAGAIN or EWOULDBLOCK, set WSLAY_ERR_WOULDBLOCK
* instead. This is important because it tells wslay_event_send() to
* stop sending data and return.
*/
typedef ssize_t (*wslay_event_send_callback)(wslay_event_context_ptr ctx,
const uint8_t *data, size_t len,
int flags, void *user_data);
/*
* Callback function invoked by wslay_event_send() when it wants new
* mask key. As described in RFC6455, only the traffic from WebSocket
* client is masked, so this callback function is only needed if an
* event-based API is initialized for WebSocket client use.
*/
typedef int (*wslay_event_genmask_callback)(wslay_event_context_ptr ctx,
uint8_t *buf, size_t len,
void *user_data);
struct wslay_event_callbacks {
wslay_event_recv_callback recv_callback;
wslay_event_send_callback send_callback;
wslay_event_genmask_callback genmask_callback;
wslay_event_on_frame_recv_start_callback on_frame_recv_start_callback;
wslay_event_on_frame_recv_chunk_callback on_frame_recv_chunk_callback;
wslay_event_on_frame_recv_end_callback on_frame_recv_end_callback;
wslay_event_on_msg_recv_callback on_msg_recv_callback;
};
/*
* Initializes ctx as WebSocket Server. user_data is an arbitrary
* pointer, which is directly passed to each callback functions as
* user_data argument.
*
* On success, returns 0. On error, returns one of following negative
* values:
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_context_server_init(
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
void *user_data);
/*
* Initializes ctx as WebSocket client. user_data is an arbitrary
* pointer, which is directly passed to each callback functions as
* user_data argument.
*
* On success, returns 0. On error, returns one of following negative
* values:
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_context_client_init(
wslay_event_context_ptr *ctx, const struct wslay_event_callbacks *callbacks,
void *user_data);
/*
* Releases allocated resources for ctx.
*/
void wslay_event_context_free(wslay_event_context_ptr ctx);
/*
* Sets a bit mask of allowed reserved bits.
* Currently only permitted values are WSLAY_RSV1_BIT to allow PMCE
* extension (see RFC-7692) or WSLAY_RSV_NONE to disable.
*
* Default: WSLAY_RSV_NONE
*/
void wslay_event_config_set_allowed_rsv_bits(wslay_event_context_ptr ctx,
uint8_t rsv);
/*
* Enables or disables buffering of an entire message for non-control
* frames. If val is 0, buffering is enabled. Otherwise, buffering is
* disabled. If wslay_event_on_msg_recv_callback is invoked when
* buffering is disabled, the msg_length member of struct
* wslay_event_on_msg_recv_arg is set to 0.
*
* The control frames are always buffered regardless of this function call.
*
* This function must not be used after the first invocation of
* wslay_event_recv() function.
*/
void wslay_event_config_set_no_buffering(wslay_event_context_ptr ctx, int val);
/*
* Sets maximum length of a message that can be received. The length
* of message is checked by wslay_event_recv() function. If the length
* of a message is larger than this value, reading operation is
* disabled (same effect with wslay_event_shutdown_read() call) and
* close control frame with WSLAY_CODE_MESSAGE_TOO_BIG is queued. If
* buffering for non-control frames is disabled, the library checks
* each frame payload length and does not check length of entire
* message.
*
* The default value is (1u << 31)-1.
*/
void wslay_event_config_set_max_recv_msg_length(wslay_event_context_ptr ctx,
uint64_t val);
/*
* Sets callbacks to ctx. The callbacks previously set by this function
* or wslay_event_context_server_init() or
* wslay_event_context_client_init() are replaced with callbacks.
*/
void wslay_event_config_set_callbacks(
wslay_event_context_ptr ctx, const struct wslay_event_callbacks *callbacks);
/*
* Receives messages from peer. When receiving
* messages, it uses wslay_event_recv_callback function. Single call
* of this function receives multiple messages until
* wslay_event_recv_callback function sets error code
* WSLAY_ERR_WOULDBLOCK.
*
* When close control frame is received, this function automatically
* queues close control frame. Also this function calls
* wslay_event_set_read_enabled() with second argument 0 to disable
* further read from peer.
*
* When ping control frame is received, this function automatically
* queues pong control frame.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_read_enabled() with second argument
* 0 to disable further read from peer.
*
* wslay_event_recv() returns 0 if it succeeds, or one of the
* following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_recv() and must close WebSocket
* connection.
*/
int wslay_event_recv(wslay_event_context_ptr ctx);
/*
* Sends queued messages to peer. When sending a
* message, it uses wslay_event_send_callback function. Single call of
* wslay_event_send() sends multiple messages until
* wslay_event_send_callback sets error code WSLAY_ERR_WOULDBLOCK.
*
* If ctx is initialized for WebSocket client use, wslay_event_send()
* uses wslay_event_genmask_callback to get new mask key.
*
* When a message queued using wslay_event_queue_fragmented_msg() is
* sent, wslay_event_send() invokes
* wslay_event_fragmented_msg_callback for that message.
*
* After close control frame is sent, this function calls
* wslay_event_set_write_enabled() with second argument 0 to disable
* further transmission to peer.
*
* If there are any pending messages, wslay_event_want_write() returns
* 1, otherwise returns 0.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_write_enabled() with second argument
* 0 to disable further transmission to peer.
*
* wslay_event_send() returns 0 if it succeeds, or one of the
* following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_send() and must close WebSocket
* connection.
*/
int wslay_event_send(wslay_event_context_ptr ctx);
/*
* Writes queued messages to a buffer. Unlike wslay_event_send(), this
* function writes messages into the given buffer. It does not use
* wslay_event_send_callback function. Single call of
* wslay_event_write() writes multiple messages until there is not
* enough space left in a buffer.
*
* If ctx is initialized for WebSocket client use, wslay_event_write()
* uses wslay_event_genmask_callback to get new mask key.
*
* buf is a pointer to buffer and its capacity is given in buflen. It
* should have at least 14 bytes.
*
* When a message queued using wslay_event_queue_fragmented_msg() is
* sent, wslay_event_write() invokes
* wslay_event_fragmented_msg_callback for that message.
*
* After close control frame is sent, this function calls
* wslay_event_set_write_enabled() with second argument 0 to disable
* further transmission to peer.
*
* If there are any pending messages, wslay_event_want_write() returns
* 1, otherwise returns 0.
*
* In case of a fatal errror which leads to negative return code, this
* function calls wslay_event_set_write_enabled() with second argument
* 0 to disable further transmission to peer.
*
* wslay_event_write() returns the number of bytes written to a buffer
* if it succeeds, or one of the following negative error codes:
*
* WSLAY_ERR_CALLBACK_FAILURE
* User defined callback function is failed.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*
* When negative error code is returned, application must not make any
* further call of wslay_event_write() and must close WebSocket
* connection.
*/
ssize_t wslay_event_write(wslay_event_context_ptr ctx, uint8_t *buf,
size_t buflen);
struct wslay_event_msg {
uint8_t opcode;
const uint8_t *msg;
size_t msg_length;
};
/*
* Queues message specified in arg.
*
* This function supports both control and non-control messages and
* the given message is sent without fragmentation. If fragmentation
* is needed, use wslay_event_queue_fragmented_msg() function instead.
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_msg() returns 0 if it succeeds, or returns the
* following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_msg(wslay_event_context_ptr ctx,
const struct wslay_event_msg *arg);
/*
* Extended version of wslay_event_queue_msg which allows to set reserved bits.
*/
int wslay_event_queue_msg_ex(wslay_event_context_ptr ctx,
const struct wslay_event_msg *arg, uint8_t rsv);
/*
* Specify "source" to generate message.
*/
union wslay_event_msg_source {
int fd;
void *data;
};
/*
* Callback function called by wslay_event_send() to read message data
* from source. The implementation of
* wslay_event_fragmented_msg_callback must store at most len bytes of
* data to buf and return the number of stored bytes. If all data is
* read (i.e., EOF), set *eof to 1. If no data can be generated at the
* moment, return 0. If there is an error, return -1 and set error
* code WSLAY_ERR_CALLBACK_FAILURE using wslay_event_set_error().
*/
typedef ssize_t (*wslay_event_fragmented_msg_callback)(
wslay_event_context_ptr ctx, uint8_t *buf, size_t len,
const union wslay_event_msg_source *source, int *eof, void *user_data);
struct wslay_event_fragmented_msg {
/* opcode */
uint8_t opcode;
/* "source" to generate message data */
union wslay_event_msg_source source;
/* Callback function to read message data from source. */
wslay_event_fragmented_msg_callback read_callback;
};
/*
* Queues a fragmented message specified in arg.
*
* This function supports non-control messages only. For control frames,
* use wslay_event_queue_msg() or wslay_event_queue_close().
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_fragmented_msg() returns 0 if it succeeds, or
* returns the following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_fragmented_msg(
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg);
/*
* Extended version of wslay_event_queue_fragmented_msg which allows to set
* reserved bits.
*/
int wslay_event_queue_fragmented_msg_ex(
wslay_event_context_ptr ctx, const struct wslay_event_fragmented_msg *arg,
uint8_t rsv);
/*
* Queues close control frame. This function is provided just for
* convenience. wslay_event_queue_msg() can queue a close control
* frame as well. status_code is the status code of close control
* frame. reason is the close reason encoded in UTF-8. reason_length
* is the length of reason in bytes. reason_length must be less than
* 123 bytes.
*
* If status_code is 0, reason and reason_length is not used and close
* control frame with zero-length payload will be queued.
*
* This function just queues a message and does not send
* it. wslay_event_send() function call sends these queued messages.
*
* wslay_event_queue_close() returns 0 if it succeeds, or returns the
* following negative error codes:
*
* WSLAY_ERR_NO_MORE_MSG
* Could not queue given message. The one of possible reason is that
* close control frame has been queued/sent and no further queueing
* message is not allowed.
*
* WSLAY_ERR_INVALID_ARGUMENT
* The given message is invalid.
*
* WSLAY_ERR_NOMEM
* Out of memory.
*/
int wslay_event_queue_close(wslay_event_context_ptr ctx, uint16_t status_code,
const uint8_t *reason, size_t reason_length);
/*
* Sets error code to tell the library there is an error. This
* function is typically used in user defined callback functions. See
* the description of callback function to know which error code
* should be used.
*/
void wslay_event_set_error(wslay_event_context_ptr ctx, int val);
/*
* Query whehter the library want to read more data from peer.
*
* wslay_event_want_read() returns 1 if the library want to read more
* data from peer, or returns 0.
*/
int wslay_event_want_read(wslay_event_context_ptr ctx);
/*
* Query whehter the library want to send more data to peer.
*
* wslay_event_want_write() returns 1 if the library want to send more
* data to peer, or returns 0.
*/
int wslay_event_want_write(wslay_event_context_ptr ctx);
/*
* Prevents the event-based API context from reading any further data
* from peer.
*
* This function may be used with wslay_event_queue_close() if the
* application detects error in the data received and wants to fail
* WebSocket connection.
*/
void wslay_event_shutdown_read(wslay_event_context_ptr ctx);
/*
* Prevents the event-based API context from sending any further data
* to peer.
*/
void wslay_event_shutdown_write(wslay_event_context_ptr ctx);
/*
* Returns 1 if the event-based API context allows read operation, or
* return 0.
*
* After wslay_event_shutdown_read() is called,
* wslay_event_get_read_enabled() returns 0.
*/
int wslay_event_get_read_enabled(wslay_event_context_ptr ctx);
/*
* Returns 1 if the event-based API context allows write operation, or
* return 0.
*
* After wslay_event_shutdown_write() is called,
* wslay_event_get_write_enabled() returns 0.
*/
int wslay_event_get_write_enabled(wslay_event_context_ptr ctx);
/*
* Returns 1 if a close control frame has been received from peer, or
* returns 0.
*/
int wslay_event_get_close_received(wslay_event_context_ptr ctx);
/*
* Returns 1 if a close control frame has been sent to peer, or
* returns 0.
*/
int wslay_event_get_close_sent(wslay_event_context_ptr ctx);
/*
* Returns status code received in close control frame. If no close
* control frame has not been received, returns
* WSLAY_CODE_ABNORMAL_CLOSURE. If received close control frame has no
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
*/
uint16_t wslay_event_get_status_code_received(wslay_event_context_ptr ctx);
/*
* Returns status code sent in close control frame. If no close
* control frame has not been sent, returns
* WSLAY_CODE_ABNORMAL_CLOSURE. If sent close control frame has no
* status code, returns WSLAY_CODE_NO_STATUS_RCVD.
*/
uint16_t wslay_event_get_status_code_sent(wslay_event_context_ptr ctx);
/*
* Returns the number of queued messages.
*/
size_t wslay_event_get_queued_msg_count(wslay_event_context_ptr ctx);
/*
* Returns the sum of queued message length. It only counts the
* message length queued using wslay_event_queue_msg() or
* wslay_event_queue_close().
*/
size_t wslay_event_get_queued_msg_length(wslay_event_context_ptr ctx);
#ifdef __cplusplus
}
#endif
#endif /* WSLAY_H */
+31
View File
@@ -0,0 +1,31 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAYVER_H
#define WSLAYVER_H
/* normally substituted by the build system; pinned to the vendored release */
#define WSLAY_VERSION "1.1.1"
#endif /* WSLAYVER_H */
+9
View File
@@ -0,0 +1,9 @@
#pragma once
#ifdef _MSC_VER
#include <BaseTsd.h>
#ifndef _SSIZE_T_DEFINED
#define _SSIZE_T_DEFINED
typedef SSIZE_T ssize_t;
#endif
#endif
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_EVENT_H
#define WSLAY_EVENT_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#include "wslay_queue.h"
struct wslay_event_byte_chunk {
struct wslay_queue_entry qe;
uint8_t *data;
size_t data_length;
};
struct wslay_event_imsg {
uint8_t fin;
uint8_t rsv;
uint8_t opcode;
uint32_t utf8state;
struct wslay_queue chunks;
size_t msg_length;
};
enum wslay_event_msg_type { WSLAY_NON_FRAGMENTED, WSLAY_FRAGMENTED };
struct wslay_event_omsg {
struct wslay_queue_entry qe;
uint8_t fin;
uint8_t opcode;
uint8_t rsv;
enum wslay_event_msg_type type;
uint8_t *data;
size_t data_length;
union wslay_event_msg_source source;
wslay_event_fragmented_msg_callback read_callback;
};
struct wslay_event_frame_user_data {
wslay_event_context_ptr ctx;
void *user_data;
};
enum wslay_event_close_status {
WSLAY_CLOSE_RECEIVED = 1 << 0,
WSLAY_CLOSE_QUEUED = 1 << 1,
WSLAY_CLOSE_SENT = 1 << 2
};
enum wslay_event_config { WSLAY_CONFIG_NO_BUFFERING = 1 << 0 };
struct wslay_event_context {
/* config status, bitwise OR of enum wslay_event_config values*/
uint32_t config;
/* maximum message length that can be received */
uint64_t max_recv_msg_length;
/* 1 if initialized for server, otherwise 0 */
uint8_t server;
/* bitwise OR of enum wslay_event_close_status values */
uint8_t close_status;
/* status code in received close control frame */
uint16_t status_code_recv;
/* status code in sent close control frame */
uint16_t status_code_sent;
wslay_frame_context_ptr frame_ctx;
/* 1 if reading is enabled, otherwise 0. Upon receiving close
control frame this value set to 0. If any errors in read
operation will also set this value to 0. */
uint8_t read_enabled;
/* 1 if writing is enabled, otherwise 0 Upon completing sending
close control frame, this value set to 0. If any errors in write
opration will also set this value to 0. */
uint8_t write_enabled;
/* imsg buffer to allow interleaved control frame between
non-control frames. */
struct wslay_event_imsg imsgs[2];
/* Pointer to imsgs to indicate current used buffer. */
struct wslay_event_imsg *imsg;
/* payload length of frame currently being received. */
uint64_t ipayloadlen;
/* next byte offset of payload currently being received. */
uint64_t ipayloadoff;
/* error value set by user callback */
int error;
/* Pointer to the message currently being sent. NULL if no message
is currently sent. */
struct wslay_event_omsg *omsg;
/* Queue for non-control frames */
struct wslay_queue /*<wslay_omsg*>*/ send_queue;
/* Queue for control frames */
struct wslay_queue /*<wslay_omsg*>*/ send_ctrl_queue;
/* Size of send_queue + size of send_ctrl_queue */
size_t queued_msg_count;
/* The sum of message length in send_queue */
size_t queued_msg_length;
/* Buffer used for fragmented messages */
uint8_t obuf[4096];
uint8_t *obuflimit;
uint8_t *obufmark;
/* payload length of frame currently being sent. */
uint64_t opayloadlen;
/* next byte offset of payload currently being sent. */
uint64_t opayloadoff;
struct wslay_event_callbacks callbacks;
struct wslay_event_frame_user_data frame_user_data;
void *user_data;
uint8_t allowed_rsv_bits;
};
#endif /* WSLAY_EVENT_H */
+438
View File
@@ -0,0 +1,438 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_frame.h"
#include <stddef.h>
#include <string.h>
#include <assert.h>
#include "wslay_net.h"
#define wslay_min(A, B) (((A) < (B)) ? (A) : (B))
int wslay_frame_context_init(wslay_frame_context_ptr *ctx,
const struct wslay_frame_callbacks *callbacks,
void *user_data) {
*ctx = malloc(sizeof(struct wslay_frame_context));
if (*ctx == NULL) {
return -1;
}
memset(*ctx, 0, sizeof(struct wslay_frame_context));
(*ctx)->istate = RECV_HEADER1;
(*ctx)->ireqread = 2;
(*ctx)->ostate = PREP_HEADER;
(*ctx)->user_data = user_data;
(*ctx)->ibufmark = (*ctx)->ibuflimit = (*ctx)->ibuf;
(*ctx)->callbacks = *callbacks;
return 0;
}
void wslay_frame_context_free(wslay_frame_context_ptr ctx) { free(ctx); }
ssize_t wslay_frame_send(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb) {
if (iocb->data_length > iocb->payload_length) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (ctx->ostate == PREP_HEADER) {
uint8_t *hdptr = ctx->oheader;
memset(ctx->oheader, 0, sizeof(ctx->oheader));
*hdptr |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
*hdptr |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
/* Suppress stubborn gcc-10 warning */
*hdptr |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
++hdptr;
*hdptr |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->payload_length < 126) {
*hdptr |= (uint8_t)iocb->payload_length;
++hdptr;
} else if (iocb->payload_length < (1 << 16)) {
uint16_t len = htons((uint16_t)iocb->payload_length);
*hdptr |= 126;
++hdptr;
memcpy(hdptr, &len, 2);
hdptr += 2;
} else if (iocb->payload_length < (1ull << 63)) {
uint64_t len = hton64(iocb->payload_length);
*hdptr |= 127;
++hdptr;
memcpy(hdptr, &len, 8);
hdptr += 8;
} else {
/* Too large payload length */
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->mask) {
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
0) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->omask = 1;
memcpy(hdptr, ctx->omaskkey, 4);
hdptr += 4;
}
}
ctx->ostate = SEND_HEADER;
ctx->oheadermark = ctx->oheader;
ctx->oheaderlimit = hdptr;
ctx->opayloadlen = iocb->payload_length;
ctx->opayloadoff = 0;
}
if (ctx->ostate == SEND_HEADER) {
ptrdiff_t len = ctx->oheaderlimit - ctx->oheadermark;
ssize_t r;
int flags = 0;
if (iocb->data_length > 0) {
flags |= WSLAY_MSG_MORE;
}
r = ctx->callbacks.send_callback(ctx->oheadermark, (size_t)len, flags,
ctx->user_data);
if (r > 0) {
if (r > len) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->oheadermark += r;
if (ctx->oheadermark == ctx->oheaderlimit) {
ctx->ostate = SEND_PAYLOAD;
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
if (ctx->ostate == SEND_PAYLOAD) {
size_t totallen = 0;
if (iocb->data_length > 0) {
if (ctx->omask) {
uint8_t temp[4096];
const uint8_t *datamark = iocb->data,
*datalimit = iocb->data + iocb->data_length;
while (datamark < datalimit) {
size_t datalen = (size_t)(datalimit - datamark);
const uint8_t *writelimit =
datamark + wslay_min(sizeof(temp), datalen);
size_t writelen = (size_t)(writelimit - datamark);
ssize_t r;
size_t i;
for (i = 0; i < writelen; ++i) {
temp[i] = datamark[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
}
r = ctx->callbacks.send_callback(temp, writelen, 0, ctx->user_data);
if (r > 0) {
if ((size_t)r > writelen) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
datamark += r;
ctx->opayloadoff += (uint64_t)r;
totallen += (size_t)r;
}
} else {
if (totallen > 0) {
break;
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
}
} else {
ssize_t r;
r = ctx->callbacks.send_callback(iocb->data, iocb->data_length, 0,
ctx->user_data);
if (r > 0) {
if ((size_t)r > iocb->data_length) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->opayloadoff += (uint64_t)r;
totallen = (size_t)r;
}
} else {
return WSLAY_ERR_WANT_WRITE;
}
}
}
if (ctx->opayloadoff == ctx->opayloadlen) {
ctx->ostate = PREP_HEADER;
}
return (ssize_t)totallen;
}
return WSLAY_ERR_INVALID_ARGUMENT;
}
ssize_t wslay_frame_write(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb, uint8_t *buf,
size_t buflen, size_t *pwpayloadlen) {
uint8_t *buf_last = buf;
size_t i;
size_t hdlen;
*pwpayloadlen = 0;
if (iocb->data_length > iocb->payload_length) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
switch (ctx->ostate) {
case PREP_HEADER:
case PREP_HEADER_NOBUF:
hdlen = 2;
if (iocb->payload_length < 126) {
/* nothing to do */
} else if (iocb->payload_length < (1 << 16)) {
hdlen += 2;
} else if (iocb->payload_length < (1ull << 63)) {
hdlen += 8;
}
if (iocb->mask) {
hdlen += 4;
}
if (buflen < hdlen) {
ctx->ostate = PREP_HEADER_NOBUF;
return 0;
}
memset(buf_last, 0, hdlen);
*buf_last |= (uint8_t)((uint8_t)(iocb->fin << 7) & 0x80u);
*buf_last |= (uint8_t)((uint8_t)(iocb->rsv << 4) & 0x70u);
/* Suppress stubborn gcc-10 warning */
*buf_last |= (uint8_t)((uint8_t)(iocb->opcode << 0) & 0xfu);
++buf_last;
*buf_last |= (uint8_t)((uint8_t)(iocb->mask << 7) & 0x80u);
if (wslay_is_ctrl_frame(iocb->opcode) && iocb->payload_length > 125) {
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->payload_length < 126) {
*buf_last |= (uint8_t)iocb->payload_length;
++buf_last;
} else if (iocb->payload_length < (1 << 16)) {
uint16_t len = htons((uint16_t)iocb->payload_length);
*buf_last |= 126;
++buf_last;
memcpy(buf_last, &len, 2);
buf_last += 2;
} else if (iocb->payload_length < (1ull << 63)) {
uint64_t len = hton64(iocb->payload_length);
*buf_last |= 127;
++buf_last;
memcpy(buf_last, &len, 8);
buf_last += 8;
} else {
/* Too large payload length */
return WSLAY_ERR_INVALID_ARGUMENT;
}
if (iocb->mask) {
if (ctx->callbacks.genmask_callback(ctx->omaskkey, 4, ctx->user_data) !=
0) {
return WSLAY_ERR_INVALID_CALLBACK;
} else {
ctx->omask = 1;
memcpy(buf_last, ctx->omaskkey, 4);
buf_last += 4;
}
}
ctx->ostate = SEND_PAYLOAD;
ctx->opayloadlen = iocb->payload_length;
ctx->opayloadoff = 0;
buflen -= (size_t)(buf_last - buf);
/* fall through */
case SEND_PAYLOAD:
if (iocb->data_length > 0) {
size_t writelen = wslay_min(buflen, iocb->data_length);
if (ctx->omask) {
for (i = 0; i < writelen; ++i) {
*buf_last++ =
iocb->data[i] ^ ctx->omaskkey[(ctx->opayloadoff + i) % 4];
}
} else {
memcpy(buf_last, iocb->data, writelen);
buf_last += writelen;
}
ctx->opayloadoff += writelen;
*pwpayloadlen = writelen;
}
if (ctx->opayloadoff == ctx->opayloadlen) {
ctx->ostate = PREP_HEADER;
}
return buf_last - buf;
default:
return WSLAY_ERR_INVALID_ARGUMENT;
}
}
static void wslay_shift_ibuf(wslay_frame_context_ptr ctx) {
ptrdiff_t len = ctx->ibuflimit - ctx->ibufmark;
memmove(ctx->ibuf, ctx->ibufmark, (size_t)len);
ctx->ibuflimit = ctx->ibuf + len;
ctx->ibufmark = ctx->ibuf;
}
static ssize_t wslay_recv(wslay_frame_context_ptr ctx) {
ssize_t r;
if (ctx->ibufmark != ctx->ibuf) {
wslay_shift_ibuf(ctx);
}
r = ctx->callbacks.recv_callback(
ctx->ibuflimit, (size_t)(ctx->ibuf + sizeof(ctx->ibuf) - ctx->ibuflimit),
0, ctx->user_data);
if (r > 0) {
ctx->ibuflimit += r;
} else {
r = WSLAY_ERR_WANT_READ;
}
return r;
}
#define WSLAY_AVAIL_IBUF(ctx) ((size_t)(ctx->ibuflimit - ctx->ibufmark))
ssize_t wslay_frame_recv(wslay_frame_context_ptr ctx,
struct wslay_frame_iocb *iocb) {
ssize_t r;
if (ctx->istate == RECV_HEADER1) {
uint8_t fin, opcode, rsv, payloadlen;
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
fin = (ctx->ibufmark[0] >> 7) & 1;
rsv = (ctx->ibufmark[0] >> 4) & 7;
opcode = ctx->ibufmark[0] & 0xfu;
ctx->iom.opcode = opcode;
ctx->iom.fin = fin;
ctx->iom.rsv = rsv;
++ctx->ibufmark;
ctx->imask = (ctx->ibufmark[0] >> 7) & 1;
payloadlen = ctx->ibufmark[0] & 0x7fu;
++ctx->ibufmark;
if (wslay_is_ctrl_frame(opcode) && (payloadlen > 125 || !fin)) {
return WSLAY_ERR_PROTO;
}
if (payloadlen == 126) {
ctx->istate = RECV_EXT_PAYLOADLEN;
ctx->ireqread = 2;
} else if (payloadlen == 127) {
ctx->istate = RECV_EXT_PAYLOADLEN;
ctx->ireqread = 8;
} else {
ctx->ipayloadlen = payloadlen;
ctx->ipayloadoff = 0;
if (ctx->imask) {
ctx->istate = RECV_MASKKEY;
ctx->ireqread = 4;
} else {
ctx->istate = RECV_PAYLOAD;
}
}
}
if (ctx->istate == RECV_EXT_PAYLOADLEN) {
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
}
ctx->ipayloadlen = 0;
ctx->ipayloadoff = 0;
memcpy((uint8_t *)&ctx->ipayloadlen + (8 - ctx->ireqread), ctx->ibufmark,
ctx->ireqread);
ctx->ipayloadlen = ntoh64(ctx->ipayloadlen);
ctx->ibufmark += ctx->ireqread;
if (ctx->ireqread == 8) {
if (ctx->ipayloadlen < (1 << 16) || ctx->ipayloadlen & (1ull << 63)) {
return WSLAY_ERR_PROTO;
}
} else if (ctx->ipayloadlen < 126) {
return WSLAY_ERR_PROTO;
}
if (ctx->imask) {
ctx->istate = RECV_MASKKEY;
ctx->ireqread = 4;
} else {
ctx->istate = RECV_PAYLOAD;
}
}
if (ctx->istate == RECV_MASKKEY) {
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
if (WSLAY_AVAIL_IBUF(ctx) < ctx->ireqread) {
return WSLAY_ERR_WANT_READ;
}
}
memcpy(ctx->imaskkey, ctx->ibufmark, 4);
ctx->ibufmark += 4;
ctx->istate = RECV_PAYLOAD;
}
if (ctx->istate == RECV_PAYLOAD) {
uint8_t *readlimit, *readmark;
uint64_t rempayloadlen = ctx->ipayloadlen - ctx->ipayloadoff;
if (WSLAY_AVAIL_IBUF(ctx) == 0 && rempayloadlen > 0) {
if ((r = wslay_recv(ctx)) <= 0) {
return r;
}
}
readmark = ctx->ibufmark;
readlimit = WSLAY_AVAIL_IBUF(ctx) < rempayloadlen
? ctx->ibuflimit
: ctx->ibufmark + rempayloadlen;
if (ctx->imask) {
for (; ctx->ibufmark != readlimit; ++ctx->ibufmark, ++ctx->ipayloadoff) {
ctx->ibufmark[0] ^= ctx->imaskkey[ctx->ipayloadoff % 4];
}
} else {
ctx->ibufmark = readlimit;
ctx->ipayloadoff += (uint64_t)(readlimit - readmark);
}
iocb->fin = ctx->iom.fin;
iocb->rsv = ctx->iom.rsv;
iocb->opcode = ctx->iom.opcode;
iocb->payload_length = ctx->ipayloadlen;
iocb->mask = ctx->imask;
iocb->data = readmark;
iocb->data_length = (size_t)(ctx->ibufmark - readmark);
if (ctx->ipayloadlen == ctx->ipayloadoff) {
ctx->istate = RECV_HEADER1;
ctx->ireqread = 2;
}
return (ssize_t)iocb->data_length;
}
return WSLAY_ERR_INVALID_ARGUMENT;
}
+77
View File
@@ -0,0 +1,77 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_FRAME_H
#define WSLAY_FRAME_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
enum wslay_frame_state {
PREP_HEADER,
PREP_HEADER_NOBUF,
SEND_HEADER,
SEND_PAYLOAD,
RECV_HEADER1,
RECV_PAYLOADLEN,
RECV_EXT_PAYLOADLEN,
RECV_MASKKEY,
RECV_PAYLOAD
};
struct wslay_frame_opcode_memo {
uint8_t fin;
uint8_t opcode;
uint8_t rsv;
};
struct wslay_frame_context {
uint8_t ibuf[4096];
uint8_t *ibufmark;
uint8_t *ibuflimit;
struct wslay_frame_opcode_memo iom;
uint64_t ipayloadlen;
uint64_t ipayloadoff;
uint8_t imask;
uint8_t imaskkey[4];
enum wslay_frame_state istate;
size_t ireqread;
uint8_t oheader[14];
uint8_t *oheadermark;
uint8_t *oheaderlimit;
uint64_t opayloadlen;
uint64_t opayloadoff;
uint8_t omask;
uint8_t omaskkey[4];
enum wslay_frame_state ostate;
struct wslay_frame_callbacks callbacks;
void *user_data;
};
#endif /* WSLAY_FRAME_H */
+39
View File
@@ -0,0 +1,39 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2020 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_MACRO_H
#define WSLAY_MACRO_H
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#include <stddef.h>
#define wslay_struct_of(ptr, type, member) \
((type *)(void *)((char *)(ptr)-offsetof(type, member)))
#endif /* WSLAY_MACRO_H */
+35
View File
@@ -0,0 +1,35 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_net.h"
#ifndef WORDS_BIGENDIAN
uint64_t wslay_byteswap64(uint64_t x) {
uint64_t u = ntohl(x & 0xffffffffllu);
uint64_t l = ntohl((uint32_t)(x >> 32));
return (u << 32) | l;
}
#endif /* !WORDS_BIGENDIAN */
+54
View File
@@ -0,0 +1,54 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_NET_H
#define WSLAY_NET_H
#ifdef HAVE_CONFIG_H
# include <config.h>
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
#ifdef HAVE_ARPA_INET_H
# include <arpa/inet.h>
#endif /* HAVE_ARPA_INET_H */
#ifdef HAVE_NETINET_IN_H
# include <netinet/in.h>
#endif /* HAVE_NETINET_IN_H */
/* For Mingw build */
#ifdef HAVE_WINSOCK2_H
# include <winsock2.h>
#endif /* HAVE_WINSOCK2_H */
#ifdef WORDS_BIGENDIAN
# define ntoh64(x) (x)
# define hton64(x) (x)
#else /* !WORDS_BIGENDIAN */
uint64_t wslay_byteswap64(uint64_t x);
# define ntoh64(x) wslay_byteswap64(x)
# define hton64(x) wslay_byteswap64(x)
#endif /* !WORDS_BIGENDIAN */
#endif /* WSLAY_NET_H */
+77
View File
@@ -0,0 +1,77 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#include "wslay_queue.h"
#include <string.h>
#include <assert.h>
#include "wslay_macro.h"
void wslay_queue_init(struct wslay_queue *queue) {
queue->top = NULL;
queue->tail = &queue->top;
}
void wslay_queue_deinit(struct wslay_queue *queue) { (void)queue; }
void wslay_queue_push(struct wslay_queue *queue,
struct wslay_queue_entry *ent) {
ent->next = NULL;
*queue->tail = ent;
queue->tail = &ent->next;
}
void wslay_queue_push_front(struct wslay_queue *queue,
struct wslay_queue_entry *ent) {
ent->next = queue->top;
queue->top = ent;
if (ent->next == NULL) {
queue->tail = &ent->next;
}
}
void wslay_queue_pop(struct wslay_queue *queue) {
assert(queue->top);
queue->top = queue->top->next;
if (queue->top == NULL) {
queue->tail = &queue->top;
}
}
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue) {
assert(queue->top);
return queue->top;
}
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue) {
assert(queue->top);
return wslay_struct_of(queue->tail, struct wslay_queue_entry, next);
}
int wslay_queue_empty(struct wslay_queue *queue) {
assert(queue->top || queue->tail == &queue->top);
return queue->top == NULL;
}
+53
View File
@@ -0,0 +1,53 @@
/*
* Wslay - The WebSocket Library
*
* Copyright (c) 2011, 2012 Tatsuhiro Tsujikawa
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#ifndef WSLAY_QUEUE_H
#define WSLAY_QUEUE_H
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif /* HAVE_CONFIG_H */
#include <wslay/wslay.h>
struct wslay_queue_entry {
struct wslay_queue_entry *next;
};
struct wslay_queue {
struct wslay_queue_entry *top;
struct wslay_queue_entry **tail;
};
void wslay_queue_init(struct wslay_queue *queue);
void wslay_queue_deinit(struct wslay_queue *queue);
void wslay_queue_push(struct wslay_queue *queue, struct wslay_queue_entry *ent);
void wslay_queue_push_front(struct wslay_queue *queue,
struct wslay_queue_entry *ent);
void wslay_queue_pop(struct wslay_queue *queue);
struct wslay_queue_entry *wslay_queue_top(struct wslay_queue *queue);
struct wslay_queue_entry *wslay_queue_tail(struct wslay_queue *queue);
int wslay_queue_empty(struct wslay_queue *queue);
#endif /* WSLAY_QUEUE_H */
+41 -37
View File
@@ -9,6 +9,8 @@
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h"
#include "io.h"
#include "motion_cam.h"
#include "rgb_cam.h"
#pragma pack(push)
@@ -276,48 +278,50 @@ namespace games::drs {
void DRSGame::attach() {
Game::attach();
// TouchSDK hooks
detour::iat("??0TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
detour::iat("??1TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
if (!DISABLE_TOUCH) {
// TouchSDK hooks
detour::iat("??0TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
detour::iat("??1TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
start_touch();
} else {
log_info("drs", "no native input method detected");
log_info("drs", "touch input for dance floor disabled");
}
init_down_motion_hook();
if (RGB_CAMERA_HOOK) {
init_rgb_camera_hook();
}
+4 -3
View File
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
"P2 Up",
"P2 Down",
"P2 Left",
"P2 Right"
"P2 Right",
"Down Motion"
);
}
@@ -30,8 +31,8 @@ std::vector<Button> &games::drs::get_buttons() {
std::string games::drs::get_buttons_help() {
// keep to max 100 characters wide
return
"Motion camera required for DOWN movement.\n"
"Touchscreen supported for dance floor."
"Touchscreen supported for dance floor.\n"
"Down Motion applies to both players."
;
}
+2 -1
View File
@@ -20,7 +20,8 @@ namespace games::drs {
P2_Up,
P2_Down,
P2_Left,
P2_Right
P2_Right,
DownMotion
};
}
+109
View File
@@ -0,0 +1,109 @@
#include "games/drs/motion_cam.h"
#include <cstdint>
#include "avs/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "io.h"
namespace games::drs {
// the game reads down movement from a depth camera whose driver is compiled into the game DLL,
// so there is no import to replace.
//
// with no camera the tracked height never changes, so the motion state never leaves "still"
// and DOWN notes can never be hit.
//
// rather than answer CInputManager::IsDown, the button drives the same state pair the camera
// would; that leaves the game's own edge test untouched and keeps every note in a frame seeing
// one answer.
static int32_t *MOTION_STATE = nullptr;
static int32_t *MOTION_STATE_PREV = nullptr;
static constexpr int32_t MOTION_STATE_STILL = 1;
static constexpr int32_t MOTION_STATE_DOWN = 2;
static void (*UpdateMotion_orig)() = nullptr;
static bool DOWN_MOTION_HELD = false;
static void InputManager_UpdateMotion() {
UpdateMotion_orig();
auto &buttons = get_buttons();
const bool held = GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::DownMotion));
// a state of DOWN only counts as movement when the frame before it was something else
if (held) {
*MOTION_STATE_PREV = DOWN_MOTION_HELD ? MOTION_STATE_DOWN : MOTION_STATE_STILL;
*MOTION_STATE = MOTION_STATE_DOWN;
}
DOWN_MOTION_HELD = held;
}
void init_down_motion_hook() {
// only hook motion camera if the user has down motion button bound to something
auto &buttons = get_buttons();
if (!buttons.at(Buttons::DownMotion).isSet()) {
return;
}
// CInputManager::IsDown - read the state pair out of its two operands.
// Identical in game versions 2020121400 and 2022121400 / 2024120300:
//
// 83 3D xx xx xx xx 02 cmp dword [rip+prev], 2 ; state one frame ago
// 74 0C je FALSE ; already down, so no new edge
// 83 3D xx xx xx xx 02 cmp dword [rip+cur], 2 ; state this frame
// 75 03 jne FALSE ; not down
// B0 01 mov al, 1 ; went down on this frame
// C3 ret
// 32 C0 FALSE: xor al, al
// C3 ret
auto is_down = reinterpret_cast<uint8_t *>(find_pattern(
avs::game::DLL_INSTANCE,
"833D0000000002740C833D00000000027503B001C332C0C3",
"XX????XXXXX????XXXXXXXXX",
0, 0));
if (is_down == nullptr) {
log_warning("drs", "motion sensor state not found, DOWN motion button unavailable");
return;
}
// rip-relative displacements resolve against the end of their own instruction, and each
// cmp above is 7 bytes with its displacement 4 bytes in: the first ends at +7 with its
// displacement at +2, the second starts at +9 so it ends at +16 with its own at +11.
// read signed, since a target sitting below the instruction encodes as negative.
MOTION_STATE_PREV = (int32_t *) (is_down + 7 + *(int32_t *) (is_down + 2));
MOTION_STATE = (int32_t *) (is_down + 16 + *(int32_t *) (is_down + 11));
// CInputManager::UpdateMotion is hooked for the sole purpose of reading the Down Motion
// button mapping frequently enough so that the rising edge can be detected.
//
// Matched over the prologue up to the camera read; the wildcards are a float and a callee:
//
// 48 83 EC 38 sub rsp, 0x38
// 0F 29 74 24 20 movaps [rsp+0x20], xmm6
// F3 0F 10 35 xx xx xx xx movss xmm6, [rip+height] ; reading as of last frame
// E8 xx xx xx xx call GetPlayVideoProcess
// 48 8B C8 mov rcx, rax
// 48 8B 10 mov rdx, [rax]
// FF 52 60 call [rdx+0x60] ; reading off the newest frame
auto update = reinterpret_cast<void *>(find_pattern(
avs::game::DLL_INSTANCE,
"4883EC380F29742420F30F103500000000E800000000488BC8488B10FF5260",
"XXXXXXXXXXXXX????X????XXXXXXXXX",
0, 0));
if (update == nullptr ||
!detour::trampoline_try(
update, (void *) InputManager_UpdateMotion, (void **) &UpdateMotion_orig)) {
log_warning("drs", "motion update not hooked, DOWN motion button unavailable");
return;
}
log_info("drs", "hooked DOWN motion at +{:#x}",
(uintptr_t) ((uint8_t *) update - (uint8_t *) avs::game::DLL_INSTANCE));
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::drs {
void init_down_motion_hook();
}
+45
View File
@@ -39,10 +39,38 @@ namespace games::gitadora {
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
std::pair<UINT, UINT> arena_subscreen_host_size() {
if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value();
}
if (arena_subscreen_landscape()) {
return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT };
}
return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT };
}
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height) {
if (host_width <= 0 || host_height <= 0) {
return RECT {};
}
LONG width = MulDiv(host_height, ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT);
LONG height = host_height;
if (width > host_width) {
width = host_width;
height = MulDiv(host_width, ARENA_SUBSCREEN_HEIGHT, ARENA_SUBSCREEN_WIDTH);
}
const LONG left = (host_width - width) / 2;
const LONG top = (host_height - height) / 2;
return RECT { left, top, left + width, top + height };
}
/*
* Prevent GitaDora from creating folders on F drive
*/
@@ -343,6 +371,23 @@ namespace games::gitadora {
"gitadora",
"arena model: unsupported window count: {}", count);
}
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 = ArenaSubscreenLandscape::Off;
}
if (ARENA_TWO_HEAD_EXCLUSIVE) {
const auto [host_width, host_height] = arena_subscreen_host_size();
if (host_width != ARENA_SUBSCREEN_WIDTH
|| host_height != ARENA_SUBSCREEN_HEIGHT) {
log_info(
"gitadora",
"arena model: SMALL head runs at {}x{}, subscreen is scaled to fit",
host_width, host_height);
}
}
}
#endif
+30
View File
@@ -1,6 +1,7 @@
#pragma once
#include <optional>
#include <utility>
#include <windows.h>
#include <mmreg.h>
@@ -11,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;
@@ -21,14 +30,35 @@ namespace games::gitadora {
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE;
extern std::optional<std::string> 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;
// used when a landscape monitor drives the SMALL head and -forceressub is unset
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_WIDTH = 1920;
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_HEIGHT = 1080;
// resolution the SMALL head actually runs at; the portrait panel size unless
// -forceressub or the landscape option overrides it
std::pair<UINT, UINT> arena_subscreen_host_size();
// area of the host the portrait subscreen occupies, centered and aspect-preserved;
// whatever is left over on either side stays black
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height);
class GitaDoraGame : public games::Game {
public:
GitaDoraGame();
+38 -25
View File
@@ -2,6 +2,7 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <d3d9.h>
#include "mf_wrappers.h"
#include "avs/game.h"
@@ -46,9 +47,7 @@ struct PredefinedHook {
uintptr_t hook_afp_texture_offset;
};
PredefinedHook g_predefinedHooks[] = {};
const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks);
std::array<PredefinedHook, 0> g_predefinedHooks = {};
namespace games::iidx {
@@ -110,14 +109,14 @@ namespace games::iidx {
auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);
for (DWORD i = 0; i < g_predefinedHooksLength; i++) {
if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) {
for (auto &hook : g_predefinedHooks) {
if (pe.compare(hook.pe_identifier) == 0) {
log_misc("iidx:camhook", "Found predefined addresses");
addr_hook_a = g_predefinedHooks[i].hook_a;
addr_textures = g_predefinedHooks[i].hook_textures;
addr_camera_manager = g_predefinedHooks[i].hook_camera_manager;
addr_device_offset = g_predefinedHooks[i].hook_device_offset;
addr_afp_texture_offset = g_predefinedHooks[i].hook_afp_texture_offset;
addr_hook_a = hook.hook_a;
addr_textures = hook.hook_textures;
addr_camera_manager = hook.hook_camera_manager;
addr_device_offset = hook.hook_device_offset;
addr_afp_texture_offset = hook.hook_afp_texture_offset;
return TRUE;
}
}
@@ -200,11 +199,11 @@ namespace games::iidx {
addr_camera_manager = (addr_camera_manager_ptr - (uint8_t*) iidx_module) + disp_camera_manager + 7;
// addr_device_offset
search_from = (addr_hook_a_ptr - (uint8_t*) iidx_module);
search_from = addr_hook_a;
uint8_t *addr_device_ptr = reinterpret_cast<uint8_t *>(find_pattern_from(
iidx_module,
"488B89",
"XXX",
"488B8F00000000488B014C8B870000000033D2FF9028010000",
"XXX????XXXXXX????XXXXXXXX",
3, 0, search_from));
if (addr_device_ptr == nullptr) {
@@ -212,7 +211,7 @@ namespace games::iidx {
return FALSE;
}
addr_device_offset = *addr_device_ptr;
addr_device_offset = *reinterpret_cast<uint32_t*>(addr_device_ptr);
// --- addr_afp_texture_offset ---
uint8_t *addr_afp_texture_ptr = reinterpret_cast<uint8_t *>(find_pattern(
@@ -234,13 +233,27 @@ namespace games::iidx {
static void **__fastcall camera_hook_a(PBYTE a1) {
std::call_once(hook_a_init, [&]{
device = *reinterpret_cast<LPDIRECT3DDEVICE9EX*>(a1 + addr_device_offset);
auto const preview = *reinterpret_cast<LPDIRECT3DTEXTURE9**>((uint8_t*)iidx_module + addr_textures);
auto const registry = reinterpret_cast<Camera::TextureRegistry*>(
reinterpret_cast<uint8_t*>(iidx_module) + addr_textures - offsetof(Camera::TextureRegistry, begin));
auto const manager = reinterpret_cast<Camera::CCameraManager2*>((uint8_t*)iidx_module + addr_camera_manager);
camera_texture_a = manager->cameras->a->d3d9_texture(addr_afp_texture_offset);
camera_texture_b = manager->cameras->b->d3d9_texture(addr_afp_texture_offset);
preview_texture_a = preview;
preview_texture_b = preview + 2;
auto const camera_count = manager->begin ? manager->end - manager->begin : 0;
auto const bind_camera = [&](size_t index, LPDIRECT3DTEXTURE9*& texture, LPDIRECT3DTEXTURE9*& preview) {
if (index >= static_cast<size_t>(camera_count)) {
return; // IIDX 34 only has the TOP camera.
}
auto const camera = manager->begin[index];
auto const afp = camera ? camera->afp_texture(addr_afp_texture_offset) : nullptr;
if (!afp || !registry->begin || afp->handle < registry->handle_base ||
afp->handle - registry->handle_base >= registry->end - registry->begin) {
log_warning("iidx:camhook", "invalid texture destination for camera {}", index);
return;
}
texture = &afp->texture;
preview = registry->begin + (afp->handle - registry->handle_base);
};
bind_camera(0, camera_texture_a, preview_texture_a);
bind_camera(1, camera_texture_b, preview_texture_b);
init_local_camera();
});
@@ -382,10 +395,10 @@ namespace games::iidx {
log_misc("iidx:camhook", "Adding user-preferred cameras");
for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
auto pActivate = ppDevices[i];
if ((int)i == preferred_top_index && top_camera == nullptr) {
if ((int)i == preferred_top_index && top_camera == nullptr && camera_texture_a != nullptr) {
log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id);
top_camera = add_top_camera(pActivate);
} else if ((int)i == preferred_front_index && front_camera == nullptr) {
} else if ((int)i == preferred_front_index && front_camera == nullptr && camera_texture_b != nullptr) {
log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id);
front_camera = add_front_camera(pActivate);
}
@@ -403,16 +416,16 @@ namespace games::iidx {
if (!FLIP_CAMS) {
// top camera first, then front
if (top_camera == nullptr) {
if (top_camera == nullptr && camera_texture_a != nullptr) {
top_camera = add_top_camera(pActivate);
} else if (front_camera == nullptr) {
} else if (front_camera == nullptr && camera_texture_b != nullptr) {
front_camera = add_front_camera(pActivate);
}
} else {
// front first, then top
if (front_camera == nullptr) {
if (front_camera == nullptr && camera_texture_b != nullptr) {
front_camera = add_front_camera(pActivate);
} else if (top_camera == nullptr) {
} else if (top_camera == nullptr && camera_texture_a != nullptr) {
top_camera = add_top_camera(pActivate);
}
}
+19 -9
View File
@@ -70,21 +70,31 @@ namespace games::iidx {
class IIDXCameraSourceReaderCallback;
namespace Camera {
struct AfpTexture {
void* vftbl;
IDirect3DTexture9* texture;
uint32_t handle;
};
struct TextureRegistry {
uint32_t handle_base;
uint32_t next_slot;
IDirect3DTexture9** begin;
IDirect3DTexture9** end;
IDirect3DTexture9** capacity_end;
};
struct PlayVideoCamera {
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
auto const afp_texture = *reinterpret_cast<uint8_t**>
(reinterpret_cast<uint8_t*>(this) + offset);
return reinterpret_cast<IDirect3DTexture9**>(afp_texture + 0x8);
AfpTexture* afp_texture(const uintptr_t offset) {
return *reinterpret_cast<AfpTexture**>(reinterpret_cast<uint8_t*>(this) + offset);
}
};
struct CCameraManager2 {
using camera_pointers = struct {
PlayVideoCamera* a;
PlayVideoCamera* b;
};
void* vftbl;
camera_pointers* cameras;
PlayVideoCamera** begin;
PlayVideoCamera** end;
PlayVideoCamera** capacity_end;
};
}
+9
View File
@@ -37,6 +37,7 @@
#include "qks/io.h"
#include "mfg/io.h"
#include "pc/io.h"
#include "udn/io.h"
namespace games {
@@ -332,6 +333,14 @@ namespace games {
analogs.insert({ pc, pc::get_analogs() });
lights.insert({ pc, pc::get_lights() });
file_hints[pc].push_back({"game/svm.exe"});
// DANCE aROUND
const std::string udn("DANCE aROUND");
games.push_back(udn);
buttons.insert({ udn, udn::get_buttons() });
buttons_help.insert({ udn, udn::get_buttons_help() });
lights.insert({ udn, udn::get_lights() });
file_hints[udn].push_back({"game/dancearound_Data"});
}
const std::vector<std::string> &get_games() {
+110 -55
View File
@@ -2,6 +2,8 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <mutex>
#include <optional>
#include <cstdint>
#include "util/detour.h"
@@ -141,10 +143,100 @@ namespace games::popn {
static AIO_SCI_COMM *aioSciComm;
static AIO_IOB5_BI3A *aioIob5Bi3a;
static constexpr size_t PIKA_BUTTON_COUNT = 9;
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
static bool pika_button_staging_dirty = false;
static std::mutex pika_button_staging_mutex;
/*
* implementations
*/
static void publish_pika_button_staging() {
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
if (!pika_button_staging_dirty) {
return;
}
data = pika_button_staging;
pika_button_staging_dirty = false;
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R,
Lights::popn_lights_t::PikaButton1_G,
Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R,
Lights::popn_lights_t::PikaButton2_G,
Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R,
Lights::popn_lights_t::PikaButton3_G,
Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R,
Lights::popn_lights_t::PikaButton4_G,
Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R,
Lights::popn_lights_t::PikaButton5_G,
Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R,
Lights::popn_lights_t::PikaButton6_G,
Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R,
Lights::popn_lights_t::PikaButton7_G,
Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R,
Lights::popn_lights_t::PikaButton8_G,
Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R,
Lights::popn_lights_t::PikaButton9_G,
Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
auto &lights = get_lights();
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
const auto data_offset = button * 3;
// special handling converting RGB data to non-RGB lights
// take the max(R, G, B) and use it to write the light value
const auto light_value = std::max({
data[data_offset],
data[data_offset + 1],
data[data_offset + 2]
});
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
// at all times, even when "off". When translating to legacy on/off lights,
// treat only values above about 60% brightness as fully on to preserve the
// dim "off" state.
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[button]),
light_value > 150 ? 1.f : 0.f);
}
for (size_t light = 0; light < data.size(); light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
data[light] / 255.f);
}
}
// libaio.dll
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
@@ -260,6 +352,11 @@ namespace games::popn {
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
if (i_pNodeMgr == aioNmgrIob5) {
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
pika_button_staging.fill(0);
pika_button_staging_dirty = false;
}
aioIob5Bi3a = new AIO_IOB5_BI3A;
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
return aioIob5Bi3a;
@@ -269,11 +366,15 @@ namespace games::popn {
}
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
RI_MGR->devices_flush_output();
if (i_pNodeCtl != aioIob5Bi3a) {
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
}
// snapshot the current state of lights and flush the output
// this closely mirrors what the game does with real I/O
publish_pika_button_staging();
RI_MGR->devices_flush_output();
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
auto &buttons = get_buttons();
@@ -413,61 +514,15 @@ namespace games::popn {
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
}
auto &lights = get_lights();
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
// special handling converting to non-RGB lights
// take the max(R, G, B) and use it to write the light value
uint8_t light_value[9] = { 0 };
for (uint32_t i = 0; i < number_of_leds; i += 3) {
light_value[i / 3] =
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
static_assert(std::size(legacy_button_lights) == 9);
for (size_t light = 0; light < 9; light++) {
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
// otherwise off, to avoid dimly lit "off" state
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[light]),
light_value[light] > 150 ? 1.f : 0.f);
}
// color buttons
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(button_lights) == 9 * 3);
for (size_t light = 0; light < number_of_leds; light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
((uint8_t *)i_pData)[light] / 255.f);
}
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
// clears the buffer (set to 0 / lights off) between animations but they are not meant
// to be seen by the I/O
// therefore, instead of writing the lights out directly here, we stage them in a buffer
// and wait for GetDeviceStatus to be called
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
pika_button_staging_dirty = true;
}
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
+150
View File
@@ -0,0 +1,150 @@
#include "projector.h"
#include <algorithm>
#include <cstring>
#include <cwchar>
#include "util/logging.h"
namespace {
// request/response layout: identifier, command, two zero bytes, payload length,
// the payload itself and a checksum over everything before it
constexpr size_t HEADER_SIZE = 5;
constexpr uint8_t RESPONSE_ID = 0x23;
constexpr uint8_t COMMAND_COMMON_DATA = 0x8A;
constexpr uint8_t COMMAND_TEMPERATURE = 0x99;
constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B;
// shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT
constexpr uint32_t LAMP_SECONDS = 0;
constexpr uint32_t TEMPERATURE_INTAKE = 25;
constexpr uint32_t TEMPERATURE_EXHAUST = 35;
// anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact
// value was already recorded in /projecter/current on an earlier boot
constexpr uint32_t LAMP_CURRENT = 1000;
void put32(uint8_t *dest, uint32_t value) {
dest[0] = (uint8_t) (value & 0xFF);
dest[1] = (uint8_t) ((value >> 8) & 0xFF);
dest[2] = (uint8_t) ((value >> 16) & 0xFF);
dest[3] = (uint8_t) ((value >> 24) & 0xFF);
}
}
bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) {
return false;
}
log_info("silentscope", "Opened COM2 (projector)");
return true;
}
int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) {
buffer[bytes_read++] = this->response.front();
this->response.pop_front();
}
return (int) bytes_read;
}
int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite);
while (this->request.size() >= HEADER_SIZE) {
const size_t packet_size = HEADER_SIZE + this->request[4] + 1;
if (this->request.size() < packet_size) {
break;
}
this->process_request(this->request.data());
this->request.erase(this->request.begin(), this->request.begin() + packet_size);
}
return (int) nNumberOfBytesToWrite;
}
size_t games::silentscope::ProjectorHandle::bytes_available() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->response.size();
}
bool games::silentscope::ProjectorHandle::close() {
std::lock_guard<std::mutex> lock(this->mutex);
this->request.clear();
this->response.clear();
log_info("silentscope", "Closed COM2 (projector)");
return true;
}
void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) {
const uint8_t command = packet[1];
const uint8_t length = packet[4];
const uint8_t *data = packet + HEADER_SIZE;
switch (command) {
case COMMAND_COMMON_DATA: {
// the game only looks at the lamp usage time near the end of the block
std::vector<uint8_t> payload(98, 0);
put32(&payload[94], LAMP_SECONDS);
this->reply(command, payload);
break;
}
case COMMAND_TEMPERATURE: {
// the requested sensor is echoed back along with its reading
const uint8_t sensor = length > 0 ? data[0] : 0;
std::vector<uint8_t> payload(5, 0);
payload[0] = sensor;
put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST);
this->reply(command, payload);
break;
}
case COMMAND_LAMP_CURRENT: {
// the three byte item selector is echoed back along with the measurement
std::vector<uint8_t> payload(7, 0);
memcpy(payload.data(), data, std::min<size_t>(length, 3));
put32(&payload[3], LAMP_CURRENT);
this->reply(command, payload);
break;
}
default:
log_misc("silentscope", "unknown projector command {:#04x}", command);
this->reply(command, {});
break;
}
}
void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector<uint8_t> &data) {
std::vector<uint8_t> packet {
RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size()
};
packet.insert(packet.end(), data.begin(), data.end());
uint8_t checksum = 0;
for (auto byte : packet) {
checksum += byte;
}
packet.push_back(checksum);
this->response.insert(this->response.end(), packet.begin(), packet.end());
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
#include "hooks/devicehook.h"
namespace games::silentscope {
// The cabinet talks to its projector over COM2. Without an answer the game stops at
// I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly").
class ProjectorHandle : public CustomHandle {
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
size_t bytes_available() override;
bool close() override;
private:
std::mutex mutex;
std::vector<uint8_t> request;
std::deque<uint8_t> response;
void process_request(const uint8_t *packet);
void reply(uint8_t command, const std::vector<uint8_t> &data);
};
}
@@ -1,10 +1,11 @@
#include "silentscope.h"
#include "acioemu/handle.h"
#include "cfg/configurator.h"
#include "hooks/devicehook.h"
#include "util/libutils.h"
#include "projector.h"
namespace games::silentscope {
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
@@ -16,9 +17,8 @@ namespace games::silentscope {
// load the game DLL so hooks apply
libutils::try_library("gamendd.dll");
// TODO(felix): implement stuff on this port
devicehook_init();
devicehook_add(new acioemu::ACIOHandle(L"COM2"));
devicehook_add(new ProjectorHandle());
}
void SilentScopeGame::detach() {
+686
View File
@@ -0,0 +1,686 @@
#include "bi2x_hook.h"
#if SPICE64
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <memory>
#include <mutex>
#include "acio/icca/icca.h"
#include "games/io.h"
#include "io.h"
#include "misc/eamuse.h"
#include "rawinput/rawinput.h"
#include "util/execexe.h"
#include "util/logging.h"
#include "util/tapeled.h"
#include "util/utils.h"
namespace games::udn {
struct AIO_SCI_COMM {
uint8_t data[0xf8];
};
struct AIO_NMGR_IOB2 {
uint8_t data[0xe30];
};
struct AIO_IOB2_BI2X_UDN {
uint8_t data[0x3b00];
};
struct AIO_IOB2_BI2X_WRFIRM {
uint8_t data[0x20f48];
};
struct AIO_IOB_ICCA;
#pragma pack(push, 1)
struct AIO_IOB2_BI2X_UDN__INPUT {
uint8_t DevIoCounter;
uint8_t bExIoAErr;
uint8_t bExIoBErr;
uint8_t bPcPowerOn;
uint8_t bPcPowerCheck;
uint8_t CoinCount;
uint8_t bTest;
uint8_t bService;
uint8_t bCoinSw;
uint8_t bCoinJam;
uint8_t bStart;
uint8_t bUp;
uint8_t bDown;
uint8_t bLeft;
uint8_t bRight;
};
struct AIO_IOB2_BI2X_UDN__DEVSTATUS {
uint8_t InputCounter;
uint8_t OutputCounter;
uint8_t TapeLedCounter;
uint8_t TapeLedRate[8];
AIO_IOB2_BI2X_UDN__INPUT Input;
uint8_t InputData[241];
uint8_t OutputData[48];
uint8_t IoResetData[4];
uint8_t PlyrInHist[18];
};
// Fixed card-status wire format used by the compatibility interface.
// The payload is exactly 58 bytes.
// Field widths remain explicit to keep the boundary stable.
struct AIO_IOB_ICCA__CARDID {
uint8_t CardType;
uint8_t cbCardId;
uint8_t CardId[10];
};
struct AIO_IOB_ICCA__INPUT {
uint8_t cuStatus;
uint8_t cuError;
AIO_IOB_ICCA__CARDID CardId;
uint8_t KeyNo;
uint8_t KeyCount;
uint8_t bKeyDataValid;
uint8_t Key2No;
uint8_t Key2Count;
uint8_t bKey2DataValid;
uint8_t bKey0;
uint8_t bKey1;
uint8_t bKey4;
uint8_t bKey7;
uint8_t bKey00;
uint8_t bKey2;
uint8_t bKey5;
uint8_t bKey8;
uint8_t bKeyBK;
uint8_t bKey3;
uint8_t bKey6;
uint8_t bKey9;
uint8_t CN1_1;
uint8_t CN1_2;
uint8_t CN1_3;
uint8_t CN1_4;
};
struct AIO_IOB_ICCA__DEVSTATUS {
uint8_t InputCounter;
uint8_t OutputCounter;
AIO_IOB_ICCA__INPUT Input;
uint8_t InputData[16];
uint8_t OutputData[4];
};
#pragma pack(pop)
static_assert(sizeof(AIO_SCI_COMM) == 0xf8);
static_assert(sizeof(AIO_NMGR_IOB2) == 0xe30);
static_assert(sizeof(AIO_IOB2_BI2X_UDN) == 0x3b00);
static_assert(sizeof(AIO_IOB2_BI2X_WRFIRM) == 0x20f48);
static_assert(sizeof(AIO_IOB2_BI2X_UDN__INPUT) == 15);
static_assert(sizeof(AIO_IOB2_BI2X_UDN__DEVSTATUS) == 337);
static_assert(sizeof(AIO_IOB_ICCA__CARDID) == 12);
static_assert(sizeof(AIO_IOB_ICCA__INPUT) == 36);
static_assert(sizeof(AIO_IOB_ICCA__DEVSTATUS) == 58);
using aioIob2Bi2xUDN_Create_t = AIO_IOB2_BI2X_UDN *(__fastcall *)(AIO_NMGR_IOB2 *, uint32_t);
using aioIob2Bi2xUDN_GetDeviceStatus_t = uint32_t (__fastcall *)(
AIO_IOB2_BI2X_UDN *, AIO_IOB2_BI2X_UDN__DEVSTATUS *, uint32_t);
using aioIob2Bi2xAC1_GetDeviceStatus_t = uint32_t (__fastcall *)(void *, void *, uint32_t);
using aioIob2Bi2xUDN_IoReset_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t);
using aioIob2Bi2xUDN_SetWatchDogTimer_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint8_t);
using aioIob2Bi2xUDN_ControlCoinBlocker_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool);
using aioIob2Bi2xUDN_AddCounter_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint32_t);
using aioIob2Bi2xUDN_SetStartLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, bool);
using aioIob2Bi2xUDN_SetPlayerButtonLamp_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, bool);
using aioIob2Bi2xUDN_SetIccrLed_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t);
using aioIob2Bi2xUDN_SetTapeLedDataLimit_t = void (__fastcall *)(
AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t, uint8_t);
using aioIob2Bi2xUDN_SetTapeLedData_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *, uint32_t, uint8_t *);
using aioIob2Bi2x_OpenSciUsbCdc_t = AIO_SCI_COMM *(__fastcall *)(uint32_t);
using aioIob2Bi2x_CreateWriteFirmContext_t = AIO_IOB2_BI2X_WRFIRM *(__fastcall *)(uint32_t, uint32_t);
using aioIob2Bi2x_DestroyWriteFirmContext_t = void (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *);
using aioIob2Bi2x_WriteFirmGetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_WRFIRM *);
using aioIobIcca_Create_t = AIO_IOB_ICCA *(__fastcall *)(void *, uint32_t, uint8_t);
using aioIobIcca_GetDeviceStatus_t = uint32_t (__fastcall *)(
AIO_IOB_ICCA *, AIO_IOB_ICCA__DEVSTATUS *, uint32_t);
using aioIobIcca_BeginGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *);
using aioIobIcca_EndGetCardId_t = void (__fastcall *)(AIO_IOB_ICCA *);
using aioNMgrIob2_Create_t = AIO_NMGR_IOB2 *(__fastcall *)(AIO_SCI_COMM *, uint32_t);
using aioNMgrIob_BeginManage_t = void (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_Destroy_t = void (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_GetState_t = int32_t (__fastcall *)(AIO_NMGR_IOB2 *);
using aioNodeMgr_IsReady_t = bool (__fastcall *)(AIO_NMGR_IOB2 *, int32_t);
using aioNodeCtl_Destroy_t = void (__fastcall *)(AIO_IOB2_BI2X_UDN *);
using aioNodeCtl_GetState_t = int32_t (__fastcall *)(AIO_IOB2_BI2X_UDN *);
using aioNodeCtl_IsReady_t = bool (__fastcall *)(AIO_IOB2_BI2X_UDN *, int32_t);
static aioIob2Bi2xUDN_Create_t aioIob2Bi2xUDN_Create_orig = nullptr;
static aioIob2Bi2xUDN_GetDeviceStatus_t aioIob2Bi2xUDN_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xAC1_GetDeviceStatus_t aioIob2Bi2xAC1_GetDeviceStatus_orig = nullptr;
static aioIob2Bi2xUDN_IoReset_t aioIob2Bi2xUDN_IoReset_orig = nullptr;
static aioIob2Bi2xUDN_SetWatchDogTimer_t aioIob2Bi2xUDN_SetWatchDogTimer_orig = nullptr;
static aioIob2Bi2xUDN_ControlCoinBlocker_t aioIob2Bi2xUDN_ControlCoinBlocker_orig = nullptr;
static aioIob2Bi2xUDN_AddCounter_t aioIob2Bi2xUDN_AddCounter_orig = nullptr;
static aioIob2Bi2xUDN_SetStartLamp_t aioIob2Bi2xUDN_SetStartLamp_orig = nullptr;
static aioIob2Bi2xUDN_SetPlayerButtonLamp_t aioIob2Bi2xUDN_SetPlayerButtonLamp_orig = nullptr;
static aioIob2Bi2xUDN_SetIccrLed_t aioIob2Bi2xUDN_SetIccrLed_orig = nullptr;
static aioIob2Bi2xUDN_SetTapeLedDataLimit_t aioIob2Bi2xUDN_SetTapeLedDataLimit_orig = nullptr;
static aioIob2Bi2xUDN_SetTapeLedData_t aioIob2Bi2xUDN_SetTapeLedData_orig = nullptr;
static aioIob2Bi2x_OpenSciUsbCdc_t aioIob2Bi2x_OpenSciUsbCdc_orig = nullptr;
static aioIob2Bi2x_CreateWriteFirmContext_t aioIob2Bi2x_CreateWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_DestroyWriteFirmContext_t aioIob2Bi2x_DestroyWriteFirmContext_orig = nullptr;
static aioIob2Bi2x_WriteFirmGetState_t aioIob2Bi2x_WriteFirmGetState_orig = nullptr;
static aioIobIcca_Create_t aioIobIcca_Create_orig = nullptr;
static aioIobIcca_GetDeviceStatus_t aioIobIcca_GetDeviceStatus_orig = nullptr;
static aioIobIcca_BeginGetCardId_t aioIobIcca_BeginGetCardId_orig = nullptr;
static aioIobIcca_EndGetCardId_t aioIobIcca_EndGetCardId_orig = nullptr;
static aioNMgrIob2_Create_t aioNMgrIob2_Create_orig = nullptr;
static aioNMgrIob_BeginManage_t aioNMgrIob_BeginManage_orig = nullptr;
static aioNodeMgr_Destroy_t aioNodeMgr_Destroy_orig = nullptr;
static aioNodeMgr_GetState_t aioNodeMgr_GetState_orig = nullptr;
static aioNodeMgr_IsReady_t aioNodeMgr_IsReady_orig = nullptr;
static aioNodeCtl_Destroy_t aioNodeCtl_Destroy_orig = nullptr;
static aioNodeCtl_GetState_t aioNodeCtl_GetState_orig = nullptr;
static aioNodeCtl_IsReady_t aioNodeCtl_IsReady_orig = nullptr;
static std::unique_ptr<AIO_SCI_COMM> aio_sci_comm;
static std::unique_ptr<AIO_NMGR_IOB2> aio_node_manager;
static AIO_NMGR_IOB2 *aio_icca_node_manager = nullptr;
static std::unique_ptr<AIO_IOB2_BI2X_UDN> aio_udn;
static std::unique_ptr<AIO_IOB2_BI2X_WRFIRM> aio_write_firm;
static uint8_t input_counter = 0;
// Native ICCA status/begin/end operations also lock the reader state.
// The managed wrapper can destroy its node from a finalizer thread.
static std::mutex icca_mutex;
static AIO_IOB_ICCA *aio_icca = nullptr;
static bool icca_reading = false;
static bool icca_card_ready = false;
static uint8_t icca_card_uid[8] {};
static uint8_t icca_input_counter = 0;
static void write_light(Lights::udn_lights_t light, float value) {
auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights.at(light), value);
}
static AIO_IOB2_BI2X_UDN *__fastcall aioIob2Bi2xUDN_Create(
AIO_NMGR_IOB2 *node_manager, uint32_t device_id) {
if (node_manager != aio_node_manager.get()) {
return aioIob2Bi2xUDN_Create_orig(node_manager, device_id);
}
if (!aio_udn) {
aio_udn = std::make_unique<AIO_IOB2_BI2X_UDN>();
log_info("udn_bi2x", "UDN node created");
}
return aio_udn.get();
}
static uint32_t __fastcall aioIob2Bi2xAC1_GetDeviceStatus(
void *node, void *status, uint32_t status_size) {
// aioIob2Bi2xUDN.GetDeviceStatus() first invokes its managed AC1 base method.
if (node == aio_udn.get()) {
if (status && status_size > 0) {
std::memset(status, 0, status_size);
}
return status_size;
}
return aioIob2Bi2xAC1_GetDeviceStatus_orig(node, status, status_size);
}
static uint32_t __fastcall aioIob2Bi2xUDN_GetDeviceStatus(
AIO_IOB2_BI2X_UDN *node, AIO_IOB2_BI2X_UDN__DEVSTATUS *status,
uint32_t status_size) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_GetDeviceStatus_orig(node, status, status_size);
}
RI_MGR->devices_flush_output();
AIO_IOB2_BI2X_UDN__DEVSTATUS current {};
current.InputCounter = input_counter;
current.Input.DevIoCounter = input_counter++;
auto &buttons = get_buttons();
current.Input.bService = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Service]) ? 1 : 0;
current.Input.bTest = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Test]) ? 1 : 0;
current.Input.bCoinSw = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::CoinMech]) ? 1 : 0;
current.Input.bStart = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Start]) ? 1 : 0;
current.Input.bUp = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Up]) ? 1 : 0;
current.Input.bDown = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Down]) ? 1 : 0;
current.Input.bLeft = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Left]) ? 1 : 0;
current.Input.bRight = GameAPI::Buttons::getState(RI_MGR, buttons[Buttons::Right]) ? 1 : 0;
// The hardware counter wraps at 256; clamping it would stop accepting
// coins once the shared stock reaches 255.
current.Input.CoinCount = static_cast<uint8_t>(eamuse_coin_get_stock());
const auto copy_size = std::min<uint32_t>(status_size, sizeof(current));
if (status && copy_size > 0) {
std::memcpy(status, &current, copy_size);
}
return copy_size;
}
static void __fastcall aioIob2Bi2xUDN_IoReset(AIO_IOB2_BI2X_UDN *node, uint32_t reset_bits) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_IoReset_orig(node, reset_bits);
}
}
static void __fastcall aioIob2Bi2xUDN_SetWatchDogTimer(AIO_IOB2_BI2X_UDN *node, uint8_t count) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetWatchDogTimer_orig(node, count);
}
}
static void __fastcall aioIob2Bi2xUDN_ControlCoinBlocker(
AIO_IOB2_BI2X_UDN *node, uint32_t slot, bool open) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_ControlCoinBlocker_orig(node, slot, open);
}
eamuse_coin_set_block(!open);
}
static void __fastcall aioIob2Bi2xUDN_AddCounter(
AIO_IOB2_BI2X_UDN *node, uint32_t counter, uint32_t count) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_AddCounter_orig(node, counter, count);
}
}
static void __fastcall aioIob2Bi2xUDN_SetStartLamp(AIO_IOB2_BI2X_UDN *node, bool on) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetStartLamp_orig(node, on);
}
write_light(Lights::Start, on ? 1.f : 0.f);
}
static void __fastcall aioIob2Bi2xUDN_SetPlayerButtonLamp(
AIO_IOB2_BI2X_UDN *node, uint32_t button, bool on) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetPlayerButtonLamp_orig(node, button, on);
}
static const Lights::udn_lights_t mapping[] {
Lights::Start,
Lights::Up,
Lights::Down,
Lights::Left,
Lights::Right,
};
if (button < std::size(mapping)) {
write_light(mapping[button], on ? 1.f : 0.f);
}
}
static void __fastcall aioIob2Bi2xUDN_SetIccrLed(AIO_IOB2_BI2X_UDN *node, uint32_t rgb) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetIccrLed_orig(node, rgb);
}
write_light(Lights::CardReaderR, ((rgb >> 16) & 0xff) / 255.f);
write_light(Lights::CardReaderG, ((rgb >> 8) & 0xff) / 255.f);
write_light(Lights::CardReaderB, (rgb & 0xff) / 255.f);
}
static void __fastcall aioIob2Bi2xUDN_SetTapeLedDataLimit(
AIO_IOB2_BI2X_UDN *node, uint32_t channel, uint8_t scale, uint8_t limit) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetTapeLedDataLimit_orig(node, channel, scale, limit);
}
}
static void __fastcall aioIob2Bi2xUDN_SetTapeLedData(
AIO_IOB2_BI2X_UDN *node, uint32_t tape, uint8_t *data) {
if (node != aio_udn.get()) {
return aioIob2Bi2xUDN_SetTapeLedData_orig(node, tape, data);
}
struct TapeMapping {
size_t color_count;
Lights::udn_lights_t r;
Lights::udn_lights_t g;
Lights::udn_lights_t b;
};
static const TapeMapping mapping[] {
{62, Lights::TitleR, Lights::TitleG, Lights::TitleB},
{152, Lights::PillarRightTopR, Lights::PillarRightTopG, Lights::PillarRightTopB},
{165, Lights::PillarRightBottomR, Lights::PillarRightBottomG, Lights::PillarRightBottomB},
{9, Lights::StageLeftR, Lights::StageLeftG, Lights::StageLeftB},
{17, Lights::CabinetLeftR, Lights::CabinetLeftG, Lights::CabinetLeftB},
{152, Lights::PillarLeftTopR, Lights::PillarLeftTopG, Lights::PillarLeftTopB},
{165, Lights::PillarLeftBottomR, Lights::PillarLeftBottomG, Lights::PillarLeftBottomB},
{9, Lights::StageRightR, Lights::StageRightG, Lights::StageRightB},
{17, Lights::CabinetRightR, Lights::CabinetRightG, Lights::CabinetRightB},
};
if (data && tapeledutils::is_enabled() && tape < std::size(mapping)) {
const auto &entry = mapping[tape];
const auto color = tapeledutils::pick_color_from_led_tape(data, entry.color_count);
write_light(entry.r, color.r);
write_light(entry.g, color.g);
write_light(entry.b, color.b);
}
}
static AIO_SCI_COMM *__fastcall aioIob2Bi2x_OpenSciUsbCdc(uint32_t serial_number) {
if (!aio_sci_comm) {
aio_sci_comm = std::make_unique<AIO_SCI_COMM>();
}
return aio_sci_comm.get();
}
static AIO_IOB2_BI2X_WRFIRM *__fastcall aioIob2Bi2x_CreateWriteFirmContext(
uint32_t serial_number, uint32_t iob_mask) {
if (!aio_write_firm) {
aio_write_firm = std::make_unique<AIO_IOB2_BI2X_WRFIRM>();
}
return aio_write_firm.get();
}
static void __fastcall aioIob2Bi2x_DestroyWriteFirmContext(AIO_IOB2_BI2X_WRFIRM *context) {
if (context != aio_write_firm.get()) {
return aioIob2Bi2x_DestroyWriteFirmContext_orig(context);
}
aio_write_firm.reset();
}
static int32_t __fastcall aioIob2Bi2x_WriteFirmGetState(AIO_IOB2_BI2X_WRFIRM *context) {
if (context == aio_write_firm.get()) {
// The native state machine treats both 7 (NODEV) and 8
// (COMPLETED) as a successful terminal state. UDN additionally
// interprets state 8 as "firmware was written this boot" and
// deliberately powers the cabinet down after loading so the real
// I/O board can reboot. There is no physical BI2X board behind
// this emulation, so report NODEV: the check is complete without
// scheduling that otherwise endless firmware-update reboot.
return 7;
}
return aioIob2Bi2x_WriteFirmGetState_orig(context);
}
static AIO_IOB_ICCA *__fastcall aioIobIcca_Create(
void *node_manager, uint32_t device_id, uint8_t init_mode) {
auto *node = aioIobIcca_Create_orig(node_manager, device_id, init_mode);
if (node) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
aio_icca = node;
aio_icca_node_manager =
reinterpret_cast<AIO_NMGR_IOB2 *>(node_manager);
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
log_info("udn_bi2x", "ICCA node attached to Spice CardIO bridge");
}
return node;
}
static uint32_t __fastcall aioIobIcca_GetDeviceStatus(
AIO_IOB_ICCA *node, AIO_IOB_ICCA__DEVSTATUS *status, uint32_t status_size) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_GetDeviceStatus_orig(node, status, status_size);
}
AIO_IOB_ICCA__DEVSTATUS current {};
current.InputCounter = icca_input_counter++;
current.OutputCounter = current.InputCounter;
// DNI while idle, BUSY while waiting for a card, READY once a UID is
// available. The card-reader state machine accepts a card only on READY.
current.Input.cuStatus = icca_reading ? 1 : 0;
if (icca_reading && !icca_card_ready && eamuse_card_insert_consume(1, 0)) {
uint8_t uid[8] {};
if (eamuse_get_card(1, 0, uid)) {
std::memcpy(icca_card_uid, uid, sizeof(icca_card_uid));
icca_card_ready = true;
log_info(
"udn_bi2x",
"ICCA accepted UID {} from the Spice card queue; reporting READY to Unity",
bin2hex(icca_card_uid, sizeof(icca_card_uid)));
} else {
log_warning(
"udn_bi2x",
"ICCA consumed a card-insert event but Spice returned no UID");
}
}
if (icca_card_ready) {
current.Input.cuStatus = 2;
current.Input.CardId.CardType = is_card_uid_felica(icca_card_uid) ? 2 : 1;
current.Input.CardId.cbCardId = sizeof(icca_card_uid);
std::memcpy(current.Input.CardId.CardId, icca_card_uid, sizeof(icca_card_uid));
}
// The ICCA keypad scan-code order is identical to eamuse.h. Keeping
// these bits populated also preserves the usual Spice numpad PIN flow.
const auto keypad = eamuse_get_keypad_state(0);
auto *key_states = &current.Input.bKey0;
for (size_t key = 0; key < EAM_IO_KEYPAD_COUNT; ++key) {
key_states[key] = (keypad & (1u << key)) ? 1 : 0;
}
const auto copy_size = std::min<uint32_t>(status_size, sizeof(current));
if (status && copy_size > 0) {
std::memcpy(status, &current, copy_size);
}
return copy_size;
}
static void __fastcall aioIobIcca_BeginGetCardId(AIO_IOB_ICCA *node) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_BeginGetCardId_orig(node);
}
if (!icca_reading) {
log_info("udn_bi2x", "ICCA BeginGetCardId: waiting for the Spice/CardIO queue");
}
icca_reading = true;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
static void __fastcall aioIobIcca_EndGetCardId(AIO_IOB_ICCA *node) {
std::unique_lock<std::mutex> lock(icca_mutex);
if (node != aio_icca) {
lock.unlock();
return aioIobIcca_EndGetCardId_orig(node);
}
if (icca_reading || icca_card_ready) {
log_info("udn_bi2x", "ICCA EndGetCardId: returning to idle");
}
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
static AIO_NMGR_IOB2 *__fastcall aioNMgrIob2_Create(AIO_SCI_COMM *sci, uint32_t mode) {
if (sci != aio_sci_comm.get()) {
return aioNMgrIob2_Create_orig(sci, mode);
}
if (!aio_node_manager) {
aio_node_manager = std::make_unique<AIO_NMGR_IOB2>();
}
return aio_node_manager.get();
}
static void __fastcall aioNMgrIob_BeginManage(AIO_NMGR_IOB2 *node_manager) {
if (node_manager != aio_node_manager.get()) {
return aioNMgrIob_BeginManage_orig(node_manager);
}
}
static void __fastcall aioNodeMgr_Destroy(AIO_NMGR_IOB2 *node_manager) {
if (node_manager != aio_node_manager.get()) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
if (node_manager == aio_icca_node_manager) {
aio_icca_node_manager = nullptr;
}
}
return aioNodeMgr_Destroy_orig(node_manager);
}
aio_node_manager.reset();
}
static int32_t __fastcall aioNodeMgr_GetState(AIO_NMGR_IOB2 *node_manager) {
bool virtual_manager = node_manager == aio_node_manager.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_manager |= node_manager == aio_icca_node_manager;
}
if (virtual_manager) {
return 0;
}
return aioNodeMgr_GetState_orig(node_manager);
}
static bool __fastcall aioNodeMgr_IsReady(AIO_NMGR_IOB2 *node_manager, int32_t state) {
bool virtual_manager = node_manager == aio_node_manager.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_manager |= node_manager == aio_icca_node_manager;
}
if (virtual_manager) {
return true;
}
return aioNodeMgr_IsReady_orig(node_manager, state);
}
static void __fastcall aioNodeCtl_Destroy(AIO_IOB2_BI2X_UDN *node) {
if (node != aio_udn.get()) {
{
std::lock_guard<std::mutex> lock(icca_mutex);
if (reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca) {
aio_icca = nullptr;
aio_icca_node_manager = nullptr;
icca_reading = false;
icca_card_ready = false;
std::memset(icca_card_uid, 0, sizeof(icca_card_uid));
}
}
return aioNodeCtl_Destroy_orig(node);
}
aio_udn.reset();
}
static int32_t __fastcall aioNodeCtl_GetState(AIO_IOB2_BI2X_UDN *node) {
bool virtual_node = node == aio_udn.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_node |= reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca;
}
if (virtual_node) {
return 0;
}
return aioNodeCtl_GetState_orig(node);
}
static bool __fastcall aioNodeCtl_IsReady(AIO_IOB2_BI2X_UDN *node, int32_t state) {
bool virtual_node = node == aio_udn.get();
{
std::lock_guard<std::mutex> lock(icca_mutex);
virtual_node |= reinterpret_cast<AIO_IOB_ICCA *>(node) == aio_icca;
}
if (virtual_node) {
return true;
}
return aioNodeCtl_IsReady_orig(node, state);
}
void bi2x_hook_init() {
log_info("udn_bi2x", "init (BI2X + ICCA/CardIO bridge)");
// The runtime imports the alias DLL names, not their lib-prefixed originals.
const auto aio_iob2_video = "aio-iob2_video.dll";
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_Create",
aioIob2Bi2xUDN_Create, &aioIob2Bi2xUDN_Create_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_GetDeviceStatus",
aioIob2Bi2xUDN_GetDeviceStatus, &aioIob2Bi2xUDN_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xAC1_GetDeviceStatus",
aioIob2Bi2xAC1_GetDeviceStatus, &aioIob2Bi2xAC1_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_IoReset",
aioIob2Bi2xUDN_IoReset, &aioIob2Bi2xUDN_IoReset_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetWatchDogTimer",
aioIob2Bi2xUDN_SetWatchDogTimer, &aioIob2Bi2xUDN_SetWatchDogTimer_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_ControlCoinBlocker",
aioIob2Bi2xUDN_ControlCoinBlocker, &aioIob2Bi2xUDN_ControlCoinBlocker_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_AddCounter",
aioIob2Bi2xUDN_AddCounter, &aioIob2Bi2xUDN_AddCounter_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetStartLamp",
aioIob2Bi2xUDN_SetStartLamp, &aioIob2Bi2xUDN_SetStartLamp_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetPlayerButtonLamp",
aioIob2Bi2xUDN_SetPlayerButtonLamp, &aioIob2Bi2xUDN_SetPlayerButtonLamp_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetIccrLed",
aioIob2Bi2xUDN_SetIccrLed, &aioIob2Bi2xUDN_SetIccrLed_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedDataLimit",
aioIob2Bi2xUDN_SetTapeLedDataLimit, &aioIob2Bi2xUDN_SetTapeLedDataLimit_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2xUDN_SetTapeLedData",
aioIob2Bi2xUDN_SetTapeLedData, &aioIob2Bi2xUDN_SetTapeLedData_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_OpenSciUsbCdc",
aioIob2Bi2x_OpenSciUsbCdc, &aioIob2Bi2x_OpenSciUsbCdc_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_CreateWriteFirmContext",
aioIob2Bi2x_CreateWriteFirmContext, &aioIob2Bi2x_CreateWriteFirmContext_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_DestroyWriteFirmContext",
aioIob2Bi2x_DestroyWriteFirmContext, &aioIob2Bi2x_DestroyWriteFirmContext_orig);
execexe::trampoline_try(aio_iob2_video, "aioIob2Bi2x_WriteFirmGetState",
aioIob2Bi2x_WriteFirmGetState, &aioIob2Bi2x_WriteFirmGetState_orig);
const auto aio_iob_video = "aio-iob_video.dll";
execexe::trampoline_try(aio_iob_video, "aioIobIcca_Create",
aioIobIcca_Create, &aioIobIcca_Create_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_GetDeviceStatus",
aioIobIcca_GetDeviceStatus, &aioIobIcca_GetDeviceStatus_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_BeginGetCardId",
aioIobIcca_BeginGetCardId, &aioIobIcca_BeginGetCardId_orig);
execexe::trampoline_try(aio_iob_video, "aioIobIcca_EndGetCardId",
aioIobIcca_EndGetCardId, &aioIobIcca_EndGetCardId_orig);
const auto aio_iob = "aio-iob.dll";
execexe::trampoline_try(aio_iob, "aioNMgrIob2_Create",
aioNMgrIob2_Create, &aioNMgrIob2_Create_orig);
execexe::trampoline_try(aio_iob, "aioNMgrIob_BeginManage",
aioNMgrIob_BeginManage, &aioNMgrIob_BeginManage_orig);
const auto aio = "aio.dll";
execexe::trampoline_try(aio, "aioNodeMgr_Destroy",
aioNodeMgr_Destroy, &aioNodeMgr_Destroy_orig);
execexe::trampoline_try(aio, "aioNodeMgr_GetState",
aioNodeMgr_GetState, &aioNodeMgr_GetState_orig);
execexe::trampoline_try(aio, "aioNodeMgr_IsReady",
aioNodeMgr_IsReady, &aioNodeMgr_IsReady_orig);
execexe::trampoline_try(aio, "aioNodeCtl_Destroy",
aioNodeCtl_Destroy, &aioNodeCtl_Destroy_orig);
execexe::trampoline_try(aio, "aioNodeCtl_GetState",
aioNodeCtl_GetState, &aioNodeCtl_GetState_orig);
execexe::trampoline_try(aio, "aioNodeCtl_IsReady",
aioNodeCtl_IsReady, &aioNodeCtl_IsReady_orig);
}
}
#endif
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::udn {
void bi2x_hook_init();
}
+91
View File
@@ -0,0 +1,91 @@
#include "io.h"
#include <windows.h>
std::vector<Button> &games::udn::get_buttons() {
static std::vector<Button> buttons;
if (buttons.empty()) {
buttons = GameAPI::Buttons::getButtons("DANCE aROUND");
const std::vector<std::string> names {
"Service",
"Test",
"Coin Mech",
"Start",
"Up",
"Down",
"Left",
"Right",
};
const std::vector<unsigned short> vkey_defaults {
0xFF,
0xFF,
0xFF,
VK_RETURN,
VK_UP,
VK_DOWN,
VK_LEFT,
VK_RIGHT,
};
buttons = GameAPI::Buttons::sortButtons(buttons, names, &vkey_defaults);
}
return buttons;
}
std::string games::udn::get_buttons_help() {
return
" Arrow keys: menu directions\n"
" Enter: start/confirm\n"
" MIDI input is disabled for this game.\n";
}
std::vector<Light> &games::udn::get_lights() {
static std::vector<Light> lights;
if (lights.empty()) {
lights = GameAPI::Lights::getLights("DANCE aROUND");
GameAPI::Lights::sortLights(
&lights,
"Card Reader R",
"Card Reader G",
"Card Reader B",
"Start",
"Up",
"Down",
"Left",
"Right",
"Title R",
"Title G",
"Title B",
"Pillar Right Top R",
"Pillar Right Top G",
"Pillar Right Top B",
"Pillar Right Bottom R",
"Pillar Right Bottom G",
"Pillar Right Bottom B",
"Stage Left R",
"Stage Left G",
"Stage Left B",
"Cabinet Left R",
"Cabinet Left G",
"Cabinet Left B",
"Pillar Left Top R",
"Pillar Left Top G",
"Pillar Left Top B",
"Pillar Left Bottom R",
"Pillar Left Bottom G",
"Pillar Left Bottom B",
"Stage Right R",
"Stage Right G",
"Stage Right B",
"Cabinet Right R",
"Cabinet Right G",
"Cabinet Right B"
);
}
return lights;
}
+64
View File
@@ -0,0 +1,64 @@
#pragma once
#include <vector>
#include "cfg/api.h"
namespace games::udn {
namespace Buttons {
enum {
Service,
Test,
CoinMech,
Start,
Up,
Down,
Left,
Right,
};
}
namespace Lights {
enum udn_lights_t {
CardReaderR,
CardReaderG,
CardReaderB,
Start,
Up,
Down,
Left,
Right,
TitleR,
TitleG,
TitleB,
PillarRightTopR,
PillarRightTopG,
PillarRightTopB,
PillarRightBottomR,
PillarRightBottomG,
PillarRightBottomB,
StageLeftR,
StageLeftG,
StageLeftB,
CabinetLeftR,
CabinetLeftG,
CabinetLeftB,
PillarLeftTopR,
PillarLeftTopG,
PillarLeftTopB,
PillarLeftBottomR,
PillarLeftBottomG,
PillarLeftBottomB,
StageRightR,
StageRightG,
StageRightB,
CabinetRightR,
CabinetRightG,
CabinetRightB,
};
}
std::vector<Button> &get_buttons();
std::string get_buttons_help();
std::vector<Light> &get_lights();
}
+168
View File
@@ -0,0 +1,168 @@
// QueryDisplayConfig
#define _WIN32_WINNT 0x0601
#include "udn.h"
#include <format>
#include "acioemu/handle.h"
#include "bi2x_hook.h"
#include "cfg/screen_resize.h"
#include "hooks/devicehook.h"
#include "hooks/graphics/graphics.h"
#include "rawinput/rawinput.h"
#include "util/detour.h"
#include "util/execexe.h"
#include "util/logging.h"
#include "util/unity_player.h"
#include "util/utils.h"
namespace games::udn {
std::string UDN_INJECT_ARGS = "";
bool UDN_NO_IO = false;
static acioemu::ACIOHandle *acio_handle = nullptr;
static const std::wstring port_name = L"COM1";
static decltype(RegisterRawInputDevices) *RegisterRawInputDevices_orig = nullptr;
static decltype(EnumDisplaySettingsW) *EnumDisplaySettingsW_orig = nullptr;
static decltype(QueryDisplayConfig) *QueryDisplayConfig_orig = nullptr;
// Unity reapplies its cabinet window settings after startup. Use the
// existing graphics message callback so changes stay on the window thread.
static LRESULT CALLBACK window_proc(HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
if (!GRAPHICS_WINDOWED || GRAPHICS_HOOKED_WINDOW != window) {
return 0;
}
static bool interactive_resize = false;
switch (message) {
case WM_ENTERSIZEMOVE:
interactive_resize = true;
break;
case WM_EXITSIZEMOVE:
interactive_resize = false;
break;
case WM_STYLECHANGING:
if (w_param == static_cast<WPARAM>(GWL_STYLE)) {
auto styles = reinterpret_cast<STYLESTRUCT *>(l_param);
styles->styleNew &= ~WS_POPUP;
if (cfg::SCREENRESIZE->window_decoration == cfg::WindowDecorationMode::Borderless) {
styles->styleNew &= ~WS_OVERLAPPEDWINDOW;
} else {
styles->styleNew |= WS_OVERLAPPEDWINDOW;
}
}
break;
case WM_WINDOWPOSCHANGING:
if (!interactive_resize && cfg::SCREENRESIZE->enable_window_resize) {
auto position = reinterpret_cast<WINDOWPOS *>(l_param);
// The shared WM_MOVE/WM_SIZE handlers keep these values in
// sync with interactive changes and the overlay settings.
if (!(position->flags & SWP_NOMOVE)) {
position->x = cfg::SCREENRESIZE->window_offset_x;
position->y = cfg::SCREENRESIZE->window_offset_y;
}
if (!(position->flags & SWP_NOSIZE)) {
position->cx = cfg::SCREENRESIZE->client_width +
cfg::SCREENRESIZE->window_deco_width;
position->cy = cfg::SCREENRESIZE->client_height +
cfg::SCREENRESIZE->window_deco_height;
}
}
break;
}
return 0;
}
static BOOL WINAPI RegisterRawInputDevices_hook(
PCRAWINPUTDEVICE devices, UINT device_count, UINT structure_size) {
if (devices && device_count > 0 && devices[0].hwndTarget == RI_MGR->input_hwnd) {
return RegisterRawInputDevices_orig(devices, device_count, structure_size);
}
SetLastError(0xDEADBEEF);
return FALSE;
}
static BOOL WINAPI EnumDisplaySettingsW_hook(
LPCWSTR device_name, DWORD mode_number, DEVMODEW *mode) {
const auto result = EnumDisplaySettingsW_orig(device_name, mode_number, mode);
if (result && mode_number == ENUM_CURRENT_SETTINGS) {
mode->dmPelsWidth = 1920;
mode->dmPelsHeight = 1080;
}
return result;
}
static LONG WINAPI QueryDisplayConfig_hook(
UINT32, UINT32 *, DISPLAYCONFIG_PATH_INFO *, UINT32 *, DISPLAYCONFIG_MODE_INFO *,
DISPLAYCONFIG_TOPOLOGY_ID *) {
return ERROR_NOT_SUPPORTED;
}
void UDNGame::pre_attach() {
// The title has no MIDI controls. The legacy WinMM provider can fault in
// MMDevAPI while Unity and its audio backend initialize concurrently, so
// disable MIDI enumeration for this game.
rawinput::DISABLE_MIDI = true;
log_info("udn", "MIDI input disabled to avoid WinMM initialization errors");
}
void UDNGame::attach() {
Game::attach();
acio_handle = new acioemu::ACIOHandle(port_name.c_str(), 1);
devicehook_init_trampoline();
devicehook_add(acio_handle);
execexe::init();
execexe::init_deferred([]() {
execexe::init_port_hook(port_name, acio_handle);
// Runtime import names omit the "lib" prefix.
execexe::load_library("aio.dll");
execexe::load_library("aio-iob.dll");
execexe::load_library("aio-iob_video.dll");
execexe::load_library("aio-iob2_video.dll");
execexe::load_library("win10actlog.dll", false);
#if SPICE64
if (!UDN_NO_IO) {
bi2x_hook_init();
}
#endif
});
const auto user32_dll = "user32.dll";
detour::trampoline_try(user32_dll, "RegisterRawInputDevices",
RegisterRawInputDevices_hook, &RegisterRawInputDevices_orig);
if (GRAPHICS_WINDOWED) {
// Keep Unity's cabinet-sized UI in windowed mode. Fullscreen uses
// the real display topology and resolution.
detour::trampoline_try(user32_dll, "QueryDisplayConfig",
QueryDisplayConfig_hook, &QueryDisplayConfig_orig);
detour::trampoline_try(user32_dll, "EnumDisplaySettingsW",
EnumDisplaySettingsW_hook, &EnumDisplaySettingsW_orig);
graphics_add_wnd_proc(window_proc);
}
if (GRAPHICS_SHOW_CURSOR) {
unity_utils::force_show_cursor(true);
}
unity_utils::set_args(
std::format("{} {}{}",
GetCommandLineA(),
UDN_INJECT_ARGS,
unity_utils::get_unity_player_args()));
}
void UDNGame::detach() {
Game::detach();
graphics_remove_wnd_proc(window_proc);
devicehook_dispose();
}
}
+17
View File
@@ -0,0 +1,17 @@
#pragma once
#include "games/game.h"
namespace games::udn {
extern std::string UDN_INJECT_ARGS;
extern bool UDN_NO_IO;
class UDNGame : public games::Game {
public:
UDNGame() : Game("DANCE aROUND") {}
virtual void pre_attach() override;
virtual void attach() override;
virtual void detach() override;
};
}
@@ -29,6 +29,10 @@ DEFINE_GUID(IID_IAudioClient,
0x1cb9ad4c, 0xdbfa, 0x4c32,
0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2);
DEFINE_GUID(IID_IAudioClient3,
0x7ed4ee07, 0x8e67, 0x4cd4,
0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42);
DEFINE_GUID(IID_IAudioClock,
0xcd63314f, 0x3fba, 0x4a1b,
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
@@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {}
#include <dxgi.h>
#include <dxgi1_2.h>
#include "avs/game.h"
#include "d3d11_internal.h"
#include "util/nt_loader.h"
@@ -237,10 +238,11 @@ bool d3dcompiler_available() {
} // namespace
void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
// their startup path completely untouched (no exports patched, no poll
// thread, no LDR callback).
if (!GetModuleHandleW(L"execexe.dll")) {
// dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska
// engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their
// startup path completely untouched (no exports patched, no poll thread, no
// LDR callback).
if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) {
return;
}
@@ -26,6 +26,7 @@ namespace d3d11_hooks {
// ignore_hwnd.
void note_main_hwnd(HWND hwnd);
HWND main_hwnd();
bool is_main_game_swapchain(IDXGISwapChain *swapchain);
void ignore_hwnd(HWND hwnd);
// capture backbuffer to PNG if a screenshot was requested.
@@ -18,6 +18,7 @@
#include <dxgi1_2.h>
#include "d3d11_internal.h"
#include "d3d11_swapchain_udn.h"
#include "external/imgui/imgui.h"
#include "external/imgui/backends/imgui_impl_dx11.h"
@@ -115,34 +116,6 @@ bool looks_like_game_window(HWND hwnd) {
&& client.bottom > client.top;
}
// only the main game window; ignore sub-screens / IME helpers.
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
// checks are ordered cheapest first, since this runs on every present
void try_create_overlay(IDXGISwapChain *swapchain) {
if (!swapchain) {
@@ -160,7 +133,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
}
// ignore sub windows
if (!is_main_game_swapchain(swapchain)) {
if (!d3d11_hooks::is_main_game_swapchain(swapchain)) {
return;
}
@@ -198,7 +171,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
void pump_frame(IDXGISwapChain *swapchain) {
const bool has_overlay =
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
if (!has_overlay && !is_main_game_swapchain(swapchain)) {
if (!has_overlay && !d3d11_hooks::is_main_game_swapchain(swapchain)) {
return;
}
@@ -240,8 +213,10 @@ HRESULT STDMETHODCALLTYPE Present_hook(
{
// a test present doesn't display anything; don't pick a window or take a screenshot off it
if (!(Flags & DXGI_PRESENT_TEST)) {
d3d11_hooks::udn::schedule_window_attach(swapchain);
try_create_overlay(swapchain);
pump_frame(swapchain);
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
}
return Present_orig(swapchain, SyncInterval, Flags);
}
@@ -251,8 +226,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
const DXGI_PRESENT_PARAMETERS *pParams)
{
if (!(Flags & DXGI_PRESENT_TEST)) {
d3d11_hooks::udn::schedule_window_attach(swapchain);
try_create_overlay(swapchain);
pump_frame(swapchain);
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
}
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
}
@@ -287,6 +264,33 @@ namespace {
std::atomic<HWND> g_ignored_hwnd { nullptr };
}
bool is_main_game_swapchain(IDXGISwapChain *swapchain) {
DXGI_SWAP_CHAIN_DESC desc {};
if (!swapchain || FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return false;
}
HWND main = d3d11_hooks::main_hwnd();
if (!main) {
// no creation hook recorded a window, so fall back to the presenting one;
// the choice is permanent, so require a plausible game window
if (!looks_like_game_window(desc.OutputWindow)) {
return false;
}
log_misc(
"graphics::d3d11",
"try to notemain hwnd from swapchain present: 0x{:x}",
(uintptr_t)desc.OutputWindow);
d3d11_hooks::note_main_hwnd(desc.OutputWindow);
// it may have been ignored, or another thread may have won the slot
main = d3d11_hooks::main_hwnd();
}
return desc.OutputWindow == main;
}
void note_main_hwnd(HWND hwnd) {
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
return;
@@ -326,6 +330,8 @@ void install_swapchain_hooks(IDXGISwapChain *swapchain) {
}
}
udn::install_swapchain_hooks(swapchain);
if (!g_swapchain1_hooked) {
IDXGISwapChain1 *sc1 = nullptr;
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
@@ -0,0 +1,223 @@
// DANCE aROUND window attachment, frame pacing and display-mode overrides.
#include "d3d11_backend.h"
#ifdef SPICE_D3D11
#include <chrono>
#include <cstdint>
#include <mutex>
#include <windows.h>
#include <d3d11.h>
#include <dxgi.h>
#include "d3d11_internal.h"
#include "d3d11_swapchain_udn.h"
#include "avs/game.h"
#include "hooks/graphics/graphics.h"
#include "util/precise_timer.h"
namespace d3d11_hooks::udn {
namespace {
using SetFullscreenState_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, BOOL, IDXGIOutput *);
using ResizeTarget_t = HRESULT(STDMETHODCALLTYPE *)(
IDXGISwapChain *, const DXGI_MODE_DESC *);
SetFullscreenState_t SetFullscreenState_orig = nullptr;
ResizeTarget_t ResizeTarget_orig = nullptr;
bool g_set_fullscreen_hooked = false;
bool g_resize_target_hooked = false;
// Present and Present1 share one waitable timer and frame deadline.
std::mutex g_forced_present_pacing_mutex;
std::chrono::steady_clock::time_point g_next_forced_present {};
// Unity creates and owns its top-level window on a different thread from the
// D3D11 Present thread. Subclassing or resizing that HWND from Present can
// synchronously wait for the owner thread and deadlock Unity. A one-time
// message-queue hook performs graphics_hook_window on the HWND's owner thread.
std::mutex g_window_attach_mutex;
HHOOK g_window_attach_hook = nullptr;
HWND g_window_attach_target = nullptr;
bool g_window_attach_complete = false;
UINT window_attach_message() {
static const UINT message = RegisterWindowMessageW(
L"SpiceTools.D3D11.AttachGameWindow");
return message;
}
void pace_forced_present(UINT frame_rate) {
static timeutils::PreciseSleepTimer timer;
const auto period = std::chrono::nanoseconds(
1'000'000'000ULL / static_cast<uint64_t>(frame_rate));
std::lock_guard<std::mutex> lock(g_forced_present_pacing_mutex);
const auto now = std::chrono::steady_clock::now();
if (g_next_forced_present.time_since_epoch().count() == 0 ||
now > g_next_forced_present + period) {
g_next_forced_present = now + period;
return;
}
if (now < g_next_forced_present) {
timer.sleep(g_next_forced_present - now);
}
g_next_forced_present += period;
}
LRESULT CALLBACK window_attach_getmessage(
int code, WPARAM w_param, LPARAM l_param) {
HWND target = nullptr;
HHOOK hook = nullptr;
if (code >= 0 && w_param == PM_REMOVE) {
const auto message = reinterpret_cast<const MSG *>(l_param);
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
if (!g_window_attach_complete && message->hwnd == g_window_attach_target &&
message->message == window_attach_message()) {
target = g_window_attach_target;
hook = g_window_attach_hook;
g_window_attach_hook = nullptr;
g_window_attach_complete = true;
}
}
const LRESULT result = CallNextHookEx(nullptr, code, w_param, l_param);
if (target) {
UnhookWindowsHookEx(hook);
graphics_hook_window(target, nullptr);
log_info(
"graphics::d3d11",
"attached Spice window hooks on owner thread for hwnd=0x{:x}",
(uintptr_t) target);
}
return result;
}
HRESULT STDMETHODCALLTYPE SetFullscreenState_hook(
IDXGISwapChain *swapchain, BOOL Fullscreen, IDXGIOutput *Target) {
if (Fullscreen && GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
is_main_game_swapchain(swapchain)) {
log_info(
"graphics::d3d11",
"blocked UDN request to enter DXGI exclusive fullscreen");
return SetFullscreenState_orig(swapchain, FALSE, nullptr);
}
return SetFullscreenState_orig(swapchain, Fullscreen, Target);
}
HRESULT STDMETHODCALLTYPE ResizeTarget_hook(
IDXGISwapChain *swapchain, const DXGI_MODE_DESC *NewTargetParameters) {
if (GRAPHICS_WINDOWED && avs::game::is_model("UDN") &&
is_main_game_swapchain(swapchain)) {
if (NewTargetParameters) {
log_info(
"graphics::d3d11",
"blocked UDN output-mode resize to {}x{}",
NewTargetParameters->Width,
NewTargetParameters->Height);
}
return S_OK;
}
return ResizeTarget_orig(swapchain, NewTargetParameters);
}
} // namespace
void schedule_window_attach(IDXGISwapChain *swapchain) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
!is_main_game_swapchain(swapchain)) {
return;
}
DXGI_SWAP_CHAIN_DESC desc {};
if (FAILED(swapchain->GetDesc(&desc)) || !desc.OutputWindow) {
return;
}
const HWND hwnd = desc.OutputWindow;
std::lock_guard<std::mutex> lock(g_window_attach_mutex);
if (g_window_attach_complete || g_window_attach_hook != nullptr) {
return;
}
if (GRAPHICS_HOOKED_WINDOW.has_value()) {
g_window_attach_complete = true;
return;
}
DWORD process_id = 0;
const DWORD thread_id = GetWindowThreadProcessId(hwnd, &process_id);
if (!thread_id || process_id != GetCurrentProcessId()) {
log_warning(
"graphics::d3d11",
"could not identify owner thread for hwnd=0x{:x}",
(uintptr_t) hwnd);
return;
}
const HHOOK hook = SetWindowsHookExW(
WH_GETMESSAGE, window_attach_getmessage, nullptr, thread_id);
if (!hook) {
log_warning(
"graphics::d3d11",
"could not schedule owner-thread window attach for hwnd=0x{:x}: {}",
(uintptr_t) hwnd,
GetLastError());
return;
}
g_window_attach_target = hwnd;
g_window_attach_hook = hook;
if (!PostMessageW(hwnd, window_attach_message(), 0, 0)) {
const DWORD error = GetLastError();
g_window_attach_hook = nullptr;
g_window_attach_target = nullptr;
UnhookWindowsHookEx(hook);
log_warning(
"graphics::d3d11",
"could not post owner-thread window attach for hwnd=0x{:x}: {}",
(uintptr_t) hwnd,
error);
return;
}
log_info(
"graphics::d3d11",
"scheduled Spice window attach on owner thread {} for hwnd=0x{:x}",
thread_id,
(uintptr_t) hwnd);
}
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN") ||
!is_main_game_swapchain(swapchain)) {
return;
}
// Interval two gives 60 FPS on the cabinet's 120 Hz display, but only
// 37.5 FPS on a 75 Hz desktop. Pace windowed mode with an explicit clock.
// Fullscreen keeps the game's own presentation interval and flags.
pace_forced_present(60);
sync_interval = 0;
}
void install_swapchain_hooks(IDXGISwapChain *swapchain) {
if (!GRAPHICS_WINDOWED || !avs::game::is_model("UDN")) {
return;
}
// The shared installer holds its mutex while calling this function.
if (!g_set_fullscreen_hooked) {
g_set_fullscreen_hooked = hook_vtbl(
swapchain, 10, (void *) SetFullscreenState_hook,
(void **) &SetFullscreenState_orig,
"IDXGISwapChain::SetFullscreenState");
}
if (!g_resize_target_hooked) {
g_resize_target_hooked = hook_vtbl(
swapchain, 14, (void *) ResizeTarget_hook,
(void **) &ResizeTarget_orig, "IDXGISwapChain::ResizeTarget");
}
}
} // namespace d3d11_hooks::udn
#endif // SPICE_D3D11
@@ -0,0 +1,17 @@
#pragma once
#ifdef SPICE_D3D11
#include <windows.h>
struct IDXGISwapChain;
namespace d3d11_hooks::udn {
void schedule_window_attach(IDXGISwapChain *swapchain);
void apply_present_policy(IDXGISwapChain *swapchain, UINT &sync_interval);
void install_swapchain_hooks(IDXGISwapChain *swapchain);
}
#endif
@@ -22,6 +22,7 @@
#include "launcher/shutdown.h"
#include "misc/wintouchemu.h"
#include "overlay/overlay.h"
#include "sdk/d3d9.h"
#include "util/detour.h"
#include "util/deferlog.h"
#include "util/flags_helper.h"
@@ -944,9 +945,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
auto *wrapped = new WrappedIDirect3DDevice9(
hFocusWindow,
*ppReturnedDeviceInterface);
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
*ppReturnedDeviceInterface = wrapped;
}
// return result
@@ -1030,14 +1034,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
return D3DERR_INVALIDCALL;
}
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_warning(
"graphics::d3d9",
"-forceressub is unavailable; SMALL must remain {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT);
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1;
@@ -1093,7 +1089,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
} else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_misc(
"graphics::d3d9",
"use custom sub resolution {}x{} => {}x{}",
@@ -1137,7 +1134,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
} else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
}
@@ -1287,14 +1285,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
ppReturnedDeviceInterface);
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.presentation_parameters[1];
if (pFullscreenDisplayMode != nullptr) {
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
gfdm_publish_two_head_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters);
}
// check for error
@@ -1307,13 +1301,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
graphics_hook_window(hFocusWindow, pPresentationParameters);
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
auto *wrapped = new WrappedIDirect3DDevice9(
hFocusWindow,
*ppReturnedDeviceInterface,
gfdm_parameters.logical_small_swapchain,
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
*ppReturnedDeviceInterface = wrapped;
// 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)) {
@@ -1515,6 +1512,8 @@ void graphics_d3d9_on_present(
device->EndScene();
}
sdk::d3d9::draw(hFocusWindow, device);
// after the overlay render so the screenshot includes toasts / menus
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
graphics_d3d9_process_screenshot(device, wrapped_device);
@@ -11,6 +11,7 @@
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "overlay/overlay.h"
#include "sdk/d3d9.h"
#include "util/flags_helper.h"
#include "util/utils.h"
#include "cfg/screen_resize.h"
@@ -136,6 +137,7 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
// release owned objects if there are no more references
if (local_refs == 0) {
sdk::d3d9::destroy(this->pReal);
if (this->main_swapchain) {
this->main_swapchain->Release();
this->main_swapchain = nullptr;
@@ -157,6 +159,9 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
}
}
// holds a reference on the device, so it has to go before the counts are compared
this->gfdm_small_head.release();
d3d9_readback::release_device_resources(this->pReal);
if (overlay::ENABLED) {
@@ -629,12 +634,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
}
}
sdk::d3d9::invalidate(pReal);
// reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate();
}
gfdm_small_head.release();
// Reset refuses to run while any default pool resource is outstanding
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)) {
@@ -659,7 +672,15 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
graphics_d3d9_on_present(hFocusWindow, pReal, this);
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
// 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(this);
}
const HRESULT result = pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
sdk::d3d9::present_complete(pReal, result);
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
@@ -680,6 +701,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
if (gfdm_small_head.scaled()
&& iBackBuffer == 0
&& Type == D3DBACKBUFFER_TYPE_MONO)
{
CHECK_RESULT(gfdm_small_head.backbuffer(pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
iBackBuffer,
@@ -2068,8 +2095,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::PresentEx(
graphics_d3d9_on_present(hFocusWindow, pReal, this);
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
const HRESULT result = static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
sdk::d3d9::present_complete(pReal, result);
CHECK_RESULT(result);
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGPUThreadPriority(
@@ -2316,15 +2345,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
}
}
sdk::d3d9::invalidate(pReal);
// reset overlay
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
overlay::OVERLAY->reset_invalidate();
}
gfdm_small_head.release();
// ResetEx refuses to run while any default pool resource is outstanding
d3d9_readback::discard_snapshot_targets(pReal);
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->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)
@@ -2335,14 +2373,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
}
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.presentation_parameters[1];
if (pFullscreenDisplayMode != nullptr) {
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
gfdm_publish_two_head_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters);
}
// recreate overlay
@@ -11,6 +11,7 @@
#include "util/logging.h"
#include "d3d9_fake_swapchain.h"
#include "d3d9_gfdm.h"
#include "d3d9_swapchain.h"
/*
@@ -264,6 +265,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
IDirect3DDevice9 *pReal;
bool is_d3d9ex = false;
// set from the creation flags; without it the runtime does no locking of its own, so
// nothing but the present thread may touch the device
bool device_multithreaded = false;
std::atomic_ulong refs = 1;
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
@@ -275,6 +280,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
bool gfdm_logical_group_parameters_valid = false;
IDirect3D9 *gfdm_parent_d3d = nullptr;
GfdmSmallHead gfdm_small_head;
std::mutex gfdm_recovery_mutex;
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
@@ -2,20 +2,73 @@
#include <algorithm>
#include <cassert>
#include <cstring>
#include <mutex>
#include "games/gitadora/gitadora.h"
#include "hooks/graphics/graphics.h"
#include "sdk/d3d9.h"
#include "util/logging.h"
#include "d3d9_device.h"
// windows.h defines many stupid things
#undef small
bool gfdm_two_head_exclusive() {
return games::gitadora::is_arena_model()
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
&& !GRAPHICS_WINDOWED;
}
static std::pair<UINT, UINT> gfdm_small_head_size() {
if (!gfdm_two_head_exclusive()) {
return { GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT };
}
return games::gitadora::arena_subscreen_host_size();
}
// scaling is only needed when the head is not the panel size the game draws into
static bool gfdm_small_head_scaled() {
const auto [width, height] = gfdm_small_head_size();
return width != GFDM_SMALL_WIDTH || height != GFDM_SMALL_HEIGHT;
}
// Hand the resolved MAIN/SMALL heads back to the game's own array. The SMALL entry keeps
// the portrait size the game asked for: that is what it renders, and the next reset finds
// the SMALL head by matching it.
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state)
{
if (logical_presentation_parameters == nullptr) {
return;
}
const UINT small = state.logical_small_swapchain;
const bool publish_modes = logical_fullscreen_display_modes != nullptr
&& state.fullscreen_display_modes != nullptr;
logical_presentation_parameters[0] = state.presentation_parameters[0];
logical_presentation_parameters[small] = state.presentation_parameters[1];
if (publish_modes) {
logical_fullscreen_display_modes[0] = state.fullscreen_display_modes[0];
logical_fullscreen_display_modes[small] = state.fullscreen_display_modes[1];
}
if (!gfdm_small_head_scaled()) {
return;
}
logical_presentation_parameters[small].BackBufferWidth = GFDM_SMALL_WIDTH;
logical_presentation_parameters[small].BackBufferHeight = GFDM_SMALL_HEIGHT;
if (publish_modes) {
logical_fullscreen_display_modes[small].Width = GFDM_SMALL_WIDTH;
logical_fullscreen_display_modes[small].Height = GFDM_SMALL_HEIGHT;
}
}
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
@@ -186,6 +239,26 @@ HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
return D3DERR_INVALIDCALL;
}
// the game keeps rendering the portrait subscreen; only the head it is scanned out
// on changes size, and the scaling happens when the head is presented
if (gfdm_small_head_scaled()) {
const auto [host_width, host_height] = gfdm_small_head_size();
log_info(
"graphics::d3d9",
"two-head exclusive: {} SMALL head {}x{} -> {}x{}",
operation,
secondary.BackBufferWidth,
secondary.BackBufferHeight,
host_width,
host_height);
secondary.BackBufferWidth = host_width;
secondary.BackBufferHeight = host_height;
if (fullscreen_display_modes != nullptr) {
fullscreen_display_modes[1].Width = host_width;
fullscreen_display_modes[1].Height = host_height;
}
}
return D3D_OK;
}
@@ -318,20 +391,21 @@ HRESULT validate_gfdm_two_head_exclusive(
const auto &main = presentation_parameters[0];
const auto &small_params = presentation_parameters[1];
const auto [expected_small_width, expected_small_height] = gfdm_small_head_size();
if (main.Windowed || small_params.Windowed) {
log_warning(
"graphics::d3d9",
"two-head exclusive mode requires both group heads to be fullscreen");
return D3DERR_INVALIDCALL;
}
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
if (small_params.BackBufferWidth != expected_small_width
|| small_params.BackBufferHeight != expected_small_height)
{
log_warning(
"graphics::d3d9",
"SMALL head must be {}x{}; got {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT,
expected_small_width,
expected_small_height,
small_params.BackBufferWidth,
small_params.BackBufferHeight);
return D3DERR_INVALIDCALL;
@@ -485,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
@@ -501,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;
}
@@ -547,6 +624,7 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
{
if (presentation_parameters == nullptr) {
gfdm_logical_group_parameters_valid = false;
gfdm_small_head.resolve(nullptr);
return;
}
std::copy_n(
@@ -554,6 +632,8 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
gfdm_logical_group_parameters.size(),
gfdm_logical_group_parameters.begin());
gfdm_logical_group_parameters_valid = true;
gfdm_small_head.resolve(
&gfdm_logical_group_parameters[gfdm_logical_small_swapchain]);
}
FakeIDirect3DSwapChain9 *
@@ -599,6 +679,217 @@ void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
}
}
void GfdmSmallHead::resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters) {
release();
active = gfdm_small_head_scaled();
format = D3DFMT_X8R8G8B8;
multisample = D3DMULTISAMPLE_NONE;
multisample_quality = 0;
if (!active || logical_small_parameters == nullptr) {
return;
}
if (logical_small_parameters->BackBufferFormat != D3DFMT_UNKNOWN) {
format = logical_small_parameters->BackBufferFormat;
}
multisample = logical_small_parameters->MultiSampleType;
multisample_quality = logical_small_parameters->MultiSampleQuality;
}
void GfdmSmallHead::apply_logical_size(UINT *width, UINT *height) const {
if (!active) {
return;
}
if (width != nullptr) {
*width = GFDM_SMALL_WIDTH;
}
if (height != nullptr) {
*height = GFDM_SMALL_HEIGHT;
}
}
HRESULT GfdmSmallHead::backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out) {
if (device == nullptr || out == nullptr || !active) {
return D3DERR_INVALIDCALL;
}
if (surface == nullptr) {
const HRESULT result = device->CreateRenderTarget(
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT,
format,
multisample,
multisample_quality,
FALSE,
&surface,
nullptr);
if (FAILED(result)) {
log_warning(
"graphics::d3d9",
"two-head exclusive: could not create the portrait SMALL surface, hr={}",
FMT_HRESULT(result));
surface = nullptr;
return result;
}
device->ColorFill(surface, nullptr, D3DCOLOR_XRGB(0, 0, 0));
}
surface->AddRef();
*out = surface;
return D3D_OK;
}
void GfdmSmallHead::release() {
if (surface != nullptr) {
surface->Release();
surface = nullptr;
}
}
// 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<LONG>(source_desc.Width),
static_cast<LONG>(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(real, &proxy);
if (FAILED(result)) {
return result;
}
IDirect3DSurface9 *head = nullptr;
result = real->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
0,
D3DBACKBUFFER_TYPE_MONO,
&head);
if (FAILED(result)) {
proxy->Release();
return result;
}
D3DSURFACE_DESC desc {};
result = head->GetDesc(&desc);
if (SUCCEEDED(result)) {
const auto host_width = static_cast<LONG>(desc.Width);
const auto host_height = static_cast<LONG>(desc.Height);
const RECT content = games::gitadora::arena_subscreen_content_rect(
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
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();
proxy->Release();
return result;
}
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
gfdm_recovery_armed = false;
@@ -13,6 +13,36 @@ 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.
struct GfdmSmallHead {
GfdmSmallHead() = default;
~GfdmSmallHead() { release(); }
GfdmSmallHead(const GfdmSmallHead &) = delete;
GfdmSmallHead &operator=(const GfdmSmallHead &) = delete;
// settled with the device, so it cannot change under the game mid-session
void resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters);
bool scaled() const { return active; }
HRESULT backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out);
HRESULT compose(WrappedIDirect3DDevice9 *device);
void release();
// the game only ever sees the portrait size it asked for
void apply_logical_size(UINT *width, UINT *height) const;
private:
bool active = false;
D3DFORMAT format = D3DFMT_X8R8G8B8;
D3DMULTISAMPLE_TYPE multisample = D3DMULTISAMPLE_NONE;
DWORD multisample_quality = 0;
IDirect3DSurface9 *surface = nullptr;
};
struct GfdmTwoHeadDeviceState {
explicit GfdmTwoHeadDeviceState(
D3DPRESENT_PARAMETERS *presentation_parameters,
@@ -41,6 +71,10 @@ struct GfdmTwoHeadDeviceState {
};
bool gfdm_two_head_exclusive();
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state);
bool is_fake_subscreen_adapter(UINT adapter);
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
@@ -1,5 +1,8 @@
#include "d3d9_readback.h"
#include <array>
#include <chrono>
#include <condition_variable>
#include <mutex>
#include <vector>
@@ -10,6 +13,13 @@ namespace d3d9_readback {
namespace {
// the snapshot path stamps frames with this to recognise one left behind by a break in the
// request stream
uint64_t now_us() {
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
}
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *surface = nullptr;
const HRESULT hr = device->CreateOffscreenPlainSurface(
@@ -160,21 +170,8 @@ ReadbackPool &pool() {
return *instance;
}
} // namespace
void release_device_resources(IDirect3DDevice9 *device) {
pool().clear_device(device);
}
BackbufferCopy::~BackbufferCopy() {
if (this->pooled && this->surface) {
pool().release(this->device, std::move(this->surface));
}
}
std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
// the back buffer plus the checks every caller has to make before copying out of it
SurfacePtr open_backbuffer(IDirect3DSwapChain9 *swap_chain, int screen, D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *buffer = nullptr;
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
if (FAILED(hr) || buffer == nullptr) {
@@ -182,17 +179,17 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
"failed to get back buffer for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
return std::nullopt;
return nullptr;
}
D3DSURFACE_DESC desc {};
hr = buffer->GetDesc(&desc);
SurfacePtr surface(buffer);
hr = surface->GetDesc(&desc);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to acquire back buffer descriptor, hr={}",
FMT_HRESULT(hr));
buffer->Release();
return std::nullopt;
return nullptr;
}
// GetRenderTargetData rejects multisampled sources. no supported game has been
@@ -204,7 +201,399 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
"back buffer is multisampled ({}), screenshots and capture are unsupported",
static_cast<uint32_t>(desc.MultiSampleType));
});
buffer->Release();
return nullptr;
}
return surface;
}
SurfacePtr create_snapshot_target(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
IDirect3DSurface9 *surface = nullptr;
// matching the back buffer keeps the blit a straight copy and leaves the pixels in the
// format the conversion step would have seen without the detour
const HRESULT hr = device->CreateRenderTarget(
desc.Width, desc.Height, desc.Format,
D3DMULTISAMPLE_NONE, 0, FALSE, &surface, nullptr);
if (FAILED(hr) || surface == nullptr) {
log_warning("graphics::d3d9",
"failed to create snapshot target, hr={}",
FMT_HRESULT(hr));
return nullptr;
}
return SurfacePtr(surface);
}
// two render targets per screen, reused across frames. two because one holds the frame waiting
// out its deferral while the other takes the next one; a read only holds its target until the
// pixels are in system memory, which is far shorter than the gap between requests.
//
// unlike the readback surfaces these live in the default pool, so they have to be gone before a
// Reset and not merely before the device is released
class SnapshotTargets {
public:
SurfacePtr acquire(
IDirect3DDevice9 *device,
int screen,
const D3DSURFACE_DESC &desc,
uint64_t *out_generation) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return nullptr;
}
std::lock_guard<std::mutex> lock(this->mutex);
if (this->device != device) {
this->drop();
this->device = device;
this->usable = true;
}
auto &entry = this->screens[screen];
Slot *free_slot = nullptr;
for (size_t i = 0; i < SLOTS_PER_SCREEN; i++) {
auto &candidate = entry.slots[(entry.next + i) % SLOTS_PER_SCREEN];
if (!candidate.busy) {
free_slot = &candidate;
entry.next = (entry.next + i + 1) % SLOTS_PER_SCREEN;
break;
}
}
if (free_slot == nullptr) {
return nullptr;
}
auto &slot = *free_slot;
if (slot.target
&& (slot.width != desc.Width
|| slot.height != desc.Height
|| slot.format != desc.Format)) {
slot.target.reset();
}
if (!slot.target) {
slot.target = create_snapshot_target(device, desc);
if (!slot.target) {
// a back buffer format the device will not give us a render target for. the
// inline path can still read it, so stop trying rather than lose the stream
this->usable = false;
return nullptr;
}
slot.width = desc.Width;
slot.height = desc.Height;
slot.format = desc.Format;
}
slot.busy = true;
*out_generation = this->current_generation;
slot.target->AddRef();
return SurfacePtr(slot.target.get());
}
// holds the fresh snapshot back and returns the one from the previous request, which by now
// has had a full frame for its blit and transfer to land
std::optional<Snapshot> rotate(int screen, std::optional<Snapshot> fresh) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return std::nullopt;
}
const uint64_t now = now_us();
std::optional<Snapshot> previous;
{
std::lock_guard<std::mutex> lock(this->mutex);
previous = std::move(this->pending[screen]);
this->pending[screen] = std::move(fresh);
}
// a frame waits here for the next request rather than for a deadline, so a gap in
// requests, a client reconnecting most obviously, would otherwise hand the new session
// a frame from before the gap. showing a stale frame is worse than showing none
if (previous.has_value() && now - previous->issued_us > MAX_DEFERRAL_US) {
return std::nullopt;
}
return previous;
}
// a reader whose target was already thrown away must not free the slot, or it would free
// the snapshot that replaced it while that one is still being read
void finish(int screen, uint64_t generation, IDirect3DSurface9 *target) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return;
}
{
std::lock_guard<std::mutex> lock(this->mutex);
if (generation != this->current_generation) {
return;
}
this->free_slot(screen, target);
}
this->idle.notify_all();
}
bool still_current(uint64_t generation) {
std::lock_guard<std::mutex> lock(this->mutex);
return generation == this->current_generation;
}
bool is_usable() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->usable;
}
// Reset fails outright while the device still owns default pool resources, so dropping our
// own references is not enough and any read in flight has to finish first. the wait is
// bounded because a failed Reset is something games retry and a stalled present thread is
// not something they survive
void discard(IDirect3DDevice9 *device) {
// destroyed after the lock is released, since dropping a snapshot calls back in here
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> stale;
{
std::unique_lock<std::mutex> lock(this->mutex);
if (this->device != nullptr && this->device != device) {
return;
}
// deferred frames are abandoned rather than waited for; only a read that is already
// running has to be allowed to finish
for (auto &held : this->pending) {
if (held.has_value() && held->surface) {
this->free_slot(held->screen, held->surface.get());
}
}
stale = std::move(this->pending);
this->pending = {};
const bool drained = this->idle.wait_for(
lock,
std::chrono::milliseconds(100),
[this] { return !this->any_busy(); });
if (!drained) {
log_warning("graphics::d3d9",
"capture snapshot still in flight, discarding its target anyway");
}
this->drop();
this->device = nullptr;
}
}
private:
static constexpr size_t SLOTS_PER_SCREEN = 2;
// generous next to the frame interval this is meant to bridge, so that ordinary jitter
// never trips it and only a real break in the request stream does
static constexpr uint64_t MAX_DEFERRAL_US = 250'000;
struct Slot {
SurfacePtr target;
UINT width = 0;
UINT height = 0;
D3DFORMAT format = D3DFMT_UNKNOWN;
bool busy = false;
};
struct Screen {
std::array<Slot, SLOTS_PER_SCREEN> slots;
size_t next = 0;
};
void free_slot(int screen, IDirect3DSurface9 *target) {
for (auto &slot : this->screens[screen].slots) {
if (slot.target.get() == target) {
slot.busy = false;
return;
}
}
}
bool any_busy() const {
for (const auto &entry : this->screens) {
for (const auto &slot : entry.slots) {
if (slot.busy) {
return true;
}
}
}
return false;
}
void drop() {
for (auto &entry : this->screens) {
for (auto &slot : entry.slots) {
slot.target.reset();
slot.busy = false;
}
entry.next = 0;
}
this->current_generation++;
}
std::mutex mutex;
std::condition_variable idle;
std::array<Screen, GRAPHICS_CAPTURE_SCREEN_NO> screens;
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> pending;
IDirect3DDevice9 *device = nullptr;
uint64_t current_generation = 1;
bool usable = true;
};
// never destroyed, for the same reason the readback pool is not
SnapshotTargets &targets() {
static SnapshotTargets *instance = new SnapshotTargets();
return *instance;
}
} // namespace
void release_device_resources(IDirect3DDevice9 *device) {
targets().discard(device);
pool().clear_device(device);
}
void discard_snapshot_targets(IDirect3DDevice9 *device) {
targets().discard(device);
}
bool snapshots_supported() {
return targets().is_usable();
}
BackbufferCopy::~BackbufferCopy() {
if (this->pooled && this->surface) {
pool().release(this->device, std::move(this->surface));
}
}
Snapshot::~Snapshot() {
// still holding the target means the read never ran, and the slot would otherwise stay
// marked busy and take the screen out of capture permanently
if (this->surface) {
targets().finish(this->screen, this->generation, this->surface.get());
}
}
namespace {
std::optional<Snapshot> take_snapshot(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
const uint64_t started_us = now_us();
D3DSURFACE_DESC desc {};
auto buffer = open_backbuffer(swap_chain, screen, desc);
if (!buffer) {
return std::nullopt;
}
uint64_t generation = 0;
auto target = targets().acquire(device, screen, desc, &generation);
if (!target) {
return std::nullopt;
}
// built before the blit so that a failure below hands the slot back through the destructor
Snapshot snapshot;
snapshot.screen = screen;
snapshot.desc = desc;
snapshot.device = device;
snapshot.surface = std::move(target);
snapshot.generation = generation;
snapshot.issued_us = started_us;
// the point of the whole exercise: this is queued rather than waited on, so the game pays
// for issuing the copy and not for it completing. identical size and format, so there is
// no filtering to ask for
const HRESULT hr = device->StretchRect(
buffer.get(), nullptr, snapshot.surface.get(), nullptr, D3DTEXF_NONE);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to snapshot back buffer for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
return std::nullopt;
}
return snapshot;
}
} // namespace
std::optional<Snapshot> snapshot_backbuffer(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
return targets().rotate(screen, take_snapshot(device, swap_chain, screen));
}
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot) {
if (!snapshot.surface) {
return std::nullopt;
}
// a Reset between the blit and now means the target no longer holds the captured frame
if (!targets().still_current(snapshot.generation)) {
return std::nullopt;
}
auto destination = pool().acquire(snapshot.device, snapshot.desc);
if (!destination) {
return std::nullopt;
}
const HRESULT hr = snapshot.device->GetRenderTargetData(
snapshot.surface.get(), destination.get());
// the target is reusable as soon as the pixels are in system memory. dropping the
// reference before freeing the slot keeps the destructor from freeing it twice
const int screen = snapshot.screen;
const uint64_t generation = snapshot.generation;
IDirect3DSurface9 *target = snapshot.surface.get();
snapshot.surface.reset();
targets().finish(screen, generation, target);
if (FAILED(hr)) {
log_warning("graphics::d3d9",
"failed to read snapshot contents, hr={}",
FMT_HRESULT(hr));
pool().release(snapshot.device, std::move(destination));
return std::nullopt;
}
BackbufferCopy copy;
copy.screen = screen;
copy.desc = snapshot.desc;
copy.device = snapshot.device;
copy.surface = std::move(destination);
copy.pooled = true;
return copy;
}
std::optional<BackbufferCopy> acquire_backbuffer_copy(
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
D3DSURFACE_DESC desc {};
auto buffer = open_backbuffer(swap_chain, screen, desc);
if (!buffer) {
return std::nullopt;
}
@@ -212,12 +601,10 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
? pool().acquire(device, desc)
: create_readback_surface(device, desc);
if (!destination) {
buffer->Release();
return std::nullopt;
}
hr = device->GetRenderTargetData(buffer, destination.get());
buffer->Release();
const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get());
if (FAILED(hr)) {
log_warning("graphics::d3d9",
@@ -1,5 +1,6 @@
#pragma once
#include <cstdint>
#include <memory>
#include <optional>
@@ -39,6 +40,51 @@ namespace d3d9_readback {
int screen,
bool pooled);
// GPU side copy of a back buffer, taken while the contents are still the frame that was
// presented, so that reading them into system memory no longer has to happen before it
struct Snapshot {
int screen {};
D3DSURFACE_DESC desc {};
IDirect3DDevice9 *device = nullptr;
SurfacePtr surface;
uint64_t generation {};
// when the blit was issued, so a frame left behind by a break in the request stream can
// be recognised as stale rather than handed over
uint64_t issued_us {};
Snapshot() = default;
Snapshot(Snapshot &&) noexcept = default;
Snapshot &operator=(Snapshot &&) noexcept = default;
Snapshot(const Snapshot &) = delete;
Snapshot &operator=(const Snapshot &) = delete;
~Snapshot();
};
// for the present thread, between the last EndScene and Present. blits the current frame,
// then returns the snapshot taken on the *previous* call: waiting a frame before reading
// means the blit and its system memory transfer have already happened, so the read does not
// stall on the GPU. costs the stream one frame of latency.
//
// returns nothing on the first call of a stream, and whenever the frame could not be taken,
// which is the caller's cue to skip rather than to wait
std::optional<Snapshot> snapshot_backbuffer(
IDirect3DDevice9 *device,
IDirect3DSwapChain9 *swap_chain,
int screen);
// the expensive half, for a thread that is not the present thread. only legal on a device
// created with D3DCREATE_MULTITHREADED
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot);
// false once a device has refused to give up a render target matching its back buffer,
// which leaves reading the back buffer directly as the only way to capture it
bool snapshots_supported();
// snapshot targets live in the default pool, so unlike the readback surfaces they have to
// be gone before a Reset and not merely before the device is released
void discard_snapshot_targets(IDirect3DDevice9 *device);
// pooled surfaces hold references on the device; call this before releasing it
void release_device_resources(IDirect3DDevice9 *device);
}
@@ -488,6 +488,63 @@ ThreadPool &capture_read_pool() {
return *instance;
}
// Takes the frame on the present thread as a queued GPU blit and hands the readback to a pool
// thread, so the game waits for neither. Only viable where the whole read can go off thread,
// since the back buffer is overwritten right after Present and a snapshot the present thread
// then had to read itself would cost more than reading the back buffer directly.
//
// Returns false when the frame could not be taken, including the ordinary case of the previous
// snapshot of this screen still being read, which paces capture to what the reader sustains.
static bool snapshot_capture(
IDirect3DDevice9 *device,
WrappedIDirect3DDevice9 *wrapped_device,
int screen) {
IDirect3DSwapChain9 *swap_chain = nullptr;
const HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
if (FAILED(hr) || swap_chain == nullptr) {
log_warning("graphics::d3d9",
"failed to get swap chain for screen {}, hr={}",
screen,
FMT_HRESULT(hr));
return false;
}
auto snapshot = d3d9_readback::snapshot_backbuffer(device, swap_chain, screen);
swap_chain->Release();
if (!snapshot.has_value()) {
return false;
}
try {
capture_read_pool().add([screen, snapshot = std::move(*snapshot)]() mutable {
// an escape from here would cross a thread boundary and terminate
try {
auto copy = d3d9_readback::read_snapshot(std::move(snapshot));
if (!copy.has_value()) {
graphics_capture_skip(screen);
return;
}
read_and_dispatch_capture(screen, std::move(*copy));
} catch (const std::exception &error) {
log_warning("graphics::d3d9", "capture read failed: {}", error.what());
graphics_capture_skip(screen);
} catch (...) {
log_warning("graphics::d3d9", "capture read failed");
graphics_capture_skip(screen);
}
});
} catch (const std::exception &) {
// the snapshot went into the lambda before the queue could fail, so it is already
// destroyed and its target handed back; the client just misses this frame
return false;
}
return true;
}
// by this point the pixels are plain memory, so none of this needs the device
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
@@ -632,6 +689,18 @@ static void process_image_request(
const ImageRequest &request) {
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
if (!screenshot
&& wrapped_device->device_multithreaded
&& capture_read_off_thread(request.screen)
&& d3d9_readback::snapshots_supported()) {
if (!snapshot_capture(device, wrapped_device, request.screen)) {
graphics_capture_skip(request.screen);
}
return;
}
std::vector<int> screens { request.screen };
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
screens.clear();
@@ -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"
@@ -21,6 +22,12 @@
} \
return ret
// inert unless this is the SMALL head and it is being scaled onto a differently sized monitor
static bool scales_small_head(const WrappedIDirect3DSwapChain9 *chain) {
return chain->native_group_head == WrappedIDirect3DSwapChain9::NativeGroupHead::Small
&& chain->pDev->gfdm_small_head.scaled();
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) {
return E_POINTER;
@@ -89,6 +96,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
}
if (scales_small_head(this)) {
pDev->gfdm_small_head.compose(pDev);
}
HRESULT result = pReal->Present(
pSourceRect,
pDestRect,
@@ -130,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) {
@@ -138,6 +152,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
IDirect3DSurface9 **ppBackBuffer)
{
if (scales_small_head(this) && iBackBuffer == 0 && Type == D3DBACKBUFFER_TYPE_MONO) {
CHECK_RESULT(pDev->gfdm_small_head.backbuffer(pDev->pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
@@ -159,7 +177,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetDevice(IDirect3DDevice9
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
D3DPRESENT_PARAMETERS *pPresentationParameters)
{
CHECK_RESULT(pReal->GetPresentParameters(pPresentationParameters));
HRESULT result = pReal->GetPresentParameters(pPresentationParameters);
if (SUCCEEDED(result) && pPresentationParameters != nullptr && scales_small_head(this)) {
pDev->gfdm_small_head.apply_logical_size(
&pPresentationParameters->BackBufferWidth,
&pPresentationParameters->BackBufferHeight);
}
CHECK_RESULT(result);
}
/*
+79 -1
View File
@@ -7,6 +7,7 @@
#include <chrono>
#include <set>
#include <thread>
#include <vector>
#include <mutex>
#include <condition_variable>
@@ -52,6 +53,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr;
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
HWND NDD_MAIN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false;
// icon
@@ -231,6 +234,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)) {
@@ -390,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;
}
@@ -634,6 +646,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
const bool is_ndd = avs::game::is_model("NDD");
bool is_ndd_sub_window = is_ndd && window_name.starts_with("Aska MultiDisplay");
bool is_ndd_main_window = is_ndd && window_name == "ASKA";
const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name)
: "";
@@ -777,6 +792,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
}
if (is_ndd_sub_window) {
NDD_SUBSCREEN_WINDOW = result;
}
if (is_ndd_main_window) {
NDD_MAIN_WINDOW = result;
}
disable_touch_gestures(result);
log_misc(
"graphics",
@@ -902,6 +925,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
return value;
}
// the sub screen renders into a fixed 800x480 buffer, but the game's saved layout asks for rects
// that do not match it, and dxgi stretches the buffer to fill whatever the client area ends up as
static void ndd_subscreen_size(HWND hWnd, int &width, int &height) {
RECT rect {};
SetRect(&rect, 0, 0, 800, 480);
AdjustWindowRect(&rect, GetWindowLongA(hWnd, GWL_STYLE), 0);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
// the saved layout drops the sub window wherever it sat on the machine that wrote the file
static void ndd_subscreen_position(int &x, int &y) {
RECT main {};
if (NDD_MAIN_WINDOW != nullptr && GetWindowRect(NDD_MAIN_WINDOW, &main)) {
x = main.right;
y = main.top;
}
}
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
fmt::ptr(hWnd),
@@ -931,6 +974,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nHeight = rect.bottom - rect.top;
}
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
ndd_subscreen_size(hWnd, nWidth, nHeight);
ndd_subscreen_position(X, Y);
}
// iidx windowed TDJ mode
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
if (GRAPHICS_IIDX_WSUB) {
@@ -1073,6 +1121,15 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) {
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
if (!(uFlags & SWP_NOSIZE)) {
ndd_subscreen_size(hWnd, cx, cy);
}
if (!(uFlags & SWP_NOMOVE)) {
ndd_subscreen_position(X, Y);
}
}
if (is_gfdm_two_head_small_window(hWnd) &&
((uFlags & SWP_HIDEWINDOW) ||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
@@ -1481,6 +1538,27 @@ void graphics_capture_skip(int screen) {
GRAPHICS_CAPTURE_CV[screen].notify_one();
}
bool graphics_capture_last_size(int screen, int *width, int *height) {
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
return false;
}
// consuming a frame clears the pixels but leaves the size, so this survives the read
std::lock_guard<std::mutex> lock(GRAPHICS_CAPTURE_BUFFER_M[screen]);
const auto &capture = GRAPHICS_CAPTURE_BUFFER[screen];
if (!capture.width || !capture.height) {
return false;
}
if (width != nullptr) {
*width = capture.width;
}
if (height != nullptr) {
*height = capture.height;
}
return true;
}
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide, uint64_t *timestamp,
int *width, int *height) {
+7
View File
@@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
extern HWND SDVX_SUBSCREEN_WINDOW;
extern HWND POPN_SUBSCREEN_WINDOW;
extern HWND GFDM_SUBSCREEN_WINDOW;
extern HWND NDD_MAIN_WINDOW;
extern bool SUBSCREEN_FORCE_REDRAW;
extern bool FAKE_SUBSCREEN_ADAPTER;
@@ -121,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
@@ -146,6 +150,9 @@ void graphics_capture_trigger(int screen);
bool graphics_capture_consume(int *screen);
void graphics_capture_enqueue(int screen, uint8_t *data, size_t width, size_t height);
void graphics_capture_skip(int screen);
// size of the last frame captured off this screen, before any caller side downscale; false
// until one has been captured, so it cannot report a size for a screen the game never drew
bool graphics_capture_last_size(int screen, int *width, int *height);
// on success `out` owns packed 24bpp RGB pixels, width * height * 3 bytes
bool graphics_capture_receive_raw(int screen, std::shared_ptr<uint8_t[]> &out,
int divide = 0,
+109 -26
View File
@@ -1,5 +1,6 @@
#include "icmphook_net.h"
#include "hooks/nicspoof.h"
#include "util/detour.h"
#include "util/logging.h"
@@ -353,7 +354,11 @@ int WINAPI bind_hook_ws2(SOCKET s, const sockaddr *name, int namelen) {
{
std::lock_guard<std::recursive_mutex> lock(g_mu);
auto it = g_socks.find(s);
if (it != g_socks.end() && name && namelen >= (int) sizeof(sockaddr_in)) {
if (it != g_socks.end()) {
if (!name || namelen < (int) sizeof(sockaddr_in)) {
WSASetLastError(WSAEFAULT);
return SOCKET_ERROR;
}
auto *in = reinterpret_cast<const sockaddr_in *>(name);
if (in->sin_family != AF_INET) {
WSASetLastError(WSAEAFNOSUPPORT);
@@ -618,7 +623,10 @@ void install_icmphook_hooks() {
}
done = true;
const bool defer_divert = nicspoof_tunnel_enabled();
bool ok = true;
// Always own socket creation so raw ICMP sockets become emulated.
ok &= detour::trampoline_try(
"ws2_32.dll", "socket",
(void *) socket_hook, (void **) &socket_orig);
@@ -628,37 +636,52 @@ void install_icmphook_hooks() {
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASocketA",
(void *) WSASocketA_hook, (void **) &WSASocketA_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "closesocket",
(void *) closesocket_hook, (void **) &closesocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "bind",
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "sendto",
(void *) sendto_hook, (void **) &sendto_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "recvfrom",
(void *) recvfrom_hook, (void **) &recvfrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASendTo",
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSARecvFrom",
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "ioctlsocket",
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "setsockopt",
(void *) setsockopt_hook, (void **) &setsockopt_orig);
if (!ok) {
if (defer_divert) {
// NIC tunnel already hooked bind/sendto/recvfrom/closesocket/ioctlsocket.
// Tunnel hooks call icmphook_try_* for emulated ICMP sockets.
log_info("network",
"ICMP emulation: socket hooks installed; divert deferred to "
"NIC tunnel (icmphook_try_*)");
} else {
ok &= detour::trampoline_try(
"ws2_32.dll", "closesocket",
(void *) closesocket_hook, (void **) &closesocket_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "bind",
(void *) bind_hook_ws2, (void **) &bind_trampoline_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "sendto",
(void *) sendto_hook, (void **) &sendto_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "recvfrom",
(void *) recvfrom_hook, (void **) &recvfrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSASendTo",
(void *) WSASendTo_hook, (void **) &WSASendTo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "WSARecvFrom",
(void *) WSARecvFrom_hook, (void **) &WSARecvFrom_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "ioctlsocket",
(void *) ioctlsocket_hook, (void **) &ioctlsocket_orig);
if (!ok) {
log_warning(
"network",
"ICMP emulation: one or more ws2_32 hooks failed to install");
} else {
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
}
}
if (defer_divert && !ok) {
log_warning(
"network",
"ICMP emulation: one or more ws2_32 hooks failed to install");
} else {
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
"ICMP emulation: one or more socket-creation hooks failed");
}
g_installed.store(true, std::memory_order_release);
@@ -686,6 +709,66 @@ bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *
return true;
}
bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags,
const sockaddr *to, int tolen, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::sendto_hook(s, buf, len, flags, to, tolen);
return true;
}
bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags,
sockaddr *from, int *fromlen, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::recvfrom_hook(s, buf, len, flags, from, fromlen);
return true;
}
bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo,
int iTolen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::WSASendTo_hook(
s, lpBuffers, dwBufferCount, lpNumberOfBytesSent, dwFlags, lpTo,
iTolen, lpOverlapped, lpCompletionRoutine);
return true;
}
bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom,
LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::WSARecvFrom_hook(
s, lpBuffers, dwBufferCount, lpNumberOfBytesRecvd, lpFlags, lpFrom,
lpFromlen, lpOverlapped, lpCompletionRoutine);
return true;
}
bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::ioctlsocket_hook(s, cmd, argp);
return true;
}
bool icmphook_try_closesocket(SOCKET s, int *out_result) {
if (!icmphook_is_emulated_socket(s)) {
return false;
}
*out_result = icmphook_internal::closesocket_hook(s);
return true;
}
void icmphook_net_init() {
icmphook_internal::install_icmphook_hooks();
icmphook_iphlpapi_install();
+20 -1
View File
@@ -6,8 +6,27 @@ bool icmphook_is_emulated_socket(SOCKET s);
/*!
* Handle bind() for emulated ICMP sockets: records interface address and succeeds without kernel bind.
* Returns true if this socket was handled (caller should return 0).
* Returns true if this socket was handled (caller should return *out_result).
*/
bool icmphook_try_bind(SOCKET s, const struct sockaddr *name, int namelen, int *out_result);
/*!
* Divert-path helpers for when NIC tunnel owns the MinHook slots on these APIs.
* Return true if the socket is an emulated ICMP socket and was fully handled.
*/
bool icmphook_try_sendto(SOCKET s, const char *buf, int len, int flags,
const sockaddr *to, int tolen, int *out_result);
bool icmphook_try_recvfrom(SOCKET s, char *buf, int len, int flags,
sockaddr *from, int *fromlen, int *out_result);
bool icmphook_try_WSASendTo(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent, DWORD dwFlags, const sockaddr *lpTo,
int iTolen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result);
bool icmphook_try_WSARecvFrom(SOCKET s, LPWSABUF lpBuffers, DWORD dwBufferCount,
LPDWORD lpNumberOfBytesRecvd, LPDWORD lpFlags, sockaddr *lpFrom,
LPINT lpFromlen, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine, int *out_result);
bool icmphook_try_ioctlsocket(SOCKET s, long cmd, u_long *argp, int *out_result);
bool icmphook_try_closesocket(SOCKET s, int *out_result);
void icmphook_net_init();
+6 -3
View File
@@ -336,9 +336,11 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig);
}
// for TDJ subscreen search keyboard and T44 narrow-string handling
// for TDJ subscreen search keyboard
// T44 narrow-string handling
// NDD text measuring
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
avs::game::is_model({ "T44", "NDD" })) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
@@ -358,8 +360,9 @@ void hooks::lang::early_init() {
#endif
#ifdef SPICE64
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model("T44");
games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" });
#else
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
+13 -1
View File
@@ -1,11 +1,16 @@
#include "libraryhook.h"
#include <mutex>
#include <shared_mutex>
#include "external/robin_hood.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"
static bool LHOOK_ENABLED = false;
// SDK plugins can add aliases while other threads load DLLs.
static std::shared_mutex LIBRARIES_MUTEX;
static robin_hood::unordered_map<std::string, HMODULE> LIBRARIES_A;
static robin_hood::unordered_map<std::wstring, HMODULE> LIBRARIES_W;
static robin_hood::unordered_map<std::string, FARPROC> PROCS;
@@ -21,6 +26,7 @@ static HMODULE WINAPI LoadLibraryA_hook(LPCTSTR lpFileName) {
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_A.find(lpFileName);
if (module != LIBRARIES_A.end()) {
return module->second;
@@ -35,6 +41,7 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -49,6 +56,7 @@ static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWOR
// check hooks
if (lpFileName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpFileName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -63,6 +71,7 @@ static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
// check hooks
if (lpModuleName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_A.find(lpModuleName);
if (module != LIBRARIES_A.end()) {
return module->second;
@@ -77,6 +86,7 @@ static HMODULE WINAPI GetModuleHandleW_hook(LPCWSTR lpModuleName) {
// check hooks
if (lpModuleName) {
std::shared_lock lock(LIBRARIES_MUTEX);
auto module = LIBRARIES_W.find(lpModuleName);
if (module != LIBRARIES_W.end()) {
return module->second;
@@ -129,8 +139,10 @@ void libraryhook_enable(HMODULE module) {
void libraryhook_hook_library(std::string library_name, HMODULE library_address) {
const auto library_name_w = s2ws(library_name);
std::unique_lock lock(LIBRARIES_MUTEX);
// add library to list
LIBRARIES_W.insert_or_assign(s2ws(library_name), library_address);
LIBRARIES_W.insert_or_assign(library_name_w, library_address);
LIBRARIES_A.insert_or_assign(std::move(library_name), library_address);
}
+645
View File
@@ -0,0 +1,645 @@
#define _WIN32_WINNT 0x0601
#include "nicspoof.h"
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <iphlpapi.h>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include "util/detour.h"
#include "util/logging.h"
#include "hooks/nicspoof_tunnel.h"
namespace nicspoof_detail {
NicSpoofConfig g_cfg;
constexpr uint32_t k_default_local_ip = 0x0A64640Au; /* 10.100.100.10 */
constexpr uint32_t k_gateway = 0x0A646401u; /* 10.100.100.1 */
constexpr uint32_t k_dns1 = 0x0A02010Au; /* 10.2.1.10 */
constexpr uint32_t k_dns2 = 0x0A02011Eu; /* 10.2.1.30 */
constexpr uint32_t k_dhcp = 0xC0A80001u; /* 192.168.0.1 */
constexpr uint32_t k_mask = 0xFF000000u; /* 255.0.0.0 */
constexpr int k_prefix_len = 8;
constexpr DWORD k_ifindex = 77;
constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37};
constexpr char k_hostname[] = "N1C5P00F";
constexpr char k_domain[] = "sp2x";
constexpr char k_hostname_fqdn[] = "N1C5P00F.sp2x";
constexpr wchar_t k_domain_w[] = L"sp2x";
constexpr char k_adapter_name[] = "{NICSPOOF-0880-0001-4250-4E6963537066}";
constexpr char k_adapter_desc[] = "NicSpoof Virtual Ethernet";
constexpr wchar_t k_friendly_name[] = L"NicSpoof";
constexpr wchar_t k_adapter_desc_w[] = L"NicSpoof Virtual Ethernet";
uint32_t g_local_ip = k_default_local_ip;
uint32_t g_subnet = k_default_local_ip & k_mask;
#define ALIGN_UP_PTR(p, a) \
((BYTE *)(((ULONG_PTR)(p) + ((ULONG_PTR)(a) - 1)) & ~((ULONG_PTR)(a) - 1)))
bool g_log_adapters = true;
bool g_log_params = true;
[[maybe_unused]] decltype(GetAdaptersAddresses) *GetAdaptersAddresses_orig = nullptr;
[[maybe_unused]] decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
[[maybe_unused]] decltype(GetNetworkParams) *GetNetworkParams_orig = nullptr;
[[maybe_unused]] decltype(getaddrinfo) *getaddrinfo_orig = nullptr;
[[maybe_unused]] decltype(freeaddrinfo) *freeaddrinfo_orig = nullptr;
void ip_to_str(uint32_t ip, char *out, size_t n) {
if (!out || n == 0) {
return;
}
snprintf(out, n, "%u.%u.%u.%u",
(ip >> 24) & 255, (ip >> 16) & 255, (ip >> 8) & 255, ip & 255);
out[n - 1] = 0;
}
uint32_t parse_ipv4(const char *s) {
unsigned a = 0;
unsigned b = 0;
unsigned c = 0;
unsigned d = 0;
char trail = 0;
if (!s) {
return 0;
}
if (sscanf(s, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
return 0;
}
if (a > 255 || b > 255 || c > 255 || d > 255) {
return 0;
}
return (a << 24) | (b << 16) | (c << 8) | d;
}
bool name_is_local(const char *name) {
if (!name || !name[0]) {
return true;
}
if (_stricmp(name, "localhost") == 0) {
return true;
}
if (_stricmp(name, k_hostname) == 0) {
return true;
}
if (_stricmp(name, k_hostname_fqdn) == 0) {
return true;
}
return false;
}
bool name_is_numeric_ipv4(const char *name) {
unsigned a = 0;
unsigned b = 0;
unsigned c = 0;
unsigned d = 0;
char trail = 0;
if (!name) {
return false;
}
if (sscanf(name, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
return false;
}
return a <= 255 && b <= 255 && c <= 255 && d <= 255;
}
DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) {
ULONG need = sizeof(IP_ADAPTER_INFO) + 32;
char ip[16];
char gw[16];
char dhcp[16];
if (!s) {
return ERROR_INVALID_PARAMETER;
}
if (!p || *s < need) {
*s = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(p, 0, need);
p->Index = k_ifindex;
strcpy(p->AdapterName, k_adapter_name);
strcpy(p->Description, k_adapter_desc);
p->AddressLength = 6;
memcpy(p->Address, k_fake_mac, 6);
p->Type = MIB_IF_TYPE_ETHERNET;
p->DhcpEnabled = 1;
ip_to_str(g_local_ip, ip, sizeof(ip));
ip_to_str(k_gateway, gw, sizeof(gw));
ip_to_str(k_dhcp, dhcp, sizeof(dhcp));
strcpy(p->IpAddressList.IpAddress.String, ip);
ip_to_str(k_mask, p->IpAddressList.IpMask.String,
sizeof(p->IpAddressList.IpMask.String));
p->IpAddressList.Context = 0;
p->Next = nullptr;
strcpy(p->GatewayList.IpAddress.String, gw);
p->GatewayList.IpMask.String[0] = 0;
p->GatewayList.Context = 0;
p->GatewayList.Next = nullptr;
strcpy(p->DhcpServer.IpAddress.String, dhcp);
p->DhcpServer.IpMask.String[0] = 0;
p->DhcpServer.Context = 0;
p->DhcpServer.Next = nullptr;
*s = need;
if (g_log_adapters) {
log_misc("network", "NIC spoof GetAdaptersInfo ip={} gw={} ifindex={}",
ip, gw, static_cast<unsigned>(k_ifindex));
g_log_adapters = false;
}
return ERROR_SUCCESS;
}
DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) {
/* FIXED_INFO embeds the first IP_ADDR_STRING; second follows in the buffer. */
ULONG need = static_cast<ULONG>(sizeof(FIXED_INFO) + sizeof(IP_ADDR_STRING));
char dns1[16];
char dns2[16];
PIP_ADDR_STRING dns_second = nullptr;
if (!s) {
return ERROR_INVALID_PARAMETER;
}
if (!p || *s < need) {
*s = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(p, 0, need);
strncpy(p->HostName, k_hostname, sizeof(p->HostName) - 1);
strncpy(p->DomainName, k_domain, sizeof(p->DomainName) - 1);
ip_to_str(k_dns1, dns1, sizeof(dns1));
ip_to_str(k_dns2, dns2, sizeof(dns2));
strcpy(p->DnsServerList.IpAddress.String, dns1);
p->DnsServerList.IpMask.String[0] = 0;
p->DnsServerList.Context = 0;
dns_second = reinterpret_cast<PIP_ADDR_STRING>(
reinterpret_cast<BYTE *>(p) + sizeof(FIXED_INFO));
memset(dns_second, 0, sizeof(*dns_second));
strcpy(dns_second->IpAddress.String, dns2);
dns_second->IpMask.String[0] = 0;
dns_second->Context = 0;
dns_second->Next = nullptr;
p->DnsServerList.Next = dns_second;
p->CurrentDnsServer = &p->DnsServerList;
p->NodeType = 1;
p->EnableDns = 1;
*s = need;
if (g_log_params) {
log_misc("network",
"NIC spoof GetNetworkParams host={} domain={} dns={} dns2={}",
k_hostname, k_domain, dns1, dns2);
g_log_params = false;
}
return ERROR_SUCCESS;
}
ULONG WINAPI GetAdaptersAddresses_hook(
ULONG Family,
ULONG Flags,
PVOID Reserved,
PIP_ADAPTER_ADDRESSES AdapterAddresses,
PULONG SizePointer) {
enum { STR_SLOT = 64 };
/*
* Layout is fixed: adapter header, 4 string slots, then unicast/prefix/
* gateway/dns + sockaddrs with alignment slack. need is computed to always
* fit; never return BUFFER_OVERFLOW after a successful size probe.
*/
ULONG need = static_cast<ULONG>(sizeof(IP_ADAPTER_ADDRESSES) + STR_SLOT * 4 +
sizeof(void *) * 8 +
sizeof(IP_ADAPTER_UNICAST_ADDRESS) +
sizeof(IP_ADAPTER_PREFIX) +
sizeof(IP_ADAPTER_DNS_SERVER_ADDRESS) * 2 +
sizeof(IP_ADAPTER_GATEWAY_ADDRESS) +
sizeof(sockaddr_in) * 5 + 64);
BYTE *blob = nullptr;
PIP_ADAPTER_ADDRESSES a = nullptr;
PIP_ADAPTER_UNICAST_ADDRESS u = nullptr;
PIP_ADAPTER_PREFIX pref = nullptr;
PIP_ADAPTER_GATEWAY_ADDRESS gw = nullptr;
PIP_ADAPTER_DNS_SERVER_ADDRESS dns = nullptr;
PIP_ADAPTER_DNS_SERVER_ADDRESS dns2 = nullptr;
sockaddr_in *sa = nullptr;
sockaddr_in *sm = nullptr;
sockaddr_in *sg = nullptr;
sockaddr_in *ds = nullptr;
sockaddr_in *ds2 = nullptr;
(void)Reserved;
(void)Flags;
if (Family != AF_INET && Family != AF_UNSPEC) {
if (SizePointer) {
*SizePointer = 0;
}
return ERROR_NO_DATA;
}
if (!SizePointer) {
return ERROR_INVALID_PARAMETER;
}
if (!AdapterAddresses || *SizePointer < need) {
*SizePointer = need;
return ERROR_BUFFER_OVERFLOW;
}
memset(AdapterAddresses, 0, need);
a = AdapterAddresses;
blob = reinterpret_cast<BYTE *>(a + 1);
a->Length = sizeof(IP_ADAPTER_ADDRESSES);
a->IfIndex = k_ifindex;
a->AdapterName = reinterpret_cast<PCHAR>(blob);
strcpy(reinterpret_cast<char *>(blob), k_adapter_name);
blob += STR_SLOT;
a->FriendlyName = reinterpret_cast<PWCHAR>(blob);
wcscpy(reinterpret_cast<wchar_t *>(blob), k_friendly_name);
blob += STR_SLOT;
a->Description = reinterpret_cast<PWCHAR>(blob);
wcscpy(reinterpret_cast<wchar_t *>(blob), k_adapter_desc_w);
blob += STR_SLOT;
a->PhysicalAddressLength = 6;
memcpy(a->PhysicalAddress, k_fake_mac, 6);
a->Flags = IP_ADAPTER_DHCP_ENABLED;
a->Mtu = 1500;
a->IfType = IF_TYPE_ETHERNET_CSMACD;
a->OperStatus = IfOperStatusUp;
a->Ipv4Enabled = 1;
a->DnsSuffix = reinterpret_cast<PWCHAR>(blob);
wcsncpy(reinterpret_cast<wchar_t *>(blob), k_domain_w,
(STR_SLOT / sizeof(wchar_t)) - 1);
reinterpret_cast<wchar_t *>(blob)[(STR_SLOT / sizeof(wchar_t)) - 1] = 0;
blob += STR_SLOT;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
u = reinterpret_cast<PIP_ADAPTER_UNICAST_ADDRESS>(blob);
blob += sizeof(*u);
memset(u, 0, sizeof(*u));
u->Length = sizeof(*u);
u->DadState = IpDadStatePreferred;
u->OnLinkPrefixLength = static_cast<UCHAR>(k_prefix_len);
sa = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sa);
memset(sa, 0, sizeof(*sa));
sa->sin_family = AF_INET;
sa->sin_addr.s_addr = htonl(g_local_ip);
u->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sa);
u->Address.iSockaddrLength = sizeof(*sa);
a->FirstUnicastAddress = u;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
pref = reinterpret_cast<PIP_ADAPTER_PREFIX>(blob);
blob += sizeof(*pref);
memset(pref, 0, sizeof(*pref));
pref->Length = sizeof(*pref);
pref->PrefixLength = static_cast<ULONG>(k_prefix_len);
sm = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sm);
memset(sm, 0, sizeof(*sm));
sm->sin_family = AF_INET;
sm->sin_addr.s_addr = htonl(g_subnet);
pref->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sm);
pref->Address.iSockaddrLength = sizeof(*sm);
a->FirstPrefix = pref;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
gw = reinterpret_cast<PIP_ADAPTER_GATEWAY_ADDRESS>(blob);
blob += sizeof(*gw);
memset(gw, 0, sizeof(*gw));
gw->Length = sizeof(*gw);
sg = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*sg);
memset(sg, 0, sizeof(*sg));
sg->sin_family = AF_INET;
sg->sin_addr.s_addr = htonl(k_gateway);
gw->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(sg);
gw->Address.iSockaddrLength = sizeof(*sg);
a->FirstGatewayAddress = gw;
blob = ALIGN_UP_PTR(blob, sizeof(void *));
dns = reinterpret_cast<PIP_ADAPTER_DNS_SERVER_ADDRESS>(blob);
blob += sizeof(*dns);
memset(dns, 0, sizeof(*dns));
ds = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*ds);
memset(ds, 0, sizeof(*ds));
ds->sin_family = AF_INET;
ds->sin_addr.s_addr = htonl(k_dns1);
dns->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds);
dns->Address.iSockaddrLength = sizeof(*ds);
blob = ALIGN_UP_PTR(blob, sizeof(void *));
dns2 = reinterpret_cast<PIP_ADAPTER_DNS_SERVER_ADDRESS>(blob);
blob += sizeof(*dns2);
memset(dns2, 0, sizeof(*dns2));
ds2 = reinterpret_cast<sockaddr_in *>(blob);
blob += sizeof(*ds2);
memset(ds2, 0, sizeof(*ds2));
ds2->sin_family = AF_INET;
ds2->sin_addr.s_addr = htonl(k_dns2);
dns2->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds2);
dns2->Address.iSockaddrLength = sizeof(*ds2);
dns2->Next = nullptr;
dns->Next = dns2;
a->FirstDnsServerAddress = dns;
a->Next = nullptr;
*SizePointer = need;
return ERROR_SUCCESS;
}
constexpr uint32_t AI_MAGIC = 0x4E535041u; /* 'NSPA' */
struct SpoofAiHdr {
uint32_t magic;
ADDRINFOA ai;
};
SpoofAiHdr *ai_hdr_from_ai(ADDRINFOA *ai) {
SpoofAiHdr *h = nullptr;
if (!ai) {
return nullptr;
}
h = reinterpret_cast<SpoofAiHdr *>(
reinterpret_cast<BYTE *>(ai) - offsetof(SpoofAiHdr, ai));
if (h->magic != AI_MAGIC) {
return nullptr;
}
return h;
}
/* Wine packed getaddrinfo: sockaddr lives immediately after addrinfo. */
bool ai_is_wine_packed(ADDRINFOA *ai) {
BYTE *p = reinterpret_cast<BYTE *>(ai);
BYTE *addr = nullptr;
if (!ai || !ai->ai_addr) {
return false;
}
addr = reinterpret_cast<BYTE *>(ai->ai_addr);
return addr >= p + sizeof(*ai) &&
addr < p + sizeof(*ai) + sizeof(SOCKADDR_STORAGE) + 64;
}
/*
* Dual-safe free for Wine + native Windows:
* - Our results: magic header + separate mallocs for addr/canon.
* - Wine packed foreign: single free(ai).
* - Else (native Windows foreign): free canon, addr, node per link.
*/
void WSAAPI freeaddrinfo_hook(PADDRINFOA ai) {
while (ai) {
ADDRINFOA *next = ai->ai_next;
SpoofAiHdr *hdr = ai_hdr_from_ai(ai);
if (hdr) {
free(ai->ai_canonname);
free(ai->ai_addr);
free(hdr);
} else if (ai_is_wine_packed(ai)) {
free(ai);
break;
} else {
free(ai->ai_canonname);
free(ai->ai_addr);
free(ai);
}
ai = next;
}
}
INT WSAAPI getaddrinfo_hook(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult) {
SpoofAiHdr *hdr = nullptr;
ADDRINFOA *ai = nullptr;
sockaddr_in *sa = nullptr;
char *canon = nullptr;
const char *name_src = nullptr;
size_t name_len = 0;
uint32_t ip = 0;
int port = 0;
if (!ppResult) {
return EAI_FAIL;
}
*ppResult = nullptr;
if (pHints && pHints->ai_family == AF_INET6) {
return EAI_FAMILY;
}
if (!pNodeName || !pNodeName[0] || name_is_local(pNodeName)) {
if (pNodeName && (_stricmp(pNodeName, k_hostname) == 0 ||
_stricmp(pNodeName, k_hostname_fqdn) == 0)) {
ip = g_local_ip;
} else {
ip = 0x7F000001u;
}
} else if (name_is_numeric_ipv4(pNodeName)) {
ip = parse_ipv4(pNodeName);
} else {
/* offline=1: map external names to local_ip */
ip = g_local_ip;
}
if (pServiceName && pServiceName[0]) {
port = atoi(pServiceName);
if (port < 0 || port > 65535) {
port = 0;
}
}
name_src = (pNodeName && pNodeName[0]) ? pNodeName : "localhost";
name_len = strlen(name_src) + 1;
if (name_len > 256) {
name_len = 256;
}
hdr = static_cast<SpoofAiHdr *>(calloc(1, sizeof(*hdr)));
sa = static_cast<sockaddr_in *>(calloc(1, sizeof(*sa)));
canon = static_cast<char *>(malloc(name_len));
if (!hdr || !sa || !canon) {
free(hdr);
free(sa);
free(canon);
return EAI_MEMORY;
}
hdr->magic = AI_MAGIC;
ai = &hdr->ai;
memcpy(canon, name_src, name_len - 1);
canon[name_len - 1] = 0;
sa->sin_family = AF_INET;
sa->sin_addr.s_addr = htonl(ip);
if (port > 0) {
sa->sin_port = htons(static_cast<u_short>(port));
}
ai->ai_family = AF_INET;
ai->ai_socktype = pHints && pHints->ai_socktype ?
pHints->ai_socktype : SOCK_STREAM;
ai->ai_protocol = pHints && pHints->ai_protocol ?
pHints->ai_protocol : IPPROTO_TCP;
ai->ai_addrlen = sizeof(*sa);
ai->ai_addr = reinterpret_cast<struct sockaddr *>(sa);
ai->ai_canonname = canon;
ai->ai_next = nullptr;
*ppResult = ai;
return 0;
}
void install_nicspoof_hooks() {
static bool done = false;
if (done) {
return;
}
done = true;
char ipstr[16];
char maskstr[16];
char gwstr[16];
ip_to_str(g_local_ip, ipstr, sizeof(ipstr));
ip_to_str(k_mask, maskstr, sizeof(maskstr));
ip_to_str(k_gateway, gwstr, sizeof(gwstr));
log_info("network",
"NIC spoof enabled ip={} mask={}/{} gw={} "
"mac={:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X} host={} ifindex={}",
ipstr, maskstr, k_prefix_len, gwstr,
static_cast<unsigned>(k_fake_mac[0]),
static_cast<unsigned>(k_fake_mac[1]),
static_cast<unsigned>(k_fake_mac[2]),
static_cast<unsigned>(k_fake_mac[3]),
static_cast<unsigned>(k_fake_mac[4]),
static_cast<unsigned>(k_fake_mac[5]),
k_hostname, static_cast<unsigned>(k_ifindex));
bool ok = true;
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetAdaptersAddresses",
(void *) GetAdaptersAddresses_hook,
(void **) &GetAdaptersAddresses_orig);
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetAdaptersInfo",
(void *) GetAdaptersInfo_hook,
(void **) &GetAdaptersInfo_orig);
ok &= detour::trampoline_try(
"iphlpapi.dll", "GetNetworkParams",
(void *) GetNetworkParams_hook,
(void **) &GetNetworkParams_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "getaddrinfo",
(void *) getaddrinfo_hook,
(void **) &getaddrinfo_orig);
ok &= detour::trampoline_try(
"ws2_32.dll", "freeaddrinfo",
(void *) freeaddrinfo_hook,
(void **) &freeaddrinfo_orig);
{
HMODULE ws = GetModuleHandleA("ws2_32.dll");
void *faa = nullptr;
void *fai = nullptr;
if (ws) {
faa = reinterpret_cast<void *>(GetProcAddress(ws, "FreeAddrInfoA"));
fai = reinterpret_cast<void *>(GetProcAddress(ws, "freeaddrinfo"));
}
if (faa && faa != fai) {
decltype(freeaddrinfo) *FreeAddrInfoA_orig = nullptr;
if (!detour::trampoline_try(
"ws2_32.dll", "FreeAddrInfoA",
(void *) freeaddrinfo_hook,
(void **) &FreeAddrInfoA_orig)) {
log_warning("network",
"NIC spoof: FreeAddrInfoA hook was not installed");
}
}
}
if (!ok) {
log_warning("network",
"NIC spoof: one or more hooks failed to install");
} else {
log_info("network",
"NIC spoof hooks installed "
"(GAA/GAI/GNP/getaddrinfo/freeaddrinfo)");
}
}
bool ip_in_overlay_range(uint32_t ip) {
const uint32_t net = k_gateway & k_mask;
const uint32_t bcast = net | ~k_mask;
if ((ip & k_mask) != net) {
return false;
}
if (ip == net || ip == bcast || ip == k_gateway ||
ip == k_dns1 || ip == k_dns2) {
return false;
}
return true;
}
void init_impl() {
if (g_cfg.mode == NicSpoofMode::Off) {
return;
}
g_local_ip = g_cfg.local_ip ? g_cfg.local_ip : k_default_local_ip;
if (!ip_in_overlay_range(g_local_ip)) {
char bad[16];
ip_to_str(g_local_ip, bad, sizeof(bad));
log_warning("network",
"NIC spoof: IP {} is outside 10.0.0.0/8 usable range; "
"using 10.100.100.10",
bad);
g_local_ip = k_default_local_ip;
}
g_subnet = g_local_ip & k_mask;
const char *mode_str = "offline";
if (g_cfg.mode == NicSpoofMode::TunnelHost) {
mode_str = "tunnelhost";
} else if (g_cfg.mode == NicSpoofMode::TunnelClient) {
mode_str = "tunnelclient";
}
log_info("network", "NIC spoof mode={}", mode_str);
install_nicspoof_hooks();
if (g_cfg.mode == NicSpoofMode::TunnelHost ||
g_cfg.mode == NicSpoofMode::TunnelClient) {
nicspoof_tunnel_init(g_cfg);
}
}
} // namespace nicspoof_detail
void nicspoof_configure(const NicSpoofConfig &cfg) {
nicspoof_detail::g_cfg = cfg;
}
bool nicspoof_tunnel_enabled() {
return nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelHost ||
nicspoof_detail::g_cfg.mode == NicSpoofMode::TunnelClient;
}
uint32_t nicspoof_local_ip() {
return nicspoof_detail::g_local_ip;
}
uint32_t nicspoof_mask() {
return nicspoof_detail::k_mask;
}
uint32_t nicspoof_subnet() {
return nicspoof_detail::g_subnet;
}
void nicspoof_init() {
nicspoof_detail::init_impl();
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <cstdint>
#include <string>
enum class NicSpoofMode {
Off,
Offline,
TunnelHost,
TunnelClient,
};
struct NicSpoofConfig {
NicSpoofMode mode = NicSpoofMode::Off;
uint32_t local_ip = 0x0A64640Au; /* 10.100.100.10 */
uint16_t tunnel_port = 51820;
std::string hub_host;
};
void nicspoof_configure(const NicSpoofConfig &cfg);
void nicspoof_init();
/*! True when mode is TunnelHost or TunnelClient (divert hooks own ws2 sendto/etc.). */
bool nicspoof_tunnel_enabled();
uint32_t nicspoof_local_ip();
uint32_t nicspoof_mask();
uint32_t nicspoof_subnet();
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
#pragma once
#include "nicspoof.h"
void nicspoof_tunnel_init(const NicSpoofConfig &cfg);
+87 -3
View File
@@ -4,6 +4,7 @@
#include <vector>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <shlwapi.h>
#include <windows.h>
@@ -71,6 +72,7 @@
#include "games/qks/qks.h"
#include "games/mfg/mfg.h"
#include "games/pc/pc.h"
#include "games/udn/udn.h"
#include "games/museca/museca.h"
#include "hooks/avshook.h"
#include "hooks/audio/audio.h"
@@ -84,6 +86,7 @@
#include "hooks/lang.h"
#include "hooks/networkhook.h"
#include "hooks/icmphook_net.h"
#include "hooks/nicspoof.h"
#include "hooks/unisintrhook.h"
#include "launcher/launcher.h"
#include "launcher/logger.h"
@@ -246,6 +249,7 @@ int main_implementation(int argc, char *argv[]) {
bool attach_qks = false;
bool attach_mfg = false;
bool attach_pc = false;
bool attach_udn = false;
bool attach_museca = false;
bool show_cursor_if_no_touch = false;
@@ -257,6 +261,7 @@ int main_implementation(int argc, char *argv[]) {
bool load_stubs = false;
bool netfix_disable = false;
bool icmphook_enable = false;
NicSpoofConfig nicspoof_cfg;
bool lang_disable = false;
std::string process_priority_str = "high";
bool cardio_enabled = false;
@@ -671,6 +676,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();
}
@@ -771,6 +786,9 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::LoadPCModule].value_bool()) {
attach_pc = true;
}
if (options[launcher::Options::LoadUDNModule].value_bool()) {
attach_udn = true;
}
if (options[launcher::Options::LoadMusecaModule].value_bool()) {
attach_museca = true;
}
@@ -789,6 +807,44 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::EnableICMPHook].value_bool()) {
icmphook_enable = true;
}
if (options[launcher::Options::EnableNICSpoof].is_active()) {
const auto &mode = options[launcher::Options::EnableNICSpoof].value_text();
if (mode == "offline" || mode == "/ENABLED") {
nicspoof_cfg.mode = NicSpoofMode::Offline;
} else if (mode == "tunnelhost") {
nicspoof_cfg.mode = NicSpoofMode::TunnelHost;
} else if (mode == "tunnelclient") {
nicspoof_cfg.mode = NicSpoofMode::TunnelClient;
} else {
log_warning("launcher", "unknown -nicspoof value '{}', ignoring", mode);
}
}
if (nicspoof_cfg.mode != NicSpoofMode::Off) {
if (options[launcher::Options::NICSpoofIP].is_active()) {
unsigned a = 0, b = 0, c = 0, d = 0;
char trail = 0;
const auto &ip = options[launcher::Options::NICSpoofIP].value_text();
if (sscanf(ip.c_str(), "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) == 4 &&
a <= 255 && b <= 255 && c <= 255 && d <= 255) {
nicspoof_cfg.local_ip = (a << 24) | (b << 16) | (c << 8) | d;
} else {
log_warning("launcher", "invalid -nicspoofip '{}'", ip);
}
}
if (options[launcher::Options::NICSpoofHostRealIP].is_active()) {
nicspoof_cfg.hub_host =
options[launcher::Options::NICSpoofHostRealIP].value_text();
}
if (options[launcher::Options::NICSpoofPort].is_active()) {
const uint32_t p =
options[launcher::Options::NICSpoofPort].value_uint32();
if (p == 0 || p > 65535) {
log_warning("launcher", "invalid -nicspoofport {}", p);
} else {
nicspoof_cfg.tunnel_port = static_cast<uint16_t>(p);
}
}
}
if (options[launcher::Options::DisableACPHook].value_bool()) {
lang_disable = true;
}
@@ -1438,6 +1494,12 @@ int main_implementation(int argc, char *argv[]) {
if (options[launcher::Options::PCKnobMode].value_bool()) {
games::pc::PC_KNOB_MODE = true;
}
if (options[launcher::Options::UDNArgs].is_active()) {
games::udn::UDN_INJECT_ARGS = options[launcher::Options::UDNArgs].value_text();
}
if (options[launcher::Options::UDNNoIO].is_active()) {
games::udn::UDN_NO_IO = options[launcher::Options::UDNNoIO].value_bool();
}
if (options[launcher::Options::spice2x_EnableSMXStage].value_bool()) {
rawinput::ENABLE_SMX_STAGE = true;
}
@@ -2252,6 +2314,16 @@ int main_implementation(int argc, char *argv[]) {
break;
}
// DANCE aROUND
if (check_dll("kamunity.dll") && fileutils::dir_exists("game/dancearound_Data")) {
avs::game::DLL_NAME = "kamunity.dll";
attach_io = true;
attach_udn = true;
launcher::signal::USE_VEH_WORKAROUND = true;
show_cursor_if_no_touch = true;
break;
}
// Busou Shinki: Armored Princess Battle Conductor
if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) {
avs::game::DLL_NAME = "kamunity.dll";
@@ -2417,6 +2489,10 @@ int main_implementation(int argc, char *argv[]) {
avs::core::HEAP_SIZE = 536870912; // 512MB
games.push_back(new games::pc::PCGame());
}
if (attach_udn) {
avs::core::HEAP_SIZE = 1073741824; // 1GB
games.push_back(new games::udn::UDNGame());
}
// apply user heap size, if defined
if (user_heap_size > 0) {
@@ -2483,7 +2559,6 @@ int main_implementation(int argc, char *argv[]) {
// initialize raw input
RI_MGR = std::make_unique<rawinput::RawInputManager>();
hotkeys::enable_raw_input();
for (const auto &device : sextet_devices) {
RI_MGR->sextet_register(device);
}
@@ -2507,6 +2582,7 @@ int main_implementation(int argc, char *argv[]) {
dump_analog_bindings();
// mappings are ready; begin screenshot and coin polling during late startup
hotkeys::enable_raw_input();
hotkeys::enable_input();
// for certain games, show cursor if no touch is available (must be called after RI_MGR is available)
@@ -2543,7 +2619,15 @@ int main_implementation(int argc, char *argv[]) {
avs::core::load_dll();
avs::ea3::load_dll();
// ICMP emulation (opt-in; before games open raw ICMP sockets)
// NIC spoof / matching tunnel first so divert owns overlapping ws2_32
// MinHook slots (bind/sendto/recvfrom/...). ICMP then installs only the
// non-overlapping socket-creation hooks and is reached via icmphook_try_*.
if (nicspoof_cfg.mode != NicSpoofMode::Off) {
nicspoof_configure(nicspoof_cfg);
nicspoof_init();
}
// ICMP emulation (opt-in; after tunnel so hooks do not collide)
if (icmphook_enable) {
icmphook_net_init();
}
@@ -2849,7 +2933,7 @@ int main_implementation(int argc, char *argv[]) {
bt5api_dispose();
}
sdk::fini_sdk_modules();
sdk::fini_sdk_modules(true);
// stop raw input
hotkeys::disable_raw_input();
File diff suppressed because it is too large Load Diff
+11 -1
View File
@@ -122,6 +122,7 @@ namespace launcher {
GitaDoraSubOverlaySize,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraArenaSubLayout,
GitaDoraArenaAsioDriver,
GitaDoraArenaRealtekAccess,
LoadJubeatModule,
@@ -149,6 +150,7 @@ namespace launcher {
LoadQKSModule,
LoadMFGModule,
LoadPCModule,
LoadUDNModule,
LoadMusecaModule,
PathToModules,
ScreenshotFolder,
@@ -249,6 +251,8 @@ namespace launcher {
PCArgs,
PCNoIO,
PCKnobMode,
UDNArgs,
UDNNoIO,
spice2x_LightsOverallBrightness,
spice2x_WindowBorder,
spice2x_WindowSize,
@@ -315,6 +319,10 @@ namespace launcher {
OtocaCamHook,
DisableHighResTimer,
EnableICMPHook,
EnableNICSpoof,
NICSpoofIP,
NICSpoofHostRealIP,
NICSpoofPort,
AutoElevate,
CfgForceSoftwareRender,
OBSWebSocketEnabled,
@@ -323,7 +331,9 @@ namespace launcher {
OBSWebSocketPassword,
OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens
ScreenshotSubscreens,
_Count,
};
enum class OptionsCategory {
+49 -4
View File
@@ -1402,10 +1402,50 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Contributions
Wslay (MIT)
-------------------------------------------
cardio - Felix - MIT License
scard - nolm - MIT License
Copyright (c) 2011, 2012, 2015 Tatsuhiro Tsujikawa
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Wslay UTF-8 decoder (MIT)
-------------------------------------------
Copyright (c) 2008-2010 Bjoern Hoehrmann <bjoern@hoehrmann.de>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
x264 (GPL-2.0-or-later)
-------------------------------------------
@@ -1425,4 +1465,9 @@ FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Street, Fifth Floor, Boston, MA 02110-1301, USA.
Contributions
-------------------------------------------
cardio - Felix - MIT License
scard - nolm - MIT License
+7 -3
View File
@@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) {
return 1;
}
static int __cdecl device_set_coinblocker_open(char number, char open) {
return 0;
static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) {
// libdevice exposes one open flag per coin slot. Since spice has a shared
// software coin stock, consider it blocked only when both slots are closed.
eamuse_coin_set_block(!slot1_open && !slot2_open);
}
static void __cdecl device_set_coincounter_merge() {
@@ -617,7 +619,9 @@ void spicedevice_attach() {
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock", "?device_get_coinstock@@YAXPEAG0@Z"}));
DEVICE_INSTANCE, {"device_get_coinstock",
"?device_get_coinstock@@YAXPEAG0@Z",
"?device_get_coinstock@@YAXPAG0@Z"}));
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock_all",
"?device_get_coinstock_all@@YAXPEAG0@Z",
+34 -16
View File
@@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
static std::string EAMUSE_GAME_NAME;
static ConfigKeypadBindings KEYPAD_BINDINGS {};
static std::mutex KEYPAD_BINDINGS_LOCK;
// auto card
bool AUTO_INSERT_CARD[2] = {false, false};
@@ -87,23 +88,23 @@ bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
return true;
}
// get file path
std::filesystem::path path;
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
path = KEYPAD_BINDINGS.card_paths[index];
} else {
path = index > 0 ? "card1.txt" : "card0.txt";
}
// call the next function
return eamuse_get_card(path, card, index);
return eamuse_get_card(eamuse_get_card_path(index), card, index);
}
std::filesystem::path eamuse_get_card_path(size_t index) {
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
if (index >= std::size(KEYPAD_BINDINGS.card_paths)) {
return {};
}
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
return KEYPAD_BINDINGS.card_paths[index];
}
return index > 0 ? "card1.txt" : "card0.txt";
}
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
// do a quick copy under lock
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
const auto card_override = CARD_OVERRIDES[index];
lock.unlock();
const auto card_override = eamuse_get_card_override(index);
// Check if card overrides are present
if (!card_override.empty()) {
@@ -149,6 +150,14 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
return eamuse_get_card_from_file(path, card, index);
}
std::string eamuse_get_card_override(size_t index) {
std::lock_guard<std::mutex> lock(CARD_OVERRIDES_LOCK);
if (index >= std::size(CARD_OVERRIDES)) {
return {};
}
return CARD_OVERRIDES[index];
}
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
// open file
@@ -322,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() {
@@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
}
bool eamuse_keypad_state_naive() {
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
}
@@ -595,7 +605,9 @@ void eamuse_set_game(std::string game) {
}
void eamuse_update_keypad_bindings() {
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
KEYPAD_BINDINGS = std::move(bindings);
}
const std::string &eamuse_get_game() {
@@ -609,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") ||
@@ -625,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;
@@ -718,6 +732,10 @@ void eamuse_autodetect_game() {
eamuse_set_game("Mahjong Fight Girl");
else if (avs::game::is_model("XIF"))
eamuse_set_game("Polaris Chord");
else if (avs::game::is_model("UDN"))
eamuse_set_game("DANCE aROUND");
else if (avs::game::is_model("NDD"))
eamuse_set_game("Silent Scope: Bone Eater");
else {
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
eamuse_set_game("unknown");

Some files were not shown because too many files have changed in this diff Show More