## Link to GitHub Issue or related Pull Request, if one exists #676 #452 ## Description of change When spice SDK or API sets or clears button and analog override, we don't tell MDXF about it, so it may fall behind. This is a special case for DDR MDXF I/O emulation. For Naive/Xinput I think we are getting lucky because they usually also generate WM_INPUT events. Update the C++ SDK sample to provide more interesting example of emulating DDR arrows with the keyboard. ## Testing
598 lines
16 KiB
C++
598 lines
16 KiB
C++
#include <vector>
|
|
#include <mutex>
|
|
#include <shared_mutex>
|
|
|
|
#include "sdk.h"
|
|
#include "avs/game.h"
|
|
#include "games/io.h"
|
|
#include "launcher/launcher.h"
|
|
#include "misc/eamuse.h"
|
|
#include "sdk/include/spicesdk.h"
|
|
#include "touch/touch.h"
|
|
#include "util/logging.h"
|
|
#include "util/utils.h"
|
|
#include "acio/mdxf/mdxf_poll.h"
|
|
|
|
namespace sdk {
|
|
|
|
static spice_sdk_init_func sdk_init;
|
|
static spice_sdk_log_func sdk_log;
|
|
static spice_sdk_get_avs_info_func sdk_get_avs_info;
|
|
static spice_sdk_get_game_info_func sdk_get_game_info;
|
|
static spice_sdk_get_button_func sdk_get_button;
|
|
static spice_sdk_set_button_func sdk_set_button;
|
|
static spice_sdk_get_analog_func sdk_get_analog;
|
|
static spice_sdk_set_analog_func sdk_set_analog;
|
|
static spice_sdk_get_light_func sdk_get_light;
|
|
static spice_sdk_set_light_func sdk_set_light;
|
|
static spice_sdk_set_touch_func sdk_set_touch;
|
|
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;
|
|
|
|
struct SdkModule {
|
|
std::string dll;
|
|
HINSTANCE module;
|
|
};
|
|
|
|
// DLLs
|
|
static int sdk_modules_count = 0;
|
|
static std::vector<SdkModule> sdk_modules_list;
|
|
static std::shared_mutex sdk_global_mutex;
|
|
|
|
// internal
|
|
static bool sdk_initialized = false;
|
|
static bool sdk_shutting_down = false;
|
|
static std::vector<Button> *buttons;
|
|
static std::vector<Analog> *analogs;
|
|
static std::vector<Light> *lights;
|
|
|
|
// callbacks
|
|
static std::mutex sdk_callback_registration_mutex;
|
|
static std::vector<spice_sdk_destroy_callback_func *> callbacks_destroy;
|
|
|
|
void register_sdk_hooks(std::string dll, HINSTANCE module) {
|
|
sdk_modules_list.emplace_back(std::move(dll), module);
|
|
sdk_modules_count += 1;
|
|
}
|
|
|
|
void init_sdk_modules() {
|
|
if (sdk_modules_count == 0) {
|
|
return;
|
|
}
|
|
|
|
// prepare
|
|
buttons = games::get_buttons(eamuse_get_game());
|
|
analogs = games::get_analogs(eamuse_get_game());
|
|
lights = games::get_lights(eamuse_get_game());
|
|
|
|
// under exclusive lock, mark the SDK helpers as being available
|
|
{
|
|
std::unique_lock lock(sdk_global_mutex);
|
|
sdk_initialized = true;
|
|
}
|
|
|
|
// without any locks, call into each DLL; this may call back into SDK functions
|
|
for (auto &module : sdk_modules_list) {
|
|
// get a pointer to dll's spice_sdk_entry_point
|
|
const auto entry_point = reinterpret_cast<spice_sdk_entry_point_func *>(
|
|
GetProcAddress(module.module, "spice_sdk_entry_point"));
|
|
if (!entry_point) {
|
|
continue;
|
|
}
|
|
|
|
// call into it on this thread
|
|
log_info("sdk", "registering SDK hooks for {}", module.dll);
|
|
const auto ret = entry_point(sdk_init);
|
|
log_info("sdk", "spice_sdk_entry_point returned {}", ret);
|
|
}
|
|
}
|
|
|
|
void fini_sdk_modules() {
|
|
// prevent multiple calls and further calls into sdk_init
|
|
{
|
|
std::unique_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return;
|
|
}
|
|
sdk_shutting_down = true;
|
|
}
|
|
|
|
// call into destroy callback of each DLL
|
|
// this may call back into SDK functions (e.g., for logging)
|
|
// so we leave sdk_initialized as-is
|
|
{
|
|
log_info("sdk", "calling destroy callbacks...");
|
|
std::lock_guard lock(sdk_callback_registration_mutex);
|
|
for (const auto& destroy : callbacks_destroy) {
|
|
destroy();
|
|
}
|
|
}
|
|
|
|
// under exclusive lock, mark the SDK functions as no longer available
|
|
{
|
|
std::unique_lock lock(sdk_global_mutex);
|
|
sdk_initialized = false;
|
|
}
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_init(
|
|
uint32_t version,
|
|
spice_sdk_destroy_callback_func *destroy_callback,
|
|
void *sdk_functions
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (sdk_shutting_down || !sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
uint32_t size;
|
|
if (version != 0) {
|
|
log_warning("sdk", "sdk_init returning NOT_SUPPORTED due to invalid version: {}", version);
|
|
return SPICE_SDK_STATUS_NOT_SUPPORTED;
|
|
}
|
|
|
|
if (!destroy_callback) {
|
|
log_warning("sdk", "sdk_init returning INVALID_ARGUMENT_2 due to invalid destroy_callback pointer");
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
if (!sdk_functions) {
|
|
log_warning("sdk", "sdk_init returning INVALID_ARGUMENT_3 due to invalid sdk_functions pointer");
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_3;
|
|
}
|
|
|
|
auto *v0 = reinterpret_cast<SPICE_SDK_V0 *>(sdk_functions);
|
|
if (v0->size < RTL_SIZEOF_THROUGH_FIELD(SPICE_SDK_V0, set_keypad)) {
|
|
log_warning("sdk", "sdk_init returning TOO_SMALL due to size field of SPICE_SDK_V0 not being set");
|
|
return SPICE_SDK_STATUS_TOO_SMALL;
|
|
}
|
|
|
|
// we are trusting the size passed in by the caller
|
|
size = v0->size;
|
|
memset(v0, 0, size);
|
|
v0->size = size;
|
|
|
|
v0->log = sdk_log;
|
|
v0->get_game_info = sdk_get_game_info;
|
|
v0->get_avs_info = sdk_get_avs_info;
|
|
v0->get_button = sdk_get_button;
|
|
v0->set_button = sdk_set_button;
|
|
v0->get_analog = sdk_get_analog;
|
|
v0->set_analog = sdk_set_analog;
|
|
v0->get_light = sdk_get_light;
|
|
v0->set_light = sdk_set_light;
|
|
v0->set_touch = sdk_set_touch;
|
|
v0->clear_touch = sdk_clear_touch;
|
|
v0->insert_card = sdk_insert_card;
|
|
v0->set_keypad = sdk_set_keypad;
|
|
// end of 0.1
|
|
// any newer minor iterations will need to check the size
|
|
|
|
{
|
|
// destroy callbacks are only called upon successful return from this routine
|
|
std::lock_guard lock(sdk_callback_registration_mutex);
|
|
callbacks_destroy.push_back(destroy_callback);
|
|
}
|
|
|
|
log_info("sdk", "sdk_init returning SUCCESS");
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_log(
|
|
SPICE_SDK_LOG_LEVEL level,
|
|
const char *facility,
|
|
const char *message
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (facility == nullptr) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
|
|
const std::string facility_str = fmt::format("sdk::{}", facility);
|
|
|
|
switch (level) {
|
|
case SPICE_SDK_LOG_LEVEL_MISC:
|
|
log_misc(facility_str.c_str(), "{}", message);
|
|
break;
|
|
case SPICE_SDK_LOG_LEVEL_INFO:
|
|
log_info(facility_str.c_str(), "{}", message);
|
|
break;
|
|
case SPICE_SDK_LOG_LEVEL_WARNING:
|
|
log_warning(facility_str.c_str(), "{}", message);
|
|
break;
|
|
case SPICE_SDK_LOG_LEVEL_FATAL:
|
|
log_fatal(facility_str.c_str(), "{}", message);
|
|
break;
|
|
default:
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_get_avs_info(
|
|
SPICE_SDK_AVS_INFO *info
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!info) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
// MDXJAA2025061002
|
|
strncpy(info->model, avs::game::MODEL, sizeof(info->model));
|
|
info->dest = avs::game::DEST[0];
|
|
info->spec = avs::game::SPEC[0];
|
|
info->rev = avs::game::REV[0];
|
|
strncpy(info->ext, avs::game::EXT, sizeof(info->ext));
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_get_game_info(
|
|
SPICE_SDK_GAME_INFO *info
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!info) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
|
|
*info = {};
|
|
|
|
const std::string game_name = eamuse_get_game();
|
|
strncpy(info->name, game_name.c_str(), sizeof(info->name));
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_get_button (
|
|
uint32_t button_id,
|
|
bool *pressed,
|
|
float *velocity
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!buttons) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (button_id >= buttons->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
|
|
Button &button = (*buttons)[button_id];
|
|
if (pressed) {
|
|
*pressed = (GameAPI::Buttons::getState(RI_MGR, button) == GameAPI::Buttons::BUTTON_PRESSED);
|
|
}
|
|
if (velocity) {
|
|
*velocity = GameAPI::Buttons::getVelocity(RI_MGR, button);
|
|
}
|
|
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_set_button (
|
|
uint32_t button_id,
|
|
bool pressed,
|
|
float velocity
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!buttons) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (button_id >= buttons->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
if (pressed) {
|
|
velocity = std::clamp(velocity, 0.f, 1.f);
|
|
}
|
|
|
|
Button &button = (*buttons)[button_id];
|
|
if (pressed) {
|
|
button.override_state = pressed ? GameAPI::Buttons::BUTTON_PRESSED : GameAPI::Buttons::BUTTON_NOT_PRESSED;
|
|
button.override_velocity = velocity;
|
|
}
|
|
button.override_enabled = pressed;
|
|
mdxf_poll(true);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_get_analog (
|
|
uint32_t analog_id,
|
|
float *value
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!analogs) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (analog_id >= analogs->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
if (!value) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
|
|
Analog &analog = (*analogs)[analog_id];
|
|
*value = GameAPI::Analogs::getState(RI_MGR, analog);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_set_analog (
|
|
uint32_t analog_id,
|
|
bool override_active,
|
|
float value
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!analogs) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (analog_id >= analogs->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
if (override_active) {
|
|
value = std::clamp(value, 0.f, 1.f);
|
|
}
|
|
|
|
Analog &analog = (*analogs)[analog_id];
|
|
if (override_active) {
|
|
analog.override_state = value;
|
|
}
|
|
analog.override_enabled = override_active;
|
|
mdxf_poll(true);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_get_light(
|
|
uint32_t light_id,
|
|
float *value
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!lights) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (light_id >= lights->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
if (!value) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
Light &light = (*lights)[light_id];
|
|
*value = GameAPI::Lights::readLight(RI_MGR, light);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_set_light(
|
|
uint32_t light_id,
|
|
bool override_active,
|
|
float value
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (!lights) {
|
|
return SPICE_SDK_STATUS_NOT_INITIALIZED;
|
|
}
|
|
if (light_id >= lights->size()) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
if (override_active) {
|
|
value = std::clamp(value, 0.f, 1.f);
|
|
}
|
|
|
|
Light &light = (*lights)[light_id];
|
|
if (override_active) {
|
|
light.override_state = value;
|
|
}
|
|
light.override_enabled = override_active;
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_set_touch(
|
|
const SPICE_SDK_TOUCH_POINT *points,
|
|
uint32_t count
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (count == 0 || !points) {
|
|
return SPICE_SDK_STATUS_TOO_SMALL;
|
|
}
|
|
|
|
std::vector<TouchPoint> touch_points;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
const SPICE_SDK_TOUCH_POINT &point = points[i];
|
|
touch_points.emplace_back(TouchPoint {
|
|
.id = point.id,
|
|
.x = point.x,
|
|
.y = point.y,
|
|
.mouse = false,
|
|
});
|
|
}
|
|
touch_write_points(&touch_points);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_clear_touch(
|
|
const uint32_t *ids,
|
|
uint32_t count
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (count == 0 || !ids) {
|
|
return SPICE_SDK_STATUS_TOO_SMALL;
|
|
}
|
|
|
|
std::vector<DWORD> touch_point_ids;
|
|
for (uint32_t i = 0; i < count; i++) {
|
|
touch_point_ids.push_back(ids[i]);
|
|
}
|
|
touch_remove_points(&touch_point_ids);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_insert_card(
|
|
uint8_t unit,
|
|
const char *card_id
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (unit >= 2) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
|
|
if (!card_id) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
if (strlen(card_id) != 16) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
|
|
// convert to binary
|
|
uint8_t card_bin[8] {};
|
|
if (!hex2bin(card_id, card_bin)) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
|
|
// insert
|
|
eamuse_card_insert(unit & 1, card_bin);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
struct KeypadMapping {
|
|
char character;
|
|
uint16_t state;
|
|
};
|
|
|
|
static KeypadMapping KEYPAD_MAPPINGS[] = {
|
|
{ '0', 1 << EAM_IO_KEYPAD_0 },
|
|
{ '1', 1 << EAM_IO_KEYPAD_1 },
|
|
{ '2', 1 << EAM_IO_KEYPAD_2 },
|
|
{ '3', 1 << EAM_IO_KEYPAD_3 },
|
|
{ '4', 1 << EAM_IO_KEYPAD_4 },
|
|
{ '5', 1 << EAM_IO_KEYPAD_5 },
|
|
{ '6', 1 << EAM_IO_KEYPAD_6 },
|
|
{ '7', 1 << EAM_IO_KEYPAD_7 },
|
|
{ '8', 1 << EAM_IO_KEYPAD_8 },
|
|
{ '9', 1 << EAM_IO_KEYPAD_9 },
|
|
{ 'A', 1 << EAM_IO_KEYPAD_00 },
|
|
{ 'D', 1 << EAM_IO_KEYPAD_DECIMAL },
|
|
};
|
|
|
|
SPICE_SDK_STATUS_CODE
|
|
__cdecl
|
|
sdk_set_keypad(
|
|
uint8_t unit,
|
|
char key
|
|
)
|
|
{
|
|
std::shared_lock lock(sdk_global_mutex);
|
|
if (!sdk_initialized) {
|
|
return SPICE_SDK_STATUS_TOO_LATE;
|
|
}
|
|
|
|
if (unit >= 2) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_1;
|
|
}
|
|
|
|
if (key == 0) {
|
|
eamuse_set_keypad_overrides(unit, 0);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
// find mapping
|
|
uint16_t state = 0;
|
|
bool mapping_found = false;
|
|
for (auto &mapping : KEYPAD_MAPPINGS) {
|
|
if (mapping.character == key) {
|
|
state |= mapping.state;
|
|
mapping_found = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// check for error
|
|
if (!mapping_found) {
|
|
return SPICE_SDK_STATUS_INVALID_ARGUMENT_2;
|
|
}
|
|
|
|
// set
|
|
eamuse_set_keypad_overrides(unit, state);
|
|
return SPICE_SDK_STATUS_SUCCESS;
|
|
}
|
|
|
|
|
|
} // namespace sdk
|