mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-10-01 06:24:55 -07:00
Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 531e7a54b3 | |||
| f11b9d0189 | |||
| 112b1aecb0 | |||
| de466db29f | |||
| 08e99dea4a | |||
| 00e904539e | |||
| eaab4698eb | |||
| a48eccab93 | |||
| 81c913726d | |||
| 0169fe1e31 | |||
| 357e6c86f1 | |||
| 67963389ef | |||
| 4009bb8a84 | |||
| 8fb6d94000 | |||
| 8d3685415d | |||
| 50b38b856f | |||
| dad88caf01 |
@@ -29,4 +29,33 @@ jobs:
|
||||
with:
|
||||
name: spice2x-ci-${{ env.COMMIT_SHORT_SHA }}
|
||||
path: src/spice2x/bin
|
||||
if-no-files-found: error
|
||||
if-no-files-found: error
|
||||
|
||||
msvc-ci:
|
||||
name: Build (MSVC ${{ matrix.arch }}-bit)
|
||||
# windows-latest ships Visual Studio 2026; pin to the VS 2022 image, which is
|
||||
# the toolset spice2x is actually developed against
|
||||
runs-on: windows-2022
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: 32
|
||||
platform: Win32
|
||||
target: spicetools_spice spicetools_cfg
|
||||
- arch: 64
|
||||
platform: x64
|
||||
target: spicetools_spice64
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./src/spice2x
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- name: Configure
|
||||
run: >
|
||||
cmake -S . -B cmake-build-msvc-${{ matrix.arch }}
|
||||
-A ${{ matrix.platform }}
|
||||
- name: Compile
|
||||
run: >
|
||||
cmake --build cmake-build-msvc-${{ matrix.arch }}
|
||||
--config Release --target ${{ matrix.target }}
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
@@ -614,6 +616,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
|
||||
hooks/networkhook.cpp
|
||||
hooks/icmphook_net.cpp
|
||||
hooks/icmphook_iphlpapi.cpp
|
||||
hooks/nicspoof.cpp
|
||||
hooks/powrprof.cpp
|
||||
#hooks/rom.cpp
|
||||
hooks/setupapihook.cpp
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
||||
private:
|
||||
|
||||
// function definitions
|
||||
void get_cards(Request &req, Response &res);
|
||||
void insert(Request &req, Response &res);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Vendored
+7
-3
@@ -50,6 +50,7 @@ static const BYTE UID_CMD[5] = { 0xFFu, 0xCAu, 0x00u, 0x00u, 0x00u };
|
||||
enum scard_atr_protocol {
|
||||
SCARD_ATR_PROTOCOL_ISO14443_PART3 = 0x03,
|
||||
SCARD_ATR_PROTOCOL_ISO15693_PART3 = 0x0B,
|
||||
SCARD_ATR_PROTOCOL_ISO15693_PART4 = 0x0C,
|
||||
SCARD_ATR_PROTOCOL_FELICA_212K = 0x11,
|
||||
SCARD_ATR_PROTOCOL_FELICA_424K = 0x12,
|
||||
};
|
||||
@@ -121,11 +122,14 @@ void scard_update(SCARDCONTEXT hContext, LPCTSTR readerName, uint8_t unit_no) {
|
||||
BYTE cardProtocol = atr[12];
|
||||
BOOL shouldReverseUid = false;
|
||||
bool is_felica = false;
|
||||
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
|
||||
if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
|
||||
log_info("scard", "card protocol: ISO14443_PART3");
|
||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART3) {
|
||||
log_info("scard", "card protocol: ISO15693_PART3");
|
||||
shouldReverseUid = true;
|
||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO14443_PART3) {
|
||||
log_info("scard", "card protocol: ISO14443_PART3");
|
||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_ISO15693_PART4) {
|
||||
log_info("scard", "card protocol: ISO15693_PART4");
|
||||
shouldReverseUid = true;
|
||||
} else if (cardProtocol == SCARD_ATR_PROTOCOL_FELICA_212K) {
|
||||
log_info("scard", "card protocol: FELICA_212K");
|
||||
is_felica = true;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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."
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,7 +20,8 @@ namespace games::drs {
|
||||
P2_Up,
|
||||
P2_Down,
|
||||
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<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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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 "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);
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -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))) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
#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"
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t k_local_ip = 0x0A390302u; /* 10.57.3.2 */
|
||||
constexpr uint32_t k_gateway = 0x0A390301u; /* 10.57.3.1 */
|
||||
constexpr uint32_t k_dns = 0x0A390301u; /* 10.57.3.1 */
|
||||
constexpr uint32_t k_dhcp = 0x0A390301u; /* 10.57.3.1 */
|
||||
constexpr uint32_t k_mask = 0xFFFFFF00u; /* 255.255.255.0 */
|
||||
constexpr uint32_t k_subnet = 0x0A390300u;
|
||||
constexpr int k_prefix_len = 24;
|
||||
constexpr DWORD k_ifindex = 77;
|
||||
constexpr uint8_t k_fake_mac[6] = {0x12, 0x37, 0x13, 0x37, 0x13, 0x37};
|
||||
constexpr char k_hostname[] = "N1CSP00F";
|
||||
constexpr char k_domain[] = "local";
|
||||
constexpr char k_hostname_fqdn[] = "N1CSP00F.local";
|
||||
constexpr wchar_t k_domain_w[] = L"local";
|
||||
constexpr char k_adapter_name[] = "{NICSPOOF-0880-0001-4250-4E6963537066}";
|
||||
constexpr char k_adapter_desc[] = "NicSpoof Virtual Ethernet";
|
||||
constexpr wchar_t k_friendly_name[] = L"NicSpoof";
|
||||
constexpr wchar_t k_adapter_desc_w[] = L"NicSpoof Virtual Ethernet";
|
||||
|
||||
#define ALIGN_UP_PTR(p, a) \
|
||||
((BYTE *)(((ULONG_PTR)(p) + ((ULONG_PTR)(a) - 1)) & ~((ULONG_PTR)(a) - 1)))
|
||||
|
||||
bool g_log_adapters = true;
|
||||
bool g_log_params = true;
|
||||
|
||||
[[maybe_unused]] decltype(GetAdaptersAddresses) *GetAdaptersAddresses_orig = nullptr;
|
||||
[[maybe_unused]] decltype(GetAdaptersInfo) *GetAdaptersInfo_orig = nullptr;
|
||||
[[maybe_unused]] decltype(GetNetworkParams) *GetNetworkParams_orig = nullptr;
|
||||
[[maybe_unused]] decltype(getaddrinfo) *getaddrinfo_orig = nullptr;
|
||||
[[maybe_unused]] decltype(freeaddrinfo) *freeaddrinfo_orig = nullptr;
|
||||
|
||||
void ip_to_str(uint32_t ip, char *out, size_t n) {
|
||||
if (!out || n == 0) {
|
||||
return;
|
||||
}
|
||||
snprintf(out, n, "%u.%u.%u.%u",
|
||||
(ip >> 24) & 255, (ip >> 16) & 255, (ip >> 8) & 255, ip & 255);
|
||||
out[n - 1] = 0;
|
||||
}
|
||||
|
||||
uint32_t parse_ipv4(const char *s) {
|
||||
unsigned a = 0;
|
||||
unsigned b = 0;
|
||||
unsigned c = 0;
|
||||
unsigned d = 0;
|
||||
char trail = 0;
|
||||
if (!s) {
|
||||
return 0;
|
||||
}
|
||||
if (sscanf(s, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
|
||||
return 0;
|
||||
}
|
||||
if (a > 255 || b > 255 || c > 255 || d > 255) {
|
||||
return 0;
|
||||
}
|
||||
return (a << 24) | (b << 16) | (c << 8) | d;
|
||||
}
|
||||
|
||||
bool name_is_local(const char *name) {
|
||||
if (!name || !name[0]) {
|
||||
return true;
|
||||
}
|
||||
if (_stricmp(name, "localhost") == 0) {
|
||||
return true;
|
||||
}
|
||||
if (_stricmp(name, k_hostname) == 0) {
|
||||
return true;
|
||||
}
|
||||
if (_stricmp(name, k_hostname_fqdn) == 0) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool name_is_numeric_ipv4(const char *name) {
|
||||
unsigned a = 0;
|
||||
unsigned b = 0;
|
||||
unsigned c = 0;
|
||||
unsigned d = 0;
|
||||
char trail = 0;
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
if (sscanf(name, "%u.%u.%u.%u%c", &a, &b, &c, &d, &trail) != 4) {
|
||||
return false;
|
||||
}
|
||||
return a <= 255 && b <= 255 && c <= 255 && d <= 255;
|
||||
}
|
||||
|
||||
DWORD WINAPI GetAdaptersInfo_hook(PIP_ADAPTER_INFO p, PULONG s) {
|
||||
ULONG need = sizeof(IP_ADAPTER_INFO) + 32;
|
||||
char ip[16];
|
||||
char gw[16];
|
||||
char dhcp[16];
|
||||
|
||||
if (!s) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
if (!p || *s < need) {
|
||||
*s = need;
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
memset(p, 0, need);
|
||||
p->Index = k_ifindex;
|
||||
strcpy(p->AdapterName, k_adapter_name);
|
||||
strcpy(p->Description, k_adapter_desc);
|
||||
p->AddressLength = 6;
|
||||
memcpy(p->Address, k_fake_mac, 6);
|
||||
p->Type = MIB_IF_TYPE_ETHERNET;
|
||||
p->DhcpEnabled = 1;
|
||||
ip_to_str(k_local_ip, ip, sizeof(ip));
|
||||
ip_to_str(k_gateway, gw, sizeof(gw));
|
||||
ip_to_str(k_dhcp, dhcp, sizeof(dhcp));
|
||||
strcpy(p->IpAddressList.IpAddress.String, ip);
|
||||
ip_to_str(k_mask, p->IpAddressList.IpMask.String,
|
||||
sizeof(p->IpAddressList.IpMask.String));
|
||||
p->IpAddressList.Context = 0;
|
||||
p->Next = nullptr;
|
||||
strcpy(p->GatewayList.IpAddress.String, gw);
|
||||
p->GatewayList.IpMask.String[0] = 0;
|
||||
p->GatewayList.Context = 0;
|
||||
p->GatewayList.Next = nullptr;
|
||||
strcpy(p->DhcpServer.IpAddress.String, dhcp);
|
||||
p->DhcpServer.IpMask.String[0] = 0;
|
||||
p->DhcpServer.Context = 0;
|
||||
p->DhcpServer.Next = nullptr;
|
||||
*s = need;
|
||||
if (g_log_adapters) {
|
||||
log_misc("network", "NIC spoof GetAdaptersInfo ip={} gw={} ifindex={}",
|
||||
ip, gw, static_cast<unsigned>(k_ifindex));
|
||||
g_log_adapters = false;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
DWORD WINAPI GetNetworkParams_hook(PFIXED_INFO p, PULONG s) {
|
||||
ULONG need = static_cast<ULONG>(sizeof(FIXED_INFO));
|
||||
char dns[16];
|
||||
|
||||
if (!s) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
if (!p || *s < need) {
|
||||
*s = need;
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
memset(p, 0, need);
|
||||
strncpy(p->HostName, k_hostname, sizeof(p->HostName) - 1);
|
||||
strncpy(p->DomainName, k_domain, sizeof(p->DomainName) - 1);
|
||||
ip_to_str(k_dns, dns, sizeof(dns));
|
||||
strcpy(p->DnsServerList.IpAddress.String, dns);
|
||||
p->DnsServerList.IpMask.String[0] = 0;
|
||||
p->DnsServerList.Context = 0;
|
||||
p->DnsServerList.Next = nullptr;
|
||||
p->CurrentDnsServer = &p->DnsServerList;
|
||||
p->NodeType = 1;
|
||||
p->EnableDns = 1;
|
||||
*s = need;
|
||||
if (g_log_params) {
|
||||
log_misc("network", "NIC spoof GetNetworkParams host={} domain={} dns={}",
|
||||
k_hostname, k_domain, dns);
|
||||
g_log_params = false;
|
||||
}
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
ULONG WINAPI GetAdaptersAddresses_hook(
|
||||
ULONG Family,
|
||||
ULONG Flags,
|
||||
PVOID Reserved,
|
||||
PIP_ADAPTER_ADDRESSES AdapterAddresses,
|
||||
PULONG SizePointer) {
|
||||
enum { STR_SLOT = 64 };
|
||||
/*
|
||||
* Layout is fixed: adapter header, 4 string slots, then unicast/prefix/
|
||||
* gateway/dns + sockaddrs with alignment slack. need is computed to always
|
||||
* fit; never return BUFFER_OVERFLOW after a successful size probe.
|
||||
*/
|
||||
ULONG need = static_cast<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) +
|
||||
sizeof(IP_ADAPTER_GATEWAY_ADDRESS) +
|
||||
sizeof(sockaddr_in) * 4 + 64);
|
||||
BYTE *blob = nullptr;
|
||||
PIP_ADAPTER_ADDRESSES a = nullptr;
|
||||
PIP_ADAPTER_UNICAST_ADDRESS u = nullptr;
|
||||
PIP_ADAPTER_PREFIX pref = nullptr;
|
||||
PIP_ADAPTER_GATEWAY_ADDRESS gw = nullptr;
|
||||
PIP_ADAPTER_DNS_SERVER_ADDRESS dns = nullptr;
|
||||
sockaddr_in *sa = nullptr;
|
||||
sockaddr_in *sm = nullptr;
|
||||
sockaddr_in *sg = nullptr;
|
||||
sockaddr_in *ds = nullptr;
|
||||
|
||||
(void)Reserved;
|
||||
(void)Flags;
|
||||
|
||||
if (Family != AF_INET && Family != AF_UNSPEC) {
|
||||
if (SizePointer) {
|
||||
*SizePointer = 0;
|
||||
}
|
||||
return ERROR_NO_DATA;
|
||||
}
|
||||
if (!SizePointer) {
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
}
|
||||
if (!AdapterAddresses || *SizePointer < need) {
|
||||
*SizePointer = need;
|
||||
return ERROR_BUFFER_OVERFLOW;
|
||||
}
|
||||
|
||||
memset(AdapterAddresses, 0, need);
|
||||
a = AdapterAddresses;
|
||||
blob = reinterpret_cast<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(k_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(k_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_dns);
|
||||
dns->Address.lpSockaddr = reinterpret_cast<LPSOCKADDR>(ds);
|
||||
dns->Address.iSockaddrLength = sizeof(*ds);
|
||||
dns->Next = nullptr;
|
||||
a->FirstDnsServerAddress = dns;
|
||||
|
||||
a->Next = nullptr;
|
||||
*SizePointer = need;
|
||||
return ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
constexpr uint32_t AI_MAGIC = 0x4E535041u; /* 'NSPA' */
|
||||
|
||||
struct SpoofAiHdr {
|
||||
uint32_t magic;
|
||||
ADDRINFOA ai;
|
||||
};
|
||||
|
||||
SpoofAiHdr *ai_hdr_from_ai(ADDRINFOA *ai) {
|
||||
SpoofAiHdr *h = nullptr;
|
||||
if (!ai) {
|
||||
return nullptr;
|
||||
}
|
||||
h = reinterpret_cast<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 = k_local_ip;
|
||||
} else {
|
||||
ip = 0x7F000001u;
|
||||
}
|
||||
} else if (name_is_numeric_ipv4(pNodeName)) {
|
||||
ip = parse_ipv4(pNodeName);
|
||||
} else {
|
||||
/* offline=1: map external names to local_ip */
|
||||
ip = k_local_ip;
|
||||
}
|
||||
|
||||
if (pServiceName && pServiceName[0]) {
|
||||
port = atoi(pServiceName);
|
||||
if (port < 0 || port > 65535) {
|
||||
port = 0;
|
||||
}
|
||||
}
|
||||
|
||||
name_src = (pNodeName && pNodeName[0]) ? pNodeName : "localhost";
|
||||
name_len = strlen(name_src) + 1;
|
||||
if (name_len > 256) {
|
||||
name_len = 256;
|
||||
}
|
||||
|
||||
hdr = static_cast<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(k_local_ip, ipstr, sizeof(ipstr));
|
||||
ip_to_str(k_mask, maskstr, sizeof(maskstr));
|
||||
ip_to_str(k_gateway, gwstr, sizeof(gwstr));
|
||||
log_info("network",
|
||||
"NIC spoof enabled ip={} mask={}/{} gw={} "
|
||||
"mac={:02X}:{:02X}:{:02X}:{:02X}:{:02X}:{:02X} host={} ifindex={}",
|
||||
ipstr, maskstr, k_prefix_len, gwstr,
|
||||
static_cast<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);
|
||||
|
||||
/* FreeAddrInfoA is often the same address; only hook if distinct. */
|
||||
{
|
||||
HMODULE ws = GetModuleHandleA("ws2_32.dll");
|
||||
void *faa = nullptr;
|
||||
void *fai = nullptr;
|
||||
if (ws) {
|
||||
faa = reinterpret_cast<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)");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void nicspoof_init() {
|
||||
install_nicspoof_hooks();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
#pragma once
|
||||
|
||||
void nicspoof_init();
|
||||
@@ -84,6 +84,7 @@
|
||||
#include "hooks/lang.h"
|
||||
#include "hooks/networkhook.h"
|
||||
#include "hooks/icmphook_net.h"
|
||||
#include "hooks/nicspoof.h"
|
||||
#include "hooks/unisintrhook.h"
|
||||
#include "launcher/launcher.h"
|
||||
#include "launcher/logger.h"
|
||||
@@ -257,6 +258,7 @@ int main_implementation(int argc, char *argv[]) {
|
||||
bool load_stubs = false;
|
||||
bool netfix_disable = false;
|
||||
bool icmphook_enable = false;
|
||||
bool nicspoof_enable = false;
|
||||
bool lang_disable = false;
|
||||
std::string process_priority_str = "high";
|
||||
bool cardio_enabled = false;
|
||||
@@ -671,6 +673,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();
|
||||
}
|
||||
@@ -789,6 +801,9 @@ int main_implementation(int argc, char *argv[]) {
|
||||
if (options[launcher::Options::EnableICMPHook].value_bool()) {
|
||||
icmphook_enable = true;
|
||||
}
|
||||
if (options[launcher::Options::EnableNICSpoof].value_bool()) {
|
||||
nicspoof_enable = true;
|
||||
}
|
||||
if (options[launcher::Options::DisableACPHook].value_bool()) {
|
||||
lang_disable = true;
|
||||
}
|
||||
@@ -2548,6 +2563,11 @@ int main_implementation(int argc, char *argv[]) {
|
||||
icmphook_net_init();
|
||||
}
|
||||
|
||||
// NIC spoof (opt-in; fake Ethernet with no real NIC)
|
||||
if (nicspoof_enable) {
|
||||
nicspoof_init();
|
||||
}
|
||||
|
||||
// net fix
|
||||
if (!netfix_disable) {
|
||||
networkhook_init();
|
||||
|
||||
+661
-811
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,7 @@ namespace launcher {
|
||||
GitaDoraSubOverlaySize,
|
||||
GitaDoraArenaSingleWindow,
|
||||
GitaDoraArenaWindowLayout,
|
||||
GitaDoraArenaSubLayout,
|
||||
GitaDoraArenaAsioDriver,
|
||||
GitaDoraArenaRealtekAccess,
|
||||
LoadJubeatModule,
|
||||
@@ -315,6 +316,7 @@ namespace launcher {
|
||||
OtocaCamHook,
|
||||
DisableHighResTimer,
|
||||
EnableICMPHook,
|
||||
EnableNICSpoof,
|
||||
AutoElevate,
|
||||
CfgForceSoftwareRender,
|
||||
OBSWebSocketEnabled,
|
||||
@@ -323,7 +325,9 @@ namespace launcher {
|
||||
OBSWebSocketPassword,
|
||||
OBSWebSocketDebug,
|
||||
ScreenshotIncludeOverlay,
|
||||
ScreenshotSubscreens
|
||||
ScreenshotSubscreens,
|
||||
|
||||
_Count,
|
||||
};
|
||||
|
||||
enum class OptionsCategory {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <windows.h>
|
||||
#include <winternl.h>
|
||||
|
||||
#define LDR_DLL_NOTIFICATION_REASON_LOADED 1
|
||||
@@ -43,4 +44,4 @@ NTSTATUS NTAPI LdrRegisterDllNotification(
|
||||
|
||||
NTSTATUS NTAPI LdrUnregisterDllNotification(
|
||||
PVOID Cookie
|
||||
);
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user