api: sdvx tape led (#857)

## Link to GitHub Issue or related Pull Request, if one exists
Fixes #852

## Description of change
Add SDVX valk cab tape LED output over API

## Testing
tested with custom python script over api
This commit is contained in:
bicarus
2026-08-07 00:38:17 -07:00
committed by GitHub
parent b53447bed5
commit baa550037a
13 changed files with 182 additions and 32 deletions
+1
View File
@@ -332,6 +332,7 @@ set(SOURCE_FILES ${SOURCE_FILES}
api/modules/control.cpp api/modules/control.cpp
api/modules/touch.cpp api/modules/touch.cpp
api/modules/iidx.cpp api/modules/iidx.cpp
api/modules/sdvx.cpp
api/serial.cpp api/serial.cpp
api/modules/drs.cpp api/modules/drs.cpp
api/modules/lcd.cpp api/modules/lcd.cpp
+14
View File
@@ -267,6 +267,20 @@ which also means that your hex edits are applicable directly.
- `Side Panel Right Inner` - `Side Panel Right Inner`
- `Side Panel Right` - `Side Panel Right`
#### SDVX
- tapeled_get(name: str, ...)
- returns a list containing a dict of the current tape LED states. The dict keys are:
- `Title`
- `Upper Left Speaker`
- `Upper Right Speaker`
- `Left Wing`
- `Right Wing`
- `Control Panel`
- `Lower Left Speaker`
- `Lower Right Speaker`
- `Woofer`
- `V Unit`
#### LCD #### LCD
- info() - info()
- returns information about the serial LCD controller some games use - returns information about the serial LCD controller some games use
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#include <atomic>
#include <cstdint>
namespace api {
extern std::atomic_uint32_t CLIENT_COUNT;
inline bool has_clients() {
return CLIENT_COUNT.load(std::memory_order_relaxed) > 0;
}
}
+9
View File
@@ -5,6 +5,7 @@
#include <utility> #include <utility>
#include "client.h"
#include "cfg/configurator.h" #include "cfg/configurator.h"
#include "external/rapidjson/document.h" #include "external/rapidjson/document.h"
#include "util/crypt.h" #include "util/crypt.h"
@@ -28,6 +29,7 @@
#include "modules/lcd.h" #include "modules/lcd.h"
#include "modules/lights.h" #include "modules/lights.h"
#include "modules/memory.h" #include "modules/memory.h"
#include "modules/sdvx.h"
#include "modules/touch.h" #include "modules/touch.h"
#include "modules/resize.h" #include "modules/resize.h"
#include "request.h" #include "request.h"
@@ -36,6 +38,8 @@
using namespace rapidjson; using namespace rapidjson;
using namespace api; using namespace api;
std::atomic_uint32_t api::CLIENT_COUNT = 0;
Controller::Controller(unsigned short port, std::string password, bool pretty) Controller::Controller(unsigned short port, std::string password, bool pretty)
: port(port), password(std::move(password)), pretty(pretty) : port(port), password(std::move(password)), pretty(pretty)
{ {
@@ -411,8 +415,11 @@ void Controller::init_state(api::ClientState *state) {
state->modules.push_back(new modules::LCD()); state->modules.push_back(new modules::LCD());
state->modules.push_back(new modules::Lights()); state->modules.push_back(new modules::Lights());
state->modules.push_back(new modules::Memory()); state->modules.push_back(new modules::Memory());
state->modules.push_back(new modules::SDVX());
state->modules.push_back(new modules::Touch()); state->modules.push_back(new modules::Touch());
state->modules.push_back(new modules::Resize()); state->modules.push_back(new modules::Resize());
CLIENT_COUNT.fetch_add(1, std::memory_order_relaxed);
} }
void Controller::free_state(api::ClientState *state) { void Controller::free_state(api::ClientState *state) {
@@ -424,6 +431,8 @@ void Controller::free_state(api::ClientState *state) {
// free cipher // free cipher
delete state->cipher; delete state->cipher;
CLIENT_COUNT.fetch_sub(1, std::memory_order_relaxed);
} }
void Controller::free_socket() { void Controller::free_socket() {
+4 -2
View File
@@ -106,14 +106,16 @@ namespace api::modules {
void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) { void IIDX::copy_tapeled_data(Response &res, Value &response_object, const tapeledutils::tape_led &mapping) {
// Create an array for the light state // Create an array for the light state
Value light_state(kArrayType); Value light_state(kArrayType);
light_state.Reserve(mapping.data.capacity() * 3, res.doc()->GetAllocator()); light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) { for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator()); light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator()); light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator()); light_state.PushBack(b, res.doc()->GetAllocator());
} }
// Can't use StringRef here, turns some strings partially into null bytes for some reason // can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator()); Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator()); response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
} }
+67
View File
@@ -0,0 +1,67 @@
#include "sdvx.h"
#include <functional>
using namespace std::placeholders;
using namespace rapidjson;
namespace api::modules {
SDVX::SDVX() : Module("sdvx") {
functions["tapeled_get"] = std::bind(&SDVX::tapeled_get, this, _1, _2);
for (auto &light : games::sdvx::TAPELED_MAPPING) {
lights_by_names.emplace(light.lightName, light);
}
}
/**
* tapeled_get()
* tapeled_get(name: str, ...)
*/
void SDVX::tapeled_get(Request &req, Response &res) {
Value response_object(kObjectType);
// all tape leds
if (req.params.Size() == 0) {
// iterate through each device and dump its lights data into the response
for (const auto &mapping : games::sdvx::TAPELED_MAPPING) {
copy_tapeled_data(res, response_object, mapping);
}
} else {
// specified light names
for (Value &param : req.params.GetArray()) {
// check params
if (!param.IsString()) {
error_type(res, "name", "string");
return;
}
const auto name = param.GetString();
if (const auto &it = lights_by_names.find(name); it != lights_by_names.end()) {
copy_tapeled_data(res, response_object, it->second.get());
}
}
}
res.add_data(response_object);
}
void SDVX::copy_tapeled_data(Response &res, Value &response_object,
const tapeledutils::tape_led &mapping)
{
Value light_state(kArrayType);
light_state.Reserve(
static_cast<SizeType>(mapping.data.size() * 3),
res.doc()->GetAllocator());
for (const auto [r, g, b] : mapping.data) {
light_state.PushBack(r, res.doc()->GetAllocator());
light_state.PushBack(g, res.doc()->GetAllocator());
light_state.PushBack(b, res.doc()->GetAllocator());
}
// can't use StringRef here, turns some strings partially into null bytes for some reason
Value light_name(mapping.lightName.c_str(), res.doc()->GetAllocator());
response_object.AddMember(light_name, light_state, res.doc()->GetAllocator());
}
}
+24
View File
@@ -0,0 +1,24 @@
#pragma once
#include <functional>
#include <string>
#include "api/module.h"
#include "api/request.h"
#include "external/robin_hood.h"
#include "games/sdvx/sdvx.h"
namespace api::modules {
class SDVX : public Module {
public:
SDVX();
private:
robin_hood::unordered_map<std::string, std::reference_wrapper<tapeledutils::tape_led>> lights_by_names;
void tapeled_get(Request &req, Response &res);
void copy_tapeled_data(Response &res, rapidjson::Value &response_object,
const tapeledutils::tape_led &mapping);
};
}
@@ -7,6 +7,7 @@ from .coin import *
from .control import * from .control import *
from .exceptions import * from .exceptions import *
from .iidx import * from .iidx import *
from .sdvx import *
from .info import * from .info import *
from .keypads import * from .keypads import *
from .lights import * from .lights import *
@@ -0,0 +1,12 @@
from .connection import Connection
from .request import Request
def sdvx_tapeled_get(con: Connection, *light_names):
req = Request("sdvx", "tapeled_get")
for light_name in light_names:
req.add_param(light_name)
res = con.request(req)
return res.get_data()
+7 -4
View File
@@ -3,6 +3,7 @@
#if SPICE64 #if SPICE64
#include <cstdint> #include <cstdint>
#include "api/client.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -443,10 +444,12 @@ namespace games::iidx {
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
for (unsigned int i = 0; i < data_size; ++i) { if (api::has_clients()) {
map.data[i].r = data[i * 3]; for (size_t i = 0; i < data_size; ++i) {
map.data[i].g = data[i * 3 + 1]; map.data[i].r = data[i * 3];
map.data[i].b = data[i * 3 + 2]; map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
} }
} }
+13 -26
View File
@@ -3,6 +3,7 @@
#if SPICE64 #if SPICE64
#include <cstdint> #include <cstdint>
#include "api/client.h"
#include "util/detour.h" #include "util/detour.h"
#include "util/logging.h" #include "util/logging.h"
#include "util/utils.h" #include "util/utils.h"
@@ -349,43 +350,29 @@ namespace games::sdvx {
* 9 - v unit - 258 bytes - 86 colors * 9 - v unit - 258 bytes - 86 colors
* *
* data is stored in RGB order, 3 bytes per color * data is stored in RGB order, 3 bytes per color
*
* TODO: expose this data via API
*/ */
// data mapping
static struct TapeLedMapping {
size_t data_size;
int index_r, index_g, index_b;
TapeLedMapping(size_t data_size, int index_r, int index_g, int index_b)
: data_size(data_size), index_r(index_r), index_g(index_g), index_b(index_b) {}
} mapping[] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B },
};
// check index bounds // check index bounds
if (tapeledutils::is_enabled() && index < std::size(mapping)) { if (tapeledutils::is_enabled() && index < std::size(TAPELED_MAPPING)) {
auto &map = mapping[index]; auto &map = TAPELED_MAPPING[index];
const auto data_size = map.data.size();
// pick a color to use // pick a color to use
const auto rgb = tapeledutils::pick_color_from_led_tape(data, map.data_size); const auto rgb = tapeledutils::pick_color_from_led_tape(data, data_size);
// program the lights into API // program the lights into API
auto &lights = get_lights(); auto &lights = get_lights();
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_r], rgb.r);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_g], rgb.g);
GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b); GameAPI::Lights::writeLight(RI_MGR, lights[map.index_b], rgb.b);
if (api::has_clients()) {
for (size_t i = 0; i < data_size; ++i) {
map.data[i].r = data[i * 3];
map.data[i].g = data[i * 3 + 1];
map.data[i].b = data[i * 3 + 2];
}
}
} }
if (This != custom_node) { if (This != custom_node) {
+13
View File
@@ -61,6 +61,19 @@ namespace games::sdvx {
static HKEY real_asio_reg_handle = nullptr; static HKEY real_asio_reg_handle = nullptr;
static HKEY real_asio_device_reg_handle = nullptr; static HKEY real_asio_device_reg_handle = nullptr;
tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL] = {
{ 74, Lights::TITLE_AVG_R, Lights::TITLE_AVG_G, Lights::TITLE_AVG_B, "Title" },
{ 12, Lights::UPPER_LEFT_SPEAKER_AVG_R, Lights::UPPER_LEFT_SPEAKER_AVG_G, Lights::UPPER_LEFT_SPEAKER_AVG_B, "Upper Left Speaker" },
{ 12, Lights::UPPER_RIGHT_SPEAKER_AVG_R, Lights::UPPER_RIGHT_SPEAKER_AVG_G, Lights::UPPER_RIGHT_SPEAKER_AVG_B, "Upper Right Speaker" },
{ 56, Lights::LEFT_WING_AVG_R, Lights::LEFT_WING_AVG_G, Lights::LEFT_WING_AVG_B, "Left Wing" },
{ 56, Lights::RIGHT_WING_AVG_R, Lights::RIGHT_WING_AVG_G, Lights::RIGHT_WING_AVG_B, "Right Wing" },
{ 94, Lights::CONTROL_PANEL_AVG_R, Lights::CONTROL_PANEL_AVG_G, Lights::CONTROL_PANEL_AVG_B, "Control Panel" },
{ 12, Lights::LOWER_LEFT_SPEAKER_AVG_R, Lights::LOWER_LEFT_SPEAKER_AVG_G, Lights::LOWER_LEFT_SPEAKER_AVG_B, "Lower Left Speaker" },
{ 12, Lights::LOWER_RIGHT_SPEAKER_AVG_R, Lights::LOWER_RIGHT_SPEAKER_AVG_G, Lights::LOWER_RIGHT_SPEAKER_AVG_B, "Lower Right Speaker" },
{ 14, Lights::WOOFER_AVG_R, Lights::WOOFER_AVG_G, Lights::WOOFER_AVG_B, "Woofer" },
{ 86, Lights::V_UNIT_AVG_R, Lights::V_UNIT_AVG_G, Lights::V_UNIT_AVG_B, "V Unit" },
};
static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) { static LONG WINAPI RegOpenKeyA_hook(HKEY hKey, LPCSTR lpSubKey, PHKEY phkResult) {
if (lpSubKey != nullptr && if (lpSubKey != nullptr &&
phkResult != nullptr && phkResult != nullptr &&
+4
View File
@@ -6,6 +6,7 @@
#include "avs/game.h" #include "avs/game.h"
#include "games/game.h" #include "games/game.h"
#include "util/tapeled.h"
namespace games::sdvx { namespace games::sdvx {
@@ -25,6 +26,9 @@ namespace games::sdvx {
// states // states
extern bool SHOW_VM_MONITOR_WARNING; extern bool SHOW_VM_MONITOR_WARNING;
constexpr int SDVX_TAPELED_TOTAL = 10;
extern tapeledutils::tape_led TAPELED_MAPPING[SDVX_TAPELED_TOTAL];
static inline bool is_valkyrie_model() { static inline bool is_valkyrie_model() {
return ( return (
avs::game::is_model("KFC") && avs::game::is_model("KFC") &&