mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-03-21 17:44:21 -05:00
607 lines
16 KiB
C++
607 lines
16 KiB
C++
#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<u16, LINK_CABLE_QUEUE_SIZE>;
|
|
|
|
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 <typename F>
|
|
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
|