From 231c998cc0605b9a39e27ded3a7c191ffd0acbc6 Mon Sep 17 00:00:00 2001 From: Emma Date: Thu, 16 Apr 2026 02:53:21 +0100 Subject: [PATCH] acio: implement ICCA slotted reader emulation for iidx18 (#642) ## Link to GitHub Issue or related Pull Request, if one exists #121 ## Description of change This implements a seperate ICCA card workflow for the slotted readers acting how Resort Anthem expects, enabling cards to be inserted. Additionally this fixes a bug where both card units would be treated the same (for keypad reads) under Resort Anthem. The slotted reader workflow was implemented as a seperate function for two reasons - it would be nice to not mess with any existing code to avoid breaking all the functional games, and the way the slotted readers are handled by RA seemed different enough to how everything was already designed to where it didn't make sense to keep adding special edge cases for RA. ## Testing IIDX18 was tested and cards could be inserted on both P1 and P2 side. IIDX19 through 22 was tested to make sure nothing had broken for wavepass emulation. ## Known Issues In IIDX18, when a card isn't set but the insert key is pressed, after the card timeout the game will error out with an invalid state error. --- src/spice2x/acio/icca/icca.cpp | 142 ++++++++++++++++++++++++++++----- 1 file changed, 122 insertions(+), 20 deletions(-) diff --git a/src/spice2x/acio/icca/icca.cpp b/src/spice2x/acio/icca/icca.cpp index 82d3b4f..f8493cb 100644 --- a/src/spice2x/acio/icca/icca.cpp +++ b/src/spice2x/acio/icca/icca.cpp @@ -35,24 +35,33 @@ struct ICCA_STATUS_LA9 { static_assert(sizeof(struct ICCA_STATUS) == 24, "ICCA_STATUS must be 24 bytes"); enum ICCA_WORKFLOW { - STEP, - SLEEP, - START, - INIT, - READY, - GET_USERID, - ACTIVE, - EJECT, - EJECT_CHECK, - END, - CLOSE_EJECT, - CLOSE_E_CHK, - CLOSE_END, - ERR_GETUID = -2 + STEP = 0, + SLEEP = 1, + START = 2, + INIT = 3, + READY = 4, + GET_USERID = 5, + ACTIVE = 6, + EJECT = 7, + EJECT_CHECK = 8, + END = 9, + CLOSE_EJECT = 10, + CLOSE_E_CHK = 11, + CLOSE_END = 12, + ERR_GETUID = -2, + ERR_REMOVED = -11 }; + +// used by RA as the status_code +enum ICCA_STATUS_CODE { + EMPTY = 1, + ENGAGED = 2 +}; + struct ICCA_UNIT { struct ICCA_STATUS status {}; enum ICCA_WORKFLOW state = STEP; + enum ICCA_WORKFLOW state_jdz = SLEEP; bool card_cmd_pressed = false; bool card_in = false; double card_in_time = 0.0; @@ -75,6 +84,11 @@ static inline int icca_get_active_count() { } static inline int icca_get_unit_id(int unit_id) { + // on RA the unit_id is zero-indexed + if (avs::game::is_model("JDZ")) { + return unit_id; + } + if (icca_get_active_count() < 2) return 1; else { @@ -122,9 +136,15 @@ static inline void update_card(int unit_id) { IS_LAST_CARD_FELICA = is_card_uid_felica(unit->status.uid); unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE; + unit->state_jdz = ACTIVE; unit->status.error = 0; + unit->status.front_sensor = 1; + unit->status.rear_sensor = 1; } else { unit->state = ERR_GETUID; + unit->state_jdz = ERR_GETUID; + unit->status.front_sensor = 1; + unit->status.rear_sensor = 1; memset(unit->status.uid, 0, 8); } unit->card_in = true; @@ -132,10 +152,25 @@ static inline void update_card(int unit_id) { } else if (unit->state == EJECT_CHECK) { unit->state = SLEEP; unit->card_in = false; + unit->status.front_sensor = 0; + unit->status.rear_sensor = 0; + } else if (unit->state_jdz == READY) { + unit->state = GET_USERID; + unit->state_jdz = GET_USERID; + unit->status.error = 0; + unit->status.front_sensor = 1; + unit->status.rear_sensor = 1; + unit->card_in = true; + unit->card_in_time = get_performance_seconds(); } } else { unit->state = acio::ICCA_COMPAT_ACTIVE ? START : ACTIVE; + if (unit->state_jdz == READY) { + unit->state_jdz = GET_USERID; + } unit->status.error = 0; + unit->status.front_sensor = 1; + unit->status.rear_sensor = 1; unit->card_in = true; unit->card_in_time = get_performance_seconds(); } @@ -144,6 +179,12 @@ static inline void update_card(int unit_id) { unit->state = CLOSE_EJECT; if (fabs(get_performance_seconds() - unit->card_in_time) > CARD_TIMEOUT) { unit->card_in = false; + memset(unit->status.uid, 0, 8); + if (unit->state_jdz == ACTIVE || unit->state_jdz == GET_USERID) { + unit->state_jdz = ERR_REMOVED; + } + unit->status.front_sensor = 0; + unit->status.rear_sensor = 0; } } @@ -302,7 +343,11 @@ static char __cdecl ac_io_icca_get_status(void *a1, void *a2) { // copy state to output buffer ICCA_UNIT *unit = &ICCA_UNITS[unit_id]; - unit->status.status_code = unit->state; + if (avs::game::is_model("JDZ")) { + unit->status.status_code = unit->card_in ? ENGAGED : EMPTY; + } else { + unit->status.status_code = unit->state; + } memcpy(status, &unit->status, sizeof(struct ICCA_STATUS)); // funny workaround @@ -387,6 +432,11 @@ static char __cdecl ac_io_icca_req_uid(int unit_id) { static int __cdecl ac_io_icca_req_uid_isfinished(int unit_id, DWORD *read_state) { unit_id = icca_get_unit_id(unit_id); ICCA_UNIT *unit = &ICCA_UNITS[unit_id]; + if (avs::game::is_model("JDZ")) { + // hack for RA - only ever seen it as this + *read_state = 8; + return 1; + } if (unit->card_in) { if (fabs(get_performance_seconds() - unit->card_in_time) < CARD_TIMEOUT) { unit->state = END; @@ -403,16 +453,61 @@ static int __cdecl ac_io_icca_send_keep_alive_packet(int a1, int a2, int a3) { return 0; } +static int __cdecl ac_io_icca_workflow_jdz(int workflow, int unit_id) { + + unit_id = icca_get_unit_id(unit_id); + ICCA_UNIT *unit = &ICCA_UNITS[unit_id]; + switch (workflow) { + // RA uses "STEP" as more of a polling thing it seems + case STEP: + switch (unit->state_jdz) { + case CLOSE_EJECT: + unit->state_jdz = CLOSE_E_CHK; + break; + case CLOSE_E_CHK: + unit->state_jdz = CLOSE_END; + break; + case CLOSE_END: + unit->state_jdz = SLEEP; + break; + default: + break; + } + break; + case SLEEP: + unit->state = SLEEP; + break; + case START: + unit->state_jdz = INIT; + break; + case INIT: + case READY: + unit->state_jdz = READY; + break; + case CLOSE_EJECT: + unit->state_jdz = CLOSE_EJECT; + return CLOSE_E_CHK; + case CLOSE_END: + unit->state_jdz = SLEEP; + break; + default: + // should probably log a warning here + break; + } + return unit->state_jdz; +} + static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) { + // RA uses a different workflow process for the slotted readers; treat it differently + if (avs::game::is_model("JDZ")) { + return ac_io_icca_workflow_jdz(workflow, unit_id); + } unit_id = icca_get_unit_id(unit_id); ICCA_UNIT *unit = &ICCA_UNITS[unit_id]; switch (workflow) { case STEP: - if (avs::game::is_model("JDZ")) - unit->state = SLEEP; - else - unit->state = STEP; + unit->state = STEP; break; case SLEEP: unit->state = SLEEP; @@ -441,7 +536,6 @@ static int __cdecl ac_io_icca_workflow(int workflow, int unit_id) { default: break; } - return unit->state; } @@ -467,6 +561,14 @@ acio::ICCAModule::ICCAModule(HMODULE module, acio::HookMode hookMode) : ACIOModu void acio::ICCAModule::attach() { ACIOModule::attach(); + // workaround for RA; ac_io_icca_cardunit_init is never called so treat like both are initialized + if (avs::game::is_model("JDZ")) { + ICCA_UNITS[0].initialized = true; + ICCA_UNITS[1].initialized = true; + + CARD_TIMEOUT = 10.0; // keep the card in the slot for longer + } + // hooks ACIO_MODULE_HOOK(ac_io_icca_cardunit_init); ACIO_MODULE_HOOK(ac_io_icca_cardunit_init_isfinished);