diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4ecbf04c4..5b45e7115 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -883,6 +883,8 @@ target_sources(${PROJECT_NAME} PRIVATE
core/hw/naomi/printer.cpp
core/hw/naomi/systemsp.h
core/hw/naomi/systemsp.cpp
+ core/hw/naomi/hopper.h
+ core/hw/naomi/hopper.cpp
core/hw/pvr/elan.cpp
core/hw/pvr/elan.h
core/hw/pvr/elan_struct.h
diff --git a/core/hw/naomi/hopper.cpp b/core/hw/naomi/hopper.cpp
new file mode 100644
index 000000000..45d0eb61b
--- /dev/null
+++ b/core/hw/naomi/hopper.cpp
@@ -0,0 +1,763 @@
+/*
+ Copyright 2023 flyinghead
+
+ This file is part of Flycast.
+
+ Flycast is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ Flycast is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Flycast. If not, see .
+ */
+#include "hopper.h"
+#include "network/ggpo.h"
+#include "input/gamepad.h"
+#include "hw/maple/maple_cfg.h"
+#include "hw/sh4/sh4_sched.h"
+#include "hw/sh4/modules/modules.h"
+#include "serialize.h"
+
+#include
+#include
+#include
+
+namespace hopper
+{
+
+class BaseHopper : public SerialPipe
+{
+public:
+ BaseHopper() {
+ schedId = sh4_sched_register(0, schedCallback, this);
+ sh4_sched_request(schedId, SCHED_CYCLES);
+ }
+ virtual ~BaseHopper() {
+ sh4_sched_unregister(schedId);
+ }
+
+ u8 read() override
+ {
+ if (toSend.empty())
+ return 0;
+ else
+ {
+ u8 v = toSend.front();
+ toSend.pop_front();
+ return v;
+ }
+ }
+
+ int available() override {
+ return toSend.size();
+ }
+
+ void write(u8 data) override
+ {
+ if (recvBuffer.empty() && data != 'H') {
+ WARN_LOG(NAOMI, "Ignored data %02x %c", data, data);
+ return;
+ }
+ recvBuffer.push_back(data);
+ if (recvBuffer.size() == 3)
+ expectedBytes = data;
+ else if (recvBuffer.size() == 4)
+ expectedBytes += data << 8;
+ else if (expectedBytes > 0 && recvBuffer.size() == expectedBytes)
+ {
+ handleMessage(recvBuffer[1]);
+ recvBuffer.clear();
+ expectedBytes = 0;
+ }
+ }
+
+ virtual void serialize(Serializer& ser)
+ {
+ ser << (u32)recvBuffer.size();
+ ser.serialize(recvBuffer.data(), recvBuffer.size());
+ ser << credit0;
+ ser << credit1;
+ ser << totalCredit;
+ ser << premium;
+ ser << gameNumber;
+ ser << freePlay;
+ ser << autoPayOut;
+ ser << autoExchange;
+ ser << twoWayMode;
+ ser << coinDisc;
+ ser << currency;
+ ser << medalExchRate;
+ ser << maxHopperFloat;
+ ser << maxPay;
+ ser << maxCredit;
+ ser << hopperSize;
+ ser << maxBet;
+ ser << minBet;
+ ser << addBet;
+ ser << expectedBytes;
+ ser << (u32)toSend.size();
+ for (u8 b : toSend)
+ ser << b;
+ ser << started;
+ sh4_sched_serialize(ser, schedId);
+ }
+
+ virtual void deserialize(Deserializer& deser)
+ {
+ u32 size;
+ deser >> size;
+ recvBuffer.resize(size);
+ deser.deserialize(recvBuffer.data(), size);
+ deser >> credit0;
+ deser >> credit1;
+ deser >> totalCredit;
+ deser >> premium;
+ deser >> gameNumber;
+ deser >> freePlay;
+ deser >> autoPayOut;
+ deser >> autoExchange;
+ deser >> twoWayMode;
+ deser >> coinDisc;
+ deser >> currency;
+ deser >> medalExchRate;
+ deser >> maxHopperFloat;
+ deser >> maxPay;
+ deser >> maxCredit;
+ deser >> hopperSize;
+ deser >> maxBet;
+ deser >> minBet;
+ deser >> addBet;
+ deser >> expectedBytes;
+ deser >> size;
+ toSend.clear();
+ for (u32 i = 0; i < size; i++)
+ {
+ u8 b;
+ deser >> b;
+ toSend.push_back(b);
+ }
+ deser >> started;
+ sh4_sched_deserialize(deser, schedId);
+ }
+
+protected:
+ virtual void handleMessage(u8 command) = 0;
+ virtual void sendCoinInMessage() = 0;
+
+ void sendMessage(u8 command, const u8 *payload, size_t len)
+ {
+ DEBUG_LOG(NAOMI, "hopper sending command %x size %x", command, (int)len + 5);
+ // 'H' payload ...
+ u8 chksum = 'H' + command;
+ toSend.push_back('H');
+ toSend.push_back(command);
+ len += 5;
+ toSend.push_back(len & 0xff);
+ chksum += len & 0xff;
+ toSend.push_back(len >> 8);
+ chksum += len >> 8;
+ len -= 5;
+ for (size_t i = 0; i < len; i++, payload++) {
+ toSend.push_back(*payload);
+ chksum += *payload;
+ }
+ toSend.push_back(chksum);
+ serial_updateStatusRegister();
+ }
+
+ std::vector recvBuffer;
+ u32 credit0 = 0;
+ u32 credit1 = 0;
+ u32 totalCredit = 0;
+ u32 premium = 0;
+ u32 gameNumber = 0;
+ bool freePlay = false;
+ bool autoPayOut = false;
+ bool autoExchange = false;
+ bool twoWayMode = true;
+ bool coinDisc = true;
+ u8 currency = 0; // 0:medal, 1:pound, 2:dollar, 3:euro, 4:token
+ u8 medalExchRate = 5;
+ u32 maxHopperFloat = 200;
+ u32 maxPay = 1999900;
+ u32 maxCredit = 1999900;
+ u32 hopperSize = 39900;
+ u32 maxBet = 10000;
+ u32 minBet = 100; // normally 1000
+ u32 addBet = 100;
+
+ int schedId;
+ bool started = false;
+ bool coinKey = false;
+
+ static constexpr u32 SCHED_CYCLES = SH4_MAIN_CLOCK / 60;
+
+private:
+ static int schedCallback(int tag, int cycles, int jitter, void *p)
+ {
+ BaseHopper *board = (BaseHopper *)p;
+ if (board->started)
+ {
+ // button D is coin
+ if ((mapleInputState[0].kcode & DC_BTN_D) == 0 && !board->coinKey)
+ board->sendCoinInMessage();
+ board->coinKey = (mapleInputState[0].kcode & DC_BTN_D) == 0;
+ }
+ return SCHED_CYCLES;
+ }
+
+ u32 expectedBytes = 0;
+ std::deque toSend;
+};
+
+//
+// SEGA 837-14438 hopper board
+//
+// Used by kick'4'cash
+// Can be used by club kart prize (ver.b) and shootout pool prize (ver.b) if P1 btn 0 is pressed at boot
+//
+class Sega837_14438Hopper : public BaseHopper
+{
+protected:
+ void handleMessage(u8 command) override
+ {
+ switch (command)
+ {
+ case 0x40:
+ {
+ // VERSION
+ INFO_LOG(NAOMI, "hopper received VERSION");
+ std::array payload{};
+ payload[0] = 0x00010001;
+ payload[1] = 3;
+ sendMessage(0x20, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x41:
+ {
+ // POWER ON
+ INFO_LOG(NAOMI, "hopper received POWER ON");
+ std::array payload{};
+ payload[0] = gameNumber; // game number
+ payload[1] = 0; // atp num
+ payload[2] = 0; // atp kind and error code
+ payload[3] = status; // status bitfield. rectangle in background if != 0
+ payload[4] = freePlay | (autoPayOut << 8) | (twoWayMode << 16) | (medalExchRate << 24);
+ payload[5] = autoExchange | (coinDisc << 8);
+ payload[6] = 0; // ? 8c027124
+ payload[7] = maxPay;
+ payload[8] = maxCredit;
+ payload[9] = hopperSize;
+ payload[10] = maxBet;
+ payload[11] = minBet;
+ payload[12] = addBet;
+ payload[13] = currency; // currency (lsb, 0:medal, 1:pound, 2:dollar, 3:euro, 4:token)
+ // ? 8c027141, 8c027142, 8c027143
+ payload[14] = credit0; // credit0
+ payload[15] = credit1; // credit1
+ payload[16] = totalCredit; // totalCredit
+ payload[17] = premium; // premium
+ // FUN_8c00e26e
+ payload[18] = 0; // ? 8c02cd60
+ payload[19] = 0; // ? FUN_8c014806() or FUN_8c00817c()
+ // &payload[20] // copy of mem (8c02715c, 0x58 bytes)
+ // FUN_8c009f8a
+ payload[0x2a] = 0; // ? FUN_8c015b5a()
+ payload[0x2b] = 0; // ? FUN_8c0163d8() + FUN_8c0148d4()
+ payload[0x2c] = 0; // hopperOutLap
+ payload[0x2d] = 0; // wins
+ payload[0x2e] = 0; // ? 8c02717c
+ payload[0x2f] = 0; // ? 8c027180
+ payload[0x30] = 0; // ? 8c027188
+ payload[0x31] = 0; // ? 8c02718c
+ payload[0x32] = 0; // paid? 8c027190
+
+ payload[0x33] = maxHopperFloat;
+ payload[0x34] = 0; // showLastWinAndHopperFloat (b6: show last win b7: show hopperfloat)
+ payload[0x35] = 0; // ? 8c02714c
+ payload[0x36] = 0; // ? 8c027150
+ payload[0x37] = 0x80; // dataportCondition (b6: protocol on, bit7: forever)
+ payload[0x38] = 0; // ? 8c027f48
+ payload[0x39] = 0; // ? 8c027f4c
+ payload[0x3a] = 0; // ? 8c027f50
+ // FUN_8c00ae24
+ payload[0x3b] = 0; // ? 20 bytes
+ // 1c2 0 ?
+ // 1c9 100 or 50 depending on currency
+ // 1ca 10000 or 100 depending...
+ // 1cb (short)same as 1c9
+ // (short)499 or 5 dep...
+ // 1cc (short)500 or 5
+ // 499
+ // 1cd (short)500
+ // (short)49 or 19 dep...
+ // 1ce 50 or 20 dep...
+ // 1cf 200, 70 or 100
+ // 1d0 0 or 100
+ // 1d1 0 or 100
+ // 1d2 0, 2 or 3
+ // 1d3 (short)2000, 70 or 200
+ // (short)200, 70 or 50
+ // 1d4 100, 0 or 10
+ // 1d6 (short)100 0 0 0 0 0 0 0
+ // 1d8 (short)12 shorts?
+ // 100 200 300 400 500 1000 2000 2500 5000 10000 0 ...
+ // Default values
+ payload[0x1c0] = 0x5010000; // medal exch rate (5), 2-way (1), autoPO (0), free play (0)
+ payload[0x1c1] = 0x1640100; // ? (1), ? (100), coin disc (1), auto exch (0)
+ payload[0x1c3] = 1999900; // max pay
+ payload[0x1c4] = 1999900; // max credit
+ payload[0x1c5] = 39900; // hopper size
+ payload[0x1c6] = 10000; // max bet
+ payload[0x1c7] = 1000; // min bet
+ payload[0x1c8] = 100; // add bet
+ payload[0x1c9] = 100;
+ payload[0x1ca] = 10000;
+ payload[0x1cb] = 100 | (499 << 16);
+ payload[0x1cc] = 500 | (499 << 16);
+ payload[0x1cd] = 500 | (49 << 16);
+ payload[0x1ce] = 50;
+ payload[0x1cf] = 200;
+ payload[0x1d3] = 2000 | (200 << 16);
+ payload[0x1d4] = 100;
+ payload[0x1d6] = 100;
+ payload[0x1da] = 100 | (200 << 16);
+ payload[0x1db] = 300 | (400 << 16);
+ payload[0x1dc] = 500 | (1000 << 16);
+ payload[0x1dd] = 2000 | (2500 << 16);
+ payload[0x1de] = 5000 | (10000 << 16);
+ payload[0x1e9] = 1; // hopper ready flag
+ sendMessage(0x21, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ started = true;
+ break;
+ }
+
+ case 0x42:
+ {
+ // GET_STATUS
+ INFO_LOG(NAOMI, "hopper received GET STATUS");
+ // TODO offset 4: 0 or 2
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = 0; // atp kind and error code
+ payload[2] = status;
+ sendMessage(0x22, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x43:
+ {
+ // GAME START
+ INFO_LOG(NAOMI, "hopper received GAME START");
+ // 4: credit used?
+ credit0 -= *(u32 *)&recvBuffer[4];
+ std::array payload{};
+ payload[0] = ++gameNumber; // game#, ?
+ payload[1] = 0; // atp num
+ payload[2] = 0; // atp kind and error code
+ payload[3] = status;
+ payload[4] = credit0;
+ payload[5] = credit1;
+ payload[6] = totalCredit;
+ payload[7] = premium;
+ // &payload[8] ? 0x58 bytes
+ sendMessage(0x23, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x44:
+ {
+ // GAME END
+ INFO_LOG(NAOMI, "hopper received GAME END");
+ std::array payload{};
+ payload[0] = gameNumber; // gameNum, upper 16 bits: ? 0, -1 or -2
+ payload[1] = credit0;
+ payload[2] = credit1;
+ payload[3] = totalCredit;
+ payload[4] = premium;
+ // ? (58 bytes)
+ payload[27] = 2; // wins
+ payload[28] = 0; // last paid
+ sendMessage(0x24, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x45:
+ {
+ // TEST
+ INFO_LOG(NAOMI, "hopper received TEST");
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = 0; // atp kind and error code
+ payload[2] = status;
+ // TODO
+ // 3: memcpy 8c027028, 0x60
+ // 0x18: memcpy &errorCode, 0x80 (follows previous in ram)
+ // 0x38: bit0: dataport temp? bit1: ?
+ payload[0x38] = 0;
+ sendMessage(0x25, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x4a:
+ {
+ // SWITCH
+ INFO_LOG(NAOMI, "hopper received SWITCH");
+ // TODO ?
+ break;
+ }
+
+ case 0x4b:
+ {
+ // CONFIG_HOP
+ INFO_LOG(NAOMI, "hopper received CONFIG HOP");
+ if (recvBuffer[2] == 0x2c && recvBuffer[3] == 0)
+ {
+ freePlay = recvBuffer[4] & 1;
+ autoPayOut = recvBuffer[5] & 1;
+ twoWayMode = recvBuffer[6] & 1;
+ medalExchRate = recvBuffer[7];
+ autoExchange = recvBuffer[8] & 1;
+ coinDisc = recvBuffer[9] & 1;
+ // 0xc ??
+ maxPay = *(u32 *)&recvBuffer[0x10];
+ maxCredit = *(u32 *)&recvBuffer[0x14];
+ hopperSize = *(u32 *)&recvBuffer[0x18];
+ maxBet = *(u32 *)&recvBuffer[0x1c];
+ minBet = *(u32 *)&recvBuffer[0x20];
+ addBet = *(u32 *)&recvBuffer[0x24];
+ currency = recvBuffer[0x28]; // ??
+ }
+ std::array payload{};
+ payload[0] = freePlay | (autoPayOut << 8) | (twoWayMode << 16) | (medalExchRate << 24);
+ payload[1] = autoExchange | (coinDisc << 8);
+ payload[2] = 0; // ? 8c027124
+ payload[3] = maxPay;
+ payload[4] = maxCredit;
+ payload[5] = hopperSize;
+ payload[6] = maxBet;
+ payload[7] = minBet;
+ payload[8] = addBet;
+ payload[9] = currency; // currency (lsb, 0:medal, 1:pound, 2:dollar, 3:euro, 4:token)
+ // ? 8c027141, 8c027142, 8c027143
+ sendMessage(0x2b, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ default:
+ WARN_LOG(NAOMI, "Unexpected hopper message: %x", command);
+ break;
+ }
+ }
+
+private:
+ /*
+ void sendStatusMessage()
+ {
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = 0; // atp kind and error code
+ payload[2] = status; // status bitfield. rectangle in background if != 0
+ sendMessage(0, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ }
+ void sendErrorMessage()
+ {
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = 0; // atp kind and error code
+ payload[2] = status; // status bitfield. rectangle in background if != 0
+ sendMessage(7, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ }
+ */
+
+ void sendCoinInMessage() override
+ {
+ credit0 += 100;
+ totalCredit += 100;
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = 0; // atp kind and error code
+ payload[2] = status;
+ payload[3] = credit0;
+ payload[4] = credit1;
+ payload[5] = totalCredit;
+ payload[5] = premium;
+ sendMessage(1, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ }
+
+ // ?:1
+ // ?:1
+ // coinInEnabled?:1
+ // yenInEnabled?:1
+ // frontDoorOpen:1
+ // backDoorOpen:1
+ // cashDoorOpen:1
+ // needsRefill?:1
+ // ?:1
+ const u32 status = 0x00000000;
+};
+
+// Naomi SWP Hopper Board
+class NaomiHopper : public BaseHopper
+{
+protected:
+ void handleMessage(u8 command) override
+ {
+ switch (command)
+ {
+ case 0x20:
+ {
+ // POWER ON
+ static const char hopperErrors[][32] = {
+ "UNKNOWN ERROR",
+ "ROM IS BAD",
+ "LOW BATTERY",
+ "ROM HAS CHANGED",
+ "RAM DATA IS BAD",
+ "I/O ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "COIN IN JAM(HOPPER)",
+ "COIN IN JAM(GAME)",
+ "HOPPER OVER PAID",
+ "HOPPER RUNAWAY",
+ "HOPPER EMPTY/JAM",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "COM. TIME OUT",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "I/O BOARD IS BAD",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "UNKNOWN ERROR",
+ "ATP NONE\0\0\0\0\0\0\0\0MAX. PAY",
+ "HOPPER SIZE\0\0\0\0\0MAX. CREDIT",
+ "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0DOOR IS OPEN"
+ };
+
+ INFO_LOG(NAOMI, "hopper received POWER ON");
+ std::array payload{};
+ payload[0] = gameNumber;
+ payload[1] = 0; // atp num
+ payload[2] = status; // status bitfield. rectangle in background if != 0
+ payload[3] = freePlay | (autoPayOut << 8) | (twoWayMode << 16) | (medalExchRate << 24);
+ payload[4] = autoExchange | (coinDisc << 8);
+ payload[5] = maxPay;
+ payload[6] = maxCredit;
+ payload[7] = hopperSize;
+ payload[8] = maxBet;
+ payload[9] = minBet;
+ payload[10] = addBet;
+ payload[11] = 0; // ? BYTE_8c0c9fa8
+ payload[12] = credit0;
+ payload[13] = credit1;
+ payload[14] = totalCredit;
+ payload[15] = premium;
+ payload[16] = 1; // min count
+ payload[17] = 666; // paid amount
+
+ /* doesn't seem to matter */
+ payload[18] = ~0;
+ payload[19] = ~0;
+ payload[20] = ~0;
+ payload[21] = ~0;
+ payload[22] = ~0;
+ payload[23] = ~0;
+ payload[24] = ~0;
+
+ payload[25] = ~0; // ?
+ payload[26] = ~0; // ?
+ payload[27] = ~0; // ?
+ // don't care?
+ payload[28] = ~0;
+ payload[29] = ~0; // ?
+ payload[30] = ~0; // ?
+ payload[31] = ~0; // ?
+ // don't care?
+ payload[32] = ~0;
+ size_t idx = 0x84;
+ for (size_t i = 0; i < std::size(hopperErrors); i++) {
+ memcpy((u8 *)payload.data() + idx, hopperErrors[i], sizeof(hopperErrors[i]));
+ idx += 32;
+ }
+ // power down in game if lsb != 0
+ payload[0x139] = 1;
+ sendMessage(0x10, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+
+ started = true;
+ break;
+ }
+
+ case 0x21: // GET_STATUS
+ {
+ INFO_LOG(NAOMI, "hopper received GET STATUS");
+ std::array payload{};
+ payload[0] = 0; // atp num?
+ payload[1] = status;
+ sendMessage(0x11, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x22: // GAME START
+ {
+ INFO_LOG(NAOMI, "hopper received GAME START");
+ // 4: credit used?
+ credit0 -= *(u32 *)&recvBuffer[4];
+ std::array payload{};
+ payload[0] = ++gameNumber;
+ payload[1] = 0; // atp num
+ payload[2] = status;
+ payload[3] = credit0;
+ payload[4] = credit1;
+ payload[5] = totalCredit;
+ payload[6] = premium;
+ sendMessage(0x12, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ case 0x23: // GAME END
+ {
+ INFO_LOG(NAOMI, "hopper received GAME END");
+ // 8: amount to pay?
+ //credit0 += *(u32 *)&recvBuffer[8];
+ std::array payload{};
+ payload[0] = 1; // gameNum, upper 16 bits: ? 0, -1 or -2
+ payload[1] = credit0;
+ payload[2] = credit1;
+ payload[3] = totalCredit;
+ payload[4] = premium;
+ sendMessage(0x13, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ // TODO send PAY WIN message?
+ break;
+ }
+
+ case 0x29: // SWITCH
+ {
+ INFO_LOG(NAOMI, "hopper received SWITCH");
+ break;
+ }
+
+ case 0x2a: // CONFIG HOP
+ {
+ INFO_LOG(NAOMI, "hopper received CONFIG HOP");
+ if (recvBuffer[2] == 0x28 && recvBuffer[3] == 0)
+ {
+ freePlay = recvBuffer[4] & 1;
+ autoPayOut = recvBuffer[5] & 1;
+ twoWayMode = recvBuffer[6] & 1;
+ medalExchRate = recvBuffer[7];
+ autoExchange = recvBuffer[8] & 1;
+ coinDisc = recvBuffer[9] & 1;
+ maxPay = *(u32 *)&recvBuffer[0xc];
+ maxCredit = *(u32 *)&recvBuffer[0x10];
+ hopperSize = *(u32 *)&recvBuffer[0x14];
+ maxBet = *(u32 *)&recvBuffer[0x18];
+ minBet = *(u32 *)&recvBuffer[0x1c];
+ addBet = *(u32 *)&recvBuffer[0x20];
+ //currency = recvBuffer[0x24]; // ??
+ }
+ std::array payload{};
+ payload[0] = freePlay | (autoPayOut << 8) | (twoWayMode << 16) | (medalExchRate << 24);
+ payload[1] = autoExchange | (coinDisc << 8);
+ payload[2] = maxPay;
+ payload[3] = maxCredit;
+ payload[4] = hopperSize;
+ payload[5] = maxBet;
+ payload[6] = minBet;
+ payload[7] = addBet;
+ payload[8] = currency; // currency (lsb, 0:medal, 1:pound, 2:dollar, 3:euro, 4:token)
+ // ?, ?, ?
+ sendMessage(0x1a, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ break;
+ }
+
+ default:
+ WARN_LOG(NAOMI, "Unexpected hopper message: %x", command);
+ break;
+ }
+ }
+
+private:
+ /*
+ void sendStatusMessage()
+ {
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = status; // status bitfield
+ sendMessage(0, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ }
+ */
+
+ void sendCoinInMessage() override
+ {
+ credit0 += 100;
+ totalCredit += 100;
+ std::array payload{};
+ payload[0] = 0; // atp num
+ payload[1] = status;
+ payload[2] = credit0;
+ payload[3] = credit1;
+ payload[4] = totalCredit;
+ payload[5] = premium;
+ sendMessage(1, (const u8 *)payload.data(), payload.size() * sizeof(u32) - 1);
+ }
+
+ // error:16
+ // atpType:4
+ // doorOpen:1
+ // unk1:1
+ // unk2:1
+ // unk3:1
+ // unk4:1
+ const u32 status = 0x00000000;
+};
+
+static BaseHopper *pipe;
+
+void init()
+{
+ term();
+ if (settings.content.gameId == "KICK '4' CASH")
+ pipe = new Sega837_14438Hopper();
+ else
+ pipe = new NaomiHopper();
+ serial_setPipe(pipe);
+}
+
+void term()
+{
+ delete pipe;
+ pipe = nullptr;
+}
+
+void serialize(Serializer& ser)
+{
+ if (pipe != nullptr)
+ pipe->serialize(ser);
+}
+
+void deserialize(Deserializer& deser)
+{
+ if (pipe != nullptr)
+ pipe->deserialize(deser);
+}
+
+} // namespace hopper
diff --git a/core/hw/naomi/hopper.h b/core/hw/naomi/hopper.h
new file mode 100644
index 000000000..6c905b86c
--- /dev/null
+++ b/core/hw/naomi/hopper.h
@@ -0,0 +1,31 @@
+/*
+ Copyright 2023 flyinghead
+
+ This file is part of Flycast.
+
+ Flycast is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 2 of the License, or
+ (at your option) any later version.
+
+ Flycast is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with Flycast. If not, see .
+ */
+#pragma once
+#include "types.h"
+
+namespace hopper
+{
+
+void init();
+void term();
+
+void serialize(Serializer& ser);
+void deserialize(Deserializer& deser);
+
+}
diff --git a/core/hw/naomi/naomi_cart.cpp b/core/hw/naomi/naomi_cart.cpp
index a84c6edee..7d654eb85 100644
--- a/core/hw/naomi/naomi_cart.cpp
+++ b/core/hw/naomi/naomi_cart.cpp
@@ -45,6 +45,7 @@
#include "network/alienfnt_modem.h"
#include "netdimm.h"
#include "systemsp.h"
+#include "hopper.h"
Cartridge *CurrentCartridge;
bool bios_loaded = false;
@@ -666,6 +667,12 @@ void naomi_cart_LoadRom(const std::string& path, const std::string& fileName, Lo
{
printer::init();
}
+ if (!strcmp(CurrentCartridge->game->name, "clubkprz") || !strncmp(CurrentCartridge->game->name, "clubkpz", 7)
+ || !strncmp(CurrentCartridge->game->name, "shootpl", 7)
+ || gameId == "KICK '4' CASH")
+ {
+ hopper::init();
+ }
#ifdef NAOMI_MULTIBOARD
// Not a multiboard game but needs the same desktop environment
@@ -726,6 +733,7 @@ void naomi_cart_Close()
card_reader::initdTerm();
card_reader::barcodeTerm();
serialModemTerm();
+ hopper::term();
delete CurrentCartridge;
CurrentCartridge = nullptr;
NaomiGameInputs = nullptr;
@@ -738,6 +746,7 @@ void naomi_cart_serialize(Serializer& ser)
CurrentCartridge->Serialize(ser);
touchscreen::serialize(ser);
printer::serialize(ser);
+ hopper::serialize(ser);
}
void naomi_cart_deserialize(Deserializer& deser)
@@ -746,6 +755,7 @@ void naomi_cart_deserialize(Deserializer& deser)
CurrentCartridge->Deserialize(deser);
touchscreen::deserialize(deser);
printer::deserialize(deser);
+ hopper::deserialize(deser);
}
int naomi_cart_GetPlatform(const char *path)