mirror of
https://github.com/afska/gba-link-connection.git
synced 2026-03-21 17:44:21 -05:00
797 lines
23 KiB
C++
797 lines
23 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: (*)
|
|
// interrupt_init();
|
|
// interrupt_add(INTR_VBLANK, LINK_UNIVERSAL_ISR_VBLANK);
|
|
// interrupt_add(INTR_SERIAL, LINK_UNIVERSAL_ISR_SERIAL);
|
|
// interrupt_add(INTR_TIMER3, LINK_UNIVERSAL_ISR_TIMER);
|
|
// - 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);
|
|
// // ...
|
|
// }
|
|
// --------------------------------------------------------------------------
|
|
// (*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) For CABLE mode:
|
|
// 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 use them!
|
|
// (they mean 'disconnected' and 'no data' respectively)
|
|
// --------------------------------------------------------------------------
|
|
|
|
#ifndef LINK_DEVELOPMENT
|
|
#pragma GCC system_header
|
|
#endif
|
|
|
|
#include "_link_common.hpp"
|
|
|
|
#include "LinkCable.hpp"
|
|
#include "LinkWireless.hpp"
|
|
|
|
#ifndef LINK_UNIVERSAL_MAX_PLAYERS
|
|
/**
|
|
* @brief Maximum number of players. Default = 5
|
|
* \warning Keep in mind that LinkCable's limit is 4.
|
|
*/
|
|
#define LINK_UNIVERSAL_MAX_PLAYERS LINK_WIRELESS_MAX_PLAYERS
|
|
#endif
|
|
|
|
#ifndef LINK_UNIVERSAL_GAME_ID_FILTER
|
|
/**
|
|
* @brief Game ID Filter (`0x0000` ~ `0x7FFF`). Default = 0 (no filter)
|
|
* This restricts wireless connections to rooms with a specific game ID.
|
|
* When disabled, it connects to any game ID and uses `0x7FFF` when serving.
|
|
*/
|
|
#define LINK_UNIVERSAL_GAME_ID_FILTER 0
|
|
#endif
|
|
|
|
LINK_VERSION_TAG LINK_UNIVERSAL_VERSION = "vLinkUniversal/v8.0.3";
|
|
|
|
#define LINK_UNIVERSAL_DISCONNECTED LINK_CABLE_DISCONNECTED
|
|
#define LINK_UNIVERSAL_NO_DATA LINK_CABLE_NO_DATA
|
|
|
|
/**
|
|
* @brief A multiplayer connection for the Link Cable and the Wireless Adapter.
|
|
*/
|
|
class LinkUniversal {
|
|
private:
|
|
using u32 = Link::u32;
|
|
using u16 = Link::u16;
|
|
using u8 = Link::u8;
|
|
using s8 = Link::s8;
|
|
using U16Queue = Link::Queue<u16, LINK_CABLE_QUEUE_SIZE>;
|
|
|
|
static constexpr int MAX_ROOM_NUMBER = 32000;
|
|
static constexpr int INIT_WAIT_FRAMES = 10;
|
|
static constexpr int SWITCH_WAIT_FRAMES = 25;
|
|
static constexpr int SWITCH_WAIT_FRAMES_RANDOM = 10;
|
|
static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 10;
|
|
static constexpr int SERVE_WAIT_FRAMES = 60;
|
|
static constexpr int SERVE_WAIT_FRAMES_RANDOM = 30;
|
|
|
|
public:
|
|
enum class State { INITIALIZING = 0, WAITING = 1, CONNECTED = 2 };
|
|
enum class Mode { LINK_CABLE, LINK_WIRELESS };
|
|
enum class Protocol {
|
|
AUTODETECT,
|
|
CABLE,
|
|
WIRELESS_AUTO,
|
|
WIRELESS_SERVER,
|
|
WIRELESS_CLIENT,
|
|
WIRELESS_RESTORE_EXISTING
|
|
};
|
|
|
|
struct CableOptions {
|
|
LinkCable::BaudRate baudRate;
|
|
u32 timeout;
|
|
u16 interval;
|
|
u8 sendTimerId;
|
|
};
|
|
|
|
struct WirelessOptions {
|
|
bool forwarding;
|
|
bool retransmission;
|
|
u32 maxPlayers;
|
|
u32 timeout;
|
|
u16 interval;
|
|
u8 sendTimerId;
|
|
};
|
|
|
|
/**
|
|
* @brief Constructs a new LinkUniversal object.
|
|
* @param protocol One of the enum values from `LinkUniversal::Protocol`.
|
|
* @param gameName The game name that will be broadcasted in wireless sessions
|
|
* (max `14` characters). The string must be a null-terminated character
|
|
* array. The library uses this to only connect to servers from the same game.
|
|
* @param cableOptions All the LinkCable constructor parameters in one struct.
|
|
* @param wirelessOptions All the LinkWireless constructor parameters in one
|
|
* struct.
|
|
*/
|
|
explicit LinkUniversal(Protocol protocol = Protocol::AUTODETECT,
|
|
const char* gameName = "",
|
|
CableOptions cableOptions =
|
|
CableOptions{LinkCable::BaudRate::BAUD_RATE_1,
|
|
LINK_CABLE_DEFAULT_TIMEOUT,
|
|
LINK_CABLE_DEFAULT_INTERVAL,
|
|
LINK_CABLE_DEFAULT_SEND_TIMER_ID},
|
|
WirelessOptions wirelessOptions =
|
|
WirelessOptions{
|
|
true, true, LINK_UNIVERSAL_MAX_PLAYERS,
|
|
LINK_WIRELESS_DEFAULT_TIMEOUT,
|
|
LINK_WIRELESS_DEFAULT_INTERVAL,
|
|
LINK_WIRELESS_DEFAULT_SEND_TIMER_ID})
|
|
: linkCable(cableOptions.baudRate,
|
|
cableOptions.timeout,
|
|
cableOptions.interval,
|
|
cableOptions.sendTimerId),
|
|
linkWireless(
|
|
wirelessOptions.forwarding,
|
|
wirelessOptions.retransmission,
|
|
Link::_min(wirelessOptions.maxPlayers, LINK_UNIVERSAL_MAX_PLAYERS),
|
|
wirelessOptions.timeout,
|
|
wirelessOptions.interval,
|
|
wirelessOptions.sendTimerId) {
|
|
config.protocol = protocol;
|
|
config.gameName = gameName;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns whether the library is active or not.
|
|
*/
|
|
[[nodiscard]] bool isActive() { return isEnabled; }
|
|
|
|
/**
|
|
* @brief Activates the library.
|
|
*/
|
|
void activate() {
|
|
LINK_READ_TAG(LINK_UNIVERSAL_VERSION);
|
|
|
|
reset();
|
|
waitCount = INIT_WAIT_FRAMES;
|
|
isEnabled = true;
|
|
}
|
|
|
|
/**
|
|
* @brief Deactivates the library, disabling both cable and wireless modes.
|
|
* Returns whether the deactivation of the Wireless Adapter was successful.
|
|
* @param turnOffWireless If `true`, the Wireless Adapter will be reset
|
|
* (default behavior).
|
|
*/
|
|
bool deactivate(bool turnOffWireless = true) {
|
|
isEnabled = false;
|
|
if (linkCable.isActive())
|
|
linkCable.deactivate();
|
|
bool success = linkWireless.deactivate(turnOffWireless);
|
|
resetState();
|
|
mode = Mode::LINK_CABLE;
|
|
return success;
|
|
}
|
|
|
|
/**
|
|
* @brief Returns `true` if there are at least 2 connected players.
|
|
* \warning Stays frozen between `sync()` calls.
|
|
*/
|
|
[[nodiscard]] bool isConnected() { return state == State::CONNECTED; }
|
|
|
|
/**
|
|
* @brief Returns the number of connected players (`1~5`).
|
|
*/
|
|
[[nodiscard]] u8 playerCount() {
|
|
return Link::_min(mode == Mode::LINK_CABLE ? linkCable.playerCount()
|
|
: linkWireless.playerCount(),
|
|
LINK_UNIVERSAL_MAX_PLAYERS);
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the current player ID (`0~4`).
|
|
*/
|
|
[[nodiscard]] u8 currentPlayerId() {
|
|
return mode == Mode::LINK_CABLE ? linkCable.currentPlayerId()
|
|
: linkWireless.currentPlayerId();
|
|
}
|
|
|
|
/**
|
|
* @brief Collects available messages from interrupts for later processing
|
|
* with `read(...)`. Call this method whenever you need to fetch new data, and
|
|
* at least once per frame, as it also manages connection state, auto-pairing,
|
|
* and protocol switching. Always process all messages before calling it
|
|
* again.
|
|
*/
|
|
void sync() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
u16 keys = ~Link::_REG_KEYS & Link::_KEY_ANY;
|
|
Link::randomSeed += keys;
|
|
Link::randomSeed += Link::_REG_RCNT;
|
|
Link::randomSeed += Link::_REG_SIOCNT;
|
|
|
|
if (mode == Mode::LINK_CABLE)
|
|
linkCable.sync();
|
|
|
|
switch (state) {
|
|
case State::INITIALIZING: {
|
|
waitCount++;
|
|
if (waitCount > INIT_WAIT_FRAMES)
|
|
start();
|
|
break;
|
|
};
|
|
case State::WAITING: {
|
|
if (mode == Mode::LINK_CABLE) {
|
|
// Cable, waiting...
|
|
if (isConnectedCable()) {
|
|
state = State::CONNECTED;
|
|
goto connected;
|
|
}
|
|
} else {
|
|
// Wireless, waiting...
|
|
if (isConnectedWireless()) {
|
|
state = State::CONNECTED;
|
|
goto connected;
|
|
} else {
|
|
if (!autoDiscoverWirelessConnections())
|
|
waitCount = switchWait;
|
|
if (isConnectedWireless())
|
|
goto connected;
|
|
}
|
|
}
|
|
|
|
waitCount++;
|
|
if (waitCount > switchWait)
|
|
toggleMode();
|
|
|
|
break;
|
|
}
|
|
case State::CONNECTED: {
|
|
connected:
|
|
if (mode == Mode::LINK_CABLE) {
|
|
// Cable, connected...
|
|
if (!isConnectedCable()) {
|
|
toggleMode();
|
|
break;
|
|
}
|
|
|
|
receiveCableMessages();
|
|
} else {
|
|
// Wireless, connected...
|
|
if (!isConnectedWireless()) {
|
|
toggleMode();
|
|
break;
|
|
}
|
|
|
|
receiveWirelessMessages();
|
|
}
|
|
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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();
|
|
|
|
u8 timerId = mode == Mode::LINK_CABLE ? linkCable.config.sendTimerId
|
|
: linkWireless.config.sendTimerId;
|
|
|
|
while (isConnected() && !canRead(playerId) && !cancel()) {
|
|
Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_TIMER_IRQ_IDS[timerId]);
|
|
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 !incomingMessages[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 incomingMessages[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 incomingMessages[playerId].peek();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns whether a `send(...)` call would fail due to the queue being
|
|
* full or not.
|
|
*/
|
|
bool canSend() {
|
|
return mode == Mode::LINK_CABLE ? linkCable.canSend()
|
|
: linkWireless.canSend();
|
|
}
|
|
|
|
/**
|
|
* @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)
|
|
return false;
|
|
|
|
return mode == Mode::LINK_CABLE ? linkCable.send(data)
|
|
: linkWireless.send(data);
|
|
}
|
|
|
|
/**
|
|
* @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 = mode == Mode::LINK_CABLE
|
|
? linkCable.didQueueOverflow(clear)
|
|
: linkWireless.didQueueOverflow(clear);
|
|
|
|
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++) {
|
|
overflow = overflow || incomingMessages[i].overflow;
|
|
if (clear)
|
|
incomingMessages[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;
|
|
|
|
if (linkCable.isActive())
|
|
linkCable.resetTimeout();
|
|
if (linkWireless.isActive())
|
|
linkWireless.resetTimeout();
|
|
}
|
|
|
|
/**
|
|
* @brief Restarts the send timer without disconnecting.
|
|
* \warning Call this if you changed `config.interval`.
|
|
*/
|
|
void resetTimer() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (linkCable.isActive())
|
|
linkCable.resetTimer();
|
|
if (linkWireless.isActive())
|
|
linkWireless.resetTimer();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the current state.
|
|
* @return One of the enum values from `LinkUniversal::State`.
|
|
*/
|
|
[[nodiscard]] State getState() { return state; }
|
|
|
|
/**
|
|
* @brief Returns the active mode.
|
|
* @return One of the enum values from `LinkUniversal::Mode`.
|
|
*/
|
|
[[nodiscard]] Mode getMode() { return mode; }
|
|
|
|
/**
|
|
* @brief Returns the active protocol
|
|
* @return One of the enum values from `LinkUniversal::Protocol`.
|
|
*/
|
|
[[nodiscard]] Protocol getProtocol() { return this->config.protocol; }
|
|
|
|
/**
|
|
* @brief Sets the active `protocol`.
|
|
* @param protocol One of the enum values from `LinkUniversal::Protocol`.
|
|
*/
|
|
void setProtocol(Protocol protocol) { this->config.protocol = protocol; }
|
|
|
|
/**
|
|
* @brief Returns `true` if there are at least 2 connected players.
|
|
* \warning Can change between `sync()` calls.
|
|
*/
|
|
[[nodiscard]] bool isConnectedNow() {
|
|
return mode == Mode::LINK_CABLE ? linkCable.isConnected()
|
|
: linkWireless.isConnected();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the wireless state (same as `LinkWireless::getState()`).
|
|
*/
|
|
[[nodiscard]] LinkWireless::State getWirelessState() {
|
|
return linkWireless.getState();
|
|
}
|
|
|
|
/**
|
|
* @brief Returns the internal `LinkCable` instance (for advanced usage).
|
|
*/
|
|
[[nodiscard]] LinkCable* getLinkCable() { return &linkCable; }
|
|
|
|
/**
|
|
* @brief Returns the internal `LinkWireless` instance (for advanced usage).
|
|
*/
|
|
[[nodiscard]] LinkWireless* getLinkWireless() { return &linkWireless; }
|
|
|
|
/**
|
|
* @brief Returns the wait count.
|
|
* \warning This is internal API!
|
|
*/
|
|
[[nodiscard]] u32 _getWaitCount() { return waitCount; }
|
|
|
|
/**
|
|
* @brief Returns the sub-wait count.
|
|
* \warning This is internal API!
|
|
*/
|
|
[[nodiscard]] u32 _getSubWaitCount() { return subWaitCount; }
|
|
|
|
/**
|
|
* @brief This method is called by the VBLANK interrupt handler.
|
|
* \warning This is internal API!
|
|
*/
|
|
void _onVBlank() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (mode == Mode::LINK_CABLE)
|
|
linkCable._onVBlank();
|
|
else
|
|
linkWireless._onVBlank();
|
|
}
|
|
|
|
/**
|
|
* @brief This method is called by the SERIAL interrupt handler.
|
|
* \warning This is internal API!
|
|
*/
|
|
void _onSerial() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (mode == Mode::LINK_CABLE)
|
|
linkCable._onSerial();
|
|
else
|
|
linkWireless._onSerial();
|
|
}
|
|
|
|
/**
|
|
* @brief This method is called by the TIMER interrupt handler.
|
|
* \warning This is internal API!
|
|
*/
|
|
void _onTimer() {
|
|
if (!isEnabled)
|
|
return;
|
|
|
|
if (mode == Mode::LINK_CABLE)
|
|
linkCable._onTimer();
|
|
else
|
|
linkWireless._onTimer();
|
|
}
|
|
|
|
private:
|
|
struct Config {
|
|
Protocol protocol;
|
|
const char* gameName;
|
|
};
|
|
|
|
LinkCable linkCable;
|
|
LinkWireless linkWireless;
|
|
U16Queue incomingMessages[LINK_UNIVERSAL_MAX_PLAYERS];
|
|
Config config;
|
|
State state = State::INITIALIZING;
|
|
Mode mode = Mode::LINK_CABLE;
|
|
u32 waitCount = 0;
|
|
u32 switchWait = 0;
|
|
u32 subWaitCount = 0;
|
|
u32 serveWait = 0;
|
|
volatile bool isEnabled = false;
|
|
|
|
void receiveCableMessages() {
|
|
static constexpr u32 MAX_PLAYERS =
|
|
LINK_UNIVERSAL_MAX_PLAYERS < LINK_CABLE_MAX_PLAYERS
|
|
? LINK_UNIVERSAL_MAX_PLAYERS
|
|
: LINK_CABLE_MAX_PLAYERS;
|
|
|
|
for (u32 i = 0; i < MAX_PLAYERS; i++) {
|
|
while (linkCable.canRead(i))
|
|
incomingMessages[i].push(linkCable.read(i));
|
|
}
|
|
}
|
|
|
|
void receiveWirelessMessages() {
|
|
LinkWireless::Message messages[LINK_WIRELESS_QUEUE_SIZE];
|
|
u32 receivedCount;
|
|
linkWireless.receive(messages, receivedCount);
|
|
|
|
for (u32 i = 0; i < receivedCount; i++) {
|
|
auto message = messages[i];
|
|
|
|
if (message.playerId < LINK_UNIVERSAL_MAX_PLAYERS)
|
|
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 >= 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;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool tryConnectOrServeWirelessSession() {
|
|
LinkWireless::Server servers[LINK_WIRELESS_MAX_SERVERS];
|
|
u32 serverCount;
|
|
if (!linkWireless.getServersAsyncEnd(servers, serverCount))
|
|
return false;
|
|
|
|
u32 maxRandomNumber = 0;
|
|
u32 serverIndex = 0;
|
|
for (u32 i = 0; i < serverCount; i++) {
|
|
auto server = servers[i];
|
|
|
|
if (!server.isFull() &&
|
|
Link::areStrEqual(server.gameName, config.gameName) &&
|
|
(LINK_UNIVERSAL_GAME_ID_FILTER == 0 ||
|
|
server.gameId == LINK_UNIVERSAL_GAME_ID_FILTER)) {
|
|
u32 randomNumber = safeStoi(server.userName);
|
|
if (randomNumber > maxRandomNumber && randomNumber < MAX_ROOM_NUMBER) {
|
|
maxRandomNumber = randomNumber;
|
|
serverIndex = i;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (maxRandomNumber > 0 && config.protocol != Protocol::WIRELESS_SERVER) {
|
|
if (!linkWireless.connect(servers[serverIndex].id))
|
|
return false;
|
|
} else {
|
|
if (config.protocol == Protocol::WIRELESS_CLIENT)
|
|
return false;
|
|
|
|
subWaitCount = 0;
|
|
serveWait =
|
|
SERVE_WAIT_FRAMES + Link::_qran_range(1, SERVE_WAIT_FRAMES_RANDOM);
|
|
u32 randomNumber = Link::_qran_range(1, MAX_ROOM_NUMBER);
|
|
char randomNumberStr[6];
|
|
Link::intToStr5(randomNumberStr, randomNumber);
|
|
if (!linkWireless.serve(config.gameName, randomNumberStr,
|
|
LINK_UNIVERSAL_GAME_ID_FILTER > 0
|
|
? LINK_UNIVERSAL_GAME_ID_FILTER
|
|
: LINK_WIRELESS_MAX_GAME_ID))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool isConnectedCable() { return linkCable.isConnected(); }
|
|
bool isConnectedWireless() { return linkWireless.isConnected(); }
|
|
|
|
void reset() {
|
|
switch (config.protocol) {
|
|
case Protocol::AUTODETECT:
|
|
case Protocol::CABLE: {
|
|
setMode(Mode::LINK_CABLE);
|
|
break;
|
|
}
|
|
case Protocol::WIRELESS_AUTO:
|
|
case Protocol::WIRELESS_SERVER:
|
|
case Protocol::WIRELESS_CLIENT:
|
|
case Protocol::WIRELESS_RESTORE_EXISTING: {
|
|
setMode(Mode::LINK_WIRELESS);
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
void stop() {
|
|
if (mode == Mode::LINK_CABLE)
|
|
linkCable.deactivate();
|
|
else if (config.protocol != Protocol::WIRELESS_RESTORE_EXISTING)
|
|
linkWireless.deactivate(false);
|
|
}
|
|
|
|
void toggleMode() {
|
|
switch (config.protocol) {
|
|
case Protocol::AUTODETECT: {
|
|
setMode(mode == Mode::LINK_CABLE ? Mode::LINK_WIRELESS
|
|
: Mode::LINK_CABLE);
|
|
break;
|
|
}
|
|
case Protocol::CABLE: {
|
|
setMode(Mode::LINK_CABLE);
|
|
break;
|
|
}
|
|
case Protocol::WIRELESS_AUTO:
|
|
case Protocol::WIRELESS_SERVER:
|
|
case Protocol::WIRELESS_CLIENT:
|
|
case Protocol::WIRELESS_RESTORE_EXISTING: {
|
|
setMode(Mode::LINK_WIRELESS);
|
|
break;
|
|
}
|
|
default: {
|
|
}
|
|
}
|
|
}
|
|
|
|
void setMode(Mode mode) {
|
|
stop();
|
|
this->state = State::INITIALIZING;
|
|
this->mode = mode;
|
|
resetState();
|
|
}
|
|
|
|
void start() {
|
|
if (mode == Mode::LINK_CABLE) {
|
|
linkCable.activate();
|
|
} else {
|
|
bool success = config.protocol == Protocol::WIRELESS_RESTORE_EXISTING
|
|
? linkWireless.restoreExistingConnection()
|
|
: linkWireless.activate();
|
|
if (!success) {
|
|
toggleMode();
|
|
return;
|
|
}
|
|
}
|
|
|
|
state = State::WAITING;
|
|
resetState();
|
|
}
|
|
|
|
void resetState() {
|
|
LINK_BARRIER;
|
|
waitCount = 0;
|
|
switchWait =
|
|
SWITCH_WAIT_FRAMES + Link::_qran_range(1, SWITCH_WAIT_FRAMES_RANDOM);
|
|
subWaitCount = 0;
|
|
serveWait = 0;
|
|
for (u32 i = 0; i < LINK_UNIVERSAL_MAX_PLAYERS; i++) {
|
|
incomingMessages[i].clear();
|
|
incomingMessages[i].overflow = false;
|
|
}
|
|
LINK_BARRIER;
|
|
}
|
|
|
|
u32 safeStoi(const char* str) {
|
|
u32 num = 0;
|
|
|
|
while (*str != '\0') {
|
|
char ch = *str;
|
|
if (ch < '0' || ch > '9')
|
|
return 0;
|
|
num = num * 10 + (ch - '0');
|
|
str++;
|
|
}
|
|
|
|
return num;
|
|
}
|
|
};
|
|
|
|
extern LinkUniversal* linkUniversal;
|
|
|
|
/**
|
|
* @brief VBLANK interrupt handler.
|
|
*/
|
|
inline void LINK_UNIVERSAL_ISR_VBLANK() {
|
|
linkUniversal->_onVBlank();
|
|
}
|
|
|
|
/**
|
|
* @brief SERIAL interrupt handler.
|
|
*/
|
|
inline void LINK_UNIVERSAL_ISR_SERIAL() {
|
|
linkUniversal->_onSerial();
|
|
}
|
|
|
|
/**
|
|
* @brief TIMER interrupt handler.
|
|
*/
|
|
inline void LINK_UNIVERSAL_ISR_TIMER() {
|
|
linkUniversal->_onTimer();
|
|
}
|
|
|
|
#endif // LINK_UNIVERSAL_H
|