#ifndef LINK_CABLE_H #define LINK_CABLE_H // -------------------------------------------------------------------------- // A Link Cable connection for Multi-Play mode. // -------------------------------------------------------------------------- // Usage: // - 1) Include this header in your main.cpp file and add: // LinkCable* linkCable = new LinkCable(); // - 2) Add the required interrupt service routines: (*) // interrupt_init(); // interrupt_add(INTR_VBLANK, LINK_CABLE_ISR_VBLANK); // interrupt_add(INTR_SERIAL, LINK_CABLE_ISR_SERIAL); // interrupt_add(INTR_TIMER3, LINK_CABLE_ISR_TIMER); // - 3) Initialize the library with: // linkCable->activate(); // - 4) Sync: // linkUniversal->sync(); // // (put this line at the start of your game loop) // - 5) Send/read messages by using: // bool isConnected = linkCable->isConnected(); // u8 playerCount = linkCable->playerCount(); // u8 currentPlayerId = linkCable->currentPlayerId(); // linkCable->send(0x1234); // if (isConnected && linkCable->canRead(!currentPlayerId)) { // u16 message = linkCable->read(!currentPlayerId); // // ... // } // -------------------------------------------------------------------------- // (*1) libtonc's interrupt handler sometimes ignores interrupts due to a bug. // That causes packet loss. You REALLY want to use libugba's instead. // (see examples) // -------------------------------------------------------------------------- // (*2) The hardware is very sensitive to timing. Make sure that // `LINK_CABLE_ISR_SERIAL()` is handled on time. That means: // Be careful with DMA usage (which stops the CPU), and write short // interrupt handlers (or activate nested interrupts by setting // `REG_IME=1` at the start of your handlers). // -------------------------------------------------------------------------- // `send(...)` restrictions: // - 0xFFFF and 0x0 are reserved values, so don't send them! // (they mean 'disconnected' and 'no data' respectively) // -------------------------------------------------------------------------- #ifndef LINK_DEVELOPMENT #pragma GCC system_header #endif #include "_link_common.hpp" #include "LinkRawCable.hpp" #ifndef LINK_CABLE_QUEUE_SIZE /** * @brief Buffer size (how many incoming and outgoing messages the queues can * store at max **per player**). The default value is `15`, which seems fine for * most games. * \warning This affects how much memory is allocated. With the default value, * it's around `390` bytes. There's a double-buffered pending queue (to avoid * data races), 1 incoming queue and 1 outgoing queue. * \warning You can approximate the usage with `LINK_CABLE_QUEUE_SIZE * 26`. */ #define LINK_CABLE_QUEUE_SIZE 15 #endif LINK_VERSION_TAG LINK_CABLE_VERSION = "vLinkCable/v8.0.3"; #define LINK_CABLE_MAX_PLAYERS LINK_RAW_CABLE_MAX_PLAYERS #define LINK_CABLE_DEFAULT_TIMEOUT 3 #define LINK_CABLE_DEFAULT_INTERVAL 50 #define LINK_CABLE_DEFAULT_SEND_TIMER_ID 3 #define LINK_CABLE_DISCONNECTED LINK_RAW_CABLE_DISCONNECTED #define LINK_CABLE_NO_DATA 0x0 /** * @brief A Link Cable connection for Multi-Play mode. */ class LinkCable { private: using u32 = Link::u32; using u16 = Link::u16; using u8 = Link::u8; using vu8 = Link::vu8; using U16Queue = Link::Queue; static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024; static constexpr int MSG_TIMEOUT_OFFLINE = -1; public: using BaudRate = LinkRawCable::BaudRate; /** * @brief Constructs a new LinkCable object. * @param baudRate Sets a specific baud rate. * @param timeout Number of *frames* without a `SERIAL` IRQ to reset the * connection. * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers * *(50 = 3.052ms)*. It's the interval of Timer #`sendTimerId`. Lower values * will transfer faster but also consume more CPU. * @param sendTimerId `(0~3)` GBA Timer to use for sending. * \warning You can use `Link::perFrame(...)` to convert from *packets per * frame* to *interval values*. */ explicit LinkCable(BaudRate baudRate = BaudRate::BAUD_RATE_1, u32 timeout = LINK_CABLE_DEFAULT_TIMEOUT, u16 interval = LINK_CABLE_DEFAULT_INTERVAL, u8 sendTimerId = LINK_CABLE_DEFAULT_SEND_TIMER_ID) { config.baudRate = baudRate; config.timeout = timeout; config.interval = interval; config.sendTimerId = sendTimerId; } /** * @brief Returns whether the library is active or not. */ [[nodiscard]] bool isActive() { return isEnabled; } /** * @brief Activates the library. */ void activate() { LINK_READ_TAG(LINK_CABLE_VERSION); static_assert(LINK_CABLE_QUEUE_SIZE >= 1); LINK_BARRIER; isEnabled = false; LINK_BARRIER; reset(); clearIncomingMessages(); LINK_BARRIER; isEnabled = true; LINK_BARRIER; } /** * @brief Deactivates the library. */ void deactivate() { LINK_BARRIER; isEnabled = false; LINK_BARRIER; resetState(); stop(); clearIncomingMessages(); } /** * @brief Returns `true` if there are at least 2 connected players. */ [[nodiscard]] bool isConnected() { return state.playerCount > 1 && state.currentPlayerId < state.playerCount; } /** * @brief Returns the number of connected players (`1~4`). */ [[nodiscard]] u8 playerCount() { return state.playerCount; } /** * @brief Returns the current player ID (`0~3`). */ [[nodiscard]] u8 currentPlayerId() { return state.currentPlayerId; } /** * @brief Collects available messages from interrupts for later processing * with `read(...)`. Call this method whenever you need to fetch new data, and * always process all messages before calling it again. */ void sync() { if (!isEnabled) return; LINK_BARRIER; isReadingMessages = true; LINK_BARRIER; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) move(_state.readyToSyncMessages[i], state.syncedIncomingMessages[i]); LINK_BARRIER; isReadingMessages = false; LINK_BARRIER; if (!isConnected()) clearIncomingMessages(); } /** * @brief Waits for data from player #`playerId`. Returns `true` on success, * or `false` on disconnection. * @param playerId A player ID. */ bool waitFor(u8 playerId) { return waitFor(playerId, []() { return false; }); } /** * @brief Waits for data from player #`playerId`. Returns `true` on success, * or `false` on disconnection. * @param playerId ID of player to wait data from. * @param cancel A function that will be continuously invoked. If it returns * `true`, the wait be aborted. */ template bool waitFor(u8 playerId, F cancel) { if (!isEnabled) return false; sync(); while (isConnected() && !canRead(playerId) && !cancel()) { Link::_IntrWait( 1, Link::_IRQ_SERIAL | Link::_TIMER_IRQ_IDS[config.sendTimerId]); sync(); } return isConnected() && canRead(playerId); } /** * @brief Returns `true` if there are pending messages from player * #`playerId`. * @param playerId A player ID. * \warning Keep in mind that if this returns `false`, it will keep doing so * until you *fetch new data* with `sync()`. */ [[nodiscard]] bool canRead(u8 playerId) { return !state.syncedIncomingMessages[playerId].isEmpty(); } /** * @brief Dequeues and returns the next message from player #`playerId`. * @param playerId A player ID. * \warning If there's no data from that player, a `0` will be returned. */ u16 read(u8 playerId) { return state.syncedIncomingMessages[playerId].pop(); } /** * @brief Returns the next message from player #`playerId` without dequeuing * it. * @param playerId A player ID. * \warning If there's no data from that player, a `0` will be returned. */ [[nodiscard]] u16 peek(u8 playerId) { return state.syncedIncomingMessages[playerId].peek(); } /** * @brief Sends `data` to all connected players. * @param data The value to be sent. * \warning If `data` is invalid or the send queue is full, a `false` will be * returned. */ bool send(u16 data) { if (!isEnabled || data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA || !canSend()) return false; _state.outgoingMessages.syncPush(data); return true; } /** * @brief Returns whether a `send(...)` call would fail due to the queue being * full. */ bool canSend() { return !_state.outgoingMessages.isFull(); } /** * @brief Returns whether the internal queue lost messages at some point due * to being full. This can happen if your queue size is too low, if you * receive too much data without calling `sync(...)` enough times, or if you * don't `read(...)` enough messages before the next `sync()` call. After this * call, the overflow flag is cleared if `clear` is `true` (default behavior). */ bool didQueueOverflow(bool clear = true) { bool overflow = false; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { overflow = overflow || _state.newMessages[i].overflow || _state.readyToSyncMessages[i].overflow; if (clear) { _state.newMessages[i].overflow = false; state.syncedIncomingMessages[i].overflow = false; } } return overflow; } /** * @brief Resets other players' timeout count to `0`. * \warning Call this if you changed `config.timeout`. */ void resetTimeout() { if (!isEnabled) return; LINK_BARRIER; _state.isResetTimeoutPending = true; LINK_BARRIER; } /** * @brief Restarts the send timer without disconnecting. * \warning Call this if you changed `config.interval`. */ void resetTimer() { if (!isEnabled) return; stopTimer(); startTimer(); } /** * @brief This method is called by the VBLANK interrupt handler. * \warning This is internal API! */ void _onVBlank() { if (!isEnabled) return; if (_state.isResetTimeoutPending) { _state.IRQTimeout = 0; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) _state.msgTimeouts[i] = 0; _state.isResetTimeoutPending = false; } if (!_state.IRQFlag) _state.IRQTimeout++; _state.IRQFlag = false; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { if (isOnline(i) && !_state.msgFlags[i]) _state.msgTimeouts[i]++; _state.msgFlags[i] = false; } if (didTimeout()) { reset(); return; } copyState(); } /** * @brief This method is called by the SERIAL interrupt handler. * \warning This is internal API! */ void _onSerial() { if (!isEnabled) return; if (!LinkRawCable::allReady() || LinkRawCable::hasError()) { reset(); return; } auto response = LinkRawCable::getData(); LINK_BARRIER; state.currentPlayerId = response.playerId; LINK_BARRIER; _state.IRQFlag = true; _state.IRQTimeout = 0; u8 newPlayerCount = 0; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { u16 data = response.data[i]; if (data != LINK_CABLE_DISCONNECTED) { if (data != LINK_CABLE_NO_DATA && i != state.currentPlayerId) _state.newMessages[i].push(data); newPlayerCount++; setOnline(i); } else if (isOnline(i)) { if (_state.msgTimeouts[i] >= (int)config.timeout) { _state.newMessages[i].clear(); setOffline(i); } else { newPlayerCount++; } } } LINK_BARRIER; state.playerCount = newPlayerCount; LINK_BARRIER; LinkRawCable::setData(LINK_CABLE_NO_DATA); if (!LinkRawCable::isMasterNode()) sendPendingData(); copyState(); } /** * @brief This method is called by the TIMER interrupt handler. * \warning This is internal API! */ void _onTimer() { if (!isEnabled) return; if (LinkRawCable::isMasterNode() && LinkRawCable::allReady() && !LinkRawCable::isSending()) sendPendingData(); copyState(); } struct Config { BaudRate baudRate; u32 timeout; // can be changed in realtime, but call `resetTimeout()` u16 interval; // can be changed in realtime, but call `resetTimer()` u8 sendTimerId; }; /** * @brief LinkCable configuration. * \warning `deactivate()` first, change the config, and `activate()` again! */ Config config; #ifndef LINK_CABLE_DEBUG_MODE private: #endif struct ExternalState { U16Queue syncedIncomingMessages[LINK_CABLE_MAX_PLAYERS]; vu8 playerCount = 1; vu8 currentPlayerId = 0; }; struct InternalState { U16Queue outgoingMessages; U16Queue readyToSyncMessages[LINK_CABLE_MAX_PLAYERS]; U16Queue newMessages[LINK_CABLE_MAX_PLAYERS]; u32 IRQTimeout = 0; int msgTimeouts[LINK_CABLE_MAX_PLAYERS]; bool msgFlags[LINK_CABLE_MAX_PLAYERS]; bool IRQFlag = false; volatile bool isResetTimeoutPending = false; }; ExternalState state; InternalState _state; volatile bool isEnabled = false; volatile bool isReadingMessages = false; bool didTimeout() { return _state.IRQTimeout >= config.timeout; } void sendPendingData() { if (_state.outgoingMessages.isWriting()) return; LINK_BARRIER; transfer(_state.outgoingMessages.pop()); } void transfer(u16 data) { LinkRawCable::setData(data); if (LinkRawCable::isMasterNode()) LinkRawCable::startTransfer(); } void reset() { resetState(); stop(); start(); } void resetState() { LINK_BARRIER; state.playerCount = 1; state.currentPlayerId = 0; _state.outgoingMessages.syncClear(); for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { if (!isReadingMessages) _state.readyToSyncMessages[i].clear(); _state.newMessages[i].clear(); setOffline(i); _state.newMessages[i].overflow = false; state.syncedIncomingMessages[i].overflow = false; } _state.IRQFlag = false; _state.IRQTimeout = 0; _state.isResetTimeoutPending = false; LINK_BARRIER; } void stop() { stopTimer(); LinkRawCable::setGeneralPurposeMode(); } void start() { startTimer(); LinkRawCable::setMultiPlayMode(config.baudRate); LinkRawCable::setInterruptsOn(); } void stopTimer() { Link::_REG_TM[config.sendTimerId].cnt = Link::_REG_TM[config.sendTimerId].cnt & (~Link::_TM_ENABLE); } void startTimer() { Link::_REG_TM[config.sendTimerId].start = -config.interval; Link::_REG_TM[config.sendTimerId].cnt = Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY; } void clearIncomingMessages() { for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) state.syncedIncomingMessages[i].clear(); } void copyState() { if (isReadingMessages) return; for (u32 i = 0; i < LINK_CABLE_MAX_PLAYERS; i++) { if (isOnline(i)) move(_state.newMessages[i], _state.readyToSyncMessages[i]); else _state.readyToSyncMessages[i].clear(); } } void move(U16Queue& src, U16Queue& dst) { while (!src.isEmpty() && !dst.isFull()) dst.push(src.pop()); } bool isOnline(u8 playerId) { return _state.msgTimeouts[playerId] != MSG_TIMEOUT_OFFLINE; } void setOnline(u8 playerId) { _state.msgTimeouts[playerId] = 0; _state.msgFlags[playerId] = true; } void setOffline(u8 playerId) { _state.msgTimeouts[playerId] = MSG_TIMEOUT_OFFLINE; _state.msgFlags[playerId] = false; } }; extern LinkCable* linkCable; /** * @brief VBLANK interrupt handler. */ inline void LINK_CABLE_ISR_VBLANK() { linkCable->_onVBlank(); } /** * @brief SERIAL interrupt handler. */ inline void LINK_CABLE_ISR_SERIAL() { linkCable->_onSerial(); } /** * @brief TIMER interrupt handler. */ inline void LINK_CABLE_ISR_TIMER() { linkCable->_onTimer(); } /** * NOTES: * For end users: * - `sync()` fills an incoming queue (`syncedIncomingMessages`). * - `read(...)` pops one message from that queue. * - `send(...)` pushes one message to an outgoing queue (`outgoingMessages`). * Behind the curtains: * - On each SERIAL IRQ: * -> Each new message is pushed to `newMessages`. * - On each VBLANK, SERIAL, or TIMER IRQ: * -> **If the user is not syncing**: * -> All `newMessages` are moved to `readyToSyncMessages`. * - If (playerId == 0 && TIMER_IRQ) || (playerId > 0 && SERIAL_IRQ): * -> **If the user is not sending**: * -> Pops one message from `outgoingMessages` and transfers it. * - `sync()` moves all `readyToSyncMessages` to `syncedIncomingMessages`. */ #endif // LINK_CABLE_H