Compare commits

...

3 Commits

Author SHA1 Message Date
bicarus 4009bb8a84 sc: bone eater preliminary support (#895)
## Link to GitHub Issue or related Pull Request, if one exists
Fixes #114

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

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

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

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

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

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

## Testing
Tested with my iidx controller with HID lights.
2026-08-28 01:42:56 -07:00
12 changed files with 392 additions and 71 deletions
+4
View File
@@ -56,6 +56,10 @@ We explicitly do **NOT** have a Discord server for dicussions - we try to do eve
Please see [CONTRIBUTING page](https://github.com/spice2x/spice2x.github.io/blob/main/CONTRIBUTING.md) for a full list of guidelines when submitting code.
### AI-Use Policy
Use AI tools at your discretion, but remember that a human will review every issue and pull request. Please ensure submissions are accurate, relevant, and worth the reviewer’s time.
## Additional information
Please read [README.md](https://github.com/spice2x/spice2x.github.io/blob/main/src/spice2x/README.md) inside `src/spice2x`.
+1
View File
@@ -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
+32 -6
View File
@@ -1,5 +1,7 @@
#include "bmpu.h"
#include <algorithm>
#include "acio/icca/icca.h"
#include "avs/game.h"
#include "cfg/api.h"
@@ -9,6 +11,7 @@
#include "games/ftt/io.h"
#include "games/museca/io.h"
#include "games/silentscope/io.h"
#include "hooks/graphics/graphics.h"
#include "launcher/launcher.h"
#include "misc/eamuse.h"
@@ -530,15 +533,38 @@ static bool __cdecl ac_io_bmpu_update_control_status_buffer() {
STATUS_BUFFER[4] |= 0x20;
}
// joy stick raw input
// gun position, sent big endian; the game keeps the top 12 bits of each pair
auto &analogs = games::silentscope::get_analogs();
auto &gun_x = analogs.at(games::silentscope::Analogs::GUN_X);
auto &gun_y = analogs.at(games::silentscope::Analogs::GUN_Y);
unsigned short joy_x = 0x7FFF;
unsigned short joy_y = 0x7FFF;
if (analogs.at(games::silentscope::Analogs::GUN_X).isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_X)) * USHRT_MAX);
}
if (analogs.at(games::silentscope::Analogs::GUN_Y).isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, analogs.at(games::silentscope::Analogs::GUN_Y)) * USHRT_MAX);
if (gun_x.isSet() || gun_y.isSet()) {
if (gun_x.isSet()) {
joy_x = (unsigned short) (Analogs::getState(RI_MGR, gun_x) * USHRT_MAX);
}
if (gun_y.isSet()) {
joy_y = (unsigned short) (Analogs::getState(RI_MGR, gun_y) * USHRT_MAX);
}
} else {
// the gun reports where it is aimed, which a relative pointer cannot express - the
// calibration screen asks for the screen centre and two corners, so read the cursor
POINT cursor {};
RECT client {};
POINT origin {};
if (NDD_MAIN_WINDOW != nullptr &&
GetCursorPos(&cursor) &&
GetClientRect(NDD_MAIN_WINDOW, &client) &&
ClientToScreen(NDD_MAIN_WINDOW, &origin) &&
client.right > 1 && client.bottom > 1)
{
const LONG x = std::clamp(cursor.x - origin.x, 0L, client.right - 1);
const LONG y = std::clamp(cursor.y - origin.y, 0L, client.bottom - 1);
joy_x = (unsigned short) (x * USHRT_MAX / (client.right - 1));
joy_y = (unsigned short) (y * USHRT_MAX / (client.bottom - 1));
}
}
// invert X axis
+110 -55
View File
@@ -2,6 +2,8 @@
#if SPICE64 && !SPICE_XP
#include <array>
#include <mutex>
#include <optional>
#include <cstdint>
#include "util/detour.h"
@@ -141,10 +143,100 @@ namespace games::popn {
static AIO_SCI_COMM *aioSciComm;
static AIO_IOB5_BI3A *aioIob5Bi3a;
static constexpr size_t PIKA_BUTTON_COUNT = 9;
static constexpr size_t PIKA_BUTTON_DATA_SIZE = PIKA_BUTTON_COUNT * 3;
static std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> pika_button_staging{};
static bool pika_button_staging_dirty = false;
static std::mutex pika_button_staging_mutex;
/*
* implementations
*/
static void publish_pika_button_staging() {
std::array<uint8_t, PIKA_BUTTON_DATA_SIZE> data;
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
if (!pika_button_staging_dirty) {
return;
}
data = pika_button_staging;
pika_button_staging_dirty = false;
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R,
Lights::popn_lights_t::PikaButton1_G,
Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R,
Lights::popn_lights_t::PikaButton2_G,
Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R,
Lights::popn_lights_t::PikaButton3_G,
Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R,
Lights::popn_lights_t::PikaButton4_G,
Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R,
Lights::popn_lights_t::PikaButton5_G,
Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R,
Lights::popn_lights_t::PikaButton6_G,
Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R,
Lights::popn_lights_t::PikaButton7_G,
Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R,
Lights::popn_lights_t::PikaButton8_G,
Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R,
Lights::popn_lights_t::PikaButton9_G,
Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(legacy_button_lights) == PIKA_BUTTON_COUNT);
static_assert(std::size(button_lights) == PIKA_BUTTON_DATA_SIZE);
auto &lights = get_lights();
for (size_t button = 0; button < PIKA_BUTTON_COUNT; button++) {
const auto data_offset = button * 3;
// special handling converting RGB data to non-RGB lights
// take the max(R, G, B) and use it to write the light value
const auto light_value = std::max({
data[data_offset],
data[data_offset + 1],
data[data_offset + 2]
});
// on the new cab, buttons are colorless plastic and rely on RGB to be lit
// at all times, even when "off". When translating to legacy on/off lights,
// treat only values above about 60% brightness as fully on to preserve the
// dim "off" state.
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[button]),
light_value > 150 ? 1.f : 0.f);
}
for (size_t light = 0; light < data.size(); light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
data[light] / 255.f);
}
}
// libaio.dll
static AIO_SCI_COMM_T *__fastcall aioSciComm_Open(AIO_SCI_COMM_T *unk) {
@@ -260,6 +352,11 @@ namespace games::popn {
log_info("bi3a_hook", "aioIob5Bi3a_Create called with i_pNodeMgr={}, i={}, p={}", fmt::ptr(i_pNodeMgr), i, fmt::ptr(p));
if (i_pNodeMgr == aioNmgrIob5) {
{
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
pika_button_staging.fill(0);
pika_button_staging_dirty = false;
}
aioIob5Bi3a = new AIO_IOB5_BI3A;
log_info("bi3a_hook", "aioIob5Bi3a_Create: returning custom AIO_IOB5_BI3A: {}", fmt::ptr(aioIob5Bi3a));
return aioIob5Bi3a;
@@ -269,11 +366,15 @@ namespace games::popn {
}
static void __fastcall aioIob5Bi3a_GetDeviceStatus(AIO_IOB5_BI3A *i_pNodeCtl, AIO_IOB5_BI3A__DEVSTATUS *o_DevStatus) {
RI_MGR->devices_flush_output();
if (i_pNodeCtl != aioIob5Bi3a) {
return aioIob5Bi3a_GetDeviceStatus_orig(i_pNodeCtl, o_DevStatus);
}
// snapshot the current state of lights and flush the output
// this closely mirrors what the game does with real I/O
publish_pika_button_staging();
RI_MGR->devices_flush_output();
memset(o_DevStatus, 0, sizeof(*o_DevStatus));
auto &buttons = get_buttons();
@@ -413,61 +514,15 @@ namespace games::popn {
i_CnPin, number_of_leds, (uint8_t)i_LedType, fmt::ptr(i_pData), data);
}
auto &lights = get_lights();
if (i_CnPin == 0 && number_of_leds == 9 * 3) {
// special handling converting to non-RGB lights
// take the max(R, G, B) and use it to write the light value
uint8_t light_value[9] = { 0 };
for (uint32_t i = 0; i < number_of_leds; i += 3) {
light_value[i / 3] =
std::max({ ((uint8_t *)i_pData)[i], ((uint8_t *)i_pData)[i + 1], ((uint8_t *)i_pData)[i + 2] });
}
constexpr Lights::popn_lights_t legacy_button_lights[] = {
Lights::popn_lights_t::Button1,
Lights::popn_lights_t::Button2,
Lights::popn_lights_t::Button3,
Lights::popn_lights_t::Button4,
Lights::popn_lights_t::Button5,
Lights::popn_lights_t::Button6,
Lights::popn_lights_t::Button7,
Lights::popn_lights_t::Button8,
Lights::popn_lights_t::Button9
};
static_assert(std::size(legacy_button_lights) == 9);
for (size_t light = 0; light < 9; light++) {
// on the new cab, buttons are colorless plastic and they rely on RGB to be lit at all times, even when "off"
// when translating to legacy on/off lights, treat anything above ~60% brightness as fully on,
// otherwise off, to avoid dimly lit "off" state
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(legacy_button_lights[light]),
light_value[light] > 150 ? 1.f : 0.f);
}
// color buttons
constexpr Lights::popn_lights_t button_lights[] = {
Lights::popn_lights_t::PikaButton1_R, Lights::popn_lights_t::PikaButton1_G, Lights::popn_lights_t::PikaButton1_B,
Lights::popn_lights_t::PikaButton2_R, Lights::popn_lights_t::PikaButton2_G, Lights::popn_lights_t::PikaButton2_B,
Lights::popn_lights_t::PikaButton3_R, Lights::popn_lights_t::PikaButton3_G, Lights::popn_lights_t::PikaButton3_B,
Lights::popn_lights_t::PikaButton4_R, Lights::popn_lights_t::PikaButton4_G, Lights::popn_lights_t::PikaButton4_B,
Lights::popn_lights_t::PikaButton5_R, Lights::popn_lights_t::PikaButton5_G, Lights::popn_lights_t::PikaButton5_B,
Lights::popn_lights_t::PikaButton6_R, Lights::popn_lights_t::PikaButton6_G, Lights::popn_lights_t::PikaButton6_B,
Lights::popn_lights_t::PikaButton7_R, Lights::popn_lights_t::PikaButton7_G, Lights::popn_lights_t::PikaButton7_B,
Lights::popn_lights_t::PikaButton8_R, Lights::popn_lights_t::PikaButton8_G, Lights::popn_lights_t::PikaButton8_B,
Lights::popn_lights_t::PikaButton9_R, Lights::popn_lights_t::PikaButton9_G, Lights::popn_lights_t::PikaButton9_B
};
static_assert(std::size(button_lights) == 9 * 3);
for (size_t light = 0; light < number_of_leds; light++) {
GameAPI::Lights::writeLight(
RI_MGR,
lights.at(button_lights[light]),
((uint8_t *)i_pData)[light] / 255.f);
}
// SetTapeLedDataPart is meant to be a temporary staging buffer, so the game occasionally
// clears the buffer (set to 0 / lights off) between animations but they are not meant
// to be seen by the I/O
// therefore, instead of writing the lights out directly here, we stage them in a buffer
// and wait for GetDeviceStatus to be called
std::lock_guard<std::mutex> lock(pika_button_staging_mutex);
memcpy(pika_button_staging.data(), i_pData, pika_button_staging.size());
pika_button_staging_dirty = true;
}
if (tapeledutils::is_enabled() && i_CnPin > 0 && i_CnPin < std::size(TAPELED_MAPPING)) {
+150
View File
@@ -0,0 +1,150 @@
#include "projector.h"
#include <algorithm>
#include <cstring>
#include <cwchar>
#include "util/logging.h"
namespace {
// request/response layout: identifier, command, two zero bytes, payload length,
// the payload itself and a checksum over everything before it
constexpr size_t HEADER_SIZE = 5;
constexpr uint8_t RESPONSE_ID = 0x23;
constexpr uint8_t COMMAND_COMMON_DATA = 0x8A;
constexpr uint8_t COMMAND_TEMPERATURE = 0x99;
constexpr uint8_t COMMAND_LAMP_CURRENT = 0x9B;
// shown in the test menu as PROJTIM, PROJHEATIN and PROJHEAT
constexpr uint32_t LAMP_SECONDS = 0;
constexpr uint32_t TEMPERATURE_INTAKE = 25;
constexpr uint32_t TEMPERATURE_EXHAUST = 35;
// anything below 1000 is treated as a lamp anomaly and raises PROJERROR unless the exact
// value was already recorded in /projecter/current on an earlier boot
constexpr uint32_t LAMP_CURRENT = 1000;
void put32(uint8_t *dest, uint32_t value) {
dest[0] = (uint8_t) (value & 0xFF);
dest[1] = (uint8_t) ((value >> 8) & 0xFF);
dest[2] = (uint8_t) ((value >> 16) & 0xFF);
dest[3] = (uint8_t) ((value >> 24) & 0xFF);
}
}
bool games::silentscope::ProjectorHandle::open(LPCWSTR lpFileName) {
if (wcscmp(lpFileName, L"COM2") != 0 && wcscmp(lpFileName, L"\\\\.\\COM2") != 0) {
return false;
}
log_info("silentscope", "Opened COM2 (projector)");
return true;
}
int games::silentscope::ProjectorHandle::read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<uint8_t *>(lpBuffer);
DWORD bytes_read = 0;
while (bytes_read < nNumberOfBytesToRead && !this->response.empty()) {
buffer[bytes_read++] = this->response.front();
this->response.pop_front();
}
return (int) bytes_read;
}
int games::silentscope::ProjectorHandle::write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) {
std::lock_guard<std::mutex> lock(this->mutex);
auto buffer = reinterpret_cast<const uint8_t *>(lpBuffer);
this->request.insert(this->request.end(), buffer, buffer + nNumberOfBytesToWrite);
while (this->request.size() >= HEADER_SIZE) {
const size_t packet_size = HEADER_SIZE + this->request[4] + 1;
if (this->request.size() < packet_size) {
break;
}
this->process_request(this->request.data());
this->request.erase(this->request.begin(), this->request.begin() + packet_size);
}
return (int) nNumberOfBytesToWrite;
}
size_t games::silentscope::ProjectorHandle::bytes_available() {
std::lock_guard<std::mutex> lock(this->mutex);
return this->response.size();
}
bool games::silentscope::ProjectorHandle::close() {
std::lock_guard<std::mutex> lock(this->mutex);
this->request.clear();
this->response.clear();
log_info("silentscope", "Closed COM2 (projector)");
return true;
}
void games::silentscope::ProjectorHandle::process_request(const uint8_t *packet) {
const uint8_t command = packet[1];
const uint8_t length = packet[4];
const uint8_t *data = packet + HEADER_SIZE;
switch (command) {
case COMMAND_COMMON_DATA: {
// the game only looks at the lamp usage time near the end of the block
std::vector<uint8_t> payload(98, 0);
put32(&payload[94], LAMP_SECONDS);
this->reply(command, payload);
break;
}
case COMMAND_TEMPERATURE: {
// the requested sensor is echoed back along with its reading
const uint8_t sensor = length > 0 ? data[0] : 0;
std::vector<uint8_t> payload(5, 0);
payload[0] = sensor;
put32(&payload[1], sensor == 0 ? TEMPERATURE_INTAKE : TEMPERATURE_EXHAUST);
this->reply(command, payload);
break;
}
case COMMAND_LAMP_CURRENT: {
// the three byte item selector is echoed back along with the measurement
std::vector<uint8_t> payload(7, 0);
memcpy(payload.data(), data, std::min<size_t>(length, 3));
put32(&payload[3], LAMP_CURRENT);
this->reply(command, payload);
break;
}
default:
log_misc("silentscope", "unknown projector command {:#04x}", command);
this->reply(command, {});
break;
}
}
void games::silentscope::ProjectorHandle::reply(uint8_t command, const std::vector<uint8_t> &data) {
std::vector<uint8_t> packet {
RESPONSE_ID, command, 0x00, 0x00, (uint8_t) data.size()
};
packet.insert(packet.end(), data.begin(), data.end());
uint8_t checksum = 0;
for (auto byte : packet) {
checksum += byte;
}
packet.push_back(checksum);
this->response.insert(this->response.end(), packet.begin(), packet.end());
}
+30
View File
@@ -0,0 +1,30 @@
#pragma once
#include <cstdint>
#include <deque>
#include <mutex>
#include <vector>
#include "hooks/devicehook.h"
namespace games::silentscope {
// The cabinet talks to its projector over COM2. Without an answer the game stops at
// I/O error 5-1560-0004 (IOCOM2, "the projector is not connected correctly").
class ProjectorHandle : public CustomHandle {
public:
bool open(LPCWSTR lpFileName) override;
int read(LPVOID lpBuffer, DWORD nNumberOfBytesToRead) override;
int write(LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite) override;
size_t bytes_available() override;
bool close() override;
private:
std::mutex mutex;
std::vector<uint8_t> request;
std::deque<uint8_t> response;
void process_request(const uint8_t *packet);
void reply(uint8_t command, const std::vector<uint8_t> &data);
};
}
@@ -1,10 +1,11 @@
#include "silentscope.h"
#include "acioemu/handle.h"
#include "cfg/configurator.h"
#include "hooks/devicehook.h"
#include "util/libutils.h"
#include "projector.h"
namespace games::silentscope {
SilentScopeGame::SilentScopeGame() : Game("Silent Scope") {
@@ -16,9 +17,8 @@ namespace games::silentscope {
// load the game DLL so hooks apply
libutils::try_library("gamendd.dll");
// TODO(felix): implement stuff on this port
devicehook_init();
devicehook_add(new acioemu::ACIOHandle(L"COM2"));
devicehook_add(new ProjectorHandle());
}
void SilentScopeGame::detach() {
@@ -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;
}
+47
View File
@@ -52,6 +52,8 @@ static HWND GFDM_RIGHT_WINDOW = nullptr;
static HMONITOR GFDM_TWO_HEAD_SMALL_MONITOR = nullptr;
static HWND GFDM_TWO_HEAD_SMALL_WINDOW = nullptr;
HWND POPN_SUBSCREEN_WINDOW = nullptr;
static HWND NDD_SUBSCREEN_WINDOW = nullptr;
HWND NDD_MAIN_WINDOW = nullptr;
bool FAKE_SUBSCREEN_ADAPTER = false;
// icon
@@ -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))) {
+1
View File
@@ -103,6 +103,7 @@ extern HWND TDJ_SUBSCREEN_WINDOW;
extern HWND SDVX_SUBSCREEN_WINDOW;
extern HWND POPN_SUBSCREEN_WINDOW;
extern HWND GFDM_SUBSCREEN_WINDOW;
extern HWND NDD_MAIN_WINDOW;
extern bool SUBSCREEN_FORCE_REDRAW;
extern bool FAKE_SUBSCREEN_ADAPTER;
+6 -3
View File
@@ -336,9 +336,11 @@ void hooks::lang::early_init() {
&GetLocaleInfoA_orig);
}
// for TDJ subscreen search keyboard and T44 narrow-string handling
// for TDJ subscreen search keyboard
// T44 narrow-string handling
// NDD text measuring
if ((avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) ||
avs::game::is_model("T44")) {
avs::game::is_model({ "T44", "NDD" })) {
log_info("hooks::lang", "hooking IsDBCSLeadByte");
detour::trampoline_try(
"kernel32.dll",
@@ -358,8 +360,9 @@ void hooks::lang::early_init() {
#endif
#ifdef SPICE64
// NDD renders through GetTextExtentPoint32A, so its wide strings go back through CP_ACP first
const auto hook_wide_char_to_multi_byte =
games::gitadora::is_arena_model() || avs::game::is_model("T44");
games::gitadora::is_arena_model() || avs::game::is_model({ "T44", "NDD" });
#else
// XG2 converts UTF-8 property strings through CP_ACP before rendering.
const auto hook_wide_char_to_multi_byte = avs::game::is_model({ "K32", "K33" });
+2
View File
@@ -730,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");