From dad88caf019ed1af60f174138b895ba90cd1a626 Mon Sep 17 00:00:00 2001 From: Jiongjia Lu Date: Tue, 25 Aug 2026 15:49:31 +0800 Subject: [PATCH] api: add card lookup endpoint (#891) ## Link to GitHub Issue or related Pull Request, if one exists N/A ## Description of change I am developing [spice.nimabe.net](https://spice.nimabe.net), a static frontend for SpiceAPI. It already supports generating and inserting new card IDs, but SpiceAPI does not provide a way to discover cards already configured on the game machine. This makes the frontend inconvenient for existing users who generated and saved their card IDs locally. They currently have to open each card file on the game machine and manually copy its ID into the frontend. This change adds a `card.get_cards` API function that enumerates the current cards for readers supported by the running game. Each entry contains: - reader index - canonical 16-character card ID - `source`, set to `file` or `override` - `file_name` for file-backed cards File-backed cards expose only the save-file basename. Active `-card0` and `-card1` values are returned with `source: "override"` and no `file_name`, instead of representing an override as a file. spicefe derives the useful default import names `card0` and `card1` from the reader index. Reader enumeration uses `eamuse_get_game_keypads()` so games with only one reader do not expose a second card. This allows spicefe and other API clients to offer one-click import for existing cards, without requiring users to manually find and copy their card IDs. Because card IDs are sensitive, `card.get_cards` can only be used when the operator has configured an API password. No full filesystem path is exposed. Missing, unreadable, and invalid cards are omitted. The existing `card.insert` function remains unchanged. To avoid requiring a password for the whole card module, this change adds support for password requirements on individual API functions while preserving existing module-wide password behavior. Access to configured card paths and runtime card overrides is synchronized because API requests may read them concurrently with configuration updates. The Python and Dart API wrappers and README documentation have been updated for the new function and source metadata. ## Testing - Ran the complete `src/spice2x/build_docker.sh` build successfully. - Built all required 32-bit and 64-bit targets. - Built the Windows XP-compatible 32-bit targets. - Passed static import checks. - Passed Windows 7 and Windows XP compatibility checks. - Completed release packaging successfully. - Verified both updated Dart wrappers are correctly formatted. - Ran all 106 spicefe tests successfully. - `git diff --check` passes. *This implementation was prepared with assistance from OpenAI Codex.* --- src/spice2x/README.md | 7 ++ src/spice2x/api/controller.cpp | 8 ++- src/spice2x/api/module.cpp | 8 +++ src/spice2x/api/module.h | 10 +++ src/spice2x/api/modules/card.cpp | 64 +++++++++++++++++++ src/spice2x/api/modules/card.h | 1 + .../spiceapi-websocket/src/wrappers/card.dart | 27 ++++++++ .../dart/spiceapi/src/wrappers/card.dart | 27 ++++++++ .../api/resources/python/spiceapi/card.py | 4 ++ src/spice2x/misc/eamuse.cpp | 40 ++++++++---- src/spice2x/misc/eamuse.h | 2 + 11 files changed, 181 insertions(+), 17 deletions(-) diff --git a/src/spice2x/README.md b/src/spice2x/README.md index 4425487..342ea7e 100644 --- a/src/spice2x/README.md +++ b/src/spice2x/README.md @@ -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) diff --git a/src/spice2x/api/controller.cpp b/src/spice2x/api/controller.cpp index 1fd41d5..1a6c4dd 100644 --- a/src/spice2x/api/controller.cpp +++ b/src/spice2x/api/controller.cpp @@ -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; } diff --git a/src/spice2x/api/module.cpp b/src/spice2x/api/module.cpp index 700a562..af2a0e8 100644 --- a/src/spice2x/api/module.cpp +++ b/src/spice2x/api/module.cpp @@ -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 diff --git a/src/spice2x/api/module.h b/src/spice2x/api/module.h index d41b787..262a893 100644 --- a/src/spice2x/api/module.h +++ b/src/spice2x/api/module.h @@ -4,6 +4,7 @@ #include #include #include +#include #include #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 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); diff --git a/src/spice2x/api/modules/card.cpp b/src/spice2x/api/modules/card.cpp index 5eb262a..ac65560 100644 --- a/src/spice2x/api/modules/card.cpp +++ b/src/spice2x/api/modules/card.cpp @@ -1,4 +1,5 @@ #include "card.h" +#include #include #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); + } } /** diff --git a/src/spice2x/api/modules/card.h b/src/spice2x/api/modules/card.h index 6012176..e90fbe3 100644 --- a/src/spice2x/api/modules/card.h +++ b/src/spice2x/api/modules/card.h @@ -12,6 +12,7 @@ namespace api::modules { private: // function definitions + void get_cards(Request &req, Response &res); void insert(Request &req, Response &res); }; } diff --git a/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart index 2a42d63..1bd487d 100644 --- a/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart +++ b/src/spice2x/api/resources/dart/spiceapi-websocket/src/wrappers/card.dart @@ -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> cardGetCards(Connection con) { + var req = Request("card", "get_cards"); + return con.request(req).then((res) { + List cards = []; + for (var value in res.getData()) { + cards.add( + CardInfo( + value["index"], + value["card_id"], + value["source"], + value["file_name"], + ), + ); + } + return cards; + }); +} + Future cardInsert(Connection con, int unit, String cardID) { var req = Request("card", "insert"); req.addParam(unit); diff --git a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart index 2a42d63..1bd487d 100644 --- a/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart +++ b/src/spice2x/api/resources/dart/spiceapi/src/wrappers/card.dart @@ -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> cardGetCards(Connection con) { + var req = Request("card", "get_cards"); + return con.request(req).then((res) { + List cards = []; + for (var value in res.getData()) { + cards.add( + CardInfo( + value["index"], + value["card_id"], + value["source"], + value["file_name"], + ), + ); + } + return cards; + }); +} + Future cardInsert(Connection con, int unit, String cardID) { var req = Request("card", "insert"); req.addParam(unit); diff --git a/src/spice2x/api/resources/python/spiceapi/card.py b/src/spice2x/api/resources/python/spiceapi/card.py index d579ed8..3eff0d2 100644 --- a/src/spice2x/api/resources/python/spiceapi/card.py +++ b/src/spice2x/api/resources/python/spiceapi/card.py @@ -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) diff --git a/src/spice2x/misc/eamuse.cpp b/src/spice2x/misc/eamuse.cpp index 7e054f5..8968a00 100644 --- a/src/spice2x/misc/eamuse.cpp +++ b/src/spice2x/misc/eamuse.cpp @@ -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 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 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 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 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 lock(KEYPAD_BINDINGS_LOCK); + KEYPAD_BINDINGS = std::move(bindings); } const std::string &eamuse_get_game() { diff --git a/src/spice2x/misc/eamuse.h b/src/spice2x/misc/eamuse.h index d772bb5..366ba10 100644 --- a/src/spice2x/misc/eamuse.h +++ b/src/spice2x/misc/eamuse.h @@ -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);