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.*
This commit is contained in:
@@ -144,6 +144,13 @@ restriction is that the ID has to be a valid 64-bit unsigned integer.
|
|||||||
`busy`
|
`busy`
|
||||||
|
|
||||||
#### Card
|
#### 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)
|
- insert(index: uint, card_id: hex)
|
||||||
- inserts a card which gets read by the emulated card readers for the game
|
- 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)
|
- index has to be either 0 (for P1) or 1 (for P2)
|
||||||
|
|||||||
@@ -337,9 +337,11 @@ bool Controller::process_request(ClientState *state, const char *in, size_t in_s
|
|||||||
if (module->name == request.module) {
|
if (module->name == request.module) {
|
||||||
module_found = true;
|
module_found = true;
|
||||||
|
|
||||||
// check password force
|
// check password requirement
|
||||||
if (module->password_force && this->password.empty() && request.function != "session_refresh") {
|
if (module->requires_password(request.function)
|
||||||
Value err("Module requires the password to be set.");
|
&& this->password.empty()
|
||||||
|
&& request.function != "session_refresh") {
|
||||||
|
Value err("Function requires the password to be set.");
|
||||||
response.add_error(err);
|
response.add_error(err);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ namespace api {
|
|||||||
this->password_force = password_force;
|
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) {
|
void Module::handle(Request &req, Response &res) {
|
||||||
|
|
||||||
// log module access
|
// log module access
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <unordered_set>
|
||||||
#include <external/robin_hood.h>
|
#include <external/robin_hood.h>
|
||||||
|
|
||||||
#include "response.h"
|
#include "response.h"
|
||||||
@@ -26,6 +27,13 @@ namespace api {
|
|||||||
// default constructor
|
// default constructor
|
||||||
explicit Module(std::string name, bool password_force=false);
|
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<std::string> password_force_functions;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
|
||||||
// virtual deconstructor
|
// virtual deconstructor
|
||||||
@@ -35,6 +43,8 @@ namespace api {
|
|||||||
std::string name;
|
std::string name;
|
||||||
bool password_force;
|
bool password_force;
|
||||||
|
|
||||||
|
bool requires_password(const std::string &function) const;
|
||||||
|
|
||||||
// the magic
|
// the magic
|
||||||
void handle(Request &req, Response &res);
|
void handle(Request &req, Response &res);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "card.h"
|
#include "card.h"
|
||||||
|
#include <fstream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include "external/rapidjson/document.h"
|
#include "external/rapidjson/document.h"
|
||||||
#include "util/logging.h"
|
#include "util/logging.h"
|
||||||
@@ -11,8 +12,71 @@ using namespace rapidjson;
|
|||||||
|
|
||||||
namespace api::modules {
|
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") {
|
Card::Card() : Module("card") {
|
||||||
|
functions["get_cards"] = std::bind(&Card::get_cards, this, _1, _2);
|
||||||
functions["insert"] = std::bind(&Card::insert, 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ namespace api::modules {
|
|||||||
private:
|
private:
|
||||||
|
|
||||||
// function definitions
|
// function definitions
|
||||||
|
void get_cards(Request &req, Response &res);
|
||||||
void insert(Request &req, Response &res);
|
void insert(Request &req, Response &res);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
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<List<CardInfo>> cardGetCards(Connection con) {
|
||||||
|
var req = Request("card", "get_cards");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
List<CardInfo> cards = [];
|
||||||
|
for (var value in res.getData()) {
|
||||||
|
cards.add(
|
||||||
|
CardInfo(
|
||||||
|
value["index"],
|
||||||
|
value["card_id"],
|
||||||
|
value["source"],
|
||||||
|
value["file_name"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -1,5 +1,32 @@
|
|||||||
part of spiceapi;
|
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<List<CardInfo>> cardGetCards(Connection con) {
|
||||||
|
var req = Request("card", "get_cards");
|
||||||
|
return con.request(req).then((res) {
|
||||||
|
List<CardInfo> cards = [];
|
||||||
|
for (var value in res.getData()) {
|
||||||
|
cards.add(
|
||||||
|
CardInfo(
|
||||||
|
value["index"],
|
||||||
|
value["card_id"],
|
||||||
|
value["source"],
|
||||||
|
value["file_name"],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return cards;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
Future<void> cardInsert(Connection con, int unit, String cardID) {
|
||||||
var req = Request("card", "insert");
|
var req = Request("card", "insert");
|
||||||
req.addParam(unit);
|
req.addParam(unit);
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ from .connection import Connection
|
|||||||
from .request import Request
|
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):
|
def card_insert(con: Connection, unit: int, card_id: str):
|
||||||
req = Request("card", "insert")
|
req = Request("card", "insert")
|
||||||
req.add_param(unit)
|
req.add_param(unit)
|
||||||
|
|||||||
+25
-13
@@ -34,6 +34,7 @@ static uint16_t KEYPAD_STATE_OVERRIDES_READER[] = {0, 0};
|
|||||||
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
static uint16_t KEYPAD_STATE_OVERRIDES_OVERLAY[] = {0, 0};
|
||||||
static std::string EAMUSE_GAME_NAME;
|
static std::string EAMUSE_GAME_NAME;
|
||||||
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
static ConfigKeypadBindings KEYPAD_BINDINGS {};
|
||||||
|
static std::mutex KEYPAD_BINDINGS_LOCK;
|
||||||
|
|
||||||
// auto card
|
// auto card
|
||||||
bool AUTO_INSERT_CARD[2] = {false, false};
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// get file path
|
// call the next function
|
||||||
std::filesystem::path path;
|
return eamuse_get_card(eamuse_get_card_path(index), card, index);
|
||||||
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
|
std::filesystem::path eamuse_get_card_path(size_t index) {
|
||||||
return eamuse_get_card(path, card, index);
|
std::lock_guard<std::mutex> 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) {
|
bool eamuse_get_card(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
// do a quick copy under lock
|
const auto card_override = eamuse_get_card_override(index);
|
||||||
std::unique_lock<std::mutex> lock(CARD_OVERRIDES_LOCK);
|
|
||||||
const auto card_override = CARD_OVERRIDES[index];
|
|
||||||
lock.unlock();
|
|
||||||
|
|
||||||
// Check if card overrides are present
|
// Check if card overrides are present
|
||||||
if (!card_override.empty()) {
|
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);
|
return eamuse_get_card_from_file(path, card, index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string eamuse_get_card_override(size_t index) {
|
||||||
|
std::lock_guard<std::mutex> 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) {
|
bool eamuse_get_card_from_file(const std::filesystem::path &path, uint8_t *card, int index) {
|
||||||
|
|
||||||
// open file
|
// open file
|
||||||
@@ -584,6 +593,7 @@ std::string eamuse_get_keypad_state_str(size_t unit) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool eamuse_keypad_state_naive() {
|
bool eamuse_keypad_state_naive() {
|
||||||
|
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
|
||||||
return KEYPAD_BINDINGS.keypads[0].empty() && KEYPAD_BINDINGS.keypads[1].empty();
|
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() {
|
void eamuse_update_keypad_bindings() {
|
||||||
KEYPAD_BINDINGS = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
auto bindings = Config::getInstance().getKeypadBindings(EAMUSE_GAME_NAME);
|
||||||
|
std::lock_guard<std::mutex> lock(KEYPAD_BINDINGS_LOCK);
|
||||||
|
KEYPAD_BINDINGS = std::move(bindings);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string &eamuse_get_game() {
|
const std::string &eamuse_get_game() {
|
||||||
|
|||||||
@@ -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(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(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);
|
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);
|
||||||
void eamuse_card_insert(int unit, const uint8_t *card);
|
void eamuse_card_insert(int unit, const uint8_t *card);
|
||||||
|
|||||||
Reference in New Issue
Block a user