Compare commits

...

15 Commits

Author SHA1 Message Date
Will 112b1aecb0 Give options a designated initializer to make crossreferencing their usage easier (#905)
## Description of change
This always bothered me, you have a commandline arg and it's a massive
pain in the ass to work out where it's used because the flag never
really matches the enum name.

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

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

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

Fell out of the changes in #905

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

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

---------

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

## Description of change

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

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

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

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

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

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

## Testing
2p card was recognized in test mode. No other testing performed.
2026-09-05 00:00:41 -07:00
bicarus 0169fe1e31 gitadora: show LEFT / RIGHT screens on sub screen in landscape mode (#900)
## Link to GitHub Issue or related Pull Request, if one exists
Continues #899 

## Description of change
#899 added the ability to display the gitadora subscreen in lanscape
mode with black bars on the side. With the new option the black bars can
instead display the LEFT / RIGHT screens, completing the two-monitor
full screen use case for arena model.

## Testing
2026-08-30 22:35:12 -07:00
bicarus 357e6c86f1 gitadora: allow a landscape monitor to display subscreen (#899)
## Link to GitHub Issue or related Pull Request, if one exists
#0

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

N/A

## Description of change

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

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

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

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

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

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

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

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

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

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

## Testing

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

*This implementation was prepared with assistance from OpenAI Codex.*
2026-08-25 00:49:31 -07:00
50 changed files with 1998 additions and 999 deletions
+29
View File
@@ -30,3 +30,32 @@ jobs:
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
path: src/spice2x/bin
if-no-files-found: error
msvc-ci:
name: Build (MSVC ${{ matrix.arch }}-bit)
# windows-latest ships Visual Studio 2026; pin to the VS 2022 image, which is
# the toolset spice2x is actually developed against
runs-on: windows-2022
strategy:
fail-fast: false
matrix:
include:
- arch: 32
platform: Win32
target: spicetools_spice spicetools_cfg
- arch: 64
platform: x64
target: spicetools_spice64
defaults:
run:
working-directory: ./src/spice2x
steps:
- uses: actions/checkout@v5
- name: Configure
run: >
cmake -S . -B cmake-build-msvc-${{ matrix.arch }}
-A ${{ matrix.platform }}
- name: Compile
run: >
cmake --build cmake-build-msvc-${{ matrix.arch }}
--config Release --target ${{ matrix.target }}
+1 -1
View File
@@ -105,5 +105,5 @@ if (conditional)
* We don't use GSL.
* Don't throw exceptions.
* Stick to smart pointers. For interfacing with C Win32 API and raw buffers, use `unique_plain_ptr`.
* Writing in C is also completely acceptable.
* Writing in C is also completely acceptable but prefer to use RAII patterns.
* Other than that, there are no strict rules for code formatting, but please attempt to emulate the style around the code you are modifying.
+4
View File
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
### AI-Use Policy
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
## Additional information
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
+2
View File
@@ -520,6 +520,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/scotto/io.cpp
games/drs/drs.cpp
games/drs/io.cpp
games/drs/motion_cam.cpp
games/drs/rgb_cam.cpp
games/we/we.cpp
games/we/io.cpp
@@ -531,6 +532,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
games/otoca/p4io.cpp
games/silentscope/silentscope.cpp
games/silentscope/io.cpp
games/silentscope/projector.cpp
games/pcm/pcm.cpp
games/pcm/io.cpp
games/onpara/onpara.cpp
+7
View File
@@ -144,6 +144,13 @@ restriction is that the ID has to be a valid 64-bit unsigned integer.
`busy`
#### Card
- get_cards()
- returns the current card ID and source for each active card reader
- each entry contains `index`, `card_id`, and `source`
- `source` is `file` or `override`
- file entries also contain `file_name`, without the full configured path
- `-card0` and `-card1` overrides do not contain `file_name`
- this function only works when an API password is configured
- insert(index: uint, card_id: hex)
- inserts a card which gets read by the emulated card readers for the game
- index has to be either 0 (for P1) or 1 (for P2)
+32 -6
View File
@@ -1,5 +1,7 @@
#include "bmpu.h"
#include <algorithm>
#include "acio/icca/icca.h"
#include "avs/game.h"
#include "cfg/api.h"
@@ -9,6 +11,7 @@
#include "games/ftt/io.h"
#include "games/museca/io.h"
#include "games/silentscope/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
STATUS_BUFFER[4] |= 0x20;
}
// joy stick raw input
// gun position, sent big endian; the game keeps the top 12 bits of each pair
auto &analogs = games::silentscope::get_analogs();
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
unsigned short joy_x = 0x7FFF;
unsigned short joy_y = 0x7FFF;
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
}
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
if (gun_x.isSet() || gun_y.isSet()) {
if (gun_x.isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
}
if (gun_y.isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
}
} else {
// the gun reports where it is aimed, which a relative pointer cannot express - the
// calibration screen asks for the screen centre and two corners, so read the cursor
POINT cursor {};
RECT client {};
POINT origin {};
if (NDD_MAIN_WINDOW != nullptr &&
GetCursorPos(&cursor) &&
GetClientRect(NDD_MAIN_WINDOW, &client) &&
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
client.right > 1 && client.bottom > 1)
{
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
}
}
// invert X axis
+13 -4
View File
@@ -8,6 +8,7 @@
#include "client.h"
#include "cfg/configurator.h"
#include "external/rapidjson/document.h"
#include "external/rapidjson/error/en.h"
#include "util/crypt.h"
#include "util/logging.h"
#include "util/utils.h"
@@ -92,6 +93,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// bind socket to address
if (bind(this->server, (sockaddr *) &server_address, sizeof(sockaddr)) == -1) {
log_warning("api", "could not bind socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -102,6 +104,7 @@ Controller::Controller(unsigned short port, std::string password, bool pretty)
// set socket to listen
if (listen(this->server, server_backlog) == -1) {
log_warning("api", "could not listen to socket on port {}: {}", port, get_last_error_string());
closesocket(this->server);
this->server = INVALID_SOCKET;
if (!cfg::CONFIGURATOR_STANDALONE) {
log_fatal("api", "failed to start server");
@@ -312,6 +315,8 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
// check for parse error
if (document.HasParseError()) {
log_warning("api", "Request JSON parse error: {} (byte offset {}, decrypted request length {} bytes)",
GetParseError_En(document.GetParseError()), document.GetErrorOffset(), in_size);
// return empty response and close connection
out->push_back(0);
@@ -337,9 +342,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
if (module->name == request.module) {
module_found = true;
// check password force
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
Value err("Module requires the password to be set.");
// check password requirement
if (module->requires_password(request.function)
&& this->password.empty()
&& request.function != "session_refresh") {
Value err("Function requires the password to be set.");
response.add_error(err);
break;
}
@@ -441,7 +448,9 @@ void Controller::free_socket() {
this->server = INVALID_SOCKET;
}
this->websocket->free_socket();
if (this->websocket) {
this->websocket->free_socket();
}
for (auto &s : this->serial) {
s->free_port();
+3 -3
View File
@@ -41,21 +41,21 @@ namespace api {
bool pretty;
// server
WebSocketController *websocket;
WebSocketController *websocket = nullptr;
std::vector<SerialController *> serial;
std::vector<std::thread> server_workers;
std::vector<std::thread> server_handlers;
std::mutex server_handlers_m;
std::vector<api::ClientState *> client_states;
std::mutex client_states_m;
SOCKET server;
SOCKET server = INVALID_SOCKET;
void server_worker();
void connection_handler(ClientState client_state);
public:
// state
bool server_running;
bool server_running = false;
// constructor / destructor
Controller(unsigned short port, std::string password, bool pretty);
+8
View File
@@ -16,6 +16,14 @@ namespace api {
this->password_force = password_force;
}
void Module::require_password(const std::string &function) {
this->password_force_functions.emplace(function);
}
bool Module::requires_password(const std::string &function) const {
return this->password_force || this->password_force_functions.contains(function);
}
void Module::handle(Request &req, Response &res) {
// log module access
+10
View File
@@ -4,6 +4,7 @@
#include <map>
#include <string>
#include <sstream>
#include <unordered_set>
#include <external/robin_hood.h>
#include "response.h"
@@ -26,6 +27,13 @@ namespace api {
// default constructor
explicit Module(std::string name, bool password_force=false);
void require_password(const std::string &function);
private:
// functions which expose sensitive data or actions
std::unordered_set<std::string> password_force_functions;
public:
// virtual deconstructor
@@ -35,6 +43,8 @@ namespace api {
std::string name;
bool password_force;
bool requires_password(const std::string &function) const;
// the magic
void handle(Request &req, Response &res);
+64
View File
@@ -1,4 +1,5 @@
#include "card.h"
#include <fstream>
#include <functional>
#include "external/rapidjson/document.h"
#include "util/logging.h"
@@ -11,8 +12,71 @@ using namespace rapidjson;
namespace api::modules {
static bool normalize_card_id(const std::string &value, std::string &card_id) {
if (value.size() != 16) {
return false;
}
uint8_t card_bin[8] {};
if (!hex2bin(value.c_str(), card_bin)) {
return false;
}
card_id = bin2hex(card_bin, std::size(card_bin));
return true;
}
static bool read_card_id(const std::filesystem::path &path, std::string &card_id) {
std::ifstream file(path);
char buffer[16] {};
if (!file.read(buffer, std::size(buffer))) {
return false;
}
return normalize_card_id(std::string(buffer, std::size(buffer)), card_id);
}
Card::Card() : Module("card") {
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
functions["insert"] = std::bind(&Card::insert, this, _1, _2);
require_password("get_cards");
}
/**
* get_cards()
*/
void Card::get_cards(Request &req, Response &res) {
auto &alloc = res.doc()->GetAllocator();
for (int index = 0; index < eamuse_get_game_keypads(); index++) {
std::string card_id;
std::string filename;
const auto card_override = eamuse_get_card_override(index);
const bool has_override = !card_override.empty();
if (has_override) {
if (!normalize_card_id(card_override, card_id)) {
continue;
}
} else {
const auto path = eamuse_get_card_path(index);
if (!read_card_id(path, card_id)) {
continue;
}
const auto filename_u8 = path.filename().u8string();
filename.assign(filename_u8.begin(), filename_u8.end());
}
Value card(kObjectType);
card.AddMember("index", index, alloc);
card.AddMember("card_id", Value(card_id.c_str(), alloc), alloc);
card.AddMember("source", Value(has_override ? "override" : "file", alloc), alloc);
if (!has_override) {
card.AddMember("file_name", Value(filename.c_str(), alloc), alloc);
}
res.add_data(card);
}
}
/**
+1
View File
@@ -12,6 +12,7 @@ namespace api::modules {
private:
// function definitions
void get_cards(Request &req, Response &res);
void insert(Request &req, Response &res);
};
}
+1 -1
View File
@@ -61,7 +61,7 @@ namespace api::modules {
return error_type(res, "amount", "int");
// add to coin stock
eamuse_coin_set_stock(eamuse_coin_get_stock() + std::max(0, req.params[0].GetInt()));
eamuse_coin_add(std::max(0, req.params[0].GetInt()));
}
/*
@@ -1,5 +1,32 @@
part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
@@ -1,5 +1,32 @@
part of spiceapi;
class CardInfo {
final int index;
final String cardID;
final String source;
final String? fileName;
CardInfo(this.index, this.cardID, this.source, this.fileName);
}
Future<List<CardInfo>> cardGetCards(Connection con) {
var req = Request("card", "get_cards");
return con.request(req).then((res) {
List<CardInfo> cards = [];
for (var value in res.getData()) {
cards.add(
CardInfo(
value["index"],
value["card_id"],
value["source"],
value["file_name"],
),
);
}
return cards;
});
}
Future<void> cardInsert(Connection con, int unit, String cardID) {
var req = Request("card", "insert");
req.addParam(unit);
@@ -2,6 +2,10 @@ from .connection import Connection
from .request import Request
def card_get_cards(con: Connection):
return con.request(Request("card", "get_cards")).get_data()
def card_insert(con: Connection, unit: int, card_id: str):
req = Request("card", "insert")
req.add_param(unit)
+41 -37
View File
@@ -9,6 +9,8 @@
#include "util/logging.h"
#include "util/precise_timer.h"
#include "util/memutils.h"
#include "io.h"
#include "motion_cam.h"
#include "rgb_cam.h"
#pragma pack(push)
@@ -276,48 +278,50 @@ namespace games::drs {
void DRSGame::attach() {
Game::attach();
// TouchSDK hooks
detour::iat("??0TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
detour::iat("??1TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
if (!DISABLE_TOUCH) {
// TouchSDK hooks
detour::iat("??0TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Constructor, avs::game::DLL_INSTANCE);
detour::iat("?SendData@TouchSDK@@QEAA_NU_DeviceInfo@@QEAEH1HH@Z",
(void *) &TouchSDK_SendData, avs::game::DLL_INSTANCE);
detour::iat("?SetSignalInit@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetSignalInit, avs::game::DLL_INSTANCE);
detour::iat("??1TouchSDK@@QEAA@XZ",
(void *) &TouchSDK_Destructor, avs::game::DLL_INSTANCE);
detour::iat("?GetYLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetYLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?GetXLedTotal@TouchSDK@@QEAAHU_DeviceInfo@@H@Z",
(void *) &TouchSDK_GetXLedTotal, avs::game::DLL_INSTANCE);
detour::iat("?DisableTouch@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableTouch, avs::game::DLL_INSTANCE);
detour::iat("?DisableDrag@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableDrag, avs::game::DLL_INSTANCE);
detour::iat("?DisableWheel@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableWheel, avs::game::DLL_INSTANCE);
detour::iat("?DisableRightClick@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_DisableRightClick, avs::game::DLL_INSTANCE);
detour::iat("?SetMultiTouchMode@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_SetMultiTouchMode, avs::game::DLL_INSTANCE);
detour::iat("?EnableTouchWidthData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableTouchWidthData, avs::game::DLL_INSTANCE);
detour::iat("?EnableRawData@TouchSDK@@QEAA_NU_DeviceInfo@@H@Z",
(void *) &TouchSDK_EnableRawData, avs::game::DLL_INSTANCE);
detour::iat("?SetAllEnable@TouchSDK@@QEAA_NU_DeviceInfo@@_NH@Z",
(void *) &TouchSDK_SetAllEnable, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchDeviceCount@TouchSDK@@QEAAHXZ",
(void *) &TouchSDK_GetTouchDeviceCount, avs::game::DLL_INSTANCE);
detour::iat("?GetTouchSDKVersion@TouchSDK@@QEAAIXZ",
(void *) &TouchSDK_GetTouchSDKVersion, avs::game::DLL_INSTANCE);
detour::iat("?InitTouch@TouchSDK@@QEAAHPEAU_DeviceInfo@@HP6AXU2@PEBU_TouchPointData@@HHPEBX@ZP6AX1_N3@ZPEAX@Z",
(void *) &TouchSDK_InitTouch, avs::game::DLL_INSTANCE);
start_touch();
} else {
log_info("drs", "no native input method detected");
log_info("drs", "touch input for dance floor disabled");
}
init_down_motion_hook();
if (RGB_CAMERA_HOOK) {
init_rgb_camera_hook();
}
+4 -3
View File
@@ -20,7 +20,8 @@ std::vector<Button> &games::drs::get_buttons() {
"P2 Up",
"P2 Down",
"P2 Left",
"P2 Right"
"P2 Right",
"Down Motion"
);
}
@@ -30,8 +31,8 @@ std::vector<Button> &games::drs::get_buttons() {
std::string games::drs::get_buttons_help() {
// keep to max 100 characters wide
return
"Motion camera required for DOWN movement.\n"
"Touchscreen supported for dance floor."
"Touchscreen supported for dance floor.\n"
"Down Motion applies to both players."
;
}
+2 -1
View File
@@ -20,7 +20,8 @@ namespace games::drs {
P2_Up,
P2_Down,
P2_Left,
P2_Right
P2_Right,
DownMotion
};
}
+109
View File
@@ -0,0 +1,109 @@
#include "games/drs/motion_cam.h"
#include <cstdint>
#include "avs/game.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/sigscan.h"
#include "io.h"
namespace games::drs {
// the game reads down movement from a depth camera whose driver is compiled into the game DLL,
// so there is no import to replace.
//
// with no camera the tracked height never changes, so the motion state never leaves "still"
// and DOWN notes can never be hit.
//
// rather than answer CInputManager::IsDown, the button drives the same state pair the camera
// would; that leaves the game's own edge test untouched and keeps every note in a frame seeing
// one answer.
static int32_t *MOTION_STATE = nullptr;
static int32_t *MOTION_STATE_PREV = nullptr;
static constexpr int32_t MOTION_STATE_STILL = 1;
static constexpr int32_t MOTION_STATE_DOWN = 2;
static void (*UpdateMotion_orig)() = nullptr;
static bool DOWN_MOTION_HELD = false;
static void InputManager_UpdateMotion() {
UpdateMotion_orig();
auto &buttons = get_buttons();
const bool held = GameAPI::Buttons::getState(RI_MGR, buttons.at(Buttons::DownMotion));
// a state of DOWN only counts as movement when the frame before it was something else
if (held) {
*MOTION_STATE_PREV = DOWN_MOTION_HELD ? MOTION_STATE_DOWN : MOTION_STATE_STILL;
*MOTION_STATE = MOTION_STATE_DOWN;
}
DOWN_MOTION_HELD = held;
}
void init_down_motion_hook() {
// only hook motion camera if the user has down motion button bound to something
auto &buttons = get_buttons();
if (!buttons.at(Buttons::DownMotion).isSet()) {
return;
}
// CInputManager::IsDown - read the state pair out of its two operands.
// Identical in game versions 2020121400 and 2022121400 / 2024120300:
//
// 83 3D xx xx xx xx 02 cmp dword [rip+prev], 2 ; state one frame ago
// 74 0C je FALSE ; already down, so no new edge
// 83 3D xx xx xx xx 02 cmp dword [rip+cur], 2 ; state this frame
// 75 03 jne FALSE ; not down
// B0 01 mov al, 1 ; went down on this frame
// C3 ret
// 32 C0 FALSE: xor al, al
// C3 ret
auto is_down = reinterpret_cast<uint8_t *>(find_pattern(
avs::game::DLL_INSTANCE,
"833D0000000002740C833D00000000027503B001C332C0C3",
"XX????XXXXX????XXXXXXXXX",
0, 0));
if (is_down == nullptr) {
log_warning("drs", "motion sensor state not found, DOWN motion button unavailable");
return;
}
// rip-relative displacements resolve against the end of their own instruction, and each
// cmp above is 7 bytes with its displacement 4 bytes in: the first ends at +7 with its
// displacement at +2, the second starts at +9 so it ends at +16 with its own at +11.
// read signed, since a target sitting below the instruction encodes as negative.
MOTION_STATE_PREV = (int32_t *) (is_down + 7 + *(int32_t *) (is_down + 2));
MOTION_STATE = (int32_t *) (is_down + 16 + *(int32_t *) (is_down + 11));
// CInputManager::UpdateMotion is hooked for the sole purpose of reading the Down Motion
// button mapping frequently enough so that the rising edge can be detected.
//
// Matched over the prologue up to the camera read; the wildcards are a float and a callee:
//
// 48 83 EC 38 sub rsp, 0x38
// 0F 29 74 24 20 movaps [rsp+0x20], xmm6
// F3 0F 10 35 xx xx xx xx movss xmm6, [rip+height] ; reading as of last frame
// E8 xx xx xx xx call GetPlayVideoProcess
// 48 8B C8 mov rcx, rax
// 48 8B 10 mov rdx, [rax]
// FF 52 60 call [rdx+0x60] ; reading off the newest frame
auto update = reinterpret_cast<void *>(find_pattern(
avs::game::DLL_INSTANCE,
"4883EC380F29742420F30F103500000000E800000000488BC8488B10FF5260",
"XXXXXXXXXXXXX????X????XXXXXXXXX",
0, 0));
if (update == nullptr ||
!detour::trampoline_try(
update, (void *) InputManager_UpdateMotion, (void **) &UpdateMotion_orig)) {
log_warning("drs", "motion update not hooked, DOWN motion button unavailable");
return;
}
log_info("drs", "hooked DOWN motion at +{:#x}",
(uintptr_t) ((uint8_t *) update - (uint8_t *) avs::game::DLL_INSTANCE));
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
namespace games::drs {
void init_down_motion_hook();
}
+45
View File
@@ -39,10 +39,38 @@ namespace games::gitadora {
std::optional<socd::SocdAlgorithm> PICK_ALGO = socd::SocdAlgorithm::PreferRecent;
std::optional<uint8_t> ARENA_WINDOW_COUNT = std::nullopt;
bool ARENA_TWO_HEAD_EXCLUSIVE = false;
ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
std::optional<std::string> ASIO_DRIVER = std::nullopt;
bool ALLOW_REALTEK_AUDIO = false;
bool NATIVE_TOUCH = false;
std::pair<UINT, UINT> arena_subscreen_host_size() {
if (GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
return GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value();
}
if (arena_subscreen_landscape()) {
return { ARENA_SUBSCREEN_LANDSCAPE_WIDTH, ARENA_SUBSCREEN_LANDSCAPE_HEIGHT };
}
return { ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT };
}
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height) {
if (host_width <= 0 || host_height <= 0) {
return RECT {};
}
LONG width = MulDiv(host_height, ARENA_SUBSCREEN_WIDTH, ARENA_SUBSCREEN_HEIGHT);
LONG height = host_height;
if (width > host_width) {
width = host_width;
height = MulDiv(host_width, ARENA_SUBSCREEN_HEIGHT, ARENA_SUBSCREEN_WIDTH);
}
const LONG left = (host_width - width) / 2;
const LONG top = (host_height - height) / 2;
return RECT { left, top, left + width, top + height };
}
/*
* Prevent GitaDora from creating folders on F drive
*/
@@ -343,6 +371,23 @@ namespace games::gitadora {
"gitadora",
"arena model: unsupported window count: {}", count);
}
if (arena_subscreen_landscape() && !ARENA_TWO_HEAD_EXCLUSIVE) {
log_warning(
"gitadora",
"arena model: landscape subscreen needs full screen two-window mode, ignoring");
ARENA_SUBSCREEN_LANDSCAPE = ArenaSubscreenLandscape::Off;
}
if (ARENA_TWO_HEAD_EXCLUSIVE) {
const auto [host_width, host_height] = arena_subscreen_host_size();
if (host_width != ARENA_SUBSCREEN_WIDTH
|| host_height != ARENA_SUBSCREEN_HEIGHT) {
log_info(
"gitadora",
"arena model: SMALL head runs at {}x{}, subscreen is scaled to fit",
host_width, host_height);
}
}
}
#endif
+30
View File
@@ -1,6 +1,7 @@
#pragma once
#include <optional>
#include <utility>
#include <windows.h>
#include <mmreg.h>
@@ -11,6 +12,14 @@
namespace games::gitadora {
// what a landscape monitor driving the SMALL head does with the space beside the
// portrait subscreen image
enum class ArenaSubscreenLandscape {
Off, // SMALL head stays portrait
Small, // subscreen centered, the space either side stays black
All, // that space shows the LEFT and RIGHT heads instead
};
// settings
extern bool TWOCHANNEL;
extern bool DISABLE_FRAME_LIMITER;
@@ -21,14 +30,35 @@ namespace games::gitadora {
extern std::optional<socd::SocdAlgorithm> PICK_ALGO;
extern std::optional<uint8_t> ARENA_WINDOW_COUNT;
extern bool ARENA_TWO_HEAD_EXCLUSIVE;
extern ArenaSubscreenLandscape ARENA_SUBSCREEN_LANDSCAPE;
extern std::optional<std::string> ASIO_DRIVER;
extern bool ALLOW_REALTEK_AUDIO;
extern bool NATIVE_TOUCH;
static inline bool arena_subscreen_landscape() {
return ARENA_SUBSCREEN_LANDSCAPE != ArenaSubscreenLandscape::Off;
}
static inline bool arena_subscreen_shows_sides() {
return ARENA_SUBSCREEN_LANDSCAPE == ArenaSubscreenLandscape::All;
}
// arena SMALL subscreen (touch panel) resolution
static constexpr int ARENA_SUBSCREEN_WIDTH = 800;
static constexpr int ARENA_SUBSCREEN_HEIGHT = 1280;
// used when a landscape monitor drives the SMALL head and -forceressub is unset
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_WIDTH = 1920;
static constexpr int ARENA_SUBSCREEN_LANDSCAPE_HEIGHT = 1080;
// resolution the SMALL head actually runs at; the portrait panel size unless
// -forceressub or the landscape option overrides it
std::pair<UINT, UINT> arena_subscreen_host_size();
// area of the host the portrait subscreen occupies, centered and aspect-preserved;
// whatever is left over on either side stays black
RECT arena_subscreen_content_rect(LONG host_width, LONG host_height);
class GitaDoraGame : public games::Game {
public:
GitaDoraGame();
+9 -10
View File
@@ -2,6 +2,7 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <d3d9.h>
#include "mf_wrappers.h"
#include "avs/game.h"
@@ -46,9 +47,7 @@ struct PredefinedHook {
uintptr_t hook_afp_texture_offset;
};
PredefinedHook g_predefinedHooks[] = {};
const DWORD g_predefinedHooksLength = ARRAYSIZE(g_predefinedHooks);
std::array<PredefinedHook, 0> g_predefinedHooks = {};
namespace games::iidx {
@@ -110,14 +109,14 @@ namespace games::iidx {
auto pe = fmt::format("{:x}_{:x}", time_date_stamp, address_of_entry_point);
log_info("iidx:camhook", "Locating predefined hook addresses for LDJ-{}", pe);
for (DWORD i = 0; i < g_predefinedHooksLength; i++) {
if (pe.compare(g_predefinedHooks[i].pe_identifier) == 0) {
for (auto &hook : g_predefinedHooks) {
if (pe.compare(hook.pe_identifier) == 0) {
log_misc("iidx:camhook", "Found predefined addresses");
addr_hook_a = g_predefinedHooks[i].hook_a;
addr_textures = g_predefinedHooks[i].hook_textures;
addr_camera_manager = g_predefinedHooks[i].hook_camera_manager;
addr_device_offset = g_predefinedHooks[i].hook_device_offset;
addr_afp_texture_offset = g_predefinedHooks[i].hook_afp_texture_offset;
addr_hook_a = hook.hook_a;
addr_textures = hook.hook_textures;
addr_camera_manager = hook.hook_camera_manager;
addr_device_offset = hook.hook_device_offset;
addr_afp_texture_offset = hook.hook_afp_texture_offset;
return TRUE;
}
}
+110 -55
View File
@@ -2,6 +2,8 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <mutex>
#include <optional>
#include <cstdint>
#include "util/detour.h"
@@ -141,10 +143,100 @@ namespace games::popn {
static AIO_SCI_COMM *aioSciComm;
static AIO_IOB5_BI3A *aioIob5Bi3a;
static constexpr size_t PIKA_BUTTON_COUNT = 9;
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
static bool pika_button_staging_dirty = false;
static std::mutex pika_button_staging_mutex;
/*
* implementations
*/
static void publish_pika_button_staging() {
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
if (!pika_button_staging_dirty) {
return;
}
data = pika_button_staging;
pika_button_staging_dirty = false;
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R,
Lights::popn_lights_t::PikaButton1_G,
Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R,
Lights::popn_lights_t::PikaButton2_G,
Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R,
Lights::popn_lights_t::PikaButton3_G,
Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R,
Lights::popn_lights_t::PikaButton4_G,
Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R,
Lights::popn_lights_t::PikaButton5_G,
Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R,
Lights::popn_lights_t::PikaButton6_G,
Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R,
Lights::popn_lights_t::PikaButton7_G,
Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R,
Lights::popn_lights_t::PikaButton8_G,
Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R,
Lights::popn_lights_t::PikaButton9_G,
Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
auto &lights = get_lights();
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
const auto data_offset = button * 3;
// special handling converting RGB data to non-RGB lights
// take the max(R, G, B) and use it to write the light value
const auto light_value = std::max({
data[data_offset],
data[data_offset + 1],
data[data_offset + 2]
});
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
// at all times, even when "off". When translating to legacy on/off lights,
// treat only values above about 60% brightness as fully on to preserve the
// dim "off" state.
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[button]),
light_value > 150 ? 1.f : 0.f);
}
for (size_t light = 0; light < data.size(); light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
data[light] / 255.f);
}
}
// libaio.dll
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
@@ -260,6 +352,11 @@ namespace games::popn {
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
if (i_pNodeMgr == aioNmgrIob5) {
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
pika_button_staging.fill(0);
pika_button_staging_dirty = false;
}
aioIob5Bi3a = new AIO_IOB5_BI3A;
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
return aioIob5Bi3a;
@@ -269,11 +366,15 @@ namespace games::popn {
}
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
RI_MGR->devices_flush_output();
if (i_pNodeCtl != aioIob5Bi3a) {
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
}
// snapshot the current state of lights and flush the output
// this closely mirrors what the game does with real I/O
publish_pika_button_staging();
RI_MGR->devices_flush_output();
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
auto &buttons = get_buttons();
@@ -413,61 +514,15 @@ namespace games::popn {
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
}
auto &lights = get_lights();
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
// special handling converting to non-RGB lights
// take the max(R, G, B) and use it to write the light value
uint8_t light_value[9] = { 0 };
for (uint32_t i = 0; i < number_of_leds; i += 3) {
light_value[i / 3] =
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
static_assert(std::size(legacy_button_lights) == 9);
for (size_t light = 0; light < 9; light++) {
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
// otherwise off, to avoid dimly lit "off" state
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[light]),
light_value[light] > 150 ? 1.f : 0.f);
}
// color buttons
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(button_lights) == 9 * 3);
for (size_t light = 0; light < number_of_leds; light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
((uint8_t *)i_pData)[light] / 255.f);
}
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
// clears the buffer (set to 0 / lights off) between animations but they are not meant
// to be seen by the I/O
// therefore, instead of writing the lights out directly here, we stage them in a buffer
// and wait for GetDeviceStatus to be called
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
pika_button_staging_dirty = true;
}
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
+150
View File
@@ -0,0 +1,150 @@
#include "projector.h"
#include <algorithm>
#include <cstring>
#include <cwchar>
#include "util/logging.h"
namespace {
// request/response layout: identifier, command, two zero bytes, payload length,
// the payload itself and a checksum over everything before it
constexpr size_t HEADER_SIZE = 5;
constexpr uint8_t RESPONSE_ID = 0x23;
constexpr uint8_t COMMAND_COMMON_DATA = 0x8A;
constexpr uint8_t COMMAND_TEMPERATURE = 0x99;
constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B;
// shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT
constexpr uint32_t LAMP_SECONDS = 0;
constexpr uint32_t TEMPERATURE_INTAKE = 25;
constexpr uint32_t TEMPERATURE_EXHAUST = 35;
// anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact
// value was already recorded in /projecter/current on an earlier boot
constexpr uint32_t LAMP_CURRENT = 1000;
void put32(uint8_t *dest, uint32_t value) {
dest[0] = (uint8_t) (value & 0xFF);
dest[1] = (uint8_t) ((value >> 8) & 0xFF);
dest[2] = (uint8_t) ((value >> 16) & 0xFF);
dest[3] = (uint8_t) ((value >> 24) & 0xFF);
}
}
bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) {
return false;
}
log_info("silentscope", "Opened COM2 (projector)");
return true;
}
int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) {
buffer[bytes_read++] = this->response.front();
this->response.pop_front();
}
return (int) bytes_read;
}
int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite);
while (this->request.size() >= HEADER_SIZE) {
const size_t packet_size = HEADER_SIZE + this->request[4] + 1;
if (this->request.size() < packet_size) {
break;
}
this->process_request(this->request.data());
this->request.erase(this->request.begin(), this->request.begin() + packet_size);
}
return (int) nNumberOfBytesToWrite;
}
size_t games::silentscope::ProjectorHandle::bytes_available() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->response.size();
}
bool games::silentscope::ProjectorHandle::close() {
std::lock_guard<std::mutex> lock(this->mutex);
this->request.clear();
this->response.clear();
log_info("silentscope", "Closed COM2 (projector)");
return true;
}
void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) {
const uint8_t command = packet[1];
const uint8_t length = packet[4];
const uint8_t *data = packet + HEADER_SIZE;
switch (command) {
case COMMAND_COMMON_DATA: {
// the game only looks at the lamp usage time near the end of the block
std::vector<uint8_t> payload(98, 0);
put32(&payload[94], LAMP_SECONDS);
this->reply(command, payload);
break;
}
case COMMAND_TEMPERATURE: {
// the requested sensor is echoed back along with its reading
const uint8_t sensor = length > 0 ? data[0] : 0;
std::vector<uint8_t> payload(5, 0);
payload[0] = sensor;
put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST);
this->reply(command, payload);
break;
}
case COMMAND_LAMP_CURRENT: {
// the three byte item selector is echoed back along with the measurement
std::vector<uint8_t> payload(7, 0);
memcpy(payload.data(), data, std::min<size_t>(length, 3));
put32(&payload[3], LAMP_CURRENT);
this->reply(command, payload);
break;
}
default:
log_misc("silentscope", "unknown projector command {:#04x}", command);
this->reply(command, {});
break;
}
}
void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector<uint8_t> &data) {
std::vector<uint8_t> packet {
RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size()
};
packet.insert(packet.end(), data.begin(), data.end());
uint8_t checksum = 0;
for (auto byte : packet) {
checksum += byte;
}
packet.push_back(checksum);
this->response.insert(this->response.end(), packet.begin(), packet.end());
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
#include "hooks/devicehook.h"
namespace games::silentscope {
// The cabinet talks to its projector over COM2. Without an answer the game stops at
// I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly").
class ProjectorHandle : public CustomHandle {
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
size_t bytes_available() override;
bool close() override;
private:
std::mutex mutex;
std::vector<uint8_t> request;
std::deque<uint8_t> response;
void process_request(const uint8_t *packet);
void reply(uint8_t command, const std::vector<uint8_t> &data);
};
}
@@ -1,10 +1,11 @@
#include "silentscope.h"
#include "acioemu/handle.h"
#include "cfg/configurator.h"
#include "hooks/devicehook.h"
#include "util/libutils.h"
#include "projector.h"
namespace games::silentscope {
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
@@ -16,9 +17,8 @@ namespace games::silentscope {
// load the game DLL so hooks apply
libutils::try_library("gamendd.dll");
// TODO(felix): implement stuff on this port
devicehook_init();
devicehook_add(new acioemu::ACIOHandle(L"COM2"));
devicehook_add(new ProjectorHandle());
}
void SilentScopeGame::detach() {
@@ -29,6 +29,10 @@ DEFINE_GUID(IID_IAudioClient,
0x1cb9ad4c, 0xdbfa, 0x4c32,
0xb1, 0x78, 0xc2, 0xf5, 0x68, 0xa7, 0x03, 0xb2);
DEFINE_GUID(IID_IAudioClient3,
0x7ed4ee07, 0x8e67, 0x4cd4,
0x8c, 0x1a, 0x2b, 0x7a, 0x59, 0x87, 0xad, 0x42);
DEFINE_GUID(IID_IAudioClock,
0xcd63314f, 0x3fba, 0x4a1b,
0x81, 0x2c, 0xef, 0x96, 0x35, 0x87, 0x28, 0xe7);
@@ -30,6 +30,7 @@ void graphics_d3d11_shutdown() {}
#include <dxgi.h>
#include <dxgi1_2.h>
#include "avs/game.h"
#include "d3d11_internal.h"
#include "util/nt_loader.h"
@@ -237,10 +238,11 @@ bool d3dcompiler_available() {
} // namespace
void graphics_d3d11_init() {
// dx11 titles always run under execexe. skipping on pure-dx9 games keeps
// their startup path completely untouched (no exports patched, no poll
// thread, no LDR callback).
if (!GetModuleHandleW(L"execexe.dll")) {
// dx11 titles run under execexe, except Silent Scope: Bone Eater, whose Aska
// engine LoadLibrary's d3d11 itself. skipping on pure-dx9 games keeps their
// startup path completely untouched (no exports patched, no poll thread, no
// LDR callback).
if (!GetModuleHandleW(L"execexe.dll") && !avs::game::is_model("NDD")) {
return;
}
@@ -1033,14 +1033,6 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
return D3DERR_INVALIDCALL;
}
if (gfdm_two_head_exclusive() && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_warning(
"graphics::d3d9",
"-forceressub is unavailable; SMALL must remain {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT);
return D3DERR_INVALIDCALL;
}
DWORD orig_behavior_flags = BehaviorFlags;
size_t num_adapters = 1;
@@ -1096,7 +1088,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
params->BackBufferHeight, params->BackBufferWidth);
std::swap(params->BackBufferWidth, params->BackBufferHeight);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
} else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
log_misc(
"graphics::d3d9",
"use custom sub resolution {}x{} => {}x{}",
@@ -1140,7 +1133,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
} else if (GRAPHICS_FS_ORIENTATION_SWAP) {
std::swap(fullscreen_display_mode->Width, fullscreen_display_mode->Height);
}
} else if (i == 1 && GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
} else if (i == 1 && !gfdm_two_head_exclusive()
&& GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.has_value()) {
fullscreen_display_mode->Width = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().first;
fullscreen_display_mode->Height = GRAPHICS_FS_CUSTOM_RESOLUTION_SUB.value().second;
}
@@ -1290,14 +1284,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
ppReturnedDeviceInterface);
if (SUCCEEDED(result) && gfdm_two_head_exclusive()) {
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.presentation_parameters[1];
if (pFullscreenDisplayMode != nullptr) {
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
gfdm_publish_two_head_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters);
}
// check for error
@@ -157,6 +157,9 @@ ULONG STDMETHODCALLTYPE WrappedIDirect3DDevice9::Release() {
}
}
// holds a reference on the device, so it has to go before the counts are compared
this->gfdm_small_head.release();
d3d9_readback::release_device_resources(this->pReal);
if (overlay::ENABLED) {
@@ -634,6 +637,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
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);
@@ -662,6 +667,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Present(
graphics_d3d9_on_present(hFocusWindow, pReal, this);
// 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);
}
CHECK_RESULT(pReal->Present(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion));
}
@@ -683,6 +694,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::GetBackBuffer(
if (is_gfdm_two_head_exclusive()
&& is_gfdm_logical_small_swapchain(iSwapChain))
{
if (gfdm_small_head.scaled()
&& iBackBuffer == 0
&& Type == D3DBACKBUFFER_TYPE_MONO)
{
CHECK_RESULT(gfdm_small_head.backbuffer(pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
iBackBuffer,
@@ -2324,6 +2341,8 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
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);
@@ -2341,14 +2360,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
}
if (is_gfdm_two_head_exclusive() && SUCCEEDED(res)) {
gfdm_logical_small_swapchain = gfdm_parameters.logical_small_swapchain;
pPresentationParameters[0] = gfdm_parameters.presentation_parameters[0];
pPresentationParameters[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.presentation_parameters[1];
if (pFullscreenDisplayMode != nullptr) {
pFullscreenDisplayMode[0] = gfdm_parameters.fullscreen_display_modes[0];
pFullscreenDisplayMode[gfdm_parameters.logical_small_swapchain] =
gfdm_parameters.fullscreen_display_modes[1];
}
gfdm_publish_two_head_parameters(
pPresentationParameters,
pFullscreenDisplayMode,
gfdm_parameters);
}
// recreate overlay
@@ -11,6 +11,7 @@
#include "util/logging.h"
#include "d3d9_fake_swapchain.h"
#include "d3d9_gfdm.h"
#include "d3d9_swapchain.h"
/*
@@ -279,6 +280,7 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
std::array<D3DPRESENT_PARAMETERS, 4> gfdm_logical_group_parameters {};
bool gfdm_logical_group_parameters_valid = false;
IDirect3D9 *gfdm_parent_d3d = nullptr;
GfdmSmallHead gfdm_small_head;
std::mutex gfdm_recovery_mutex;
std::array<D3DPRESENT_PARAMETERS, 2> gfdm_recovery_parameters {};
@@ -2,6 +2,7 @@
#include <algorithm>
#include <cassert>
#include <cstring>
#include <mutex>
#include "games/gitadora/gitadora.h"
@@ -10,12 +11,63 @@
#include "d3d9_device.h"
// windows.h defines many stupid things
#undef small
bool gfdm_two_head_exclusive() {
return games::gitadora::is_arena_model()
&& games::gitadora::ARENA_TWO_HEAD_EXCLUSIVE
&& !GRAPHICS_WINDOWED;
}
static std::pair<UINT, UINT> gfdm_small_head_size() {
if (!gfdm_two_head_exclusive()) {
return { GFDM_SMALL_WIDTH, GFDM_SMALL_HEIGHT };
}
return games::gitadora::arena_subscreen_host_size();
}
// scaling is only needed when the head is not the panel size the game draws into
static bool gfdm_small_head_scaled() {
const auto [width, height] = gfdm_small_head_size();
return width != GFDM_SMALL_WIDTH || height != GFDM_SMALL_HEIGHT;
}
// Hand the resolved MAIN/SMALL heads back to the game's own array. The SMALL entry keeps
// the portrait size the game asked for: that is what it renders, and the next reset finds
// the SMALL head by matching it.
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state)
{
if (logical_presentation_parameters == nullptr) {
return;
}
const UINT small = state.logical_small_swapchain;
const bool publish_modes = logical_fullscreen_display_modes != nullptr
&& state.fullscreen_display_modes != nullptr;
logical_presentation_parameters[0] = state.presentation_parameters[0];
logical_presentation_parameters[small] = state.presentation_parameters[1];
if (publish_modes) {
logical_fullscreen_display_modes[0] = state.fullscreen_display_modes[0];
logical_fullscreen_display_modes[small] = state.fullscreen_display_modes[1];
}
if (!gfdm_small_head_scaled()) {
return;
}
logical_presentation_parameters[small].BackBufferWidth = GFDM_SMALL_WIDTH;
logical_presentation_parameters[small].BackBufferHeight = GFDM_SMALL_HEIGHT;
if (publish_modes) {
logical_fullscreen_display_modes[small].Width = GFDM_SMALL_WIDTH;
logical_fullscreen_display_modes[small].Height = GFDM_SMALL_HEIGHT;
}
}
HRESULT graphics_d3d9_gfdm_select_two_head_group_parameters(
const D3DPRESENT_PARAMETERS *logical_presentation_parameters,
const D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
@@ -186,6 +238,26 @@ HRESULT graphics_d3d9_gfdm_remap_two_head_group_parameters(
return D3DERR_INVALIDCALL;
}
// the game keeps rendering the portrait subscreen; only the head it is scanned out
// on changes size, and the scaling happens when the head is presented
if (gfdm_small_head_scaled()) {
const auto [host_width, host_height] = gfdm_small_head_size();
log_info(
"graphics::d3d9",
"two-head exclusive: {} SMALL head {}x{} -> {}x{}",
operation,
secondary.BackBufferWidth,
secondary.BackBufferHeight,
host_width,
host_height);
secondary.BackBufferWidth = host_width;
secondary.BackBufferHeight = host_height;
if (fullscreen_display_modes != nullptr) {
fullscreen_display_modes[1].Width = host_width;
fullscreen_display_modes[1].Height = host_height;
}
}
return D3D_OK;
}
@@ -318,20 +390,21 @@ HRESULT validate_gfdm_two_head_exclusive(
const auto &main = presentation_parameters[0];
const auto &small_params = presentation_parameters[1];
const auto [expected_small_width, expected_small_height] = gfdm_small_head_size();
if (main.Windowed || small_params.Windowed) {
log_warning(
"graphics::d3d9",
"two-head exclusive mode requires both group heads to be fullscreen");
return D3DERR_INVALIDCALL;
}
if (small_params.BackBufferWidth != GFDM_SMALL_WIDTH
|| small_params.BackBufferHeight != GFDM_SMALL_HEIGHT)
if (small_params.BackBufferWidth != expected_small_width
|| small_params.BackBufferHeight != expected_small_height)
{
log_warning(
"graphics::d3d9",
"SMALL head must be {}x{}; got {}x{}",
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT,
expected_small_width,
expected_small_height,
small_params.BackBufferWidth,
small_params.BackBufferHeight);
return D3DERR_INVALIDCALL;
@@ -547,6 +620,7 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
{
if (presentation_parameters == nullptr) {
gfdm_logical_group_parameters_valid = false;
gfdm_small_head.resolve(nullptr);
return;
}
std::copy_n(
@@ -554,6 +628,8 @@ void WrappedIDirect3DDevice9::set_gfdm_logical_group_parameters(
gfdm_logical_group_parameters.size(),
gfdm_logical_group_parameters.begin());
gfdm_logical_group_parameters_valid = true;
gfdm_small_head.resolve(
&gfdm_logical_group_parameters[gfdm_logical_small_swapchain]);
}
FakeIDirect3DSwapChain9 *
@@ -599,6 +675,217 @@ void WrappedIDirect3DDevice9::release_gfdm_hidden_side_swapchains() {
}
}
void GfdmSmallHead::resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters) {
release();
active = gfdm_small_head_scaled();
format = D3DFMT_X8R8G8B8;
multisample = D3DMULTISAMPLE_NONE;
multisample_quality = 0;
if (!active || logical_small_parameters == nullptr) {
return;
}
if (logical_small_parameters->BackBufferFormat != D3DFMT_UNKNOWN) {
format = logical_small_parameters->BackBufferFormat;
}
multisample = logical_small_parameters->MultiSampleType;
multisample_quality = logical_small_parameters->MultiSampleQuality;
}
void GfdmSmallHead::apply_logical_size(UINT *width, UINT *height) const {
if (!active) {
return;
}
if (width != nullptr) {
*width = GFDM_SMALL_WIDTH;
}
if (height != nullptr) {
*height = GFDM_SMALL_HEIGHT;
}
}
HRESULT GfdmSmallHead::backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out) {
if (device == nullptr || out == nullptr || !active) {
return D3DERR_INVALIDCALL;
}
if (surface == nullptr) {
const HRESULT result = device->CreateRenderTarget(
GFDM_SMALL_WIDTH,
GFDM_SMALL_HEIGHT,
format,
multisample,
multisample_quality,
FALSE,
&surface,
nullptr);
if (FAILED(result)) {
log_warning(
"graphics::d3d9",
"two-head exclusive: could not create the portrait SMALL surface, hr={}",
FMT_HRESULT(result));
surface = nullptr;
return result;
}
device->ColorFill(surface, nullptr, D3DCOLOR_XRGB(0, 0, 0));
}
surface->AddRef();
*out = surface;
return D3D_OK;
}
void GfdmSmallHead::release() {
if (surface != nullptr) {
surface->Release();
surface = nullptr;
}
}
// largest centered rect inside bounds that keeps the source's aspect ratio
static RECT gfdm_fit_centered(const RECT &bounds, LONG source_width, LONG source_height) {
const LONG bounds_width = bounds.right - bounds.left;
const LONG bounds_height = bounds.bottom - bounds.top;
if (bounds_width <= 0 || bounds_height <= 0 || source_width <= 0 || source_height <= 0) {
return RECT {};
}
LONG width = MulDiv(bounds_height, source_width, source_height);
LONG height = bounds_height;
if (width > bounds_width) {
width = bounds_width;
height = MulDiv(bounds_width, source_height, source_width);
}
const LONG left = bounds.left + (bounds_width - width) / 2;
const LONG top = bounds.top + (bounds_height - height) / 2;
return RECT { left, top, left + width, top + height };
}
static void gfdm_compose_side_heads(
WrappedIDirect3DDevice9 *device,
IDirect3DSurface9 *head,
LONG host_width,
LONG host_height,
const RECT &content)
{
if (!games::gitadora::arena_subscreen_shows_sides()) {
return;
}
const RECT bars[2] = {
{ 0, 0, content.left, host_height },
{ content.right, 0, host_width, host_height },
};
for (UINT swapchain = 1; swapchain < GFDM_LOGICAL_HEAD_COUNT; swapchain++) {
if (!device->is_gfdm_logical_side_swapchain(swapchain)) {
continue;
}
// nothing has been drawn yet if the game never asked for this head's back buffer
const auto *chain = device->fake_sub_swapchain[
device->gfdm_hidden_side_swapchain_slot(swapchain)];
if (chain == nullptr || chain->render_targets.empty()) {
continue;
}
// the head's own window is the only thing that says which side it is, so an
// unnamed one is left out rather than guessed at and possibly mirrored
const char *name = graphics_gitadora_window_name(
device->gfdm_logical_group_parameters[swapchain].hDeviceWindow);
const bool named_left = name != nullptr && strcmp(name, "LEFT") == 0;
const bool named_right = name != nullptr && strcmp(name, "RIGHT") == 0;
if (!named_left && !named_right) {
static std::once_flag warned;
std::call_once(warned, [] {
log_warning(
"graphics::d3d9",
"two-head exclusive: a side head has no LEFT or RIGHT window, "
"leaving its bar black");
});
continue;
}
const RECT &bar = bars[named_left ? 0 : 1];
if (bar.right <= bar.left) {
continue;
}
IDirect3DSurface9 *source = chain->render_targets[0];
D3DSURFACE_DESC source_desc {};
if (FAILED(source->GetDesc(&source_desc))) {
continue;
}
const RECT target = gfdm_fit_centered(
bar,
static_cast<LONG>(source_desc.Width),
static_cast<LONG>(source_desc.Height));
const HRESULT result = device->pReal->StretchRect(
source,
nullptr,
head,
&target,
D3DTEXF_LINEAR);
if (FAILED(result)) {
static std::once_flag warned;
std::call_once(warned, [result] {
log_warning(
"graphics::d3d9",
"two-head exclusive: could not draw a side head into the bars, hr={}",
FMT_HRESULT(result));
});
}
}
}
HRESULT GfdmSmallHead::compose(WrappedIDirect3DDevice9 *device) {
if (device == nullptr || device->pReal == nullptr) {
return D3DERR_INVALIDCALL;
}
IDirect3DDevice9 *real = device->pReal;
IDirect3DSurface9 *proxy = nullptr;
HRESULT result = backbuffer(real, &proxy);
if (FAILED(result)) {
return result;
}
IDirect3DSurface9 *head = nullptr;
result = real->GetBackBuffer(
GFDM_NATIVE_SMALL_SWAPCHAIN,
0,
D3DBACKBUFFER_TYPE_MONO,
&head);
if (FAILED(result)) {
proxy->Release();
return result;
}
D3DSURFACE_DESC desc {};
result = head->GetDesc(&desc);
if (SUCCEEDED(result)) {
const auto host_width = static_cast<LONG>(desc.Width);
const auto host_height = static_cast<LONG>(desc.Height);
const RECT content = games::gitadora::arena_subscreen_content_rect(
host_width,
host_height);
// discard swap effect leaves the whole head undefined every frame, so the bars
// have to be repainted along with the image
real->ColorFill(head, nullptr, D3DCOLOR_XRGB(0, 0, 0));
result = real->StretchRect(proxy, nullptr, head, &content, D3DTEXF_LINEAR);
gfdm_compose_side_heads(device, head, host_width, host_height, content);
}
head->Release();
proxy->Release();
return result;
}
void WrappedIDirect3DDevice9::gfdm_disarm_present_mode_recovery() {
std::lock_guard<std::mutex> lock(gfdm_recovery_mutex);
gfdm_recovery_armed = false;
@@ -13,6 +13,36 @@ inline constexpr UINT GFDM_SMALL_HEIGHT = games::gitadora::ARENA_SUBSCREEN_HEIGH
inline constexpr UINT GFDM_LOGICAL_HEAD_COUNT = 4;
inline constexpr UINT GFDM_NATIVE_SMALL_SWAPCHAIN = 1;
struct WrappedIDirect3DDevice9;
// Owns the portrait surface the game draws into while the SMALL head is scanned out at a
// different resolution, and composes it onto the real head. Allocates nothing and reports
// nothing until resolve() finds a head that is not the panel size.
struct GfdmSmallHead {
GfdmSmallHead() = default;
~GfdmSmallHead() { release(); }
GfdmSmallHead(const GfdmSmallHead &) = delete;
GfdmSmallHead &operator=(const GfdmSmallHead &) = delete;
// settled with the device, so it cannot change under the game mid-session
void resolve(const D3DPRESENT_PARAMETERS *logical_small_parameters);
bool scaled() const { return active; }
HRESULT backbuffer(IDirect3DDevice9 *device, IDirect3DSurface9 **out);
HRESULT compose(WrappedIDirect3DDevice9 *device);
void release();
// the game only ever sees the portrait size it asked for
void apply_logical_size(UINT *width, UINT *height) const;
private:
bool active = false;
D3DFORMAT format = D3DFMT_X8R8G8B8;
D3DMULTISAMPLE_TYPE multisample = D3DMULTISAMPLE_NONE;
DWORD multisample_quality = 0;
IDirect3DSurface9 *surface = nullptr;
};
struct GfdmTwoHeadDeviceState {
explicit GfdmTwoHeadDeviceState(
D3DPRESENT_PARAMETERS *presentation_parameters,
@@ -41,6 +71,10 @@ struct GfdmTwoHeadDeviceState {
};
bool gfdm_two_head_exclusive();
void gfdm_publish_two_head_parameters(
D3DPRESENT_PARAMETERS *logical_presentation_parameters,
D3DDISPLAYMODEEX *logical_fullscreen_display_modes,
const GfdmTwoHeadDeviceState &state);
bool is_fake_subscreen_adapter(UINT adapter);
void get_fake_subscreen_display_mode(UINT adapter, D3DDISPLAYMODE *mode);
void get_fake_subscreen_display_mode_ex(UINT adapter, D3DDISPLAYMODEEX *mode);
@@ -21,6 +21,12 @@
} \
return ret
// inert unless this is the SMALL head and it is being scaled onto a differently sized monitor
static bool scales_small_head(const WrappedIDirect3DSwapChain9 *chain) {
return chain->native_group_head == WrappedIDirect3DSwapChain9::NativeGroupHead::Small
&& chain->pDev->gfdm_small_head.scaled();
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::QueryInterface(REFIID riid, void **ppvObj) {
if (ppvObj == nullptr) {
return E_POINTER;
@@ -89,6 +95,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::Present(const RECT *pSourc
graphics_d3d9_on_present(pDev->hFocusWindow, pDev->pReal, pDev);
}
if (scales_small_head(this)) {
pDev->gfdm_small_head.compose(pDev);
}
HRESULT result = pReal->Present(
pSourceRect,
pDestRect,
@@ -138,6 +148,10 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetFrontBufferData(IDirect
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetBackBuffer(UINT iBackBuffer, D3DBACKBUFFER_TYPE Type,
IDirect3DSurface9 **ppBackBuffer)
{
if (scales_small_head(this) && iBackBuffer == 0 && Type == D3DBACKBUFFER_TYPE_MONO) {
CHECK_RESULT(pDev->gfdm_small_head.backbuffer(pDev->pReal, ppBackBuffer));
}
CHECK_RESULT(pReal->GetBackBuffer(iBackBuffer, Type, ppBackBuffer));
}
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetRasterStatus(D3DRASTER_STATUS *pRasterStatus) {
@@ -159,7 +173,14 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetDevice(IDirect3DDevice9
HRESULT STDMETHODCALLTYPE WrappedIDirect3DSwapChain9::GetPresentParameters(
D3DPRESENT_PARAMETERS *pPresentationParameters)
{
CHECK_RESULT(pReal->GetPresentParameters(pPresentationParameters));
HRESULT result = pReal->GetPresentParameters(pPresentationParameters);
if (SUCCEEDED(result) && pPresentationParameters != nullptr && scales_small_head(this)) {
pDev->gfdm_small_head.apply_logical_size(
&pPresentationParameters->BackBufferWidth,
&pPresentationParameters->BackBufferHeight);
}
CHECK_RESULT(result);
}
/*
+51
View File
@@ -52,6 +52,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr;
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
HWND NDD_MAIN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false;
// icon
@@ -231,6 +233,10 @@ bool graphics_gitadora_has_dedicated_subscreen() {
return GFDM_SUBSCREEN_WINDOW != nullptr;
}
const char *graphics_gitadora_window_name(HWND hWnd) {
return gitadora_window_name_for_hwnd(hWnd);
}
bool graphics_gitadora_prepare_two_head_device_window(
HWND hWnd, HMONITOR target_monitor, UINT desired_width, UINT desired_height) {
if (hWnd == nullptr || !IsWindow(hWnd)) {
@@ -634,6 +640,9 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
bool is_sdvx_sub_window = is_sdvx && window_name.ends_with(" Sub Screen");
bool is_sdvx_main_window = is_sdvx && window_name.ends_with(" Main Screen");
bool is_popn_sub_window = avs::game::is_model("M39") && window_name.ends_with("Sub Screen");
const bool is_ndd = avs::game::is_model("NDD");
bool is_ndd_sub_window = is_ndd && window_name.starts_with("Aska MultiDisplay");
bool is_ndd_main_window = is_ndd && window_name == "ASKA";
const std::string gfdm_window_name = games::gitadora::is_arena_model()
? gitadora_canonical_window_name(effective_window_name)
: "";
@@ -777,6 +786,14 @@ static HWND WINAPI CreateWindowExA_hook(DWORD dwExStyle, LPCSTR lpClassName, LPC
}
}
if (is_ndd_sub_window) {
NDD_SUBSCREEN_WINDOW = result;
}
if (is_ndd_main_window) {
NDD_MAIN_WINDOW = result;
}
disable_touch_gestures(result);
log_misc(
"graphics",
@@ -902,6 +919,26 @@ static BOOL WINAPI EnumDisplayDevicesA_hook(LPCTSTR lpDevice, DWORD iDevNum,
return value;
}
// the sub screen renders into a fixed 800x480 buffer, but the game's saved layout asks for rects
// that do not match it, and dxgi stretches the buffer to fill whatever the client area ends up as
static void ndd_subscreen_size(HWND hWnd, int &width, int &height) {
RECT rect {};
SetRect(&rect, 0, 0, 800, 480);
AdjustWindowRect(&rect, GetWindowLongA(hWnd, GWL_STYLE), 0);
width = rect.right - rect.left;
height = rect.bottom - rect.top;
}
// the saved layout drops the sub window wherever it sat on the machine that wrote the file
static void ndd_subscreen_position(int &x, int &y) {
RECT main {};
if (NDD_MAIN_WINDOW != nullptr && GetWindowRect(NDD_MAIN_WINDOW, &main)) {
x = main.right;
y = main.top;
}
}
static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHeight, BOOL bRepaint) {
log_misc("graphics", "MoveWindow hook hit ({}, {}, {}, {}, {}, {})",
fmt::ptr(hWnd),
@@ -931,6 +968,11 @@ static BOOL WINAPI MoveWindow_hook(HWND hWnd, int X, int Y, int nWidth, int nHei
nHeight = rect.bottom - rect.top;
}
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
ndd_subscreen_size(hWnd, nWidth, nHeight);
ndd_subscreen_position(X, Y);
}
// iidx windowed TDJ mode
if (GRAPHICS_WINDOWED && TDJ_SUBSCREEN_WINDOW && hWnd == TDJ_SUBSCREEN_WINDOW) {
if (GRAPHICS_IIDX_WSUB) {
@@ -1073,6 +1115,15 @@ static LONG WINAPI SetWindowLongW_hook(HWND hWnd, int nIndex, LONG dwNewLong) {
static BOOL WINAPI SetWindowPos_hook(HWND hWnd, HWND hWndInsertAfter,
int X, int Y, int cx, int cy, UINT uFlags) {
if (GRAPHICS_WINDOWED && NDD_SUBSCREEN_WINDOW && hWnd == NDD_SUBSCREEN_WINDOW) {
if (!(uFlags & SWP_NOSIZE)) {
ndd_subscreen_size(hWnd, cx, cy);
}
if (!(uFlags & SWP_NOMOVE)) {
ndd_subscreen_position(X, Y);
}
}
if (is_gfdm_two_head_small_window(hWnd) &&
((uFlags & SWP_HIDEWINDOW) ||
(uFlags & (SWP_NOMOVE | SWP_NOSIZE)) != (SWP_NOMOVE | SWP_NOSIZE))) {
+4
View File
@@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
extern HWND SDVX_SUBSCREEN_WINDOW;
extern HWND POPN_SUBSCREEN_WINDOW;
extern HWND GFDM_SUBSCREEN_WINDOW;
extern HWND NDD_MAIN_WINDOW;
extern bool SUBSCREEN_FORCE_REDRAW;
extern bool FAKE_SUBSCREEN_ADAPTER;
@@ -121,6 +122,9 @@ extern bool D3D9_DEVICE_HOOK_DISABLE;
void graphics_init();
void graphics_hook_window(HWND hWnd, D3DPRESENT_PARAMETERS *pPresentationParameters);
bool graphics_gitadora_has_dedicated_subscreen();
// "GITADORA", "LEFT", "RIGHT", "SMALL", or nullptr for anything else
const char *graphics_gitadora_window_name(HWND hWnd);
// The native GITADORA two-head D3D9 group uses the game's named SMALL
// device window for the native physical SMALL head. The game requests
// D3DCREATE_NOWINDOWCHANGES, so this host must be made borderless and sized
+6 -3
View File
@@ -336,9 +336,11 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig);
}
// for TDJ subscreen search keyboard and T44 narrow-string handling
// for TDJ subscreen search keyboard
// T44 narrow-string handling
// NDD text measuring
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
avs::game::is_model({ "T44", "NDD" })) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
@@ -358,8 +360,9 @@ void hooks::lang::early_init() {
#endif
#ifdef SPICE64
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model("T44");
games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" });
#else
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
+10
View File
@@ -671,6 +671,16 @@ int main_implementation(int argc, char *argv[]) {
}
}
// gitadora arena sub layout
if (options[launcher::Options::GitaDoraArenaSubLayout].is_active()) {
const auto text = options[launcher::Options::GitaDoraArenaSubLayout].value_text();
if (text == "landscape") {
games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = games::gitadora::ArenaSubscreenLandscape::Small;
} else if (text == "combine") {
games::gitadora::ARENA_SUBSCREEN_LANDSCAPE = games::gitadora::ArenaSubscreenLandscape::All;
}
}
if (options[launcher::Options::GitaDoraWailHold].is_active()) {
socd::TILT_HOLD_MS = options[launcher::Options::GitaDoraWailHold].value_uint32();
}
File diff suppressed because it is too large Load Diff
+4 -1
View File
@@ -122,6 +122,7 @@ namespace launcher {
GitaDoraSubOverlaySize,
GitaDoraArenaSingleWindow,
GitaDoraArenaWindowLayout,
GitaDoraArenaSubLayout,
GitaDoraArenaAsioDriver,
GitaDoraArenaRealtekAccess,
LoadJubeatModule,
@@ -323,7 +324,9 @@ namespace launcher {
OBSWebSocketPassword,
OBSWebSocketDebug,
ScreenshotIncludeOverlay,
ScreenshotSubscreens
ScreenshotSubscreens,
_Count,
};
enum class OptionsCategory {
+7 -3
View File
@@ -285,8 +285,10 @@ static int __cdecl device_read_secplug(int a1, int a2, int a3) {
return 1;
}
static int __cdecl device_set_coinblocker_open(char number, char open) {
return 0;
static void __cdecl device_set_coinblocker_open(char slot1_open, char slot2_open) {
// libdevice exposes one open flag per coin slot. Since spice has a shared
// software coin stock, consider it blocked only when both slots are closed.
eamuse_coin_set_block(!slot1_open && !slot2_open);
}
static void __cdecl device_set_coincounter_merge() {
@@ -617,7 +619,9 @@ void spicedevice_attach() {
detour::inline_hook((void *) device_finalize, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_finalize", "?device_finalize@@YAXXZ"}));
detour::inline_hook((void *) device_get_coinstock, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock", "?device_get_coinstock@@YAXPEAG0@Z"}));
DEVICE_INSTANCE, {"device_get_coinstock",
"?device_get_coinstock@@YAXPEAG0@Z",
"?device_get_coinstock@@YAXPAG0@Z"}));
detour::inline_hook((void *) device_get_coinstock_all, libutils::try_proc_list(
DEVICE_INSTANCE, {"device_get_coinstock_all",
"?device_get_coinstock_all@@YAXPEAG0@Z",
+32 -16
View File
@@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
static std::string EAMUSE_GAME_NAME;
static ConfigKeypadBindings KEYPAD_BINDINGS {};
static std::mutex KEYPAD_BINDINGS_LOCK;
// auto card
bool AUTO_INSERT_CARD[2] = {false, false};
@@ -87,23 +88,23 @@ bool eamuse_get_card(int active_count, int unit_id, uint8_t *card) {
return true;
}
// get file path
std::filesystem::path path;
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
path = KEYPAD_BINDINGS.card_paths[index];
} else {
path = index > 0 ? "card1.txt" : "card0.txt";
}
// call the next function
return eamuse_get_card(path, card, index);
return eamuse_get_card(eamuse_get_card_path(index), card, index);
}
std::filesystem::path eamuse_get_card_path(size_t index) {
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
if (index >= std::size(KEYPAD_BINDINGS.card_paths)) {
return {};
}
if (!KEYPAD_BINDINGS.card_paths[index].empty()) {
return KEYPAD_BINDINGS.card_paths[index];
}
return index > 0 ? "card1.txt" : "card0.txt";
}
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
// do a quick copy under lock
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
const auto card_override = CARD_OVERRIDES[index];
lock.unlock();
const auto card_override = eamuse_get_card_override(index);
// Check if card overrides are present
if (!card_override.empty()) {
@@ -149,6 +150,14 @@ bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index
return eamuse_get_card_from_file(path, card, index);
}
std::string eamuse_get_card_override(size_t index) {
std::lock_guard<std::mutex> lock(CARD_OVERRIDES_LOCK);
if (index >= std::size(CARD_OVERRIDES)) {
return {};
}
return CARD_OVERRIDES[index];
}
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
// open file
@@ -322,8 +331,8 @@ int eamuse_coin_consume_stock() {
return COIN_STOCK.exchange(0, std::memory_order_relaxed);
}
int eamuse_coin_add() {
return COIN_STOCK.fetch_add(1, std::memory_order_relaxed) + 1;
void eamuse_coin_add(int amount) {
COIN_STOCK.fetch_add(amount, std::memory_order_relaxed);
}
void eamuse_coin_insert() {
@@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
}
bool eamuse_keypad_state_naive() {
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
}
@@ -595,7 +605,9 @@ void eamuse_set_game(std::string game) {
}
void eamuse_update_keypad_bindings() {
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
KEYPAD_BINDINGS = std::move(bindings);
}
const std::string &eamuse_get_game() {
@@ -609,6 +621,7 @@ int eamuse_get_game_keypads() {
avs::game::is_model("JDX") ||
avs::game::is_model("KDX") ||
avs::game::is_model("MDX") ||
avs::game::is_model("KDM") ||
avs::game::is_model("J33") ||
avs::game::is_model("K33") ||
avs::game::is_model("L33") ||
@@ -625,6 +638,7 @@ int eamuse_get_game_keypads_name() {
if (game_name == "Beatmania IIDX" ||
game_name == "Dance Dance Revolution" ||
game_name == "Dance Evolution" ||
game_name == "GitaDora")
{
return 2;
@@ -718,6 +732,8 @@ void eamuse_autodetect_game() {
eamuse_set_game("Mahjong Fight Girl");
else if (avs::game::is_model("XIF"))
eamuse_set_game("Polaris Chord");
else if (avs::game::is_model("NDD"))
eamuse_set_game("Silent Scope: Bone Eater");
else {
log_warning("eamuse", "unknown game model: {}", avs::game::MODEL);
eamuse_set_game("unknown");
+3 -1
View File
@@ -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(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);
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, const uint8_t *card);
@@ -57,7 +59,7 @@ void eamuse_coin_set_stock(int amount);
bool eamuse_coin_consume(int amount);
int eamuse_coin_consume_stock();
int eamuse_coin_add();
void eamuse_coin_add(int amount = 1);
void eamuse_coin_insert();
void eamuse_pin_macro_start_thread();
+12
View File
@@ -237,6 +237,16 @@ typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_add_toast_func)(
const char *text
);
// insert_coin (v0.3 and up)
// adds to the shared coin stock, bypassing the coin blocker like the Spice API
//
// amount: number of coins to insert (0 to 255); use 1 for a single coin
// zero succeeds without changing the stock
typedef SPICE_SDK_STATUS_CODE (__cdecl spice_sdk_insert_coin_func)(
uint8_t amount
);
typedef struct SPICE_SDK_V0 {
uint32_t size;
@@ -262,6 +272,8 @@ typedef struct SPICE_SDK_V0 {
spice_sdk_add_toast_func *add_toast;
spice_sdk_insert_coin_func *insert_coin;
} SPICE_SDK_V0;
typedef void (__cdecl spice_sdk_destroy_callback_func)(
+1
View File
@@ -1607,6 +1607,7 @@ typedef enum SPICE_SDK_DANCERUSH_BUTTONS {
DRS_Button_P2_Down,
DRS_Button_P2_Left,
DRS_Button_P2_Right,
DRS_Button_DownMotion,
} SPICE_SDK_DANCERUSH_BUTTONS;
typedef enum SPICE_SDK_DANCERUSH_LIGHTS {
+28 -2
View File
@@ -76,9 +76,35 @@ static ArrowButton arrow_buttons[] = {
// worker thread for I/O
static void worker_thread_main(std::stop_token stop_token) {
bool coin_previous_state[10] = {};
while (!stop_token.stop_requested()) {
// insert coin
const bool control_pressed = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
for (uint8_t amount = 0; amount < 10; amount++) {
const bool coin_pressed = control_pressed &&
((GetAsyncKeyState('0' + amount) & 0x8000) != 0);
if (coin_pressed && !coin_previous_state[amount] && spice.insert_coin) {
const auto status = spice.insert_coin(amount);
if (status != SPICE_SDK_STATUS_SUCCESS) {
LOG_INFO(std::format(
"coin insertion failed: {}",
static_cast<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) {
// check for ctrl + arrow keys and trigger p1 pad arrows
if (((GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0) &&
((GetAsyncKeyState(arrow.key) & 0x8000) != 0)) {
spice.set_button(arrow.button, true, 1.f);
@@ -88,7 +114,7 @@ static void worker_thread_main(std::stop_token stop_token) {
if (spice.add_toast) {
spice.add_toast(
SPICE_SDK_TOAST_LEVEL_INFO,
std::format("let me hear you say: {}", arrow.name).c_str());
std::format("v0_cpp: let me hear you say: {}", arrow.name).c_str());
} else {
LOG_INFO(std::format("let me hear you say: {}", arrow.name).c_str());
}
+24 -1
View File
@@ -31,6 +31,7 @@ static spice_sdk_clear_touch_func sdk_clear_touch;
static spice_sdk_insert_card_func sdk_insert_card;
static spice_sdk_set_keypad_func sdk_set_keypad;
static spice_sdk_add_toast_func sdk_add_toast;
static spice_sdk_insert_coin_func sdk_insert_coin;
struct SdkModule {
std::string dll;
@@ -175,8 +176,13 @@ sdk_init(
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, add_toast)) {
v0->add_toast = sdk_add_toast;
}
// end of 0.2
if (v0->size >= RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, insert_coin)) {
v0->insert_coin = sdk_insert_coin;
}
// end of 0.3
// any newer minor iterations will need to check the size
{
@@ -219,6 +225,7 @@ sdk_log(
log_warning(facility_str.c_str(), "{}", message);
break;
case SPICE_SDK_LOG_LEVEL_FATAL:
lock.unlock();
log_fatal(facility_str.c_str(), "{}", message);
break;
default:
@@ -640,5 +647,21 @@ sdk_add_toast(
return SPICE_SDK_STATUS_SUCCESS;
}
SPICE_SDK_STATUS_CODE
__cdecl
sdk_insert_coin(
uint8_t amount
)
{
std::shared_lock lock(sdk_global_mutex);
if (!sdk_initialized) {
return SPICE_SDK_STATUS_TOO_LATE;
}
if (amount > 0) {
eamuse_coin_add(amount);
}
return SPICE_SDK_STATUS_SUCCESS;
}
} // namespace sdk
+1
View File
@@ -1,5 +1,6 @@
#pragma once
#include <windows.h>
#include <winternl.h>
#define LDR_DLL_NOTIFICATION_REASON_LOADED 1