mirror of
https://github.com/spice2x/spice2x.github.io.git
synced 2026-09-30 22:14:55 -07:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4009bb8a84 | |||
| 8fb6d94000 | |||
| 8d3685415d | |||
| 50b38b856f | |||
| dad88caf01 | |||
| 699659d4bf | |||
| 21e7d24ed3 |
@@ -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`.
|
||||
|
||||
@@ -531,6 +531,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -337,9 +337,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;
|
||||
}
|
||||
|
||||
@@ -118,7 +118,13 @@ namespace api {
|
||||
param.i_height = height;
|
||||
param.i_fps_num = this->fps;
|
||||
param.i_fps_den = 1;
|
||||
param.i_threads = 1;
|
||||
|
||||
// sliced threading, which zerolatency already selected, so a frame is split
|
||||
// across workers rather than held back to be reordered. deliberately not the
|
||||
// automatic count: this shares a machine with the game it is capturing, and
|
||||
// taking every core to encode would win back frames at the game's expense
|
||||
param.i_threads = 4;
|
||||
|
||||
param.b_annexb = 1;
|
||||
// SPS/PPS ahead of every IDR, so a client can start decoding cold
|
||||
param.b_repeat_headers = 1;
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -944,9 +944,12 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDevice(
|
||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||
|
||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||
hFocusWindow,
|
||||
*ppReturnedDeviceInterface);
|
||||
|
||||
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
|
||||
*ppReturnedDeviceInterface = wrapped;
|
||||
}
|
||||
|
||||
// return result
|
||||
@@ -1307,13 +1310,16 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3D9::CreateDeviceEx(
|
||||
} else if (!D3D9_DEVICE_HOOK_DISABLE) {
|
||||
graphics_hook_window(hFocusWindow, pPresentationParameters);
|
||||
|
||||
*ppReturnedDeviceInterface = new WrappedIDirect3DDevice9(
|
||||
auto *wrapped = new WrappedIDirect3DDevice9(
|
||||
hFocusWindow,
|
||||
*ppReturnedDeviceInterface,
|
||||
gfdm_parameters.logical_small_swapchain,
|
||||
gfdm_two_head_exclusive() ? static_cast<IDirect3D9 *>(this) : nullptr,
|
||||
gfdm_two_head_exclusive() ? pPresentationParameters : nullptr);
|
||||
|
||||
wrapped->device_multithreaded = (BehaviorFlags & D3DCREATE_MULTITHREADED) != 0;
|
||||
*ppReturnedDeviceInterface = wrapped;
|
||||
|
||||
// initialize sub screen if the game requested a multi-head context
|
||||
if (avs::game::is_model({"LDJ", "KFC", "M39", "M32"}) &&
|
||||
(orig_behavior_flags & D3DCREATE_ADAPTERGROUP_DEVICE)) {
|
||||
|
||||
@@ -634,6 +634,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::Reset(
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
}
|
||||
|
||||
// Reset refuses to run while any default pool resource is outstanding
|
||||
d3d9_readback::discard_snapshot_targets(pReal);
|
||||
|
||||
HRESULT res = pReal->Reset(pPresentationParameters);
|
||||
|
||||
// recreate overlay
|
||||
@@ -2321,6 +2324,9 @@ HRESULT STDMETHODCALLTYPE WrappedIDirect3DDevice9::ResetEx(
|
||||
overlay::OVERLAY->reset_invalidate();
|
||||
}
|
||||
|
||||
// ResetEx refuses to run while any default pool resource is outstanding
|
||||
d3d9_readback::discard_snapshot_targets(pReal);
|
||||
|
||||
HRESULT res = static_cast<IDirect3DDevice9Ex *>(pReal)->ResetEx(
|
||||
gfdm_parameters.presentation_parameters,
|
||||
gfdm_parameters.fullscreen_display_modes);
|
||||
|
||||
@@ -264,6 +264,10 @@ struct WrappedIDirect3DDevice9 : IDirect3DDevice9Ex {
|
||||
IDirect3DDevice9 *pReal;
|
||||
bool is_d3d9ex = false;
|
||||
|
||||
// set from the creation flags; without it the runtime does no locking of its own, so
|
||||
// nothing but the present thread may touch the device
|
||||
bool device_multithreaded = false;
|
||||
|
||||
std::atomic_ulong refs = 1;
|
||||
|
||||
WrappedIDirect3DSwapChain9 *main_swapchain = nullptr;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "d3d9_readback.h"
|
||||
|
||||
#include <array>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
|
||||
@@ -10,6 +13,13 @@ namespace d3d9_readback {
|
||||
|
||||
namespace {
|
||||
|
||||
// the snapshot path stamps frames with this to recognise one left behind by a break in the
|
||||
// request stream
|
||||
uint64_t now_us() {
|
||||
return static_cast<uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||
}
|
||||
|
||||
SurfacePtr create_readback_surface(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||
IDirect3DSurface9 *surface = nullptr;
|
||||
const HRESULT hr = device->CreateOffscreenPlainSurface(
|
||||
@@ -160,21 +170,8 @@ ReadbackPool &pool() {
|
||||
return *instance;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void release_device_resources(IDirect3DDevice9 *device) {
|
||||
pool().clear_device(device);
|
||||
}
|
||||
|
||||
BackbufferCopy::~BackbufferCopy() {
|
||||
if (this->pooled && this->surface) {
|
||||
pool().release(this->device, std::move(this->surface));
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
|
||||
|
||||
// the back buffer plus the checks every caller has to make before copying out of it
|
||||
SurfacePtr open_backbuffer(IDirect3DSwapChain9 *swap_chain, int screen, D3DSURFACE_DESC &desc) {
|
||||
IDirect3DSurface9 *buffer = nullptr;
|
||||
HRESULT hr = swap_chain->GetBackBuffer(0, D3DBACKBUFFER_TYPE_MONO, &buffer);
|
||||
if (FAILED(hr) || buffer == nullptr) {
|
||||
@@ -182,17 +179,17 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
"failed to get back buffer for screen {}, hr={}",
|
||||
screen,
|
||||
FMT_HRESULT(hr));
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
D3DSURFACE_DESC desc {};
|
||||
hr = buffer->GetDesc(&desc);
|
||||
SurfacePtr surface(buffer);
|
||||
|
||||
hr = surface->GetDesc(&desc);
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to acquire back buffer descriptor, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
buffer->Release();
|
||||
return std::nullopt;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// GetRenderTargetData rejects multisampled sources. no supported game has been
|
||||
@@ -204,7 +201,399 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
"back buffer is multisampled ({}), screenshots and capture are unsupported",
|
||||
static_cast<uint32_t>(desc.MultiSampleType));
|
||||
});
|
||||
buffer->Release();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return surface;
|
||||
}
|
||||
|
||||
SurfacePtr create_snapshot_target(IDirect3DDevice9 *device, const D3DSURFACE_DESC &desc) {
|
||||
IDirect3DSurface9 *surface = nullptr;
|
||||
|
||||
// matching the back buffer keeps the blit a straight copy and leaves the pixels in the
|
||||
// format the conversion step would have seen without the detour
|
||||
const HRESULT hr = device->CreateRenderTarget(
|
||||
desc.Width, desc.Height, desc.Format,
|
||||
D3DMULTISAMPLE_NONE, 0, FALSE, &surface, nullptr);
|
||||
|
||||
if (FAILED(hr) || surface == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to create snapshot target, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return SurfacePtr(surface);
|
||||
}
|
||||
|
||||
// two render targets per screen, reused across frames. two because one holds the frame waiting
|
||||
// out its deferral while the other takes the next one; a read only holds its target until the
|
||||
// pixels are in system memory, which is far shorter than the gap between requests.
|
||||
//
|
||||
// unlike the readback surfaces these live in the default pool, so they have to be gone before a
|
||||
// Reset and not merely before the device is released
|
||||
class SnapshotTargets {
|
||||
public:
|
||||
SurfacePtr acquire(
|
||||
IDirect3DDevice9 *device,
|
||||
int screen,
|
||||
const D3DSURFACE_DESC &desc,
|
||||
uint64_t *out_generation) {
|
||||
|
||||
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
|
||||
if (this->device != device) {
|
||||
this->drop();
|
||||
this->device = device;
|
||||
this->usable = true;
|
||||
}
|
||||
|
||||
auto &entry = this->screens[screen];
|
||||
|
||||
Slot *free_slot = nullptr;
|
||||
for (size_t i = 0; i < SLOTS_PER_SCREEN; i++) {
|
||||
auto &candidate = entry.slots[(entry.next + i) % SLOTS_PER_SCREEN];
|
||||
if (!candidate.busy) {
|
||||
free_slot = &candidate;
|
||||
entry.next = (entry.next + i + 1) % SLOTS_PER_SCREEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (free_slot == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto &slot = *free_slot;
|
||||
|
||||
if (slot.target
|
||||
&& (slot.width != desc.Width
|
||||
|| slot.height != desc.Height
|
||||
|| slot.format != desc.Format)) {
|
||||
slot.target.reset();
|
||||
}
|
||||
|
||||
if (!slot.target) {
|
||||
slot.target = create_snapshot_target(device, desc);
|
||||
if (!slot.target) {
|
||||
// a back buffer format the device will not give us a render target for. the
|
||||
// inline path can still read it, so stop trying rather than lose the stream
|
||||
this->usable = false;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
slot.width = desc.Width;
|
||||
slot.height = desc.Height;
|
||||
slot.format = desc.Format;
|
||||
}
|
||||
|
||||
slot.busy = true;
|
||||
*out_generation = this->current_generation;
|
||||
|
||||
slot.target->AddRef();
|
||||
return SurfacePtr(slot.target.get());
|
||||
}
|
||||
|
||||
// holds the fresh snapshot back and returns the one from the previous request, which by now
|
||||
// has had a full frame for its blit and transfer to land
|
||||
std::optional<Snapshot> rotate(int screen, std::optional<Snapshot> fresh) {
|
||||
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint64_t now = now_us();
|
||||
std::optional<Snapshot> previous;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
previous = std::move(this->pending[screen]);
|
||||
this->pending[screen] = std::move(fresh);
|
||||
}
|
||||
|
||||
// a frame waits here for the next request rather than for a deadline, so a gap in
|
||||
// requests, a client reconnecting most obviously, would otherwise hand the new session
|
||||
// a frame from before the gap. showing a stale frame is worse than showing none
|
||||
if (previous.has_value() && now - previous->issued_us > MAX_DEFERRAL_US) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return previous;
|
||||
}
|
||||
|
||||
// a reader whose target was already thrown away must not free the slot, or it would free
|
||||
// the snapshot that replaced it while that one is still being read
|
||||
void finish(int screen, uint64_t generation, IDirect3DSurface9 *target) {
|
||||
if (screen < 0 || screen >= static_cast<int>(GRAPHICS_CAPTURE_SCREEN_NO)) {
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
if (generation != this->current_generation) {
|
||||
return;
|
||||
}
|
||||
|
||||
this->free_slot(screen, target);
|
||||
}
|
||||
|
||||
this->idle.notify_all();
|
||||
}
|
||||
|
||||
bool still_current(uint64_t generation) {
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
return generation == this->current_generation;
|
||||
}
|
||||
|
||||
bool is_usable() {
|
||||
std::lock_guard<std::mutex> lock(this->mutex);
|
||||
return this->usable;
|
||||
}
|
||||
|
||||
// Reset fails outright while the device still owns default pool resources, so dropping our
|
||||
// own references is not enough and any read in flight has to finish first. the wait is
|
||||
// bounded because a failed Reset is something games retry and a stalled present thread is
|
||||
// not something they survive
|
||||
void discard(IDirect3DDevice9 *device) {
|
||||
// destroyed after the lock is released, since dropping a snapshot calls back in here
|
||||
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> stale;
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(this->mutex);
|
||||
if (this->device != nullptr && this->device != device) {
|
||||
return;
|
||||
}
|
||||
|
||||
// deferred frames are abandoned rather than waited for; only a read that is already
|
||||
// running has to be allowed to finish
|
||||
for (auto &held : this->pending) {
|
||||
if (held.has_value() && held->surface) {
|
||||
this->free_slot(held->screen, held->surface.get());
|
||||
}
|
||||
}
|
||||
|
||||
stale = std::move(this->pending);
|
||||
this->pending = {};
|
||||
|
||||
const bool drained = this->idle.wait_for(
|
||||
lock,
|
||||
std::chrono::milliseconds(100),
|
||||
[this] { return !this->any_busy(); });
|
||||
|
||||
if (!drained) {
|
||||
log_warning("graphics::d3d9",
|
||||
"capture snapshot still in flight, discarding its target anyway");
|
||||
}
|
||||
|
||||
this->drop();
|
||||
this->device = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr size_t SLOTS_PER_SCREEN = 2;
|
||||
|
||||
// generous next to the frame interval this is meant to bridge, so that ordinary jitter
|
||||
// never trips it and only a real break in the request stream does
|
||||
static constexpr uint64_t MAX_DEFERRAL_US = 250'000;
|
||||
|
||||
struct Slot {
|
||||
SurfacePtr target;
|
||||
UINT width = 0;
|
||||
UINT height = 0;
|
||||
D3DFORMAT format = D3DFMT_UNKNOWN;
|
||||
bool busy = false;
|
||||
};
|
||||
|
||||
struct Screen {
|
||||
std::array<Slot, SLOTS_PER_SCREEN> slots;
|
||||
size_t next = 0;
|
||||
};
|
||||
|
||||
void free_slot(int screen, IDirect3DSurface9 *target) {
|
||||
for (auto &slot : this->screens[screen].slots) {
|
||||
if (slot.target.get() == target) {
|
||||
slot.busy = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool any_busy() const {
|
||||
for (const auto &entry : this->screens) {
|
||||
for (const auto &slot : entry.slots) {
|
||||
if (slot.busy) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void drop() {
|
||||
for (auto &entry : this->screens) {
|
||||
for (auto &slot : entry.slots) {
|
||||
slot.target.reset();
|
||||
slot.busy = false;
|
||||
}
|
||||
|
||||
entry.next = 0;
|
||||
}
|
||||
|
||||
this->current_generation++;
|
||||
}
|
||||
|
||||
std::mutex mutex;
|
||||
std::condition_variable idle;
|
||||
std::array<Screen, GRAPHICS_CAPTURE_SCREEN_NO> screens;
|
||||
std::array<std::optional<Snapshot>, GRAPHICS_CAPTURE_SCREEN_NO> pending;
|
||||
IDirect3DDevice9 *device = nullptr;
|
||||
uint64_t current_generation = 1;
|
||||
bool usable = true;
|
||||
};
|
||||
|
||||
// never destroyed, for the same reason the readback pool is not
|
||||
SnapshotTargets &targets() {
|
||||
static SnapshotTargets *instance = new SnapshotTargets();
|
||||
return *instance;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void release_device_resources(IDirect3DDevice9 *device) {
|
||||
targets().discard(device);
|
||||
pool().clear_device(device);
|
||||
}
|
||||
|
||||
void discard_snapshot_targets(IDirect3DDevice9 *device) {
|
||||
targets().discard(device);
|
||||
}
|
||||
|
||||
bool snapshots_supported() {
|
||||
return targets().is_usable();
|
||||
}
|
||||
|
||||
BackbufferCopy::~BackbufferCopy() {
|
||||
if (this->pooled && this->surface) {
|
||||
pool().release(this->device, std::move(this->surface));
|
||||
}
|
||||
}
|
||||
|
||||
Snapshot::~Snapshot() {
|
||||
// still holding the target means the read never ran, and the slot would otherwise stay
|
||||
// marked busy and take the screen out of capture permanently
|
||||
if (this->surface) {
|
||||
targets().finish(this->screen, this->generation, this->surface.get());
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<Snapshot> take_snapshot(
|
||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
|
||||
|
||||
const uint64_t started_us = now_us();
|
||||
|
||||
D3DSURFACE_DESC desc {};
|
||||
auto buffer = open_backbuffer(swap_chain, screen, desc);
|
||||
if (!buffer) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
uint64_t generation = 0;
|
||||
auto target = targets().acquire(device, screen, desc, &generation);
|
||||
if (!target) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// built before the blit so that a failure below hands the slot back through the destructor
|
||||
Snapshot snapshot;
|
||||
snapshot.screen = screen;
|
||||
snapshot.desc = desc;
|
||||
snapshot.device = device;
|
||||
snapshot.surface = std::move(target);
|
||||
snapshot.generation = generation;
|
||||
snapshot.issued_us = started_us;
|
||||
|
||||
// the point of the whole exercise: this is queued rather than waited on, so the game pays
|
||||
// for issuing the copy and not for it completing. identical size and format, so there is
|
||||
// no filtering to ask for
|
||||
const HRESULT hr = device->StretchRect(
|
||||
buffer.get(), nullptr, snapshot.surface.get(), nullptr, D3DTEXF_NONE);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to snapshot back buffer for screen {}, hr={}",
|
||||
screen,
|
||||
FMT_HRESULT(hr));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<Snapshot> snapshot_backbuffer(
|
||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen) {
|
||||
|
||||
return targets().rotate(screen, take_snapshot(device, swap_chain, screen));
|
||||
}
|
||||
|
||||
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot) {
|
||||
if (!snapshot.surface) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
// a Reset between the blit and now means the target no longer holds the captured frame
|
||||
if (!targets().still_current(snapshot.generation)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
auto destination = pool().acquire(snapshot.device, snapshot.desc);
|
||||
if (!destination) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const HRESULT hr = snapshot.device->GetRenderTargetData(
|
||||
snapshot.surface.get(), destination.get());
|
||||
|
||||
// the target is reusable as soon as the pixels are in system memory. dropping the
|
||||
// reference before freeing the slot keeps the destructor from freeing it twice
|
||||
const int screen = snapshot.screen;
|
||||
const uint64_t generation = snapshot.generation;
|
||||
IDirect3DSurface9 *target = snapshot.surface.get();
|
||||
snapshot.surface.reset();
|
||||
targets().finish(screen, generation, target);
|
||||
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to read snapshot contents, hr={}",
|
||||
FMT_HRESULT(hr));
|
||||
pool().release(snapshot.device, std::move(destination));
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
BackbufferCopy copy;
|
||||
copy.screen = screen;
|
||||
copy.desc = snapshot.desc;
|
||||
copy.device = snapshot.device;
|
||||
copy.surface = std::move(destination);
|
||||
copy.pooled = true;
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
IDirect3DDevice9 *device, IDirect3DSwapChain9 *swap_chain, int screen, bool pooled) {
|
||||
|
||||
D3DSURFACE_DESC desc {};
|
||||
auto buffer = open_backbuffer(swap_chain, screen, desc);
|
||||
if (!buffer) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -212,12 +601,10 @@ std::optional<BackbufferCopy> acquire_backbuffer_copy(
|
||||
? pool().acquire(device, desc)
|
||||
: create_readback_surface(device, desc);
|
||||
if (!destination) {
|
||||
buffer->Release();
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
hr = device->GetRenderTargetData(buffer, destination.get());
|
||||
buffer->Release();
|
||||
const HRESULT hr = device->GetRenderTargetData(buffer.get(), destination.get());
|
||||
|
||||
if (FAILED(hr)) {
|
||||
log_warning("graphics::d3d9",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
|
||||
@@ -39,6 +40,51 @@ namespace d3d9_readback {
|
||||
int screen,
|
||||
bool pooled);
|
||||
|
||||
// GPU side copy of a back buffer, taken while the contents are still the frame that was
|
||||
// presented, so that reading them into system memory no longer has to happen before it
|
||||
struct Snapshot {
|
||||
int screen {};
|
||||
D3DSURFACE_DESC desc {};
|
||||
IDirect3DDevice9 *device = nullptr;
|
||||
SurfacePtr surface;
|
||||
uint64_t generation {};
|
||||
|
||||
// when the blit was issued, so a frame left behind by a break in the request stream can
|
||||
// be recognised as stale rather than handed over
|
||||
uint64_t issued_us {};
|
||||
|
||||
Snapshot() = default;
|
||||
Snapshot(Snapshot &&) noexcept = default;
|
||||
Snapshot &operator=(Snapshot &&) noexcept = default;
|
||||
Snapshot(const Snapshot &) = delete;
|
||||
Snapshot &operator=(const Snapshot &) = delete;
|
||||
~Snapshot();
|
||||
};
|
||||
|
||||
// for the present thread, between the last EndScene and Present. blits the current frame,
|
||||
// then returns the snapshot taken on the *previous* call: waiting a frame before reading
|
||||
// means the blit and its system memory transfer have already happened, so the read does not
|
||||
// stall on the GPU. costs the stream one frame of latency.
|
||||
//
|
||||
// returns nothing on the first call of a stream, and whenever the frame could not be taken,
|
||||
// which is the caller's cue to skip rather than to wait
|
||||
std::optional<Snapshot> snapshot_backbuffer(
|
||||
IDirect3DDevice9 *device,
|
||||
IDirect3DSwapChain9 *swap_chain,
|
||||
int screen);
|
||||
|
||||
// the expensive half, for a thread that is not the present thread. only legal on a device
|
||||
// created with D3DCREATE_MULTITHREADED
|
||||
std::optional<BackbufferCopy> read_snapshot(Snapshot snapshot);
|
||||
|
||||
// false once a device has refused to give up a render target matching its back buffer,
|
||||
// which leaves reading the back buffer directly as the only way to capture it
|
||||
bool snapshots_supported();
|
||||
|
||||
// snapshot targets live in the default pool, so unlike the readback surfaces they have to
|
||||
// be gone before a Reset and not merely before the device is released
|
||||
void discard_snapshot_targets(IDirect3DDevice9 *device);
|
||||
|
||||
// pooled surfaces hold references on the device; call this before releasing it
|
||||
void release_device_resources(IDirect3DDevice9 *device);
|
||||
}
|
||||
|
||||
@@ -488,6 +488,63 @@ ThreadPool &capture_read_pool() {
|
||||
return *instance;
|
||||
}
|
||||
|
||||
// Takes the frame on the present thread as a queued GPU blit and hands the readback to a pool
|
||||
// thread, so the game waits for neither. Only viable where the whole read can go off thread,
|
||||
// since the back buffer is overwritten right after Present and a snapshot the present thread
|
||||
// then had to read itself would cost more than reading the back buffer directly.
|
||||
//
|
||||
// Returns false when the frame could not be taken, including the ordinary case of the previous
|
||||
// snapshot of this screen still being read, which paces capture to what the reader sustains.
|
||||
static bool snapshot_capture(
|
||||
IDirect3DDevice9 *device,
|
||||
WrappedIDirect3DDevice9 *wrapped_device,
|
||||
int screen) {
|
||||
|
||||
IDirect3DSwapChain9 *swap_chain = nullptr;
|
||||
const HRESULT hr = wrapped_device->get_screenshot_swap_chain(screen, &swap_chain);
|
||||
if (FAILED(hr) || swap_chain == nullptr) {
|
||||
log_warning("graphics::d3d9",
|
||||
"failed to get swap chain for screen {}, hr={}",
|
||||
screen,
|
||||
FMT_HRESULT(hr));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto snapshot = d3d9_readback::snapshot_backbuffer(device, swap_chain, screen);
|
||||
swap_chain->Release();
|
||||
|
||||
if (!snapshot.has_value()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
capture_read_pool().add([screen, snapshot = std::move(*snapshot)]() mutable {
|
||||
// an escape from here would cross a thread boundary and terminate
|
||||
try {
|
||||
auto copy = d3d9_readback::read_snapshot(std::move(snapshot));
|
||||
if (!copy.has_value()) {
|
||||
graphics_capture_skip(screen);
|
||||
return;
|
||||
}
|
||||
|
||||
read_and_dispatch_capture(screen, std::move(*copy));
|
||||
} catch (const std::exception &error) {
|
||||
log_warning("graphics::d3d9", "capture read failed: {}", error.what());
|
||||
graphics_capture_skip(screen);
|
||||
} catch (...) {
|
||||
log_warning("graphics::d3d9", "capture read failed");
|
||||
graphics_capture_skip(screen);
|
||||
}
|
||||
});
|
||||
} catch (const std::exception &) {
|
||||
// the snapshot went into the lambda before the queue could fail, so it is already
|
||||
// destroyed and its target handed back; the client just misses this frame
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// by this point the pixels are plain memory, so none of this needs the device
|
||||
static void dispatch_screenshot_save(std::vector<PendingWrite> writes, size_t screen_count) {
|
||||
auto screenshot_process = [writes = std::move(writes), screen_count]() mutable {
|
||||
@@ -632,6 +689,18 @@ static void process_image_request(
|
||||
const ImageRequest &request) {
|
||||
const bool screenshot = request.kind == ImageRequestKind::Screenshot;
|
||||
|
||||
if (!screenshot
|
||||
&& wrapped_device->device_multithreaded
|
||||
&& capture_read_off_thread(request.screen)
|
||||
&& d3d9_readback::snapshots_supported()) {
|
||||
|
||||
if (!snapshot_capture(device, wrapped_device, request.screen)) {
|
||||
graphics_capture_skip(request.screen);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<int> screens { request.screen };
|
||||
if (screenshot && GRAPHICS_SCREENSHOT_SUBSCREENS) {
|
||||
screens.clear();
|
||||
|
||||
@@ -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
|
||||
@@ -634,6 +636,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 +782,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 +915,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 +964,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 +1111,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;
|
||||
|
||||
@@ -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" });
|
||||
|
||||
@@ -1729,7 +1729,7 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
"Value must be between 0 (poor quality) and 100 (best quality), inclusive. Default: 70.",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "(0-100)",
|
||||
.category = "Companion & API",
|
||||
.category = "API Dev",
|
||||
},
|
||||
{
|
||||
// APIScreenMirrorDivide
|
||||
@@ -1741,21 +1741,17 @@ static const std::vector<OptionDefinition> OPTION_DEFINITIONS = {
|
||||
"Value must be 1 or greater. Default: 1.",
|
||||
.type = OptionType::Integer,
|
||||
.setting_name = "1",
|
||||
.category = "Companion & API",
|
||||
.category = "API Dev",
|
||||
},
|
||||
{
|
||||
// APIStreamEnable
|
||||
.title = "API Video Stream Server Enable (EXPERIMENTAL)",
|
||||
.name = "apistream",
|
||||
.desc = "Serves the mirrored screen as a video stream, on the API port plus two; "
|
||||
"alternative to API screen capture. Requires -api.\n\n"
|
||||
"http://host:apiport+2/stream.mjpg - MJPEG\n\n"
|
||||
"http://host:apiport+2/stream.h264 - H.264\n\n"
|
||||
"Parameters: screen (0-3), fps (1-60, default 30), q (1-100, default 70).\n\n"
|
||||
"Example with -api 1337: http://host:1339/stream.h264?fps=30&q=70\n\n"
|
||||
"VIEW ONLY - touch input still requires -api. "
|
||||
"No password protection or encryption of any kind; video sent in the clear!\n\n"
|
||||
"Streaming is known to cause older games to hang and crash.",
|
||||
.desc = "Allows companion apps to receive compressed video streams over the API. "
|
||||
"Companion app must support this feature to take advantage of it.\n\n"
|
||||
"Video is served unencrypted over the network. "
|
||||
"May cause older games to hang and crash.\n\n"
|
||||
"Developers: see the wiki page for more details.",
|
||||
.type = OptionType::Bool,
|
||||
.category = "Companion & API",
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
+28
-14
@@ -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
|
||||
@@ -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() {
|
||||
@@ -718,6 +730,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);
|
||||
|
||||
Reference in New Issue
Block a user