Files
spice2x-r3d/src/spice2x/sdk/sdk.cpp
T
bicarusandGitHub 1957917270 sdk: update cpp sample for DDR, fix DDR MDXF polling when api/sdk is providing input (#679)
## 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
2026-05-07 18:10:14 -07:00

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