mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-04-01 06:54:47 -05:00
472 lines
12 KiB
C++
472 lines
12 KiB
C++
#ifndef LINK_UNIVERSAL_H
|
|
#define LINK_UNIVERSAL_H
|
|
|
|
// --------------------------------------------------------------------------
|
|
// A multiplayer connection for the Link Cable and the Wireless Adapter.
|
|
// --------------------------------------------------------------------------
|
|
// Usage:
|
|
// - 1) Include this header in your main.cpp file and add:
|
|
// LinkUniversal* linkUniversal = new LinkUniversal();
|
|
// - 2) Add the required interrupt service routines: (*)
|
|
// irq_init(NULL);
|
|
// irq_add(II_VBLANK, LINK_UNIVERSAL_ISR_VBLANK);
|
|
// irq_add(II_SERIAL, LINK_UNIVERSAL_ISR_SERIAL);
|
|
// irq_add(II_TIMER3, LINK_UNIVERSAL_ISR_TIMER);
|
|
// irq_add(II_TIMER2, LINK_UNIVERSAL_ISR_ACK_TIMER); // (optional)
|
|
// // for `LinkWireless::asyncACKTimerId` --------------^
|
|
// - 3) Initialize the library with:
|
|
// linkUniversal->activate();
|
|
// - 4) Sync:
|
|
// linkUniversal->sync();
|
|
// // (put this line at the start of your game loop)
|
|
// - 5) Send/read messages by using:
|
|
// bool isConnected = linkUniversal->isConnected();
|
|
// u8 playerCount = linkUniversal->playerCount();
|
|
// u8 currentPlayerId = linkUniversal->currentPlayerId();
|
|
// linkUniversal->send(0x1234);
|
|
// if (isConnected && linkUniversal->canRead(!currentPlayerId)) {
|
|
// u16 message = linkUniversal->read(!currentPlayerId);
|
|
// // ...
|
|
// }
|
|
// --------------------------------------------------------------------------
|
|
// (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug.
|
|
// That can cause packet loss. You might want to use libugba's instead.
|
|
// (see examples)
|
|
// --------------------------------------------------------------------------
|
|
// `send(...)` restrictions:
|
|
// - 0xFFFF and 0x0 are reserved values, so don't use them!
|
|
// (they mean 'disconnected' and 'no data' respectively)
|
|
// --------------------------------------------------------------------------
|
|
|
|
#include <tonc_core.h>
|
|
#include "LinkCable.h"
|
|
#include "LinkWireless.h"
|
|
|
|
#define LINK_UNIVERSAL_MAX_PLAYERS LINK_CABLE_MAX_PLAYERS
|
|
#define LINK_UNIVERSAL_DISCONNECTED LINK_CABLE_DISCONNECTED
|
|
#define LINK_UNIVERSAL_NO_DATA LINK_CABLE_NO_DATA
|
|
#define LINK_UNIVERSAL_MAX_ROOM_NUMBER 32000
|
|
#define LINK_UNIVERSAL_INIT_WAIT_FRAMES 10
|
|
#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES 25
|
|
#define LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM 10
|
|
#define LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES 10
|
|
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES 60
|
|
#define LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM 30
|
|
|
|
static volatile char LINK_UNIVERSAL_VERSION[] = "LinkUniversal/v5.0.2";
|
|
|
|
void LINK_UNIVERSAL_ISR_VBLANK();
|
|
void LINK_UNIVERSAL_ISR_SERIAL();
|
|
void LINK_UNIVERSAL_ISR_TIMER();
|
|
|
|
class LinkUniversal {
|
|
public:
|
|
enum State { INITIALIZING, WAITING, CONNECTED };
|
|
enum Mode { LINK_CABLE, LINK_WIRELESS };
|
|
enum Protocol { AUTODETECT, CABLE, WIRELESS_AUTO, WIRELESS_CLIENT };
|
|
|
|
struct CableOptions {
|
|
LinkCable::BaudRate baudRate;
|
|
u32 timeout;
|
|
u32 remoteTimeout;
|
|
u16 interval;
|
|
u8 sendTimerId;
|
|
};
|
|
|
|
struct WirelessOptions {
|
|
bool retransmission;
|
|
u32 maxPlayers;
|
|
u32 timeout;
|
|
u32 remoteTimeout;
|
|
u16 interval;
|
|
u8 sendTimerId;
|
|
s8 asyncACKTimerId;
|
|
};
|
|
|
|
explicit LinkUniversal(
|
|
Protocol protocol = AUTODETECT,
|
|
std::string gameName = "",
|
|
CableOptions cableOptions =
|
|
CableOptions{
|
|
LinkCable::BaudRate::BAUD_RATE_1, LINK_CABLE_DEFAULT_TIMEOUT,
|
|
LINK_CABLE_DEFAULT_REMOTE_TIMEOUT, LINK_CABLE_DEFAULT_INTERVAL,
|
|
LINK_CABLE_DEFAULT_SEND_TIMER_ID},
|
|
WirelessOptions wirelessOptions = WirelessOptions{
|
|
true, LINK_WIRELESS_MAX_PLAYERS, LINK_WIRELESS_DEFAULT_TIMEOUT,
|
|
LINK_WIRELESS_DEFAULT_REMOTE_TIMEOUT, LINK_WIRELESS_DEFAULT_INTERVAL,
|
|
LINK_WIRELESS_DEFAULT_SEND_TIMER_ID,
|
|
LINK_WIRELESS_DEFAULT_ASYNC_ACK_TIMER_ID}) {
|
|
this->linkCable = new LinkCable(
|
|
cableOptions.baudRate, cableOptions.timeout, cableOptions.remoteTimeout,
|
|
cableOptions.interval, cableOptions.sendTimerId);
|
|
this->linkWireless = new LinkWireless(
|
|
wirelessOptions.retransmission, true, wirelessOptions.maxPlayers,
|
|
wirelessOptions.timeout, wirelessOptions.remoteTimeout,
|
|
wirelessOptions.interval, wirelessOptions.sendTimerId,
|
|
wirelessOptions.asyncACKTimerId);
|
|
|
|
this->config.protocol = protocol;
|
|
this->config.gameName = gameName;
|
|
}
|
|
|
|
bool isActive() { return isEnabled; }
|
|
|
|
void activate() {
|
|
reset();
|
|
isEnabled = true;
|
|
}
|
|
|
|
void deactivate() {
|
|
isEnabled = false;
|
|
|
|
linkCable->deactivate();
|
|
linkWireless->deactivate();
|
|
}
|
|
|
|
void setProtocol(Protocol protocol) { this->config.protocol = protocol; }
|
|
Protocol getProtocol() { return this->config.protocol; }
|
|
|
|
bool isConnected() { return state == CONNECTED; }
|
|
|
|
u8 playerCount() {
|
|
return mode == LINK_CABLE ? linkCable->playerCount()
|
|
: linkWireless->playerCount();
|
|
}
|
|
|
|
u8 currentPlayerId() {
|
|
return mode == LINK_CABLE ? linkCable->currentPlayerId()
|
|
: linkWireless->currentPlayerId();
|
|
}
|
|
|
|
void sync() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
u16 keys = ~REG_KEYS & KEY_ANY;
|
|
__qran_seed += keys;
|
|
__qran_seed += REG_RCNT;
|
|
__qran_seed += REG_SIOCNT;
|
|
|
|
switch (state) {
|
|
case INITIALIZING: {
|
|
waitCount++;
|
|
if (waitCount > LINK_UNIVERSAL_INIT_WAIT_FRAMES)
|
|
start();
|
|
break;
|
|
};
|
|
case WAITING: {
|
|
if (mode == LINK_CABLE) {
|
|
// Cable, waiting...
|
|
if (isConnectedCable()) {
|
|
state = CONNECTED;
|
|
goto connected;
|
|
}
|
|
} else {
|
|
// Wireless, waiting...
|
|
if (isConnectedWireless()) {
|
|
state = CONNECTED;
|
|
goto connected;
|
|
} else {
|
|
if (!autoDiscoverWirelessConnections())
|
|
waitCount = switchWait;
|
|
if (isConnectedWireless())
|
|
goto connected;
|
|
}
|
|
}
|
|
|
|
waitCount++;
|
|
if (waitCount > switchWait)
|
|
toggleMode();
|
|
|
|
break;
|
|
}
|
|
case CONNECTED: {
|
|
connected:
|
|
if (mode == LINK_CABLE) {
|
|
// Cable, connected...
|
|
if (!isConnectedCable()) {
|
|
toggleMode();
|
|
break;
|
|
}
|
|
|
|
receiveCableMessages();
|
|
} else {
|
|
// Wireless, connected...
|
|
if (!isConnectedWireless()) {
|
|
toggleMode();
|
|
break;
|
|
}
|
|
|
|
receiveWirelessMessages();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (mode == LINK_CABLE)
|
|
linkCable->consume();
|
|
}
|
|
|
|
bool canRead(u8 playerId) { return !incomingMessages[playerId].isEmpty(); }
|
|
|
|
u16 read(u8 playerId) { return incomingMessages[playerId].pop(); }
|
|
|
|
void send(u16 data) {
|
|
if (data == LINK_CABLE_DISCONNECTED || data == LINK_CABLE_NO_DATA)
|
|
return;
|
|
|
|
if (mode == LINK_CABLE)
|
|
linkCable->send(data);
|
|
else
|
|
linkWireless->send(data);
|
|
}
|
|
|
|
State getState() { return state; }
|
|
Mode getMode() { return mode; }
|
|
LinkWireless::State getWirelessState() { return linkWireless->getState(); }
|
|
|
|
~LinkUniversal() {
|
|
delete linkCable;
|
|
delete linkWireless;
|
|
}
|
|
|
|
u32 _getWaitCount() { return waitCount; }
|
|
u32 _getSubWaitCount() { return subWaitCount; }
|
|
|
|
void _onVBlank() {
|
|
if (mode == LINK_CABLE)
|
|
linkCable->_onVBlank();
|
|
else
|
|
linkWireless->_onVBlank();
|
|
}
|
|
|
|
void _onSerial() {
|
|
if (mode == LINK_CABLE)
|
|
linkCable->_onSerial();
|
|
else
|
|
linkWireless->_onSerial();
|
|
}
|
|
|
|
void _onTimer() {
|
|
if (mode == LINK_CABLE)
|
|
linkCable->_onTimer();
|
|
else
|
|
linkWireless->_onTimer();
|
|
}
|
|
|
|
void _onACKTimer() {
|
|
if (mode == LINK_WIRELESS)
|
|
linkWireless->_onACKTimer();
|
|
}
|
|
|
|
private:
|
|
struct Config {
|
|
Protocol protocol;
|
|
std::string gameName;
|
|
};
|
|
|
|
LinkCable::U16Queue incomingMessages[LINK_UNIVERSAL_MAX_PLAYERS];
|
|
LinkCable* linkCable;
|
|
LinkWireless* linkWireless;
|
|
Config config;
|
|
State state = INITIALIZING;
|
|
Mode mode = LINK_CABLE;
|
|
u32 waitCount = 0;
|
|
u32 switchWait = 0;
|
|
u32 subWaitCount = 0;
|
|
u32 serveWait = 0;
|
|
bool isEnabled = false;
|
|
|
|
void receiveCableMessages() {
|
|
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++) {
|
|
while (linkCable->canRead(i))
|
|
incomingMessages[i].push(linkCable->read(i));
|
|
}
|
|
}
|
|
|
|
void receiveWirelessMessages() {
|
|
LinkWireless::Message messages[LINK_WIRELESS_MAX_TRANSFER_LENGTH];
|
|
linkWireless->receive(messages);
|
|
|
|
for (u32 i = 0; i < LINK_WIRELESS_MAX_TRANSFER_LENGTH; i++) {
|
|
auto message = messages[i];
|
|
if (message.packetId == LINK_WIRELESS_END)
|
|
break;
|
|
|
|
incomingMessages[message.playerId].push(message.data);
|
|
}
|
|
}
|
|
|
|
bool autoDiscoverWirelessConnections() {
|
|
switch (linkWireless->getState()) {
|
|
case LinkWireless::State::NEEDS_RESET:
|
|
case LinkWireless::State::AUTHENTICATED: {
|
|
subWaitCount = 0;
|
|
linkWireless->getServersAsyncStart();
|
|
break;
|
|
}
|
|
case LinkWireless::State::SEARCHING: {
|
|
waitCount = 0;
|
|
subWaitCount++;
|
|
|
|
if (subWaitCount >= LINK_UNIVERSAL_BROADCAST_SEARCH_WAIT_FRAMES) {
|
|
if (!tryConnectOrServeWirelessSession())
|
|
return false;
|
|
}
|
|
break;
|
|
}
|
|
case LinkWireless::State::CONNECTING: {
|
|
if (!linkWireless->keepConnecting())
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case LinkWireless::State::SERVING: {
|
|
waitCount = 0;
|
|
subWaitCount++;
|
|
|
|
if (subWaitCount > serveWait)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
case LinkWireless::State::CONNECTED: {
|
|
// (should not happen)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool tryConnectOrServeWirelessSession() {
|
|
LinkWireless::Server servers[LINK_WIRELESS_MAX_SERVERS];
|
|
if (!linkWireless->getServersAsyncEnd(servers))
|
|
return false;
|
|
|
|
u32 maxRandomNumber = 0;
|
|
u32 serverIndex = 0;
|
|
for (u32 i = 0; i < LINK_WIRELESS_MAX_SERVERS; i++) {
|
|
auto server = servers[i];
|
|
if (server.id == LINK_WIRELESS_END)
|
|
break;
|
|
|
|
u32 randomNumber = std::stoi(server.userName);
|
|
|
|
if (server.gameName == config.gameName &&
|
|
randomNumber > maxRandomNumber) {
|
|
maxRandomNumber = randomNumber;
|
|
serverIndex = i;
|
|
}
|
|
}
|
|
|
|
if (maxRandomNumber > 0) {
|
|
if (!linkWireless->connect(servers[serverIndex].id))
|
|
return false;
|
|
} else {
|
|
if (config.protocol == WIRELESS_CLIENT)
|
|
return false;
|
|
|
|
subWaitCount = 0;
|
|
serveWait = LINK_UNIVERSAL_SERVE_WAIT_FRAMES +
|
|
qran_range(1, LINK_UNIVERSAL_SERVE_WAIT_FRAMES_RANDOM);
|
|
u32 randomNumber = qran_range(1, LINK_UNIVERSAL_MAX_ROOM_NUMBER);
|
|
if (!linkWireless->serve(config.gameName, std::to_string(randomNumber)))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool isConnectedCable() { return linkCable->isConnected(); }
|
|
bool isConnectedWireless() { return linkWireless->isConnected(); }
|
|
|
|
void reset() {
|
|
switch (config.protocol) {
|
|
case AUTODETECT:
|
|
case CABLE: {
|
|
setMode(LINK_CABLE);
|
|
break;
|
|
}
|
|
case WIRELESS_AUTO:
|
|
case WIRELESS_CLIENT: {
|
|
setMode(LINK_WIRELESS);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
if (mode == LINK_CABLE)
|
|
linkCable->deactivate();
|
|
else
|
|
linkWireless->deactivate();
|
|
}
|
|
|
|
void toggleMode() {
|
|
switch (config.protocol) {
|
|
case AUTODETECT: {
|
|
setMode(mode == LINK_CABLE ? LINK_WIRELESS : LINK_CABLE);
|
|
break;
|
|
}
|
|
case CABLE: {
|
|
setMode(LINK_CABLE);
|
|
break;
|
|
}
|
|
case WIRELESS_AUTO:
|
|
case WIRELESS_CLIENT: {
|
|
setMode(LINK_WIRELESS);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
void setMode(Mode mode) {
|
|
stop();
|
|
this->state = INITIALIZING;
|
|
this->mode = mode;
|
|
resetState();
|
|
}
|
|
|
|
void start() {
|
|
if (mode == LINK_CABLE)
|
|
linkCable->activate();
|
|
else
|
|
linkWireless->activate();
|
|
|
|
state = WAITING;
|
|
resetState();
|
|
}
|
|
|
|
void resetState() {
|
|
waitCount = 0;
|
|
switchWait = LINK_UNIVERSAL_SWITCH_WAIT_FRAMES +
|
|
qran_range(1, LINK_UNIVERSAL_SWITCH_WAIT_FRAMES_RANDOM);
|
|
subWaitCount = 0;
|
|
serveWait = 0;
|
|
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++)
|
|
incomingMessages[i].clear();
|
|
}
|
|
};
|
|
|
|
extern LinkUniversal* linkUniversal;
|
|
|
|
inline void LINK_UNIVERSAL_ISR_VBLANK() {
|
|
linkUniversal->_onVBlank();
|
|
}
|
|
|
|
inline void LINK_UNIVERSAL_ISR_SERIAL() {
|
|
linkUniversal->_onSerial();
|
|
}
|
|
|
|
inline void LINK_UNIVERSAL_ISR_TIMER() {
|
|
linkUniversal->_onTimer();
|
|
}
|
|
|
|
inline void LINK_UNIVERSAL_ISR_ACK_TIMER() {
|
|
linkUniversal->_onACKTimer();
|
|
}
|
|
|
|
#endif // LINK_UNIVERSAL_H
|