mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-01 06:24:55 -07:00
Compare commits
26 Commits
1cdb7be810
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7aba7da3b6 | |||
| 197d76755b | |||
| 2b6e76730b | |||
| 4042b77065 | |||
| 4c77a3f148 | |||
| 603fbfc69d | |||
| 5e4afecd90 | |||
| 531e7a54b3 | |||
| f11b9d0189 | |||
| 112b1aecb0 | |||
| de466db29f | |||
| 08e99dea4a | |||
| 00e904539e | |||
| eaab4698eb | |||
| a48eccab93 | |||
| 81c913726d | |||
| 0169fe1e31 | |||
| 357e6c86f1 | |||
| 67963389ef | |||
| 4009bb8a84 | |||
| 8fb6d94000 | |||
| 8d3685415d | |||
| 50b38b856f | |||
| dad88caf01 | |||
| 699659d4bf | |||
| 21e7d24ed3 |
@@ -29,4 +29,33 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||||
path: src/spice2x/bin
|
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
@@ -105,5 +105,5 @@ if (conditional)
|
|||||||
* We don't use GSL.
|
* We don't use GSL.
|
||||||
* Don't throw exceptions.
|
* Don't throw exceptions.
|
||||||
* Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`.
|
* 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.
|
* Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying.
|
||||||
|
|||||||
@@ -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.
|
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
|
## Additional information
|
||||||
|
|
||||||
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
|
||||||
|
|||||||
@@ -520,6 +520,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/scotto/io.cpp
|
games/scotto/io.cpp
|
||||||
games/drs/drs.cpp
|
games/drs/drs.cpp
|
||||||
games/drs/io.cpp
|
games/drs/io.cpp
|
||||||
|
games/drs/motion_cam.cpp
|
||||||
games/drs/rgb_cam.cpp
|
games/drs/rgb_cam.cpp
|
||||||
games/we/we.cpp
|
games/we/we.cpp
|
||||||
games/we/io.cpp
|
games/we/io.cpp
|
||||||
@@ -531,6 +532,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/otoca/p4io.cpp
|
games/otoca/p4io.cpp
|
||||||
games/silentscope/silentscope.cpp
|
games/silentscope/silentscope.cpp
|
||||||
games/silentscope/io.cpp
|
games/silentscope/io.cpp
|
||||||
|
games/silentscope/projector.cpp
|
||||||
games/pcm/pcm.cpp
|
games/pcm/pcm.cpp
|
||||||
games/pcm/io.cpp
|
games/pcm/io.cpp
|
||||||
games/onpara/onpara.cpp
|
games/onpara/onpara.cpp
|
||||||
@@ -552,6 +554,9 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
games/pc/pc.cpp
|
games/pc/pc.cpp
|
||||||
games/pc/io.cpp
|
games/pc/io.cpp
|
||||||
games/pc/bi2x_hook.cpp
|
games/pc/bi2x_hook.cpp
|
||||||
|
games/udn/udn.cpp
|
||||||
|
games/udn/io.cpp
|
||||||
|
games/udn/bi2x_hook.cpp
|
||||||
|
|
||||||
# hooks
|
# hooks
|
||||||
hooks/audio/acm.cpp
|
hooks/audio/acm.cpp
|
||||||
@@ -603,6 +608,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
hooks/graphics/backends/d3d9/d3d9_texture.cpp
|
||||||
hooks/graphics/backends/d3d11/d3d11_backend.cpp
|
hooks/graphics/backends/d3d11/d3d11_backend.cpp
|
||||||
hooks/graphics/backends/d3d11/d3d11_swapchain.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_factory.cpp
|
||||||
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
|
hooks/graphics/backends/d3d11/d3d11_vtable_capture.cpp
|
||||||
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
|
hooks/graphics/backends/d3d11/d3d11_screenshot.cpp
|
||||||
@@ -614,6 +620,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
hooks/networkhook.cpp
|
hooks/networkhook.cpp
|
||||||
hooks/icmphook_net.cpp
|
hooks/icmphook_net.cpp
|
||||||
hooks/icmphook_iphlpapi.cpp
|
hooks/icmphook_iphlpapi.cpp
|
||||||
|
hooks/nicspoof.cpp
|
||||||
|
hooks/nicspoof_tunnel.cpp
|
||||||
hooks/powrprof.cpp
|
hooks/powrprof.cpp
|
||||||
#hooks/rom.cpp
|
#hooks/rom.cpp
|
||||||
hooks/setupapihook.cpp
|
hooks/setupapihook.cpp
|
||||||
@@ -715,6 +723,8 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
|||||||
reader/crypt.cpp
|
reader/crypt.cpp
|
||||||
|
|
||||||
# sdk
|
# sdk
|
||||||
|
sdk/d3d9.cpp
|
||||||
|
sdk/modules.cpp
|
||||||
sdk/sdk.cpp
|
sdk/sdk.cpp
|
||||||
|
|
||||||
# stubs
|
# stubs
|
||||||
@@ -1043,8 +1053,16 @@ if(NOT MSVC)
|
|||||||
endif()
|
endif()
|
||||||
|
|
||||||
# sdk_sample_v0_cpp.dll (64 bit)
|
# 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)
|
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 PREFIX "")
|
||||||
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
|
set_target_properties(spicetools_sdk_sample_v0_cpp_64 PROPERTIES OUTPUT_NAME "sdk_sample_v0_cpp")
|
||||||
|
|
||||||
|
|||||||
@@ -144,6 +144,13 @@ restriction is that the ID has to be a valid 64-bit unsigned integer.
|
|||||||
`busy`
|
`busy`
|
||||||
|
|
||||||
#### Card
|
#### 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)
|
- insert(index: uint, card_id: hex)
|
||||||
- inserts a card which gets read by the emulated card readers for the game
|
- 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)
|
- index has to be either 0 (for P1) or 1 (for P2)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#include "bmpu.h"
|
#include "bmpu.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "acio/icca/icca.h"
|
#include "acio/icca/icca.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "cfg/api.h"
|
#include "cfg/api.h"
|
||||||
@@ -9,6 +11,7 @@
|
|||||||
#include "games/ftt/io.h"
|
#include "games/ftt/io.h"
|
||||||
#include "games/museca/io.h"
|
#include "games/museca/io.h"
|
||||||
#include "games/silentscope/io.h"
|
#include "games/silentscope/io.h"
|
||||||
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
|
|
||||||
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
|
|||||||
STATUS_BUFFER[4] |= 0x20;
|
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 &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_x = 0x7FFF;
|
||||||
unsigned short joy_y = 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 (gun_x.isSet() || gun_y.isSet()) {
|
||||||
}
|
if (gun_x.isSet()) {
|
||||||
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
|
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
|
||||||
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * 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
|
// invert X axis
|
||||||
|
|||||||
@@ -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 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]);
|
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...
|
// 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
|
// 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
|
// 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)) {
|
if (need_felica_retries && (card_presented || key_pressed)) {
|
||||||
unit->felica_retries = 10;
|
unit->felica_retries = 10;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "client.h"
|
#include "client.h"
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
|
#include "external/rapidjson/error/en.h"
|
||||||
#include "util/crypt.h"
|
#include "util/crypt.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -92,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
|
|||||||
// bind socket to address
|
// bind socket to address
|
||||||
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
|
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
|
||||||
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
|
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
|
||||||
|
closesocket(this->server);
|
||||||
this->server = INVALID_SOCKET;
|
this->server = INVALID_SOCKET;
|
||||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
log_fatal("api", "failed to start server");
|
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
|
// set socket to listen
|
||||||
if (listen(this->server, server_backlog) == -1) {
|
if (listen(this->server, server_backlog) == -1) {
|
||||||
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
|
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
|
||||||
|
closesocket(this->server);
|
||||||
this->server = INVALID_SOCKET;
|
this->server = INVALID_SOCKET;
|
||||||
if (!cfg::CONFIGURATOR_STANDALONE) {
|
if (!cfg::CONFIGURATOR_STANDALONE) {
|
||||||
log_fatal("api", "failed to start server");
|
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
|
// check for parse error
|
||||||
if (document.HasParseError()) {
|
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
|
// return empty response and close connection
|
||||||
out->push_back(0);
|
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) {
|
if (module->name == request.module) {
|
||||||
module_found = true;
|
module_found = true;
|
||||||
|
|
||||||
// check password force
|
// check password requirement
|
||||||
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
|
if (module->requires_password(request.function)
|
||||||
Value err("Module requires the password to be set.");
|
&& this->password.empty()
|
||||||
|
&& request.function != "session_refresh") {
|
||||||
|
Value err("Function requires the password to be set.");
|
||||||
response.add_error(err);
|
response.add_error(err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -441,7 +448,9 @@ void Controller::free_socket() {
|
|||||||
this->server = INVALID_SOCKET;
|
this->server = INVALID_SOCKET;
|
||||||
}
|
}
|
||||||
|
|
||||||
this->websocket->free_socket();
|
if (this->websocket) {
|
||||||
|
this->websocket->free_socket();
|
||||||
|
}
|
||||||
|
|
||||||
for (auto &s : this->serial) {
|
for (auto &s : this->serial) {
|
||||||
s->free_port();
|
s->free_port();
|
||||||
|
|||||||
@@ -41,21 +41,21 @@ namespace api {
|
|||||||
bool pretty;
|
bool pretty;
|
||||||
|
|
||||||
// server
|
// server
|
||||||
WebSocketController *websocket;
|
WebSocketController *websocket = nullptr;
|
||||||
std::vector<SerialController *> serial;
|
std::vector<SerialController *> serial;
|
||||||
std::vector<std::thread> server_workers;
|
std::vector<std::thread> server_workers;
|
||||||
std::vector<std::thread> server_handlers;
|
std::vector<std::thread> server_handlers;
|
||||||
std::mutex server_handlers_m;
|
std::mutex server_handlers_m;
|
||||||
std::vector<api::ClientState *> client_states;
|
std::vector<api::ClientState *> client_states;
|
||||||
std::mutex client_states_m;
|
std::mutex client_states_m;
|
||||||
SOCKET server;
|
SOCKET server = INVALID_SOCKET;
|
||||||
void server_worker();
|
void server_worker();
|
||||||
void connection_handler(ClientState client_state);
|
void connection_handler(ClientState client_state);
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
// state
|
// state
|
||||||
bool server_running;
|
bool server_running = false;
|
||||||
|
|
||||||
// constructor / destructor
|
// constructor / destructor
|
||||||
Controller(unsigned short port, std::string password, bool pretty);
|
Controller(unsigned short port, std::string password, bool pretty);
|
||||||
|
|||||||
@@ -118,7 +118,13 @@ namespace api {
|
|||||||
param.i_height = height;
|
param.i_height = height;
|
||||||
param.i_fps_num = this->fps;
|
param.i_fps_num = this->fps;
|
||||||
param.i_fps_den = 1;
|
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;
|
param.b_annexb = 1;
|
||||||
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
||||||
param.b_repeat_headers = 1;
|
param.b_repeat_headers = 1;
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ namespace api {
|
|||||||
this->password_force = password_force;
|
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) {
|
void Module::handle(Request &req, Response &res) {
|
||||||
|
|
||||||
// log module access
|
// log module access
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <unordered_set>
|
||||||
#include <external/robin_hood.h>
|
#include <external/robin_hood.h>
|
||||||
|
|
||||||
#include "response.h"
|
#include "response.h"
|
||||||
@@ -26,6 +27,13 @@ namespace api {
|
|||||||
// default constructor
|
// default constructor
|
||||||
explicit Module(std::string name, bool password_force=false);
|
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:
|
public:
|
||||||
|
|
||||||
// virtual deconstructor
|
// virtual deconstructor
|
||||||
@@ -35,6 +43,8 @@ namespace api {
|
|||||||
std::string name;
|
std::string name;
|
||||||
bool password_force;
|
bool password_force;
|
||||||
|
|
||||||
|
bool requires_password(const std::string &function) const;
|
||||||
|
|
||||||
// the magic
|
// the magic
|
||||||
void handle(Request &req, Response &res);
|
void handle(Request &req, Response &res);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "card.h"
|
#include "card.h"
|
||||||
|
#include <fstream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -11,8 +12,71 @@ using namespace rapidjson;
|
|||||||
|
|
||||||
namespace api::modules {
|
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") {
|
Card::Card() : Module("card") {
|
||||||
|
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
|
||||||
functions["insert"] = std::bind(&Card::insert, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
|||||||
private:
|
private:
|
||||||
|
|
||||||
// function definitions
|
// function definitions
|
||||||
|
void get_cards(Request &req, Response &res);
|
||||||
void insert(Request &req, Response &res);
|
void insert(Request &req, Response &res);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ namespace api::modules {
|
|||||||
return error_type(res, "amount", "int");
|
return error_type(res, "amount", "int");
|
||||||
|
|
||||||
// add to coin stock
|
// 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()));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
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) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
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) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ from .connection import Connection
|
|||||||
from .request import Request
|
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):
|
def card_insert(con: Connection, unit: int, card_id: str):
|
||||||
req = Request("card", "insert")
|
req = Request("card", "insert")
|
||||||
req.add_param(unit)
|
req.add_param(unit)
|
||||||
|
|||||||
@@ -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"
|
"\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"
|
"\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");
|
"\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(
|
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"
|
"\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");
|
"\xc8\x9d\xb5\xfb\xef\x97");
|
||||||
|
|||||||
Vendored
+7
-3
@@ -50,6 +50,7 @@ static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u };
|
|||||||
enum scard_atr_protocol {
|
enum scard_atr_protocol {
|
||||||
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
|
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
|
||||||
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
|
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
|
||||||
|
SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C,
|
||||||
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
|
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
|
||||||
SCARD_ATR_PROTOCOL_FELICA_424K = 0x12,
|
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];
|
BYTE cardProtocol = atr[12];
|
||||||
BOOL shouldReverseUid = false;
|
BOOL shouldReverseUid = false;
|
||||||
bool is_felica = 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");
|
log_info("scard", "card protocol: ISO15693_PART3");
|
||||||
shouldReverseUid = true;
|
shouldReverseUid = true;
|
||||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
|
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART4) {
|
||||||
log_info("scard", "card protocol: ISO14443_PART3");
|
log_info("scard", "card protocol: ISO15693_PART4");
|
||||||
|
shouldReverseUid = true;
|
||||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
|
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
|
||||||
log_info("scard", "card protocol: FELICA_212K");
|
log_info("scard", "card protocol: FELICA_212K");
|
||||||
is_felica = true;
|
is_felica = true;
|
||||||
|
|||||||
@@ -9,6 +9,8 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/precise_timer.h"
|
#include "util/precise_timer.h"
|
||||||
#include "util/memutils.h"
|
#include "util/memutils.h"
|
||||||
|
#include "io.h"
|
||||||
|
#include "motion_cam.h"
|
||||||
#include "rgb_cam.h"
|
#include "rgb_cam.h"
|
||||||
|
|
||||||
#pragma pack(push)
|
#pragma pack(push)
|
||||||
@@ -276,48 +278,50 @@ namespace games::drs {
|
|||||||
void DRSGame::attach() {
|
void DRSGame::attach() {
|
||||||
Game::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) {
|
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();
|
start_touch();
|
||||||
} else {
|
} 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) {
|
if (RGB_CAMERA_HOOK) {
|
||||||
init_rgb_camera_hook();
|
init_rgb_camera_hook();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
|
|||||||
"P2 Up",
|
"P2 Up",
|
||||||
"P2 Down",
|
"P2 Down",
|
||||||
"P2 Left",
|
"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() {
|
std::string games::drs::get_buttons_help() {
|
||||||
// keep to max 100 characters wide
|
// keep to max 100 characters wide
|
||||||
return
|
return
|
||||||
"Motion camera required for DOWN movement.\n"
|
"Touchscreen supported for dance floor.\n"
|
||||||
"Touchscreen supported for dance floor."
|
"Down Motion applies to both players."
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ namespace games::drs {
|
|||||||
P2_Up,
|
P2_Up,
|
||||||
P2_Down,
|
P2_Down,
|
||||||
P2_Left,
|
P2_Left,
|
||||||
P2_Right
|
P2_Right,
|
||||||
|
DownMotion
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::drs {
|
||||||
|
void init_down_motion_hook();
|
||||||
|
}
|
||||||
@@ -39,10 +39,38 @@ namespace games::gitadora {
|
|||||||
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
|
||||||
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
|
||||||
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
|
||||||
|
ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
|
||||||
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
std::optional<std::string> ASIO_DRIVER = std::nullopt;
|
||||||
bool ALLOW_REALTEK_AUDIO = false;
|
bool ALLOW_REALTEK_AUDIO = false;
|
||||||
bool NATIVE_TOUCH = 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
|
* Prevent GitaDora from creating folders on F drive
|
||||||
*/
|
*/
|
||||||
@@ -343,6 +371,23 @@ namespace games::gitadora {
|
|||||||
"gitadora",
|
"gitadora",
|
||||||
"arena model: unsupported window count: {}", count);
|
"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
|
#endif
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <mmreg.h>
|
#include <mmreg.h>
|
||||||
@@ -11,6 +12,14 @@
|
|||||||
|
|
||||||
namespace games::gitadora {
|
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
|
// settings
|
||||||
extern bool TWOCHANNEL;
|
extern bool TWOCHANNEL;
|
||||||
extern bool DISABLE_FRAME_LIMITER;
|
extern bool DISABLE_FRAME_LIMITER;
|
||||||
@@ -21,14 +30,35 @@ namespace games::gitadora {
|
|||||||
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
|
||||||
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
|
||||||
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
|
||||||
|
extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE;
|
||||||
extern std::optional<std::string> ASIO_DRIVER;
|
extern std::optional<std::string> ASIO_DRIVER;
|
||||||
extern bool ALLOW_REALTEK_AUDIO;
|
extern bool ALLOW_REALTEK_AUDIO;
|
||||||
extern bool NATIVE_TOUCH;
|
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
|
// arena SMALL subscreen (touch panel) resolution
|
||||||
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
|
||||||
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
|
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 {
|
class GitaDoraGame : public games::Game {
|
||||||
public:
|
public:
|
||||||
GitaDoraGame();
|
GitaDoraGame();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <d3d9.h>
|
#include <d3d9.h>
|
||||||
#include "mf_wrappers.h"
|
#include "mf_wrappers.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
@@ -46,9 +47,7 @@ struct PredefinedHook {
|
|||||||
uintptr_t hook_afp_texture_offset;
|
uintptr_t hook_afp_texture_offset;
|
||||||
};
|
};
|
||||||
|
|
||||||
PredefinedHook g_predefinedHooks[] = {};
|
std::array<PredefinedHook, 0> g_predefinedHooks = {};
|
||||||
|
|
||||||
const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks);
|
|
||||||
|
|
||||||
namespace games::iidx {
|
namespace games::iidx {
|
||||||
|
|
||||||
@@ -110,14 +109,14 @@ namespace games::iidx {
|
|||||||
auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
|
auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
|
||||||
log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);
|
log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);
|
||||||
|
|
||||||
for (DWORD i = 0; i < g_predefinedHooksLength; i++) {
|
for (auto &hook : g_predefinedHooks) {
|
||||||
if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) {
|
if (pe.compare(hook.pe_identifier) == 0) {
|
||||||
log_misc("iidx:camhook", "Found predefined addresses");
|
log_misc("iidx:camhook", "Found predefined addresses");
|
||||||
addr_hook_a = g_predefinedHooks[i].hook_a;
|
addr_hook_a = hook.hook_a;
|
||||||
addr_textures = g_predefinedHooks[i].hook_textures;
|
addr_textures = hook.hook_textures;
|
||||||
addr_camera_manager = g_predefinedHooks[i].hook_camera_manager;
|
addr_camera_manager = hook.hook_camera_manager;
|
||||||
addr_device_offset = g_predefinedHooks[i].hook_device_offset;
|
addr_device_offset = hook.hook_device_offset;
|
||||||
addr_afp_texture_offset = g_predefinedHooks[i].hook_afp_texture_offset;
|
addr_afp_texture_offset = hook.hook_afp_texture_offset;
|
||||||
return TRUE;
|
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_camera_manager = (addr_camera_manager_ptr - (uint8_t*) iidx_module) + disp_camera_manager + 7;
|
||||||
|
|
||||||
// addr_device_offset
|
// 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(
|
uint8_t *addr_device_ptr = reinterpret_cast<uint8_t *>(find_pattern_from(
|
||||||
iidx_module,
|
iidx_module,
|
||||||
"488B89",
|
"488B8F00000000488B014C8B870000000033D2FF9028010000",
|
||||||
"XXX",
|
"XXX????XXXXXX????XXXXXXXX",
|
||||||
3, 0, search_from));
|
3, 0, search_from));
|
||||||
|
|
||||||
if (addr_device_ptr == nullptr) {
|
if (addr_device_ptr == nullptr) {
|
||||||
@@ -212,7 +211,7 @@ namespace games::iidx {
|
|||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
addr_device_offset = *addr_device_ptr;
|
addr_device_offset = *reinterpret_cast<uint32_t*>(addr_device_ptr);
|
||||||
|
|
||||||
// --- addr_afp_texture_offset ---
|
// --- addr_afp_texture_offset ---
|
||||||
uint8_t *addr_afp_texture_ptr = reinterpret_cast<uint8_t *>(find_pattern(
|
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) {
|
static void **__fastcall camera_hook_a(PBYTE a1) {
|
||||||
std::call_once(hook_a_init, [&]{
|
std::call_once(hook_a_init, [&]{
|
||||||
device = *reinterpret_cast<LPDIRECT3DDEVICE9EX*>(a1 + addr_device_offset);
|
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);
|
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);
|
auto const camera_count = manager->begin ? manager->end - manager->begin : 0;
|
||||||
camera_texture_b = manager->cameras->b->d3d9_texture(addr_afp_texture_offset);
|
auto const bind_camera = [&](size_t index, LPDIRECT3DTEXTURE9*& texture, LPDIRECT3DTEXTURE9*& preview) {
|
||||||
preview_texture_a = preview;
|
if (index >= static_cast<size_t>(camera_count)) {
|
||||||
preview_texture_b = preview + 2;
|
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();
|
init_local_camera();
|
||||||
});
|
});
|
||||||
@@ -382,10 +395,10 @@ namespace games::iidx {
|
|||||||
log_misc("iidx:camhook", "Adding user-preferred cameras");
|
log_misc("iidx:camhook", "Adding user-preferred cameras");
|
||||||
for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
|
for (size_t i = 0; i < numDevices && LOCAL_CAMERA_LIST.size() < 2; i++) {
|
||||||
auto pActivate = ppDevices[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);
|
log_misc("iidx:camhook", "Adding user-preferred top camera {} / '{}'", i, top_camera_id);
|
||||||
top_camera = add_top_camera(pActivate);
|
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);
|
log_misc("iidx:camhook", "Adding user-preferred front camera {} / '{}'", i, front_camera_id);
|
||||||
front_camera = add_front_camera(pActivate);
|
front_camera = add_front_camera(pActivate);
|
||||||
}
|
}
|
||||||
@@ -403,16 +416,16 @@ namespace games::iidx {
|
|||||||
|
|
||||||
if (!FLIP_CAMS) {
|
if (!FLIP_CAMS) {
|
||||||
// top camera first, then front
|
// top camera first, then front
|
||||||
if (top_camera == nullptr) {
|
if (top_camera == nullptr && camera_texture_a != nullptr) {
|
||||||
top_camera = add_top_camera(pActivate);
|
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);
|
front_camera = add_front_camera(pActivate);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// front first, then top
|
// front first, then top
|
||||||
if (front_camera == nullptr) {
|
if (front_camera == nullptr && camera_texture_b != nullptr) {
|
||||||
front_camera = add_front_camera(pActivate);
|
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);
|
top_camera = add_top_camera(pActivate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,21 +70,31 @@ namespace games::iidx {
|
|||||||
class IIDXCameraSourceReaderCallback;
|
class IIDXCameraSourceReaderCallback;
|
||||||
|
|
||||||
namespace Camera {
|
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 {
|
struct PlayVideoCamera {
|
||||||
IDirect3DTexture9** d3d9_texture(const uintptr_t offset) {
|
AfpTexture* afp_texture(const uintptr_t offset) {
|
||||||
auto const afp_texture = *reinterpret_cast<uint8_t**>
|
return *reinterpret_cast<AfpTexture**>(reinterpret_cast<uint8_t*>(this) + offset);
|
||||||
(reinterpret_cast<uint8_t*>(this) + offset);
|
|
||||||
return reinterpret_cast<IDirect3DTexture9**>(afp_texture + 0x8);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
struct CCameraManager2 {
|
struct CCameraManager2 {
|
||||||
using camera_pointers = struct {
|
|
||||||
PlayVideoCamera* a;
|
|
||||||
PlayVideoCamera* b;
|
|
||||||
};
|
|
||||||
void* vftbl;
|
void* vftbl;
|
||||||
camera_pointers* cameras;
|
PlayVideoCamera** begin;
|
||||||
|
PlayVideoCamera** end;
|
||||||
|
PlayVideoCamera** capacity_end;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
#include "qks/io.h"
|
#include "qks/io.h"
|
||||||
#include "mfg/io.h"
|
#include "mfg/io.h"
|
||||||
#include "pc/io.h"
|
#include "pc/io.h"
|
||||||
|
#include "udn/io.h"
|
||||||
|
|
||||||
namespace games {
|
namespace games {
|
||||||
|
|
||||||
@@ -332,6 +333,14 @@ namespace games {
|
|||||||
analogs.insert({ pc, pc::get_analogs() });
|
analogs.insert({ pc, pc::get_analogs() });
|
||||||
lights.insert({ pc, pc::get_lights() });
|
lights.insert({ pc, pc::get_lights() });
|
||||||
file_hints[pc].push_back({"game/svm.exe"});
|
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() {
|
const std::vector<std::string> &get_games() {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
#if SPICE64 && !SPICE_XP
|
#if SPICE64 && !SPICE_XP
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <mutex>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
@@ -141,10 +143,100 @@ namespace games::popn {
|
|||||||
static AIO_SCI_COMM *aioSciComm;
|
static AIO_SCI_COMM *aioSciComm;
|
||||||
static AIO_IOB5_BI3A *aioIob5Bi3a;
|
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
|
* 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
|
// libaio.dll
|
||||||
|
|
||||||
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
|
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));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
|
||||||
|
|
||||||
if (i_pNodeMgr == aioNmgrIob5) {
|
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;
|
aioIob5Bi3a = new AIO_IOB5_BI3A;
|
||||||
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
|
||||||
return 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) {
|
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) {
|
if (i_pNodeCtl != aioIob5Bi3a) {
|
||||||
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
|
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));
|
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
|
||||||
auto &buttons = get_buttons();
|
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);
|
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) {
|
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
|
||||||
// special handling converting to non-RGB lights
|
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
|
||||||
// take the max(R, G, B) and use it to write the light value
|
// clears the buffer (set to 0 / lights off) between animations but they are not meant
|
||||||
uint8_t light_value[9] = { 0 };
|
// to be seen by the I/O
|
||||||
for (uint32_t i = 0; i < number_of_leds; i += 3) {
|
// therefore, instead of writing the lights out directly here, we stage them in a buffer
|
||||||
light_value[i / 3] =
|
// and wait for GetDeviceStatus to be called
|
||||||
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
|
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;
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
@@ -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 "silentscope.h"
|
||||||
|
|
||||||
#include "acioemu/handle.h"
|
|
||||||
#include "cfg/configurator.h"
|
#include "cfg/configurator.h"
|
||||||
#include "hooks/devicehook.h"
|
#include "hooks/devicehook.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
|
|
||||||
|
#include "projector.h"
|
||||||
|
|
||||||
namespace games::silentscope {
|
namespace games::silentscope {
|
||||||
|
|
||||||
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
|
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
|
||||||
@@ -16,9 +17,8 @@ namespace games::silentscope {
|
|||||||
// load the game DLL so hooks apply
|
// load the game DLL so hooks apply
|
||||||
libutils::try_library("gamendd.dll");
|
libutils::try_library("gamendd.dll");
|
||||||
|
|
||||||
// TODO(felix): implement stuff on this port
|
|
||||||
devicehook_init();
|
devicehook_init();
|
||||||
devicehook_add(new acioemu::ACIOHandle(L"COM2"));
|
devicehook_add(new ProjectorHandle());
|
||||||
}
|
}
|
||||||
|
|
||||||
void SilentScopeGame::detach() {
|
void SilentScopeGame::detach() {
|
||||||
|
|||||||
@@ -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, ¤t, 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 = ¤t.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, ¤t, 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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace games::udn {
|
||||||
|
void bi2x_hook_init();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
0x1cb9ad4c, 0xdbfa, 0x4c32,
|
||||||
0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2);
|
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,
|
DEFINE_GUID(IID_IAudioClock,
|
||||||
0xcd63314f, 0x3fba, 0x4a1b,
|
0xcd63314f, 0x3fba, 0x4a1b,
|
||||||
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
|
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {}
|
|||||||
#include <dxgi.h>
|
#include <dxgi.h>
|
||||||
#include <dxgi1_2.h>
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
#include "d3d11_internal.h"
|
#include "d3d11_internal.h"
|
||||||
#include "util/nt_loader.h"
|
#include "util/nt_loader.h"
|
||||||
|
|
||||||
@@ -237,10 +238,11 @@ bool d3dcompiler_available() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
void graphics_d3d11_init() {
|
void graphics_d3d11_init() {
|
||||||
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
|
// dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska
|
||||||
// their startup path completely untouched (no exports patched, no poll
|
// engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their
|
||||||
// thread, no LDR callback).
|
// startup path completely untouched (no exports patched, no poll thread, no
|
||||||
if (!GetModuleHandleW(L"execexe.dll")) {
|
// LDR callback).
|
||||||
|
if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ namespace d3d11_hooks {
|
|||||||
// ignore_hwnd.
|
// ignore_hwnd.
|
||||||
void note_main_hwnd(HWND hwnd);
|
void note_main_hwnd(HWND hwnd);
|
||||||
HWND main_hwnd();
|
HWND main_hwnd();
|
||||||
|
bool is_main_game_swapchain(IDXGISwapChain *swapchain);
|
||||||
void ignore_hwnd(HWND hwnd);
|
void ignore_hwnd(HWND hwnd);
|
||||||
|
|
||||||
// capture backbuffer to PNG if a screenshot was requested.
|
// capture backbuffer to PNG if a screenshot was requested.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
#include <dxgi1_2.h>
|
#include <dxgi1_2.h>
|
||||||
|
|
||||||
#include "d3d11_internal.h"
|
#include "d3d11_internal.h"
|
||||||
|
#include "d3d11_swapchain_udn.h"
|
||||||
|
|
||||||
#include "external/imgui/imgui.h"
|
#include "external/imgui/imgui.h"
|
||||||
#include "external/imgui/backends/imgui_impl_dx11.h"
|
#include "external/imgui/backends/imgui_impl_dx11.h"
|
||||||
@@ -115,34 +116,6 @@ bool looks_like_game_window(HWND hwnd) {
|
|||||||
&& client.bottom > client.top;
|
&& 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
|
// checks are ordered cheapest first, since this runs on every present
|
||||||
void try_create_overlay(IDXGISwapChain *swapchain) {
|
void try_create_overlay(IDXGISwapChain *swapchain) {
|
||||||
if (!swapchain) {
|
if (!swapchain) {
|
||||||
@@ -160,7 +133,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore sub windows
|
// ignore sub windows
|
||||||
if (!is_main_game_swapchain(swapchain)) {
|
if (!d3d11_hooks::is_main_game_swapchain(swapchain)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +171,7 @@ void try_create_overlay(IDXGISwapChain *swapchain) {
|
|||||||
void pump_frame(IDXGISwapChain *swapchain) {
|
void pump_frame(IDXGISwapChain *swapchain) {
|
||||||
const bool has_overlay =
|
const bool has_overlay =
|
||||||
overlay::OVERLAY && overlay::OVERLAY->uses_swapchain(swapchain);
|
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;
|
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
|
// a test present doesn't display anything; don't pick a window or take a screenshot off it
|
||||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
|
d3d11_hooks::udn::schedule_window_attach(swapchain);
|
||||||
try_create_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
pump_frame(swapchain);
|
pump_frame(swapchain);
|
||||||
|
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
|
||||||
}
|
}
|
||||||
return Present_orig(swapchain, SyncInterval, Flags);
|
return Present_orig(swapchain, SyncInterval, Flags);
|
||||||
}
|
}
|
||||||
@@ -251,8 +226,10 @@ HRESULT STDMETHODCALLTYPE Present1_hook(
|
|||||||
const DXGI_PRESENT_PARAMETERS *pParams)
|
const DXGI_PRESENT_PARAMETERS *pParams)
|
||||||
{
|
{
|
||||||
if (!(Flags & DXGI_PRESENT_TEST)) {
|
if (!(Flags & DXGI_PRESENT_TEST)) {
|
||||||
|
d3d11_hooks::udn::schedule_window_attach(swapchain);
|
||||||
try_create_overlay(swapchain);
|
try_create_overlay(swapchain);
|
||||||
pump_frame(swapchain);
|
pump_frame(swapchain);
|
||||||
|
d3d11_hooks::udn::apply_present_policy(swapchain, SyncInterval);
|
||||||
}
|
}
|
||||||
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
return Present1_orig(swapchain, SyncInterval, Flags, pParams);
|
||||||
}
|
}
|
||||||
@@ -287,6 +264,33 @@ namespace {
|
|||||||
std::atomic<HWND> g_ignored_hwnd { nullptr };
|
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) {
|
void note_main_hwnd(HWND hwnd) {
|
||||||
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
if (!hwnd || hwnd == g_ignored_hwnd.load()) {
|
||||||
return;
|
return;
|
||||||
@@ -326,6 +330,8 @@ void install_swapchain_hooks(IDXGISwapChain *swapchain) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
udn::install_swapchain_hooks(swapchain);
|
||||||
|
|
||||||
if (!g_swapchain1_hooked) {
|
if (!g_swapchain1_hooked) {
|
||||||
IDXGISwapChain1 *sc1 = nullptr;
|
IDXGISwapChain1 *sc1 = nullptr;
|
||||||
if (SUCCEEDED(swapchain->QueryInterface(IID_PPV_ARGS(&sc1))) && sc1) {
|
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 "launcher/shutdown.h"
|
||||||
#include "misc/wintouchemu.h"
|
#include "misc/wintouchemu.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "sdk/d3d9.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/deferlog.h"
|
#include "util/deferlog.h"
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
@@ -944,9 +945,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
*ppReturnedDeviceInterface);
|
*ppReturnedDeviceInterface);
|
||||||
|
|
||||||
|
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
|
||||||
|
*ppReturnedDeviceInterface = wrapped;
|
||||||
}
|
}
|
||||||
|
|
||||||
// return result
|
// return result
|
||||||
@@ -1030,14 +1034,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
|
|
||||||
return D3DERR_INVALIDCALL;
|
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;
|
DWORD orig_behavior_flags = BehaviorFlags;
|
||||||
size_t num_adapters = 1;
|
size_t num_adapters = 1;
|
||||||
@@ -1093,7 +1089,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
params->BackBufferHeight, params->BackBufferWidth);
|
params->BackBufferHeight, params->BackBufferWidth);
|
||||||
std::swap(params->BackBufferWidth, params->BackBufferHeight);
|
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(
|
log_misc(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"use custom sub resolution {}x{} => {}x{}",
|
"use custom sub resolution {}x{} => {}x{}",
|
||||||
@@ -1137,7 +1134,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
|
||||||
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
|
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->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
|
||||||
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
|
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
|
||||||
}
|
}
|
||||||
@@ -1287,14 +1285,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
ppReturnedDeviceInterface);
|
ppReturnedDeviceInterface);
|
||||||
|
|
||||||
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
|
||||||
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
gfdm_publish_two_head_parameters(
|
||||||
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
pPresentationParameters,
|
||||||
gfdm_parameters.presentation_parameters[1];
|
pFullscreenDisplayMode,
|
||||||
if (pFullscreenDisplayMode != nullptr) {
|
gfdm_parameters);
|
||||||
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
|
||||||
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
|
||||||
gfdm_parameters.fullscreen_display_modes[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check for error
|
// check for error
|
||||||
@@ -1307,13 +1301,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
|||||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||||
|
|
||||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||||
hFocusWindow,
|
hFocusWindow,
|
||||||
*ppReturnedDeviceInterface,
|
*ppReturnedDeviceInterface,
|
||||||
gfdm_parameters.logical_small_swapchain,
|
gfdm_parameters.logical_small_swapchain,
|
||||||
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
||||||
gfdm_two_head_exclusive() ? pPresentationParameters : 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
|
// initialize sub screen if the game requested a multi-head context
|
||||||
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||||
@@ -1515,6 +1512,8 @@ void graphics_d3d9_on_present(
|
|||||||
device->EndScene();
|
device->EndScene();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sdk::d3d9::draw(hFocusWindow, device);
|
||||||
|
|
||||||
// after the overlay render so the screenshot includes toasts / menus
|
// after the overlay render so the screenshot includes toasts / menus
|
||||||
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
if (GRAPHICS_SCREENSHOT_INCLUDE_OVERLAY) {
|
||||||
graphics_d3d9_process_screenshot(device, wrapped_device);
|
graphics_d3d9_process_screenshot(device, wrapped_device);
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
#include "overlay/overlay.h"
|
#include "overlay/overlay.h"
|
||||||
|
#include "sdk/d3d9.h"
|
||||||
#include "util/flags_helper.h"
|
#include "util/flags_helper.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
#include "cfg/screen_resize.h"
|
#include "cfg/screen_resize.h"
|
||||||
@@ -136,6 +137,7 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
|
|||||||
|
|
||||||
// release owned objects if there are no more references
|
// release owned objects if there are no more references
|
||||||
if (local_refs == 0) {
|
if (local_refs == 0) {
|
||||||
|
sdk::d3d9::destroy(this->pReal);
|
||||||
if (this->main_swapchain) {
|
if (this->main_swapchain) {
|
||||||
this->main_swapchain->Release();
|
this->main_swapchain->Release();
|
||||||
this->main_swapchain = nullptr;
|
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);
|
d3d9_readback::release_device_resources(this->pReal);
|
||||||
|
|
||||||
if (overlay::ENABLED) {
|
if (overlay::ENABLED) {
|
||||||
@@ -629,12 +634,20 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sdk::d3d9::invalidate(pReal);
|
||||||
|
|
||||||
// reset overlay
|
// reset overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||||
overlay::OVERLAY->reset_invalidate();
|
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);
|
HRESULT res = pReal->Reset(pPresentationParameters);
|
||||||
|
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal) && SUCCEEDED(res)) {
|
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);
|
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(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
||||||
@@ -680,6 +701,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
|
|||||||
if (is_gfdm_two_head_exclusive()
|
if (is_gfdm_two_head_exclusive()
|
||||||
&& is_gfdm_logical_small_swapchain(iSwapChain))
|
&& 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(
|
CHECK_RESULT(pReal->GetBackBuffer(
|
||||||
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
GFDM_NATIVE_SMALL_SWAPCHAIN,
|
||||||
iBackBuffer,
|
iBackBuffer,
|
||||||
@@ -2068,8 +2095,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::PresentEx(
|
|||||||
|
|
||||||
graphics_d3d9_on_present(hFocusWindow, pReal, this);
|
graphics_d3d9_on_present(hFocusWindow, pReal, this);
|
||||||
|
|
||||||
CHECK_RESULT(static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
|
const HRESULT result = static_cast<IDirect3DDevice9Ex *>(pReal)->PresentEx(
|
||||||
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags));
|
pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion, dwFlags);
|
||||||
|
sdk::d3d9::present_complete(pReal, result);
|
||||||
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGPUThreadPriority(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetGPUThreadPriority(
|
||||||
@@ -2316,15 +2345,24 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sdk::d3d9::invalidate(pReal);
|
||||||
|
|
||||||
// reset overlay
|
// reset overlay
|
||||||
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
if (overlay::OVERLAY && overlay::OVERLAY->uses_device(pReal)) {
|
||||||
overlay::OVERLAY->reset_invalidate();
|
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(
|
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
||||||
gfdm_parameters.presentation_parameters,
|
gfdm_parameters.presentation_parameters,
|
||||||
gfdm_parameters.fullscreen_display_modes);
|
gfdm_parameters.fullscreen_display_modes);
|
||||||
|
|
||||||
|
sdk::d3d9::reset_complete(pReal, SUCCEEDED(res));
|
||||||
|
|
||||||
if (is_gfdm_two_head_exclusive()
|
if (is_gfdm_two_head_exclusive()
|
||||||
&& SUCCEEDED(res)
|
&& SUCCEEDED(res)
|
||||||
&& gfdm_parameters.recovery_candidate)
|
&& gfdm_parameters.recovery_candidate)
|
||||||
@@ -2335,14 +2373,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
|||||||
}
|
}
|
||||||
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
|
||||||
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
|
||||||
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
|
gfdm_publish_two_head_parameters(
|
||||||
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
|
pPresentationParameters,
|
||||||
gfdm_parameters.presentation_parameters[1];
|
pFullscreenDisplayMode,
|
||||||
if (pFullscreenDisplayMode != nullptr) {
|
gfdm_parameters);
|
||||||
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
|
|
||||||
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
|
|
||||||
gfdm_parameters.fullscreen_display_modes[1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// recreate overlay
|
// recreate overlay
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
#include "d3d9_fake_swapchain.h"
|
#include "d3d9_fake_swapchain.h"
|
||||||
|
#include "d3d9_gfdm.h"
|
||||||
#include "d3d9_swapchain.h"
|
#include "d3d9_swapchain.h"
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -264,6 +265,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
IDirect3DDevice9 *pReal;
|
IDirect3DDevice9 *pReal;
|
||||||
bool is_d3d9ex = false;
|
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;
|
std::atomic_ulong refs = 1;
|
||||||
|
|
||||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||||
@@ -275,6 +280,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
|||||||
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
|
||||||
bool gfdm_logical_group_parameters_valid = false;
|
bool gfdm_logical_group_parameters_valid = false;
|
||||||
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
IDirect3D9 *gfdm_parent_d3d = nullptr;
|
||||||
|
GfdmSmallHead gfdm_small_head;
|
||||||
|
|
||||||
std::mutex gfdm_recovery_mutex;
|
std::mutex gfdm_recovery_mutex;
|
||||||
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
|
||||||
|
|||||||
@@ -2,20 +2,73 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
|
#include <cstring>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
|
||||||
#include "games/gitadora/gitadora.h"
|
#include "games/gitadora/gitadora.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "sdk/d3d9.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
|
|
||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
|
|
||||||
|
// windows.h defines many stupid things
|
||||||
|
#undef small
|
||||||
|
|
||||||
bool gfdm_two_head_exclusive() {
|
bool gfdm_two_head_exclusive() {
|
||||||
return games::gitadora::is_arena_model()
|
return games::gitadora::is_arena_model()
|
||||||
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
|
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
|
||||||
&& !GRAPHICS_WINDOWED;
|
&& !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(
|
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
|
||||||
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
|
||||||
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
|
||||||
@@ -186,6 +239,26 @@ HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
|
|||||||
return D3DERR_INVALIDCALL;
|
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;
|
return D3D_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,20 +391,21 @@ HRESULT validate_gfdm_two_head_exclusive(
|
|||||||
|
|
||||||
const auto &main = presentation_parameters[0];
|
const auto &main = presentation_parameters[0];
|
||||||
const auto &small_params = presentation_parameters[1];
|
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) {
|
if (main.Windowed || small_params.Windowed) {
|
||||||
log_warning(
|
log_warning(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"two-head exclusive mode requires both group heads to be fullscreen");
|
"two-head exclusive mode requires both group heads to be fullscreen");
|
||||||
return D3DERR_INVALIDCALL;
|
return D3DERR_INVALIDCALL;
|
||||||
}
|
}
|
||||||
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|
if (small_params.BackBufferWidth != expected_small_width
|
||||||
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
|
|| small_params.BackBufferHeight != expected_small_height)
|
||||||
{
|
{
|
||||||
log_warning(
|
log_warning(
|
||||||
"graphics::d3d9",
|
"graphics::d3d9",
|
||||||
"SMALL head must be {}x{}; got {}x{}",
|
"SMALL head must be {}x{}; got {}x{}",
|
||||||
GFDM_SMALL_WIDTH,
|
expected_small_width,
|
||||||
GFDM_SMALL_HEIGHT,
|
expected_small_height,
|
||||||
small_params.BackBufferWidth,
|
small_params.BackBufferWidth,
|
||||||
small_params.BackBufferHeight);
|
small_params.BackBufferHeight);
|
||||||
return D3DERR_INVALIDCALL;
|
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_parameters[1].FullScreen_RefreshRateInHz = alternate_small.RefreshRate;
|
||||||
temporary_modes[1] = alternate_small;
|
temporary_modes[1] = alternate_small;
|
||||||
|
|
||||||
|
sdk::d3d9::invalidate(device);
|
||||||
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
|
HRESULT temporary_result = device->ResetEx(temporary_parameters, temporary_modes);
|
||||||
const bool temporary_settled =
|
const bool temporary_settled =
|
||||||
temporary_result == D3D_OK
|
temporary_result == D3D_OK
|
||||||
@@ -501,6 +576,8 @@ HRESULT graphics_d3d9_gfdm_recover_two_head_present_mode(
|
|||||||
restore_result == D3D_OK
|
restore_result == D3D_OK
|
||||||
&& gfdm_wait_for_small_mode(desired_parameters[1].hDeviceWindow, desired_modes[1]);
|
&& 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) {
|
if (temporary_result != D3D_OK) {
|
||||||
return temporary_result;
|
return temporary_result;
|
||||||
}
|
}
|
||||||
@@ -547,6 +624,7 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
|||||||
{
|
{
|
||||||
if (presentation_parameters == nullptr) {
|
if (presentation_parameters == nullptr) {
|
||||||
gfdm_logical_group_parameters_valid = false;
|
gfdm_logical_group_parameters_valid = false;
|
||||||
|
gfdm_small_head.resolve(nullptr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
std::copy_n(
|
std::copy_n(
|
||||||
@@ -554,6 +632,8 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
|
|||||||
gfdm_logical_group_parameters.size(),
|
gfdm_logical_group_parameters.size(),
|
||||||
gfdm_logical_group_parameters.begin());
|
gfdm_logical_group_parameters.begin());
|
||||||
gfdm_logical_group_parameters_valid = true;
|
gfdm_logical_group_parameters_valid = true;
|
||||||
|
gfdm_small_head.resolve(
|
||||||
|
&gfdm_logical_group_parameters[gfdm_logical_small_swapchain]);
|
||||||
}
|
}
|
||||||
|
|
||||||
FakeIDirect3DSwapChain9 *
|
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() {
|
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
|
||||||
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
|
||||||
gfdm_recovery_armed = false;
|
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_LOGICAL_HEAD_COUNT = 4;
|
||||||
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
|
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 {
|
struct GfdmTwoHeadDeviceState {
|
||||||
explicit GfdmTwoHeadDeviceState(
|
explicit GfdmTwoHeadDeviceState(
|
||||||
D3DPRESENT_PARAMETERS *presentation_parameters,
|
D3DPRESENT_PARAMETERS *presentation_parameters,
|
||||||
@@ -41,6 +71,10 @@ struct GfdmTwoHeadDeviceState {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool gfdm_two_head_exclusive();
|
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);
|
bool is_fake_subscreen_adapter(UINT adapter);
|
||||||
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
|
||||||
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "d3d9_readback.h"
|
#include "d3d9_readback.h"
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -10,6 +13,13 @@ namespace d3d9_readback {
|
|||||||
|
|
||||||
namespace {
|
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) {
|
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||||
IDirect3DSurface9 *surface = nullptr;
|
IDirect3DSurface9 *surface = nullptr;
|
||||||
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
||||||
@@ -160,21 +170,8 @@ ReadbackPool &pool() {
|
|||||||
return *instance;
|
return *instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
// 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) {
|
||||||
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) {
|
|
||||||
|
|
||||||
IDirect3DSurface9 *buffer = nullptr;
|
IDirect3DSurface9 *buffer = nullptr;
|
||||||
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||||
if (FAILED(hr) || buffer == nullptr) {
|
if (FAILED(hr) || buffer == nullptr) {
|
||||||
@@ -182,17 +179,17 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|||||||
"failed to get back buffer for screen {}, hr={}",
|
"failed to get back buffer for screen {}, hr={}",
|
||||||
screen,
|
screen,
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
return std::nullopt;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
D3DSURFACE_DESC desc {};
|
SurfacePtr surface(buffer);
|
||||||
hr = buffer->GetDesc(&desc);
|
|
||||||
|
hr = surface->GetDesc(&desc);
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
"failed to acquire back buffer descriptor, hr={}",
|
"failed to acquire back buffer descriptor, hr={}",
|
||||||
FMT_HRESULT(hr));
|
FMT_HRESULT(hr));
|
||||||
buffer->Release();
|
return nullptr;
|
||||||
return std::nullopt;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetRenderTargetData rejects multisampled sources. no supported game has been
|
// 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",
|
"back buffer is multisampled ({}), screenshots and capture are unsupported",
|
||||||
static_cast<uint32_t>(desc.MultiSampleType));
|
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;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,12 +601,10 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
|||||||
? pool().acquire(device, desc)
|
? pool().acquire(device, desc)
|
||||||
: create_readback_surface(device, desc);
|
: create_readback_surface(device, desc);
|
||||||
if (!destination) {
|
if (!destination) {
|
||||||
buffer->Release();
|
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
hr = device->GetRenderTargetData(buffer, destination.get());
|
const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get());
|
||||||
buffer->Release();
|
|
||||||
|
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
log_warning("graphics::d3d9",
|
log_warning("graphics::d3d9",
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
|
|
||||||
@@ -39,6 +40,51 @@ namespace d3d9_readback {
|
|||||||
int screen,
|
int screen,
|
||||||
bool pooled);
|
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
|
// pooled surfaces hold references on the device; call this before releasing it
|
||||||
void release_device_resources(IDirect3DDevice9 *device);
|
void release_device_resources(IDirect3DDevice9 *device);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -488,6 +488,63 @@ ThreadPool &capture_read_pool() {
|
|||||||
return *instance;
|
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
|
// 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) {
|
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
|
||||||
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
||||||
@@ -632,6 +689,18 @@ static void process_image_request(
|
|||||||
const ImageRequest &request) {
|
const ImageRequest &request) {
|
||||||
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
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 };
|
std::vector<int> screens { request.screen };
|
||||||
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
||||||
screens.clear();
|
screens.clear();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "hooks/graphics/graphics.h"
|
#include "hooks/graphics/graphics.h"
|
||||||
|
#include "sdk/d3d9.h"
|
||||||
|
|
||||||
#include "d3d9_backend.h"
|
#include "d3d9_backend.h"
|
||||||
#include "d3d9_device.h"
|
#include "d3d9_device.h"
|
||||||
@@ -21,6 +22,12 @@
|
|||||||
} \
|
} \
|
||||||
return ret
|
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) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
|
||||||
if (ppvObj == nullptr) {
|
if (ppvObj == nullptr) {
|
||||||
return E_POINTER;
|
return E_POINTER;
|
||||||
@@ -89,6 +96,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (scales_small_head(this)) {
|
||||||
|
pDev->gfdm_small_head.compose(pDev);
|
||||||
|
}
|
||||||
|
|
||||||
HRESULT result = pReal->Present(
|
HRESULT result = pReal->Present(
|
||||||
pSourceRect,
|
pSourceRect,
|
||||||
pDestRect,
|
pDestRect,
|
||||||
@@ -130,6 +141,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
|
|||||||
result = recovery_failure;
|
result = recovery_failure;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (should_run_hooks) {
|
||||||
|
sdk::d3d9::present_complete(pDev->pReal, result);
|
||||||
|
}
|
||||||
CHECK_RESULT(result);
|
CHECK_RESULT(result);
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect3DSurface9 *pDestSurface) {
|
||||||
@@ -138,6 +152,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect
|
|||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
|
||||||
IDirect3DSurface9 **ppBackBuffer)
|
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));
|
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
|
||||||
}
|
}
|
||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
|
||||||
@@ -159,7 +177,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetDevice(IDirect3DDevice9
|
|||||||
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
|
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
|
||||||
D3DPRESENT_PARAMETERS *pPresentationParameters)
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <thread>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
@@ -52,6 +53,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
|
|||||||
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
|
||||||
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
|
||||||
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
HWND POPN_SUBSCREEN_WINDOW = nullptr;
|
||||||
|
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
|
||||||
|
HWND NDD_MAIN_WINDOW = nullptr;
|
||||||
bool FAKE_SUBSCREEN_ADAPTER = false;
|
bool FAKE_SUBSCREEN_ADAPTER = false;
|
||||||
|
|
||||||
// icon
|
// icon
|
||||||
@@ -231,6 +234,10 @@ bool graphics_gitadora_has_dedicated_subscreen() {
|
|||||||
return GFDM_SUBSCREEN_WINDOW != nullptr;
|
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(
|
bool graphics_gitadora_prepare_two_head_device_window(
|
||||||
HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) {
|
HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) {
|
||||||
if (hWnd == nullptr || !IsWindow(hWnd)) {
|
if (hWnd == nullptr || !IsWindow(hWnd)) {
|
||||||
@@ -390,7 +397,12 @@ static LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM l
|
|||||||
// terminate
|
// terminate
|
||||||
if (uMsg == WM_CLOSE) {
|
if (uMsg == WM_CLOSE) {
|
||||||
log_info("graphics", "detected WM_CLOSE, terminating...");
|
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;
|
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_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_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");
|
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()
|
const std::string gfdm_window_name = games::gitadora::is_arena_model()
|
||||||
? gitadora_canonical_window_name(effective_window_name)
|
? 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);
|
disable_touch_gestures(result);
|
||||||
log_misc(
|
log_misc(
|
||||||
"graphics",
|
"graphics",
|
||||||
@@ -902,6 +925,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
|
|||||||
return value;
|
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) {
|
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
|
||||||
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
|
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
|
||||||
fmt::ptr(hWnd),
|
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;
|
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
|
// iidx windowed TDJ mode
|
||||||
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
|
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
|
||||||
if (GRAPHICS_IIDX_WSUB) {
|
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,
|
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
|
||||||
int X, int Y, int cx, int cy, UINT uFlags) {
|
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) &&
|
if (is_gfdm_two_head_small_window(hWnd) &&
|
||||||
((uFlags & SWP_HIDEWINDOW) ||
|
((uFlags & SWP_HIDEWINDOW) ||
|
||||||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
|
|||||||
extern HWND SDVX_SUBSCREEN_WINDOW;
|
extern HWND SDVX_SUBSCREEN_WINDOW;
|
||||||
extern HWND POPN_SUBSCREEN_WINDOW;
|
extern HWND POPN_SUBSCREEN_WINDOW;
|
||||||
extern HWND GFDM_SUBSCREEN_WINDOW;
|
extern HWND GFDM_SUBSCREEN_WINDOW;
|
||||||
|
extern HWND NDD_MAIN_WINDOW;
|
||||||
|
|
||||||
extern bool SUBSCREEN_FORCE_REDRAW;
|
extern bool SUBSCREEN_FORCE_REDRAW;
|
||||||
extern bool FAKE_SUBSCREEN_ADAPTER;
|
extern bool FAKE_SUBSCREEN_ADAPTER;
|
||||||
@@ -121,6 +122,9 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
|
|||||||
void graphics_init();
|
void graphics_init();
|
||||||
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
|
||||||
bool graphics_gitadora_has_dedicated_subscreen();
|
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
|
// The native GITADORA two-head D3D9 group uses the game's named SMALL
|
||||||
// device window for the native physical SMALL head. The game requests
|
// device window for the native physical SMALL head. The game requests
|
||||||
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "icmphook_net.h"
|
#include "icmphook_net.h"
|
||||||
|
|
||||||
|
#include "hooks/nicspoof.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.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);
|
std::lock_guard<std::recursive_mutex> lock(g_mu);
|
||||||
auto it = g_socks.find(s);
|
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);
|
auto *in = reinterpret_cast<const sockaddr_in *>(name);
|
||||||
if (in->sin_family != AF_INET) {
|
if (in->sin_family != AF_INET) {
|
||||||
WSASetLastError(WSAEAFNOSUPPORT);
|
WSASetLastError(WSAEAFNOSUPPORT);
|
||||||
@@ -618,7 +623,10 @@ void install_icmphook_hooks() {
|
|||||||
}
|
}
|
||||||
done = true;
|
done = true;
|
||||||
|
|
||||||
|
const bool defer_divert = nicspoof_tunnel_enabled();
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
|
|
||||||
|
// Always own socket creation so raw ICMP sockets become emulated.
|
||||||
ok &= detour::trampoline_try(
|
ok &= detour::trampoline_try(
|
||||||
"ws2_32.dll", "socket",
|
"ws2_32.dll", "socket",
|
||||||
(void *) socket_hook, (void **) &socket_orig);
|
(void *) socket_hook, (void **) &socket_orig);
|
||||||
@@ -628,37 +636,52 @@ void install_icmphook_hooks() {
|
|||||||
ok &= detour::trampoline_try(
|
ok &= detour::trampoline_try(
|
||||||
"ws2_32.dll", "WSASocketA",
|
"ws2_32.dll", "WSASocketA",
|
||||||
(void *) WSASocketA_hook, (void **) &WSASocketA_orig);
|
(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(
|
ok &= detour::trampoline_try(
|
||||||
"ws2_32.dll", "setsockopt",
|
"ws2_32.dll", "setsockopt",
|
||||||
(void *) setsockopt_hook, (void **) &setsockopt_orig);
|
(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(
|
log_warning(
|
||||||
"network",
|
"network",
|
||||||
"ICMP emulation: one or more ws2_32 hooks failed to install");
|
"ICMP emulation: one or more socket-creation hooks failed");
|
||||||
} else {
|
|
||||||
log_info("network", "ICMP emulation hooks installed (raw ICMP sockets)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
g_installed.store(true, std::memory_order_release);
|
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;
|
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() {
|
void icmphook_net_init() {
|
||||||
icmphook_internal::install_icmphook_hooks();
|
icmphook_internal::install_icmphook_hooks();
|
||||||
icmphook_iphlpapi_install();
|
icmphook_iphlpapi_install();
|
||||||
|
|||||||
@@ -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.
|
* 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);
|
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();
|
void icmphook_net_init();
|
||||||
|
|||||||
@@ -336,9 +336,11 @@ void hooks::lang::early_init() {
|
|||||||
&GetLocaleInfoA_orig);
|
&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) ||
|
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");
|
log_info("hooks::lang", "hooking IsDBCSLeadByte");
|
||||||
detour::trampoline_try(
|
detour::trampoline_try(
|
||||||
"kernel32.dll",
|
"kernel32.dll",
|
||||||
@@ -358,8 +360,9 @@ void hooks::lang::early_init() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef SPICE64
|
#ifdef SPICE64
|
||||||
|
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
|
||||||
const auto hook_wide_char_to_multi_byte =
|
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
|
#else
|
||||||
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
|
// 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" });
|
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
#include "libraryhook.h"
|
#include "libraryhook.h"
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
|
|
||||||
#include "external/robin_hood.h"
|
#include "external/robin_hood.h"
|
||||||
#include "util/detour.h"
|
#include "util/detour.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
|
|
||||||
static bool LHOOK_ENABLED = false;
|
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::string, HMODULE> LIBRARIES_A;
|
||||||
static robin_hood::unordered_map<std::wstring, HMODULE> LIBRARIES_W;
|
static robin_hood::unordered_map<std::wstring, HMODULE> LIBRARIES_W;
|
||||||
static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
static robin_hood::unordered_map<std::string, FARPROC> PROCS;
|
||||||
@@ -21,6 +26,7 @@ static HMODULE WINAPI LoadLibraryA_hook(LPCTSTR lpFileName) {
|
|||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
if (lpFileName) {
|
if (lpFileName) {
|
||||||
|
std::shared_lock lock(LIBRARIES_MUTEX);
|
||||||
auto module = LIBRARIES_A.find(lpFileName);
|
auto module = LIBRARIES_A.find(lpFileName);
|
||||||
if (module != LIBRARIES_A.end()) {
|
if (module != LIBRARIES_A.end()) {
|
||||||
return module->second;
|
return module->second;
|
||||||
@@ -35,6 +41,7 @@ static HMODULE WINAPI LoadLibraryW_hook(LPCWSTR lpFileName) {
|
|||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
if (lpFileName) {
|
if (lpFileName) {
|
||||||
|
std::shared_lock lock(LIBRARIES_MUTEX);
|
||||||
auto module = LIBRARIES_W.find(lpFileName);
|
auto module = LIBRARIES_W.find(lpFileName);
|
||||||
if (module != LIBRARIES_W.end()) {
|
if (module != LIBRARIES_W.end()) {
|
||||||
return module->second;
|
return module->second;
|
||||||
@@ -49,6 +56,7 @@ static HMODULE WINAPI LoadLibraryExW_hook(LPCWSTR lpFileName, HANDLE hFile, DWOR
|
|||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
if (lpFileName) {
|
if (lpFileName) {
|
||||||
|
std::shared_lock lock(LIBRARIES_MUTEX);
|
||||||
auto module = LIBRARIES_W.find(lpFileName);
|
auto module = LIBRARIES_W.find(lpFileName);
|
||||||
if (module != LIBRARIES_W.end()) {
|
if (module != LIBRARIES_W.end()) {
|
||||||
return module->second;
|
return module->second;
|
||||||
@@ -63,6 +71,7 @@ static HMODULE WINAPI GetModuleHandleA_hook(LPCSTR lpModuleName) {
|
|||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
if (lpModuleName) {
|
if (lpModuleName) {
|
||||||
|
std::shared_lock lock(LIBRARIES_MUTEX);
|
||||||
auto module = LIBRARIES_A.find(lpModuleName);
|
auto module = LIBRARIES_A.find(lpModuleName);
|
||||||
if (module != LIBRARIES_A.end()) {
|
if (module != LIBRARIES_A.end()) {
|
||||||
return module->second;
|
return module->second;
|
||||||
@@ -77,6 +86,7 @@ static HMODULE WINAPI GetModuleHandleW_hook(LPCWSTR lpModuleName) {
|
|||||||
|
|
||||||
// check hooks
|
// check hooks
|
||||||
if (lpModuleName) {
|
if (lpModuleName) {
|
||||||
|
std::shared_lock lock(LIBRARIES_MUTEX);
|
||||||
auto module = LIBRARIES_W.find(lpModuleName);
|
auto module = LIBRARIES_W.find(lpModuleName);
|
||||||
if (module != LIBRARIES_W.end()) {
|
if (module != LIBRARIES_W.end()) {
|
||||||
return module->second;
|
return module->second;
|
||||||
@@ -129,8 +139,10 @@ void libraryhook_enable(HMODULE module) {
|
|||||||
|
|
||||||
void libraryhook_hook_library(std::string library_name, HMODULE library_address) {
|
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
|
// 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);
|
LIBRARIES_A.insert_or_assign(std::move(library_name), library_address);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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
@@ -0,0 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "nicspoof.h"
|
||||||
|
|
||||||
|
void nicspoof_tunnel_init(const NicSpoofConfig &cfg);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <shlwapi.h>
|
#include <shlwapi.h>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
@@ -71,6 +72,7 @@
|
|||||||
#include "games/qks/qks.h"
|
#include "games/qks/qks.h"
|
||||||
#include "games/mfg/mfg.h"
|
#include "games/mfg/mfg.h"
|
||||||
#include "games/pc/pc.h"
|
#include "games/pc/pc.h"
|
||||||
|
#include "games/udn/udn.h"
|
||||||
#include "games/museca/museca.h"
|
#include "games/museca/museca.h"
|
||||||
#include "hooks/avshook.h"
|
#include "hooks/avshook.h"
|
||||||
#include "hooks/audio/audio.h"
|
#include "hooks/audio/audio.h"
|
||||||
@@ -84,6 +86,7 @@
|
|||||||
#include "hooks/lang.h"
|
#include "hooks/lang.h"
|
||||||
#include "hooks/networkhook.h"
|
#include "hooks/networkhook.h"
|
||||||
#include "hooks/icmphook_net.h"
|
#include "hooks/icmphook_net.h"
|
||||||
|
#include "hooks/nicspoof.h"
|
||||||
#include "hooks/unisintrhook.h"
|
#include "hooks/unisintrhook.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "launcher/logger.h"
|
#include "launcher/logger.h"
|
||||||
@@ -246,6 +249,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bool attach_qks = false;
|
bool attach_qks = false;
|
||||||
bool attach_mfg = false;
|
bool attach_mfg = false;
|
||||||
bool attach_pc = false;
|
bool attach_pc = false;
|
||||||
|
bool attach_udn = false;
|
||||||
bool attach_museca = false;
|
bool attach_museca = false;
|
||||||
bool show_cursor_if_no_touch = false;
|
bool show_cursor_if_no_touch = false;
|
||||||
|
|
||||||
@@ -257,6 +261,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bool load_stubs = false;
|
bool load_stubs = false;
|
||||||
bool netfix_disable = false;
|
bool netfix_disable = false;
|
||||||
bool icmphook_enable = false;
|
bool icmphook_enable = false;
|
||||||
|
NicSpoofConfig nicspoof_cfg;
|
||||||
bool lang_disable = false;
|
bool lang_disable = false;
|
||||||
std::string process_priority_str = "high";
|
std::string process_priority_str = "high";
|
||||||
bool cardio_enabled = false;
|
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()) {
|
if (options[launcher::Options::GitaDoraWailHold].is_active()) {
|
||||||
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
|
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()) {
|
if (options[launcher::Options::LoadPCModule].value_bool()) {
|
||||||
attach_pc = true;
|
attach_pc = true;
|
||||||
}
|
}
|
||||||
|
if (options[launcher::Options::LoadUDNModule].value_bool()) {
|
||||||
|
attach_udn = true;
|
||||||
|
}
|
||||||
if (options[launcher::Options::LoadMusecaModule].value_bool()) {
|
if (options[launcher::Options::LoadMusecaModule].value_bool()) {
|
||||||
attach_museca = true;
|
attach_museca = true;
|
||||||
}
|
}
|
||||||
@@ -789,6 +807,44 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::EnableICMPHook].value_bool()) {
|
if (options[launcher::Options::EnableICMPHook].value_bool()) {
|
||||||
icmphook_enable = true;
|
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()) {
|
if (options[launcher::Options::DisableACPHook].value_bool()) {
|
||||||
lang_disable = true;
|
lang_disable = true;
|
||||||
}
|
}
|
||||||
@@ -1438,6 +1494,12 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
if (options[launcher::Options::PCKnobMode].value_bool()) {
|
if (options[launcher::Options::PCKnobMode].value_bool()) {
|
||||||
games::pc::PC_KNOB_MODE = true;
|
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()) {
|
if (options[launcher::Options::spice2x_EnableSMXStage].value_bool()) {
|
||||||
rawinput::ENABLE_SMX_STAGE = true;
|
rawinput::ENABLE_SMX_STAGE = true;
|
||||||
}
|
}
|
||||||
@@ -2252,6 +2314,16 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
break;
|
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
|
// Busou Shinki: Armored Princess Battle Conductor
|
||||||
if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) {
|
if (check_dll("kamunity.dll") && fileutils::file_exists("game/bsac_app.exe")) {
|
||||||
avs::game::DLL_NAME = "kamunity.dll";
|
avs::game::DLL_NAME = "kamunity.dll";
|
||||||
@@ -2417,6 +2489,10 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
avs::core::HEAP_SIZE = 536870912; // 512MB
|
avs::core::HEAP_SIZE = 536870912; // 512MB
|
||||||
games.push_back(new games::pc::PCGame());
|
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
|
// apply user heap size, if defined
|
||||||
if (user_heap_size > 0) {
|
if (user_heap_size > 0) {
|
||||||
@@ -2543,7 +2619,15 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
avs::core::load_dll();
|
avs::core::load_dll();
|
||||||
avs::ea3::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) {
|
if (icmphook_enable) {
|
||||||
icmphook_net_init();
|
icmphook_net_init();
|
||||||
}
|
}
|
||||||
@@ -2849,7 +2933,7 @@ int main_implementation(int argc, char *argv[]) {
|
|||||||
bt5api_dispose();
|
bt5api_dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
sdk::fini_sdk_modules();
|
sdk::fini_sdk_modules(true);
|
||||||
|
|
||||||
// stop raw input
|
// stop raw input
|
||||||
hotkeys::disable_raw_input();
|
hotkeys::disable_raw_input();
|
||||||
|
|||||||
+739
-822
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@ namespace launcher {
|
|||||||
GitaDoraSubOverlaySize,
|
GitaDoraSubOverlaySize,
|
||||||
GitaDoraArenaSingleWindow,
|
GitaDoraArenaSingleWindow,
|
||||||
GitaDoraArenaWindowLayout,
|
GitaDoraArenaWindowLayout,
|
||||||
|
GitaDoraArenaSubLayout,
|
||||||
GitaDoraArenaAsioDriver,
|
GitaDoraArenaAsioDriver,
|
||||||
GitaDoraArenaRealtekAccess,
|
GitaDoraArenaRealtekAccess,
|
||||||
LoadJubeatModule,
|
LoadJubeatModule,
|
||||||
@@ -149,6 +150,7 @@ namespace launcher {
|
|||||||
LoadQKSModule,
|
LoadQKSModule,
|
||||||
LoadMFGModule,
|
LoadMFGModule,
|
||||||
LoadPCModule,
|
LoadPCModule,
|
||||||
|
LoadUDNModule,
|
||||||
LoadMusecaModule,
|
LoadMusecaModule,
|
||||||
PathToModules,
|
PathToModules,
|
||||||
ScreenshotFolder,
|
ScreenshotFolder,
|
||||||
@@ -249,6 +251,8 @@ namespace launcher {
|
|||||||
PCArgs,
|
PCArgs,
|
||||||
PCNoIO,
|
PCNoIO,
|
||||||
PCKnobMode,
|
PCKnobMode,
|
||||||
|
UDNArgs,
|
||||||
|
UDNNoIO,
|
||||||
spice2x_LightsOverallBrightness,
|
spice2x_LightsOverallBrightness,
|
||||||
spice2x_WindowBorder,
|
spice2x_WindowBorder,
|
||||||
spice2x_WindowSize,
|
spice2x_WindowSize,
|
||||||
@@ -315,6 +319,10 @@ namespace launcher {
|
|||||||
OtocaCamHook,
|
OtocaCamHook,
|
||||||
DisableHighResTimer,
|
DisableHighResTimer,
|
||||||
EnableICMPHook,
|
EnableICMPHook,
|
||||||
|
EnableNICSpoof,
|
||||||
|
NICSpoofIP,
|
||||||
|
NICSpoofHostRealIP,
|
||||||
|
NICSpoofPort,
|
||||||
AutoElevate,
|
AutoElevate,
|
||||||
CfgForceSoftwareRender,
|
CfgForceSoftwareRender,
|
||||||
OBSWebSocketEnabled,
|
OBSWebSocketEnabled,
|
||||||
@@ -323,7 +331,9 @@ namespace launcher {
|
|||||||
OBSWebSocketPassword,
|
OBSWebSocketPassword,
|
||||||
OBSWebSocketDebug,
|
OBSWebSocketDebug,
|
||||||
ScreenshotIncludeOverlay,
|
ScreenshotIncludeOverlay,
|
||||||
ScreenshotSubscreens
|
ScreenshotSubscreens,
|
||||||
|
|
||||||
|
_Count,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum class OptionsCategory {
|
enum class OptionsCategory {
|
||||||
|
|||||||
@@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
static int __cdecl device_set_coinblocker_open(char number, char open) {
|
static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) {
|
||||||
return 0;
|
// 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() {
|
static void __cdecl device_set_coincounter_merge() {
|
||||||
@@ -617,7 +619,9 @@ void spicedevice_attach() {
|
|||||||
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
|
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
|
||||||
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
|
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
|
||||||
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
|
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(
|
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
|
||||||
DEVICE_INSTANCE, {"device_get_coinstock_all",
|
DEVICE_INSTANCE, {"device_get_coinstock_all",
|
||||||
"?device_get_coinstock_all@@YAXPEAG0@Z",
|
"?device_get_coinstock_all@@YAXPEAG0@Z",
|
||||||
|
|||||||
+34
-16
@@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
|
|||||||
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
||||||
static std::string EAMUSE_GAME_NAME;
|
static std::string EAMUSE_GAME_NAME;
|
||||||
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
||||||
|
static std::mutex KEYPAD_BINDINGS_LOCK;
|
||||||
|
|
||||||
// auto card
|
// auto card
|
||||||
bool AUTO_INSERT_CARD[2] = {false, false};
|
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;
|
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
|
// 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) {
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
// do a quick copy under lock
|
const auto card_override = eamuse_get_card_override(index);
|
||||||
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
|
|
||||||
const auto card_override = CARD_OVERRIDES[index];
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// Check if card overrides are present
|
// Check if card overrides are present
|
||||||
if (!card_override.empty()) {
|
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);
|
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) {
|
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
|
|
||||||
// open file
|
// open file
|
||||||
@@ -322,8 +331,8 @@ int eamuse_coin_consume_stock() {
|
|||||||
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
|
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
int eamuse_coin_add() {
|
void eamuse_coin_add(int amount) {
|
||||||
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
|
COIN_STOCK.fetch_add(amount, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
void eamuse_coin_insert() {
|
void eamuse_coin_insert() {
|
||||||
@@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_keypad_state_naive() {
|
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();
|
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() {
|
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() {
|
const std::string &eamuse_get_game() {
|
||||||
@@ -609,6 +621,7 @@ int eamuse_get_game_keypads() {
|
|||||||
avs::game::is_model("JDX") ||
|
avs::game::is_model("JDX") ||
|
||||||
avs::game::is_model("KDX") ||
|
avs::game::is_model("KDX") ||
|
||||||
avs::game::is_model("MDX") ||
|
avs::game::is_model("MDX") ||
|
||||||
|
avs::game::is_model("KDM") ||
|
||||||
avs::game::is_model("J33") ||
|
avs::game::is_model("J33") ||
|
||||||
avs::game::is_model("K33") ||
|
avs::game::is_model("K33") ||
|
||||||
avs::game::is_model("L33") ||
|
avs::game::is_model("L33") ||
|
||||||
@@ -625,6 +638,7 @@ int eamuse_get_game_keypads_name() {
|
|||||||
|
|
||||||
if (game_name == "Beatmania IIDX" ||
|
if (game_name == "Beatmania IIDX" ||
|
||||||
game_name == "Dance Dance Revolution" ||
|
game_name == "Dance Dance Revolution" ||
|
||||||
|
game_name == "Dance Evolution" ||
|
||||||
game_name == "GitaDora")
|
game_name == "GitaDora")
|
||||||
{
|
{
|
||||||
return 2;
|
return 2;
|
||||||
@@ -718,6 +732,10 @@ void eamuse_autodetect_game() {
|
|||||||
eamuse_set_game("Mahjong Fight Girl");
|
eamuse_set_game("Mahjong Fight Girl");
|
||||||
else if (avs::game::is_model("XIF"))
|
else if (avs::game::is_model("XIF"))
|
||||||
eamuse_set_game("Polaris Chord");
|
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 {
|
else {
|
||||||
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
|
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
|
||||||
eamuse_set_game("unknown");
|
eamuse_set_game("unknown");
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ extern std::string AUTO_PIN_MACRO_TRIGGER[2];
|
|||||||
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
bool eamuse_get_card(int active_count, int unit_id, uint8_t *card);
|
||||||
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int unit_id);
|
||||||
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index);
|
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index);
|
||||||
|
std::filesystem::path eamuse_get_card_path(size_t index);
|
||||||
|
std::string eamuse_get_card_override(size_t index);
|
||||||
|
|
||||||
void eamuse_card_insert(int unit);
|
void eamuse_card_insert(int unit);
|
||||||
void eamuse_card_insert(int unit, const uint8_t *card);
|
void eamuse_card_insert(int unit, const uint8_t *card);
|
||||||
@@ -57,7 +59,7 @@ void eamuse_coin_set_stock(int amount);
|
|||||||
bool eamuse_coin_consume(int amount);
|
bool eamuse_coin_consume(int amount);
|
||||||
int eamuse_coin_consume_stock();
|
int eamuse_coin_consume_stock();
|
||||||
|
|
||||||
int eamuse_coin_add();
|
void eamuse_coin_add(int amount = 1);
|
||||||
void eamuse_coin_insert();
|
void eamuse_coin_insert();
|
||||||
|
|
||||||
void eamuse_pin_macro_start_thread();
|
void eamuse_pin_macro_start_thread();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
namespace rawinput {
|
namespace rawinput {
|
||||||
|
|
||||||
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
static MidiNoteAlgorithm MIDI_NOTE_ALGORITHM = MidiNoteAlgorithm::V2;
|
||||||
|
bool DISABLE_MIDI = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
rawinput::MidiNoteAlgorithm rawinput::get_midi_algorithm() {
|
||||||
@@ -37,6 +38,10 @@ void rawinput::set_midi_algorithm(rawinput::MidiNoteAlgorithm new_algo) {
|
|||||||
|
|
||||||
void rawinput::RawInputManager::midi_scan_start() {
|
void rawinput::RawInputManager::midi_scan_start() {
|
||||||
|
|
||||||
|
if (rawinput::DISABLE_MIDI) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// single-flight: only one scan runs at a time. if one is already running, set
|
// single-flight: only one scan runs at a time. if one is already running, set
|
||||||
// the pending flag so it rescans once more when it finishes - MIDI hotplug
|
// the pending flag so it rescans once more when it finishes - MIDI hotplug
|
||||||
// events fire while the slow enumeration is still going and must not be lost.
|
// events fire while the slow enumeration is still going and must not be lost.
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ namespace rawinput {
|
|||||||
extern bool DUMP_HID_DEVICES_TO_LOG;
|
extern bool DUMP_HID_DEVICES_TO_LOG;
|
||||||
|
|
||||||
extern uint32_t MIDI_NOTE_SUSTAIN;
|
extern uint32_t MIDI_NOTE_SUSTAIN;
|
||||||
|
extern bool DISABLE_MIDI;
|
||||||
|
|
||||||
extern bool NAIVE_REQUIRE_FOCUS;
|
extern bool NAIVE_REQUIRE_FOCUS;
|
||||||
extern bool RAWINPUT_REQUIRE_FOCUS;
|
extern bool RAWINPUT_REQUIRE_FOCUS;
|
||||||
|
|||||||
@@ -0,0 +1,403 @@
|
|||||||
|
#include "d3d9.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <chrono>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <memory>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace sdk::d3d9 {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct Renderer {
|
||||||
|
spice_sdk_d3d9_callback_func *callback;
|
||||||
|
void *userdata;
|
||||||
|
bool ready = false;
|
||||||
|
bool started = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
// keep registration separate from serialized device access and callback execution.
|
||||||
|
std::mutex registry_mutex;
|
||||||
|
std::recursive_mutex execution_mutex;
|
||||||
|
std::condition_variable_any shutdown_condition;
|
||||||
|
std::vector<std::shared_ptr<Renderer>> renderers;
|
||||||
|
|
||||||
|
SPICE_SDK_D3D9_FRAME frame{sizeof(SPICE_SDK_D3D9_FRAME), nullptr, nullptr, 0, 0};
|
||||||
|
bool stopped = false;
|
||||||
|
bool resetting = false;
|
||||||
|
bool device_lost = false;
|
||||||
|
bool reset_in_progress = false;
|
||||||
|
bool shutdown_complete = false;
|
||||||
|
DWORD device_thread = 0;
|
||||||
|
|
||||||
|
// reject reentrant SDK graphics calls made by a renderer callback.
|
||||||
|
thread_local bool dispatching = false;
|
||||||
|
struct DispatchScope {
|
||||||
|
DispatchScope() {
|
||||||
|
dispatching = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~DispatchScope() {
|
||||||
|
dispatching = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename Interface>
|
||||||
|
struct ComReference {
|
||||||
|
Interface *value = nullptr;
|
||||||
|
|
||||||
|
~ComReference() {
|
||||||
|
if (value) {
|
||||||
|
value->Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// restore game state after each renderer, including early exits during setup.
|
||||||
|
struct GraphicsState {
|
||||||
|
IDirect3DDevice9 *device;
|
||||||
|
ComReference<IDirect3DStateBlock9> block;
|
||||||
|
std::array<ComReference<IDirect3DSurface9>, 4> targets;
|
||||||
|
ComReference<IDirect3DSurface9> depth;
|
||||||
|
D3DVIEWPORT9 viewport{};
|
||||||
|
std::array<D3DMATRIX, 3> transforms{};
|
||||||
|
std::array<bool, 3> have_transform{};
|
||||||
|
DWORD target_count = 0;
|
||||||
|
bool captured = false;
|
||||||
|
|
||||||
|
explicit GraphicsState(IDirect3DDevice9 *device) : device(device) {
|
||||||
|
D3DCAPS9 caps{};
|
||||||
|
if (FAILED(device->GetDeviceCaps(&caps)) ||
|
||||||
|
FAILED(device->CreateStateBlock(D3DSBT_ALL, &block.value)) ||
|
||||||
|
FAILED(block.value->Capture()) || FAILED(device->GetViewport(&viewport))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// state blocks do not capture render targets or the depth surface.
|
||||||
|
target_count = std::min<DWORD>(caps.NumSimultaneousRTs, targets.size());
|
||||||
|
for (DWORD index = 0; index < target_count; ++index) {
|
||||||
|
const auto status = device->GetRenderTarget(index, &targets[index].value);
|
||||||
|
if (FAILED(status) && status != D3DERR_NOTFOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto status = device->GetDepthStencilSurface(&depth.value);
|
||||||
|
if (FAILED(status) && status != D3DERR_NOTFOUND) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
|
||||||
|
for (size_t index = 0; index < kinds.size(); ++index) {
|
||||||
|
have_transform[index] = SUCCEEDED(
|
||||||
|
device->GetTransform(kinds[index], &transforms[index]));
|
||||||
|
}
|
||||||
|
captured = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
~GraphicsState() {
|
||||||
|
if (!captured) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unbind auxiliary surfaces before restoring targets of potentially different sizes.
|
||||||
|
device->SetDepthStencilSurface(nullptr);
|
||||||
|
for (DWORD index = 1; index < target_count; ++index) {
|
||||||
|
device->SetRenderTarget(index, nullptr);
|
||||||
|
}
|
||||||
|
device->SetRenderTarget(0, targets[0].value);
|
||||||
|
for (DWORD index = 1; index < target_count; ++index) {
|
||||||
|
device->SetRenderTarget(index, targets[index].value);
|
||||||
|
}
|
||||||
|
device->SetDepthStencilSurface(depth.value);
|
||||||
|
|
||||||
|
block.value->Apply();
|
||||||
|
const std::array kinds{D3DTS_WORLD, D3DTS_VIEW, D3DTS_PROJECTION};
|
||||||
|
for (size_t index = 0; index < kinds.size(); ++index) {
|
||||||
|
if (have_transform[index]) {
|
||||||
|
device->SetTransform(kinds[index], &transforms[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
device->SetViewport(&viewport);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::shared_ptr<Renderer>> snapshot() {
|
||||||
|
// callbacks run without the registry lock, while their entries remain alive.
|
||||||
|
std::lock_guard lock(registry_mutex);
|
||||||
|
return renderers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy_renderers() {
|
||||||
|
for (const auto &renderer : snapshot()) {
|
||||||
|
if (renderer->started) {
|
||||||
|
renderer->callback(SPICE_SDK_D3D9_DESTROY, &frame, renderer->userdata);
|
||||||
|
}
|
||||||
|
renderer->ready = false;
|
||||||
|
renderer->started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = {sizeof(frame), nullptr, nullptr, 0, 0};
|
||||||
|
resetting = false;
|
||||||
|
device_lost = false;
|
||||||
|
reset_in_progress = false;
|
||||||
|
device_thread = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void finish_shutdown() {
|
||||||
|
if (shutdown_complete) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy_renderers();
|
||||||
|
{
|
||||||
|
std::lock_guard lock(registry_mutex);
|
||||||
|
renderers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
shutdown_complete = true;
|
||||||
|
shutdown_condition.notify_all();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
|
||||||
|
spice_sdk_d3d9_callback_func *callback, void *userdata) {
|
||||||
|
if (dispatching) {
|
||||||
|
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
if (!callback) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only registered SDK modules may supply callbacks.
|
||||||
|
HMODULE owner = nullptr;
|
||||||
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
|
reinterpret_cast<LPCWSTR>(callback), &owner) ||
|
||||||
|
std::none_of(modules.begin(), modules.end(), [&](const SdkModule &module) {
|
||||||
|
return module.module == owner;
|
||||||
|
})) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(registry_mutex);
|
||||||
|
if (stopped) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
if (std::any_of(renderers.begin(), renderers.end(), [&](const auto &renderer) {
|
||||||
|
return renderer->callback == callback && renderer->userdata == userdata;
|
||||||
|
})) {
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keep callback code loaded for the lifetime of the process.
|
||||||
|
try {
|
||||||
|
auto renderer = std::make_shared<Renderer>(Renderer{callback, userdata});
|
||||||
|
if (!GetModuleHandleExW(
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_PIN,
|
||||||
|
reinterpret_cast<LPCWSTR>(callback), &owner)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
renderers.push_back(std::move(renderer));
|
||||||
|
} catch (...) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw(HWND window, IDirect3DDevice9 *device) {
|
||||||
|
if (dispatching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(execution_mutex);
|
||||||
|
DispatchScope scope;
|
||||||
|
|
||||||
|
if (frame.device && frame.device != device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
device_thread = GetCurrentThreadId();
|
||||||
|
if (stopped) {
|
||||||
|
if (!reset_in_progress) {
|
||||||
|
finish_shutdown();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// latch the first presented device even before any renderers register.
|
||||||
|
frame.device = device;
|
||||||
|
frame.window = window;
|
||||||
|
|
||||||
|
const auto current = snapshot();
|
||||||
|
if (current.empty() || resetting || device_lost) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ComReference<IDirect3DSurface9> backbuffer;
|
||||||
|
D3DSURFACE_DESC description{};
|
||||||
|
if (FAILED(device->GetBackBuffer(0, 0, D3DBACKBUFFER_TYPE_MONO, &backbuffer.value)) ||
|
||||||
|
FAILED(backbuffer.value->GetDesc(&description))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame = {sizeof(frame), device, window, description.Width, description.Height};
|
||||||
|
|
||||||
|
for (const auto &renderer : current) {
|
||||||
|
// isolate each renderer and give it the full backbuffer in its own scene.
|
||||||
|
GraphicsState state(device);
|
||||||
|
if (!state.captured) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (FAILED(device->SetDepthStencilSurface(nullptr))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool target_ready = true;
|
||||||
|
for (DWORD index = 1; index < state.target_count; ++index) {
|
||||||
|
if (FAILED(device->SetRenderTarget(index, nullptr))) {
|
||||||
|
target_ready = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const D3DVIEWPORT9 viewport{0, 0, frame.width, frame.height, 0.0f, 1.0f};
|
||||||
|
if (!target_ready || FAILED(device->SetRenderTarget(0, backbuffer.value)) ||
|
||||||
|
FAILED(device->SetViewport(&viewport)) || FAILED(device->BeginScene())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispatch READY to also recreate resources released before a successful reset.
|
||||||
|
if (!renderer->ready) {
|
||||||
|
renderer->started = true;
|
||||||
|
renderer->callback(SPICE_SDK_D3D9_READY, &frame, renderer->userdata);
|
||||||
|
renderer->ready = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
renderer->callback(SPICE_SDK_D3D9_DRAW, &frame, renderer->userdata);
|
||||||
|
device->EndScene();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void present_complete(IDirect3DDevice9 *device, HRESULT result) {
|
||||||
|
if (dispatching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(execution_mutex);
|
||||||
|
DispatchScope scope;
|
||||||
|
|
||||||
|
if (frame.device == device) {
|
||||||
|
device_thread = GetCurrentThreadId();
|
||||||
|
if (stopped && !reset_in_progress) {
|
||||||
|
finish_shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a busy nonblocking present does not change the known device-loss state.
|
||||||
|
if (SUCCEEDED(result)) {
|
||||||
|
device_lost = false;
|
||||||
|
} else if (result != D3DERR_WASSTILLDRAWING) {
|
||||||
|
device_lost = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void invalidate(IDirect3DDevice9 *device) {
|
||||||
|
if (dispatching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(execution_mutex);
|
||||||
|
DispatchScope scope;
|
||||||
|
|
||||||
|
if (frame.device != device) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
device_thread = GetCurrentThreadId();
|
||||||
|
if (stopped) {
|
||||||
|
finish_shutdown();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// default-pool resources must be released before the native reset starts.
|
||||||
|
reset_in_progress = true;
|
||||||
|
resetting = true;
|
||||||
|
for (const auto &renderer : snapshot()) {
|
||||||
|
if (renderer->ready) {
|
||||||
|
renderer->callback(SPICE_SDK_D3D9_INVALIDATE, &frame, renderer->userdata);
|
||||||
|
}
|
||||||
|
renderer->ready = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset_complete(IDirect3DDevice9 *device, bool success) {
|
||||||
|
if (dispatching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(execution_mutex);
|
||||||
|
DispatchScope scope;
|
||||||
|
|
||||||
|
if (frame.device == device) {
|
||||||
|
device_thread = GetCurrentThreadId();
|
||||||
|
reset_in_progress = false;
|
||||||
|
resetting = !success;
|
||||||
|
device_lost = !success;
|
||||||
|
|
||||||
|
if (stopped) {
|
||||||
|
finish_shutdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy(IDirect3DDevice9 *device) {
|
||||||
|
if (dispatching) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::lock_guard lock(execution_mutex);
|
||||||
|
DispatchScope scope;
|
||||||
|
|
||||||
|
if (frame.device == device) {
|
||||||
|
if (stopped) {
|
||||||
|
finish_shutdown();
|
||||||
|
} else {
|
||||||
|
destroy_renderers();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shutdown(bool graphics_stopped) {
|
||||||
|
std::unique_lock lock(execution_mutex);
|
||||||
|
if (shutdown_complete) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// close registration before deciding where device resources can be released.
|
||||||
|
{
|
||||||
|
std::lock_guard registry_lock(registry_mutex);
|
||||||
|
stopped = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto current = snapshot();
|
||||||
|
const bool resources_started = std::any_of(
|
||||||
|
current.begin(), current.end(), [](const auto &renderer) {
|
||||||
|
return renderer->started;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!resources_started || (!reset_in_progress &&
|
||||||
|
(graphics_stopped || device_thread == GetCurrentThreadId()))) {
|
||||||
|
DispatchScope scope;
|
||||||
|
finish_shutdown();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a graphics boundary must drain callbacks when another thread requests shutdown.
|
||||||
|
return shutdown_condition.wait_for(lock, std::chrono::milliseconds(100), [] {
|
||||||
|
return shutdown_complete;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <d3d9.h>
|
||||||
|
#include "modules.h"
|
||||||
|
|
||||||
|
namespace sdk::d3d9 {
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE register_d3d9(const std::vector<SdkModule> &modules,
|
||||||
|
spice_sdk_d3d9_callback_func *callback, void *userdata);
|
||||||
|
void draw(HWND window, IDirect3DDevice9 *device);
|
||||||
|
void present_complete(IDirect3DDevice9 *device, HRESULT result);
|
||||||
|
void invalidate(IDirect3DDevice9 *device);
|
||||||
|
void reset_complete(IDirect3DDevice9 *device, bool success);
|
||||||
|
void destroy(IDirect3DDevice9 *device);
|
||||||
|
bool shutdown(bool graphics_stopped);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
|
#define SPICE_SDK_ENTRY_POINT extern "C" __declspec(dllexport) int __cdecl
|
||||||
@@ -237,6 +238,138 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
|
|||||||
const char *text
|
const char *text
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// insert_coin (v0.3 and up)
|
||||||
|
// adds to the shared coin stock, bypassing the coin blocker like the Spice API
|
||||||
|
//
|
||||||
|
// amount: number of coins to insert (0 to 255); use 1 for a single coin
|
||||||
|
// zero succeeds without changing the stock
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)(
|
||||||
|
uint8_t amount
|
||||||
|
);
|
||||||
|
|
||||||
|
// get_coin_blocker (v0.6 and up)
|
||||||
|
// gets the current shared coin blocker state
|
||||||
|
// insert_coin bypasses the blocker regardless of this state
|
||||||
|
//
|
||||||
|
// blocked: receives true when coins are blocked, false when allowed
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_coin_blocker_func)(
|
||||||
|
bool *blocked
|
||||||
|
);
|
||||||
|
|
||||||
|
// set_coin_blocker (v0.6 and up)
|
||||||
|
// sets the shared coin blocker state used by normal coin input
|
||||||
|
// this is not a persistent override; the game can change the state again
|
||||||
|
// insert_coin bypasses the blocker regardless of this state
|
||||||
|
//
|
||||||
|
// blocked: true to block coins, false to allow them
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_set_coin_blocker_func)(
|
||||||
|
bool blocked
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef struct SPICE_SDK_MODULE_INFO {
|
||||||
|
uint32_t size; // initialize to sizeof(SPICE_SDK_MODULE_INFO)
|
||||||
|
uintptr_t base; // loaded address, not the preferred PE image base
|
||||||
|
uint32_t image_size;
|
||||||
|
uint32_t timestamp;
|
||||||
|
uint32_t entry_point; // RVA, not an absolute address
|
||||||
|
uint16_t machine; // PE IMAGE_FILE_MACHINE_* value
|
||||||
|
} SPICE_SDK_MODULE_INFO;
|
||||||
|
|
||||||
|
// get_module_info (v0.4 and up)
|
||||||
|
// gets PE info about an already loaded module
|
||||||
|
//
|
||||||
|
// does not load or permanently pin the module; the caller must keep it loaded
|
||||||
|
// while using the returned base address
|
||||||
|
// returns GENERIC_ERROR if not found or unreadable, NOT_SUPPORTED for non-PE32/PE32+
|
||||||
|
//
|
||||||
|
// module_name: null-terminated UTF-16 module name or full path
|
||||||
|
// info: receives module info; initialize size to sizeof(SPICE_SDK_MODULE_INFO)
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_module_info_func)(
|
||||||
|
const wchar_t *module_name,
|
||||||
|
SPICE_SDK_MODULE_INFO *info
|
||||||
|
);
|
||||||
|
|
||||||
|
// get_plugin_directory (v0.4 and up)
|
||||||
|
// gets the directory containing a registered SDK DLL
|
||||||
|
//
|
||||||
|
// available during entry-point initialization and until destroy callbacks finish
|
||||||
|
// returns INVALID_ARGUMENT_1 for addresses outside registered SDK DLLs
|
||||||
|
//
|
||||||
|
// plugin_address: address of a function or static object in the plugin DLL
|
||||||
|
// used only to identify the DLL; the pointed-to object is not read
|
||||||
|
// address of any global variable in your DLL will work
|
||||||
|
// buffer: caller-owned UTF-16 output, or NULL to query the required size
|
||||||
|
// size: input capacity and output required wchar_t count, including the terminator
|
||||||
|
// NULL buffer or insufficient capacity returns TOO_SMALL and sets the
|
||||||
|
// required size without partial output
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_get_plugin_directory_func)(
|
||||||
|
const void *plugin_address,
|
||||||
|
wchar_t *buffer,
|
||||||
|
uint32_t *size
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef enum SPICE_SDK_D3D9_EVENT {
|
||||||
|
SPICE_SDK_D3D9_READY = 0,
|
||||||
|
SPICE_SDK_D3D9_DRAW = 1,
|
||||||
|
SPICE_SDK_D3D9_INVALIDATE = 2,
|
||||||
|
SPICE_SDK_D3D9_DESTROY = 3,
|
||||||
|
} SPICE_SDK_D3D9_EVENT;
|
||||||
|
|
||||||
|
typedef struct SPICE_SDK_D3D9_FRAME {
|
||||||
|
uint32_t size;
|
||||||
|
void *device; // borrowed IDirect3DDevice9*, not Spice's wrapper
|
||||||
|
void *window; // HWND
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
} SPICE_SDK_D3D9_FRAME;
|
||||||
|
|
||||||
|
typedef void (__cdecl spice_sdk_d3d9_callback_func)(
|
||||||
|
SPICE_SDK_D3D9_EVENT event,
|
||||||
|
const SPICE_SDK_D3D9_FRAME *frame,
|
||||||
|
void *userdata
|
||||||
|
);
|
||||||
|
|
||||||
|
// register_d3d9 (v0.4 and up)
|
||||||
|
// registers a process-lifetime renderer for the primary D3D9 presentation target
|
||||||
|
//
|
||||||
|
// READY precedes drawing on a usable device and follows each successful reset
|
||||||
|
// DRAW runs on top of all Spice overlays, even when the overlay is closed; the host
|
||||||
|
// binds the backbuffer, brackets the scene, and restores graphics state
|
||||||
|
// INVALIDATE precedes reset; release default-pool resources, even if reset fails
|
||||||
|
// DESTROY releases all device references before device destruction or SDK shutdown
|
||||||
|
// callbacks are serialized; DRAW/READY run on the presentation thread; teardown
|
||||||
|
// runs at a graphics-thread boundary, or after the game stops rendering
|
||||||
|
// do not throw, block, reset/present the
|
||||||
|
// device, or shut down Spice from a callback; frame is valid only during the call
|
||||||
|
// no callbacks run after the plugin's SDK destroy callback begins
|
||||||
|
// registration from inside a render callback is not supported
|
||||||
|
// input capture and non-D3D9 backends are not provided
|
||||||
|
//
|
||||||
|
// callback: function in a registered plugin DLL; the DLL is retained until exit
|
||||||
|
// userdata: opaque plugin state passed unchanged to each callback
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_register_d3d9_func)(
|
||||||
|
spice_sdk_d3d9_callback_func *callback,
|
||||||
|
void *userdata
|
||||||
|
);
|
||||||
|
|
||||||
|
// hook_library (v0.5 and up)
|
||||||
|
//
|
||||||
|
// maps an exact LoadLibrary name to a module that the plugin has already loaded
|
||||||
|
//
|
||||||
|
// library_name: exact ANSI library name or path used by the target
|
||||||
|
// module: module handle returned by LoadLibrary; keep it loaded until shutdown
|
||||||
|
|
||||||
|
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_hook_library_func)(
|
||||||
|
const char *library_name,
|
||||||
|
void *module
|
||||||
|
);
|
||||||
|
|
||||||
typedef struct SPICE_SDK_V0 {
|
typedef struct SPICE_SDK_V0 {
|
||||||
uint32_t size;
|
uint32_t size;
|
||||||
|
|
||||||
@@ -262,6 +395,18 @@ typedef struct SPICE_SDK_V0 {
|
|||||||
|
|
||||||
spice_sdk_add_toast_func *add_toast;
|
spice_sdk_add_toast_func *add_toast;
|
||||||
|
|
||||||
|
spice_sdk_insert_coin_func *insert_coin;
|
||||||
|
|
||||||
|
spice_sdk_get_module_info_func *get_module_info;
|
||||||
|
spice_sdk_get_plugin_directory_func *get_plugin_directory;
|
||||||
|
|
||||||
|
spice_sdk_register_d3d9_func *register_d3d9;
|
||||||
|
|
||||||
|
spice_sdk_hook_library_func *hook_library;
|
||||||
|
|
||||||
|
spice_sdk_get_coin_blocker_func *get_coin_blocker;
|
||||||
|
spice_sdk_set_coin_blocker_func *set_coin_blocker;
|
||||||
|
|
||||||
} SPICE_SDK_V0;
|
} SPICE_SDK_V0;
|
||||||
|
|
||||||
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
typedef void (__cdecl spice_sdk_destroy_callback_func)(
|
||||||
|
|||||||
@@ -1607,6 +1607,7 @@ typedef enum SPICE_SDK_DANCERUSH_BUTTONS {
|
|||||||
DRS_Button_P2_Down,
|
DRS_Button_P2_Down,
|
||||||
DRS_Button_P2_Left,
|
DRS_Button_P2_Left,
|
||||||
DRS_Button_P2_Right,
|
DRS_Button_P2_Right,
|
||||||
|
DRS_Button_DownMotion,
|
||||||
} SPICE_SDK_DANCERUSH_BUTTONS;
|
} SPICE_SDK_DANCERUSH_BUTTONS;
|
||||||
|
|
||||||
typedef enum SPICE_SDK_DANCERUSH_LIGHTS {
|
typedef enum SPICE_SDK_DANCERUSH_LIGHTS {
|
||||||
@@ -1898,6 +1899,57 @@ typedef enum SPICE_SDK_POLARIS_CHORD_LIGHTS {
|
|||||||
PC_Light_Button12_B,
|
PC_Light_Button12_B,
|
||||||
} SPICE_SDK_POLARIS_CHORD_LIGHTS;
|
} SPICE_SDK_POLARIS_CHORD_LIGHTS;
|
||||||
|
|
||||||
|
// DANCE aROUND
|
||||||
|
|
||||||
|
typedef enum SPICE_SDK_DANCE_AROUND_BUTTONS {
|
||||||
|
UDN_Button_Service,
|
||||||
|
UDN_Button_Test,
|
||||||
|
UDN_Button_CoinMech,
|
||||||
|
UDN_Button_Start,
|
||||||
|
UDN_Button_Up,
|
||||||
|
UDN_Button_Down,
|
||||||
|
UDN_Button_Left,
|
||||||
|
UDN_Button_Right,
|
||||||
|
} SPICE_SDK_DANCE_AROUND_BUTTONS;
|
||||||
|
|
||||||
|
typedef enum SPICE_SDK_DANCE_AROUND_LIGHTS {
|
||||||
|
UDN_Light_CardReaderR,
|
||||||
|
UDN_Light_CardReaderG,
|
||||||
|
UDN_Light_CardReaderB,
|
||||||
|
UDN_Light_Start,
|
||||||
|
UDN_Light_Up,
|
||||||
|
UDN_Light_Down,
|
||||||
|
UDN_Light_Left,
|
||||||
|
UDN_Light_Right,
|
||||||
|
UDN_Light_TitleR,
|
||||||
|
UDN_Light_TitleG,
|
||||||
|
UDN_Light_TitleB,
|
||||||
|
UDN_Light_PillarRightTopR,
|
||||||
|
UDN_Light_PillarRightTopG,
|
||||||
|
UDN_Light_PillarRightTopB,
|
||||||
|
UDN_Light_PillarRightBottomR,
|
||||||
|
UDN_Light_PillarRightBottomG,
|
||||||
|
UDN_Light_PillarRightBottomB,
|
||||||
|
UDN_Light_StageLeftR,
|
||||||
|
UDN_Light_StageLeftG,
|
||||||
|
UDN_Light_StageLeftB,
|
||||||
|
UDN_Light_CabinetLeftR,
|
||||||
|
UDN_Light_CabinetLeftG,
|
||||||
|
UDN_Light_CabinetLeftB,
|
||||||
|
UDN_Light_PillarLeftTopR,
|
||||||
|
UDN_Light_PillarLeftTopG,
|
||||||
|
UDN_Light_PillarLeftTopB,
|
||||||
|
UDN_Light_PillarLeftBottomR,
|
||||||
|
UDN_Light_PillarLeftBottomG,
|
||||||
|
UDN_Light_PillarLeftBottomB,
|
||||||
|
UDN_Light_StageRightR,
|
||||||
|
UDN_Light_StageRightG,
|
||||||
|
UDN_Light_StageRightB,
|
||||||
|
UDN_Light_CabinetRightR,
|
||||||
|
UDN_Light_CabinetRightG,
|
||||||
|
UDN_Light_CabinetRightB,
|
||||||
|
} SPICE_SDK_DANCE_AROUND_LIGHTS;
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
} // extern "C"
|
} // extern "C"
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#include "modules.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace sdk::modules {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// RAII wrapper for a module handle
|
||||||
|
struct ModuleReference {
|
||||||
|
HMODULE handle = nullptr;
|
||||||
|
|
||||||
|
ModuleReference() = default;
|
||||||
|
ModuleReference(const ModuleReference &) = delete;
|
||||||
|
ModuleReference &operator=(const ModuleReference &) = delete;
|
||||||
|
~ModuleReference() {
|
||||||
|
if (handle) {
|
||||||
|
FreeLibrary(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename Value>
|
||||||
|
bool read_module_value(uintptr_t base, size_t offset, Value &value) {
|
||||||
|
if (offset > std::numeric_limits<uintptr_t>::max() - base ||
|
||||||
|
sizeof(Value) > std::numeric_limits<uintptr_t>::max() - (base + offset)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SIZE_T copied = 0;
|
||||||
|
return ReadProcessMemory(GetCurrentProcess(), reinterpret_cast<const void *>(base + offset),
|
||||||
|
&value, sizeof(value), &copied) && copied == sizeof(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
|
||||||
|
if (!module_name || !module_name[0]) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
if (!info) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
||||||
|
}
|
||||||
|
if (info->size < sizeof(SPICE_SDK_MODULE_INFO)) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_SMALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep the module loaded while inspecting its headers.
|
||||||
|
ModuleReference module;
|
||||||
|
if (!GetModuleHandleExW(0, module_name, &module.handle)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Locate the PE header through the DOS header.
|
||||||
|
const auto base = reinterpret_cast<uintptr_t>(module.handle);
|
||||||
|
IMAGE_DOS_HEADER dos{};
|
||||||
|
if (!read_module_value(base, 0, dos) || dos.e_magic != IMAGE_DOS_SIGNATURE ||
|
||||||
|
dos.e_lfanew < static_cast<LONG>(sizeof(dos))) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto nt_offset = static_cast<size_t>(dos.e_lfanew);
|
||||||
|
DWORD signature{};
|
||||||
|
IMAGE_FILE_HEADER header{};
|
||||||
|
if (!read_module_value(base, nt_offset, signature) || signature != IMAGE_NT_SIGNATURE ||
|
||||||
|
!read_module_value(base, nt_offset + sizeof(signature), header)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The optional-header magic selects the PE32 or PE32+ layout.
|
||||||
|
const auto optional_offset = nt_offset + sizeof(signature) + sizeof(header);
|
||||||
|
WORD magic{};
|
||||||
|
if (header.SizeOfOptionalHeader < sizeof(magic) || !read_module_value(base, optional_offset, magic)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_MODULE_INFO result{};
|
||||||
|
result.size = info->size;
|
||||||
|
result.base = base;
|
||||||
|
result.timestamp = header.TimeDateStamp;
|
||||||
|
result.machine = header.Machine;
|
||||||
|
|
||||||
|
if (magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) {
|
||||||
|
IMAGE_OPTIONAL_HEADER64 optional{};
|
||||||
|
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
result.image_size = optional.SizeOfImage;
|
||||||
|
result.entry_point = optional.AddressOfEntryPoint;
|
||||||
|
} else if (magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) {
|
||||||
|
IMAGE_OPTIONAL_HEADER32 optional{};
|
||||||
|
if (header.SizeOfOptionalHeader < sizeof(optional) || !read_module_value(base, optional_offset, optional)) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
result.image_size = optional.SizeOfImage;
|
||||||
|
result.entry_point = optional.AddressOfEntryPoint;
|
||||||
|
} else {
|
||||||
|
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reject headers or entry points outside the image.
|
||||||
|
if (optional_offset > result.image_size || header.SizeOfOptionalHeader > result.image_size - optional_offset ||
|
||||||
|
result.entry_point >= result.image_size) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave the caller's output untouched on failure.
|
||||||
|
*info = result;
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> ®istered_modules,
|
||||||
|
const void *plugin_address, wchar_t *buffer, uint32_t *size) {
|
||||||
|
if (!plugin_address) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
if (!size) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the address and require a registered plugin.
|
||||||
|
ModuleReference module;
|
||||||
|
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
|
||||||
|
reinterpret_cast<LPCWSTR>(plugin_address), &module.handle) ||
|
||||||
|
std::none_of(registered_modules.begin(), registered_modules.end(), [&](const SdkModule &entry) {
|
||||||
|
return entry.module == module.handle;
|
||||||
|
})) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Reject truncated paths before extracting the directory.
|
||||||
|
std::array<wchar_t, 32768> filename{};
|
||||||
|
const auto length = GetModuleFileNameW(module.handle, filename.data(), static_cast<DWORD>(filename.size()));
|
||||||
|
if (!length || length >= filename.size()) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto path = std::filesystem::path(filename.data()).parent_path();
|
||||||
|
|
||||||
|
// Report the full UTF-16 capacity, including the terminator.
|
||||||
|
const auto &text = path.native();
|
||||||
|
const auto required = static_cast<uint32_t>(text.size() + 1);
|
||||||
|
const auto capacity = *size;
|
||||||
|
*size = required;
|
||||||
|
if (!buffer || capacity < required) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_SMALL;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy only when the entire path fits.
|
||||||
|
std::copy_n(text.c_str(), required, buffer);
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
return SPICE_SDK_STATUS_GENERIC_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "sdk/include/spicesdk.h"
|
||||||
|
|
||||||
|
namespace sdk {
|
||||||
|
|
||||||
|
struct SdkModule {
|
||||||
|
std::string dll;
|
||||||
|
HINSTANCE module;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace modules {
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info);
|
||||||
|
SPICE_SDK_STATUS_CODE get_plugin_directory(const std::vector<SdkModule> ®istered_modules,
|
||||||
|
const void *plugin_address, wchar_t *buffer, uint32_t *size);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
#include <format>
|
#include <format>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <array>
|
||||||
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "v0_cpp_imgui.h"
|
||||||
#include "sdk/include/spicesdk.h"
|
#include "sdk/include/spicesdk.h"
|
||||||
#include "sdk/include/spicesdk_io.h"
|
#include "sdk/include/spicesdk_io.h"
|
||||||
|
|
||||||
@@ -17,6 +21,24 @@ static spice_sdk_destroy_callback_func destroy_callback;
|
|||||||
static std::jthread worker_thread;
|
static std::jthread worker_thread;
|
||||||
static void worker_thread_main(std::stop_token stop_token);
|
static void worker_thread_main(std::stop_token stop_token);
|
||||||
|
|
||||||
|
static std::optional<std::string> utf16_to_utf8(const wchar_t *text) {
|
||||||
|
const auto bytes = WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1, nullptr, 0, nullptr, nullptr);
|
||||||
|
if (bytes == 0) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string result(bytes, '\0');
|
||||||
|
if (WideCharToMultiByte(
|
||||||
|
CP_UTF8, WC_ERR_INVALID_CHARS, text, -1,
|
||||||
|
result.data(), bytes, nullptr, nullptr) != bytes) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.pop_back();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// main entry point into the DLL
|
// main entry point into the DLL
|
||||||
// spice executable will call into this shortly after the game is partially
|
// spice executable will call into this shortly after the game is partially
|
||||||
// initialized (but not quite fully booted just yet)
|
// initialized (but not quite fully booted just yet)
|
||||||
@@ -42,6 +64,37 @@ spice_sdk_entry_point(
|
|||||||
|
|
||||||
LOG_INFO("plugin loaded");
|
LOG_INFO("plugin loaded");
|
||||||
|
|
||||||
|
if (spice.get_plugin_directory) {
|
||||||
|
std::array<wchar_t, 32768> directory{};
|
||||||
|
uint32_t size = static_cast<uint32_t>(directory.size());
|
||||||
|
if (spice.get_plugin_directory(&spice, directory.data(), &size) ==
|
||||||
|
SPICE_SDK_STATUS_SUCCESS) {
|
||||||
|
if (const auto path = utf16_to_utf8(directory.data())) {
|
||||||
|
LOG_INFO(std::format("plugin directory: {}", *path).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spice.get_module_info) {
|
||||||
|
SPICE_SDK_MODULE_INFO info{};
|
||||||
|
info.size = sizeof(info);
|
||||||
|
if (spice.get_module_info(L"kernel32.dll", &info) == SPICE_SDK_STATUS_SUCCESS) {
|
||||||
|
LOG_INFO(std::format(
|
||||||
|
"kernel32 PE: {:x}_{:x}, base={:x}, size={:x}",
|
||||||
|
info.timestamp, info.entry_point, info.base, info.image_size).c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spice.register_d3d9) {
|
||||||
|
status = sample_imgui::initialize(spice);
|
||||||
|
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
|
LOG_INFO(std::format(
|
||||||
|
"D3D9 registration failed: {}", static_cast<int>(status)).c_str());
|
||||||
|
} else {
|
||||||
|
LOG_INFO("D3D9 renderer registered; Ctrl+Enter toggles the sample window");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// spin up a worker thread
|
// spin up a worker thread
|
||||||
worker_thread = std::jthread(worker_thread_main);
|
worker_thread = std::jthread(worker_thread_main);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -76,9 +129,43 @@ static ArrowButton arrow_buttons[] = {
|
|||||||
|
|
||||||
// worker thread for I/O
|
// worker thread for I/O
|
||||||
static void worker_thread_main(std::stop_token stop_token) {
|
static void worker_thread_main(std::stop_token stop_token) {
|
||||||
|
bool coin_previous_state[10] = {};
|
||||||
|
bool window_toggle_previous_state = false;
|
||||||
while (!stop_token.stop_requested()) {
|
while (!stop_token.stop_requested()) {
|
||||||
|
|
||||||
|
const bool control_pressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||||
|
const bool window_toggle_pressed = control_pressed &&
|
||||||
|
((GetAsyncKeyState(VK_RETURN) & 0x8000) != 0);
|
||||||
|
if (window_toggle_pressed && !window_toggle_previous_state) {
|
||||||
|
sample_imgui::toggle();
|
||||||
|
}
|
||||||
|
window_toggle_previous_state = window_toggle_pressed;
|
||||||
|
|
||||||
|
// insert coin
|
||||||
|
for (uint8_t amount = 0; amount < 10; amount++) {
|
||||||
|
const bool coin_pressed = control_pressed &&
|
||||||
|
((GetAsyncKeyState('0' + amount) & 0x8000) != 0);
|
||||||
|
|
||||||
|
if (coin_pressed && !coin_previous_state[amount] && spice.insert_coin) {
|
||||||
|
const auto status = spice.insert_coin(amount);
|
||||||
|
if (status != SPICE_SDK_STATUS_SUCCESS) {
|
||||||
|
LOG_INFO(std::format(
|
||||||
|
"coin insertion failed: {}",
|
||||||
|
static_cast<int>(status)).c_str());
|
||||||
|
} else {
|
||||||
|
const auto message = std::format(
|
||||||
|
"v0_cpp: inserted {} coin{}", amount, amount == 1 ? "" : "s");
|
||||||
|
LOG_INFO(message.c_str());
|
||||||
|
if (spice.add_toast) {
|
||||||
|
spice.add_toast(SPICE_SDK_TOAST_LEVEL_SUCCESS, message.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
coin_previous_state[amount] = coin_pressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for ctrl + arrow keys and trigger p1 pad arrows
|
||||||
for (auto& arrow : arrow_buttons) {
|
for (auto& arrow : arrow_buttons) {
|
||||||
// check for ctrl + arrow keys and trigger p1 pad arrows
|
|
||||||
if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) &&
|
if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) &&
|
||||||
((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) {
|
((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) {
|
||||||
spice.set_button(arrow.button, true, 1.f);
|
spice.set_button(arrow.button, true, 1.f);
|
||||||
@@ -88,7 +175,7 @@ static void worker_thread_main(std::stop_token stop_token) {
|
|||||||
if (spice.add_toast) {
|
if (spice.add_toast) {
|
||||||
spice.add_toast(
|
spice.add_toast(
|
||||||
SPICE_SDK_TOAST_LEVEL_INFO,
|
SPICE_SDK_TOAST_LEVEL_INFO,
|
||||||
std::format("let me hear you say: {}", arrow.name).c_str());
|
std::format("v0_cpp: let me hear you say: {}", arrow.name).c_str());
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
|
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
#include "v0_cpp_imgui.h"
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cfloat>
|
||||||
|
#include <windows.h>
|
||||||
|
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "backends/imgui_impl_dx9.h"
|
||||||
|
|
||||||
|
namespace sample_imgui {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
struct SampleRenderer {
|
||||||
|
ImGuiContext *context = nullptr;
|
||||||
|
bool backend_initialized = false;
|
||||||
|
bool was_visible = false;
|
||||||
|
bool dummy_enabled = true;
|
||||||
|
float dummy_value = 0.5f;
|
||||||
|
int32_t dummy_mode = 0;
|
||||||
|
uint32_t click_count = 0;
|
||||||
|
std::chrono::steady_clock::time_point previous_frame;
|
||||||
|
};
|
||||||
|
|
||||||
|
SampleRenderer renderer;
|
||||||
|
std::atomic_bool window_visible = false;
|
||||||
|
|
||||||
|
void update_mouse(const SPICE_SDK_D3D9_FRAME &frame) {
|
||||||
|
auto &io = ImGui::GetIO();
|
||||||
|
const auto window = static_cast<HWND>(frame.window);
|
||||||
|
const bool focused = window &&
|
||||||
|
GetAncestor(GetForegroundWindow(), GA_ROOT) == GetAncestor(window, GA_ROOT);
|
||||||
|
io.AddFocusEvent(focused);
|
||||||
|
|
||||||
|
POINT position{};
|
||||||
|
RECT client{};
|
||||||
|
|
||||||
|
// can window receive mouse input?
|
||||||
|
const bool mouse_available =
|
||||||
|
focused &&
|
||||||
|
GetCursorPos(&position) &&
|
||||||
|
ScreenToClient(window, &position) &&
|
||||||
|
GetClientRect(window, &client) &&
|
||||||
|
client.right > 0 &&
|
||||||
|
client.bottom > 0;
|
||||||
|
|
||||||
|
// add mouse position
|
||||||
|
if (mouse_available) {
|
||||||
|
io.AddMousePosEvent(
|
||||||
|
static_cast<float>(position.x) * frame.width / client.right,
|
||||||
|
static_cast<float>(position.y) * frame.height / client.bottom);
|
||||||
|
} else {
|
||||||
|
io.AddMousePosEvent(-FLT_MAX, -FLT_MAX);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add click to imgui
|
||||||
|
const bool swapped = GetSystemMetrics(SM_SWAPBUTTON) != 0;
|
||||||
|
io.AddMouseButtonEvent(0, mouse_available &&
|
||||||
|
((GetAsyncKeyState(swapped ? VK_RBUTTON : VK_LBUTTON) & 0x8000) != 0));
|
||||||
|
io.AddMouseButtonEvent(1, mouse_available &&
|
||||||
|
((GetAsyncKeyState(swapped ? VK_LBUTTON : VK_RBUTTON) & 0x8000) != 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_window(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
|
||||||
|
auto &io = ImGui::GetIO();
|
||||||
|
|
||||||
|
ImGui::SetNextWindowPos(
|
||||||
|
ImVec2(io.DisplaySize.x * 0.5f, io.DisplaySize.y * 0.5f),
|
||||||
|
ImGuiCond_Once, ImVec2(0.5f, 0.5f));
|
||||||
|
ImGui::SetNextWindowBgAlpha(0.8f);
|
||||||
|
|
||||||
|
if (ImGui::Begin("SDK sample", nullptr,
|
||||||
|
ImGuiWindowFlags_AlwaysAutoResize |
|
||||||
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoCollapse)) {
|
||||||
|
|
||||||
|
ImGui::Text("%u x %u", frame.width, frame.height);
|
||||||
|
ImGui::Text("%.1f FPS", io.Framerate);
|
||||||
|
|
||||||
|
ImGui::Separator();
|
||||||
|
|
||||||
|
ImGui::Checkbox("Enable dummy widgets", &state.dummy_enabled);
|
||||||
|
|
||||||
|
ImGui::BeginDisabled(!state.dummy_enabled);
|
||||||
|
{
|
||||||
|
ImGui::SliderFloat("Value", &state.dummy_value, 0.0f, 1.0f, "%.2f",
|
||||||
|
ImGuiSliderFlags_NoInput);
|
||||||
|
ImGui::Combo("Mode", &state.dummy_mode, "Off\0Low\0High\0");
|
||||||
|
if (ImGui::Button("Increment")) {
|
||||||
|
++state.click_count;
|
||||||
|
}
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (ImGui::Button("Reset")) {
|
||||||
|
state.click_count = 0;
|
||||||
|
state.dummy_value = 0.5f;
|
||||||
|
state.dummy_mode = 0;
|
||||||
|
}
|
||||||
|
ImGui::Text("Count: %u", state.click_count);
|
||||||
|
}
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
ImGui::End();
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_frame(SampleRenderer &state, const SPICE_SDK_D3D9_FRAME &frame) {
|
||||||
|
if (!state.backend_initialized) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const float elapsed = std::chrono::duration<float>(now - state.previous_frame).count();
|
||||||
|
state.previous_frame = now;
|
||||||
|
const bool visible = window_visible.load(std::memory_order_relaxed);
|
||||||
|
if (!visible && !state.was_visible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.was_visible = visible;
|
||||||
|
|
||||||
|
// update imgui state
|
||||||
|
auto &io = ImGui::GetIO();
|
||||||
|
io.DisplaySize = ImVec2(
|
||||||
|
static_cast<float>(frame.width), static_cast<float>(frame.height));
|
||||||
|
io.DeltaTime = elapsed > 0.0f ? elapsed : 1.0f / 60.0f;
|
||||||
|
|
||||||
|
// update mouse i/o
|
||||||
|
if (visible) {
|
||||||
|
update_mouse(frame);
|
||||||
|
} else {
|
||||||
|
io.ClearEventsQueue();
|
||||||
|
io.ClearInputMouse();
|
||||||
|
io.AddFocusEvent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui_ImplDX9_NewFrame();
|
||||||
|
ImGui::NewFrame();
|
||||||
|
if (!visible) {
|
||||||
|
ImGui::SetWindowFocus(nullptr);
|
||||||
|
ImGui::EndFrame();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
draw_window(state, frame);
|
||||||
|
ImGui::Render();
|
||||||
|
ImGui_ImplDX9_RenderDrawData(ImGui::GetDrawData());
|
||||||
|
}
|
||||||
|
|
||||||
|
void __cdecl render_callback(
|
||||||
|
SPICE_SDK_D3D9_EVENT event,
|
||||||
|
const SPICE_SDK_D3D9_FRAME *frame,
|
||||||
|
void *userdata) {
|
||||||
|
|
||||||
|
auto &state = *static_cast<SampleRenderer *>(userdata);
|
||||||
|
auto *previous_context = ImGui::GetCurrentContext();
|
||||||
|
|
||||||
|
// init imgui context
|
||||||
|
if (event == SPICE_SDK_D3D9_READY && !state.context) {
|
||||||
|
ImGui::SetAllocatorFunctions(
|
||||||
|
[](size_t size, void *) -> void * { return std::malloc(size); },
|
||||||
|
[](void *memory, void *) { std::free(memory); });
|
||||||
|
IMGUI_CHECKVERSION();
|
||||||
|
state.context = ImGui::CreateContext();
|
||||||
|
ImGui::SetCurrentContext(state.context);
|
||||||
|
auto &io = ImGui::GetIO();
|
||||||
|
io.IniFilename = nullptr;
|
||||||
|
io.LogFilename = nullptr;
|
||||||
|
io.MouseDrawCursor = true;
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
state.backend_initialized = ImGui_ImplDX9_Init(
|
||||||
|
static_cast<IDirect3DDevice9 *>(frame->device));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (state.context) {
|
||||||
|
ImGui::SetCurrentContext(state.context);
|
||||||
|
switch (event) {
|
||||||
|
case SPICE_SDK_D3D9_READY:
|
||||||
|
state.previous_frame = std::chrono::steady_clock::now();
|
||||||
|
if (state.backend_initialized) {
|
||||||
|
ImGui_ImplDX9_CreateDeviceObjects();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_D3D9_DRAW:
|
||||||
|
draw_frame(state, *frame);
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_D3D9_INVALIDATE:
|
||||||
|
if (state.backend_initialized) {
|
||||||
|
ImGui_ImplDX9_InvalidateDeviceObjects();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case SPICE_SDK_D3D9_DESTROY:
|
||||||
|
if (state.backend_initialized) {
|
||||||
|
ImGui_ImplDX9_Shutdown();
|
||||||
|
}
|
||||||
|
ImGui::DestroyContext(state.context);
|
||||||
|
state.context = nullptr;
|
||||||
|
state.backend_initialized = false;
|
||||||
|
state.was_visible = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCurrentContext(previous_context);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice) {
|
||||||
|
if (!spice.register_d3d9) {
|
||||||
|
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
||||||
|
}
|
||||||
|
return spice.register_d3d9(render_callback, &renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
void toggle() {
|
||||||
|
const bool visible = !window_visible.load(std::memory_order_relaxed);
|
||||||
|
window_visible.store(visible, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "sdk/include/spicesdk.h"
|
||||||
|
|
||||||
|
namespace sample_imgui {
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE initialize(const SPICE_SDK_V0 &spice);
|
||||||
|
void toggle();
|
||||||
|
|
||||||
|
}
|
||||||
+156
-8
@@ -1,10 +1,14 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <shared_mutex>
|
#include <shared_mutex>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
#include "sdk.h"
|
#include "sdk.h"
|
||||||
|
#include "modules.h"
|
||||||
|
#include "d3d9.h"
|
||||||
#include "avs/game.h"
|
#include "avs/game.h"
|
||||||
#include "games/io.h"
|
#include "games/io.h"
|
||||||
|
#include "hooks/libraryhook.h"
|
||||||
#include "launcher/launcher.h"
|
#include "launcher/launcher.h"
|
||||||
#include "misc/eamuse.h"
|
#include "misc/eamuse.h"
|
||||||
#include "overlay/notifications.h"
|
#include "overlay/notifications.h"
|
||||||
@@ -31,11 +35,13 @@ static spice_sdk_clear_touch_func sdk_clear_touch;
|
|||||||
static spice_sdk_insert_card_func sdk_insert_card;
|
static spice_sdk_insert_card_func sdk_insert_card;
|
||||||
static spice_sdk_set_keypad_func sdk_set_keypad;
|
static spice_sdk_set_keypad_func sdk_set_keypad;
|
||||||
static spice_sdk_add_toast_func sdk_add_toast;
|
static spice_sdk_add_toast_func sdk_add_toast;
|
||||||
|
static spice_sdk_insert_coin_func sdk_insert_coin;
|
||||||
struct SdkModule {
|
static spice_sdk_get_coin_blocker_func sdk_get_coin_blocker;
|
||||||
std::string dll;
|
static spice_sdk_set_coin_blocker_func sdk_set_coin_blocker;
|
||||||
HINSTANCE module;
|
static spice_sdk_get_module_info_func sdk_get_module_info;
|
||||||
};
|
static spice_sdk_get_plugin_directory_func sdk_get_plugin_directory;
|
||||||
|
static spice_sdk_register_d3d9_func sdk_register_d3d9;
|
||||||
|
static spice_sdk_hook_library_func sdk_hook_library;
|
||||||
|
|
||||||
// DLLs
|
// DLLs
|
||||||
static int sdk_modules_count = 0;
|
static int sdk_modules_count = 0;
|
||||||
@@ -45,6 +51,7 @@ static std::shared_mutex sdk_global_mutex;
|
|||||||
// internal
|
// internal
|
||||||
static bool sdk_initialized = false;
|
static bool sdk_initialized = false;
|
||||||
static bool sdk_shutting_down = false;
|
static bool sdk_shutting_down = false;
|
||||||
|
static bool sdk_finalizing = false;
|
||||||
static std::vector<Button> *buttons;
|
static std::vector<Button> *buttons;
|
||||||
static std::vector<Analog> *analogs;
|
static std::vector<Analog> *analogs;
|
||||||
static std::vector<Light> *lights;
|
static std::vector<Light> *lights;
|
||||||
@@ -90,14 +97,22 @@ void init_sdk_modules() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void fini_sdk_modules() {
|
void fini_sdk_modules(bool graphics_stopped) {
|
||||||
// prevent multiple calls and further calls into sdk_init
|
// prevent multiple calls and further calls into sdk_init
|
||||||
{
|
{
|
||||||
std::unique_lock lock(sdk_global_mutex);
|
std::unique_lock lock(sdk_global_mutex);
|
||||||
if (!sdk_initialized) {
|
if (!sdk_initialized || sdk_finalizing) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
sdk_shutting_down = true;
|
sdk_shutting_down = true;
|
||||||
|
sdk_finalizing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!d3d9::shutdown(graphics_stopped)) {
|
||||||
|
std::unique_lock lock(sdk_global_mutex);
|
||||||
|
sdk_finalizing = false;
|
||||||
|
log_warning("sdk", "deferring plugin teardown: waiting for D3D9 rendering to stop");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// call into destroy callback of each DLL
|
// call into destroy callback of each DLL
|
||||||
@@ -175,8 +190,37 @@ sdk_init(
|
|||||||
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
|
||||||
v0->add_toast = sdk_add_toast;
|
v0->add_toast = sdk_add_toast;
|
||||||
}
|
}
|
||||||
|
|
||||||
// end of 0.2
|
// end of 0.2
|
||||||
|
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, insert_coin)) {
|
||||||
|
v0->insert_coin = sdk_insert_coin;
|
||||||
|
}
|
||||||
|
// end of 0.3
|
||||||
|
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_module_info)) {
|
||||||
|
v0->get_module_info = sdk_get_module_info;
|
||||||
|
}
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_plugin_directory)) {
|
||||||
|
v0->get_plugin_directory = sdk_get_plugin_directory;
|
||||||
|
}
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, register_d3d9)) {
|
||||||
|
v0->register_d3d9 = sdk_register_d3d9;
|
||||||
|
}
|
||||||
|
// end of 0.4
|
||||||
|
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, hook_library)) {
|
||||||
|
v0->hook_library = sdk_hook_library;
|
||||||
|
}
|
||||||
|
// end of 0.5
|
||||||
|
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, get_coin_blocker)) {
|
||||||
|
v0->get_coin_blocker = sdk_get_coin_blocker;
|
||||||
|
}
|
||||||
|
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, set_coin_blocker)) {
|
||||||
|
v0->set_coin_blocker = sdk_set_coin_blocker;
|
||||||
|
}
|
||||||
|
// end of 0.6
|
||||||
|
|
||||||
// any newer minor iterations will need to check the size
|
// any newer minor iterations will need to check the size
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -189,6 +233,36 @@ sdk_init(
|
|||||||
return SPICE_SDK_STATUS_SUCCESS;
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_get_module_info(const wchar_t *module_name, SPICE_SDK_MODULE_INFO *info) {
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
return modules::get_module_info(module_name, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_get_plugin_directory(const void *plugin_address, wchar_t *buffer, uint32_t *size) {
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
return modules::get_plugin_directory(sdk_modules_list, plugin_address, buffer, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_register_d3d9(spice_sdk_d3d9_callback_func *callback, void *userdata) {
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized || sdk_shutting_down) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
return d3d9::register_d3d9(sdk_modules_list, callback, userdata);
|
||||||
|
}
|
||||||
|
|
||||||
SPICE_SDK_STATUS_CODE
|
SPICE_SDK_STATUS_CODE
|
||||||
__cdecl
|
__cdecl
|
||||||
sdk_log(
|
sdk_log(
|
||||||
@@ -219,6 +293,7 @@ sdk_log(
|
|||||||
log_warning(facility_str.c_str(), "{}", message);
|
log_warning(facility_str.c_str(), "{}", message);
|
||||||
break;
|
break;
|
||||||
case SPICE_SDK_LOG_LEVEL_FATAL:
|
case SPICE_SDK_LOG_LEVEL_FATAL:
|
||||||
|
lock.unlock();
|
||||||
log_fatal(facility_str.c_str(), "{}", message);
|
log_fatal(facility_str.c_str(), "{}", message);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -640,5 +715,78 @@ sdk_add_toast(
|
|||||||
return SPICE_SDK_STATUS_SUCCESS;
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_hook_library(
|
||||||
|
const char *library_name,
|
||||||
|
void *module
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (sdk_shutting_down || !sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!library_name || !library_name[0]) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
if (!module) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
libraryhook_hook_library(library_name, static_cast<HMODULE>(module));
|
||||||
|
// launcher::signal::attach enables library hooks before SDK entry points run.
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_insert_coin(
|
||||||
|
uint8_t amount
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount > 0) {
|
||||||
|
eamuse_coin_add(amount);
|
||||||
|
}
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_get_coin_blocker(
|
||||||
|
bool *blocked
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
if (!blocked) {
|
||||||
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*blocked = eamuse_coin_get_block();
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
SPICE_SDK_STATUS_CODE
|
||||||
|
__cdecl
|
||||||
|
sdk_set_coin_blocker(
|
||||||
|
bool blocked
|
||||||
|
)
|
||||||
|
{
|
||||||
|
std::shared_lock lock(sdk_global_mutex);
|
||||||
|
if (!sdk_initialized) {
|
||||||
|
return SPICE_SDK_STATUS_TOO_LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
eamuse_coin_set_block(blocked);
|
||||||
|
return SPICE_SDK_STATUS_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace sdk
|
} // namespace sdk
|
||||||
@@ -7,6 +7,6 @@ namespace sdk {
|
|||||||
|
|
||||||
void register_sdk_hooks(std::string dll, HINSTANCE module);
|
void register_sdk_hooks(std::string dll, HINSTANCE module);
|
||||||
void init_sdk_modules();
|
void init_sdk_modules();
|
||||||
void fini_sdk_modules();
|
void fini_sdk_modules(bool graphics_stopped = false);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
#include "execexe.h"
|
#include "execexe.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "avs/game.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
#include "util/libutils.h"
|
#include "util/libutils.h"
|
||||||
#include "util/utils.h"
|
#include "util/utils.h"
|
||||||
@@ -22,6 +25,23 @@ namespace execexe {
|
|||||||
static bool port_opened = false;
|
static bool port_opened = false;
|
||||||
static std::function<void()> deferred_function = nullptr;
|
static std::function<void()> deferred_function = nullptr;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
static T resolve_export_thunk(T entry) {
|
||||||
|
// Some execexe builds expose ordinal-only five-byte near jumps backed
|
||||||
|
// only by INT3 padding. MinHook cannot construct a trampoline from
|
||||||
|
// those tiny export stubs, while the jump destination has a normal
|
||||||
|
// prologue.
|
||||||
|
const auto bytes = reinterpret_cast<const uint8_t *>(entry);
|
||||||
|
if (!bytes || bytes[0] != 0xE9) {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32_t displacement = 0;
|
||||||
|
std::memcpy(&displacement, bytes + 1, sizeof(displacement));
|
||||||
|
return reinterpret_cast<T>(
|
||||||
|
const_cast<uint8_t *>(bytes + 5 + displacement));
|
||||||
|
}
|
||||||
|
|
||||||
static HANDLE WINAPI execexe_CreateFileA_hook(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
|
static HANDLE WINAPI execexe_CreateFileA_hook(LPCSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode,
|
||||||
LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition,
|
LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition,
|
||||||
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
|
DWORD dwFlagsAndAttributes, HANDLE hTemplateFile) {
|
||||||
@@ -92,6 +112,13 @@ namespace execexe {
|
|||||||
execexe_CreateFileW = libutils::get_proc<decltype(&CreateFileW)>(execexe_module, MAKEINTRESOURCE(11));
|
execexe_CreateFileW = libutils::get_proc<decltype(&CreateFileW)>(execexe_module, MAKEINTRESOURCE(11));
|
||||||
execexe_PreLoadLibraries = libutils::get_proc<decltype(execexe_PreLoadLibraries)>(execexe_module, MAKEINTRESOURCE(48));
|
execexe_PreLoadLibraries = libutils::get_proc<decltype(execexe_PreLoadLibraries)>(execexe_module, MAKEINTRESOURCE(48));
|
||||||
|
|
||||||
|
if (avs::game::is_model("UDN")) {
|
||||||
|
execexe_CloseHandle = resolve_export_thunk(execexe_CloseHandle);
|
||||||
|
execexe_CreateFileA = resolve_export_thunk(execexe_CreateFileA);
|
||||||
|
execexe_CreateFileW = resolve_export_thunk(execexe_CreateFileW);
|
||||||
|
execexe_PreLoadLibraries = resolve_export_thunk(execexe_PreLoadLibraries);
|
||||||
|
}
|
||||||
|
|
||||||
auto module_path = libutils::module_file_name(nullptr);
|
auto module_path = libutils::module_file_name(nullptr);
|
||||||
module_path = module_path.replace_extension("");
|
module_path = module_path.replace_extension("");
|
||||||
module_path = module_path.replace_filename(module_path.filename().wstring() + L"_Data");
|
module_path = module_path.replace_filename(module_path.filename().wstring() + L"_Data");
|
||||||
@@ -105,6 +132,30 @@ namespace execexe {
|
|||||||
log_fatal("execexe", "deferred init function is already set");
|
log_fatal("execexe", "deferred init function is already set");
|
||||||
}
|
}
|
||||||
deferred_function = std::move(init_func);
|
deferred_function = std::move(init_func);
|
||||||
|
|
||||||
|
// DANCE aROUND calls PreLoadLibraries through kamunity's ordinal
|
||||||
|
// import. Hook that call site directly: execexe can be loaded in the
|
||||||
|
// low address range immediately next to the AVS heap, where MinHook
|
||||||
|
// may be unable to reserve a nearby trampoline page.
|
||||||
|
if (avs::game::is_model("UDN")) {
|
||||||
|
const auto kamunity_module = GetModuleHandleW(L"kamunity.dll");
|
||||||
|
const auto imported_pre_load_libraries = kamunity_module ? detour::iat_try_ordinal(
|
||||||
|
"execexe.dll",
|
||||||
|
48,
|
||||||
|
execexe_PreLoadLibraries_hook,
|
||||||
|
kamunity_module) : nullptr;
|
||||||
|
if (imported_pre_load_libraries) {
|
||||||
|
execexe_PreLoadLibraries = reinterpret_cast<decltype(execexe_PreLoadLibraries)>(
|
||||||
|
imported_pre_load_libraries);
|
||||||
|
log_info("execexe", "hooked PreLoadLibraries through kamunity import");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
detour::trampoline(execexe_PreLoadLibraries,
|
||||||
|
execexe_PreLoadLibraries_hook, &execexe_PreLoadLibraries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
detour::trampoline("execexe.dll", MAKEINTRESOURCE(48),
|
detour::trampoline("execexe.dll", MAKEINTRESOURCE(48),
|
||||||
execexe_PreLoadLibraries_hook, &execexe_PreLoadLibraries);
|
execexe_PreLoadLibraries_hook, &execexe_PreLoadLibraries);
|
||||||
}
|
}
|
||||||
@@ -117,6 +168,16 @@ namespace execexe {
|
|||||||
|
|
||||||
port_name = portName;
|
port_name = portName;
|
||||||
acio = acioHandle;
|
acio = acioHandle;
|
||||||
|
if (avs::game::is_model("UDN")) {
|
||||||
|
detour::trampoline_try(execexe_CloseHandle,
|
||||||
|
execexe_CloseHandle_hook, &execexe_CloseHandle);
|
||||||
|
detour::trampoline_try(execexe_CreateFileA,
|
||||||
|
execexe_CreateFileA_hook, &execexe_CreateFileA);
|
||||||
|
detour::trampoline_try(execexe_CreateFileW,
|
||||||
|
execexe_CreateFileW_hook, &execexe_CreateFileW);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(7),
|
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(7),
|
||||||
execexe_CloseHandle_hook, &execexe_CloseHandle);
|
execexe_CloseHandle_hook, &execexe_CloseHandle);
|
||||||
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(9),
|
detour::trampoline_try("execexe.dll", MAKEINTRESOURCE(9),
|
||||||
@@ -183,6 +244,10 @@ namespace execexe {
|
|||||||
HMODULE module = get_module(dll);
|
HMODULE module = get_module(dll);
|
||||||
FARPROC proc = get_proc(module, func);
|
FARPROC proc = get_proc(module, func);
|
||||||
|
|
||||||
|
if (avs::game::is_model("UDN")) {
|
||||||
|
// The existing detour helper reads *orig as the target to enable.
|
||||||
|
*orig = reinterpret_cast<void *>(proc);
|
||||||
|
}
|
||||||
return detour::trampoline(
|
return detour::trampoline(
|
||||||
reinterpret_cast<void *>(proc),
|
reinterpret_cast<void *>(proc),
|
||||||
hook,
|
hook,
|
||||||
@@ -201,6 +266,9 @@ namespace execexe {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (avs::game::is_model("UDN")) {
|
||||||
|
*orig = reinterpret_cast<void *>(proc);
|
||||||
|
}
|
||||||
return detour::trampoline(
|
return detour::trampoline(
|
||||||
reinterpret_cast<void *>(proc),
|
reinterpret_cast<void *>(proc),
|
||||||
hook,
|
hook,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <windows.h>
|
||||||
#include <winternl.h>
|
#include <winternl.h>
|
||||||
|
|
||||||
#define LDR_DLL_NOTIFICATION_REASON_LOADED 1
|
#define LDR_DLL_NOTIFICATION_REASON_LOADED 1
|
||||||
@@ -43,4 +44,4 @@ NTSTATUS NTAPI LdrRegisterDllNotification(
|
|||||||
|
|
||||||
NTSTATUS NTAPI LdrUnregisterDllNotification(
|
NTSTATUS NTAPI LdrUnregisterDllNotification(
|
||||||
PVOID Cookie
|
PVOID Cookie
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user