This is a proof of concept demo application showing how 2+ Nintendo DS consoles can interact with each other using local wireless communication.
The code to allow NiFi on the NDS originated from CTurt/dsgmLib. I've refactored it into a clean event-driven library and integrated it into a fork of the devkitpro dswifi library.
NiFi (Near Field Communication) enables DS-to-DS multiplayer without requiring a WiFi access point. The DS runs in promiscuous WiFi mode to send/receive packets directly between devices on the same channel.
The NiFi library uses circular packet buffers by design:
- Fixed memory usage (critical on NDS with only 4MB RAM)
- Old unprocessed packets are overwritten when buffers fill
- Optimal for real-time games where stale data should be discarded
- Similar to UDP: fast and lossy, prioritizing recent events over old ones
- You may see "Overwriting packet" warnings in high-traffic scenarios - this is expected
For most real-time multiplayer games (racing, action, platformers), dropping old position updates is better than queuing them and introducing lag.
- Install devkitpro v3.0.3 (latest)
- Clone jpenny1993/dswifi
- Open a terminal like msys2 in the root of the repo
- Run the
makecommand to build the dswifi library - Run the
make installcommand to replace the stock dswifi library provided with devkitpro
- Open a terminal in the root of the repo
- Run the
makecommand to build the .nds file
Copy the .nds file from the root of the repo onto your NDS flashcart
Launch the .nds application on both NDS consoles, tap the touch screen with your stylus and the co-ordinates of the touch event should be sent to the other NDS.
The library provides an event-driven API with 9 callback hooks:
// 1. Register your event handlers
NiFi_OnRoomAnnounced(OnRoomAnnounced);
NiFi_OnJoinAccepted(OnJoinAccepted);
NiFi_OnClientConnected(OnClientConnected);
// ... register other handlers as needed
// 2. Initialize NiFi
NiFi_Init(wifiChannel, timerId, "GAME");
// 3. Create or join a room
NiFi_CreateRoom(); // Host
// or
NiFi_ScanRooms(); // Client (auto-calls OnRoomAnnounced when found)| Hook | When Called | Use Case |
|---|---|---|
OnRoomAnnounced |
Room discovered during scan | Show available rooms, auto-join |
OnJoinAccepted |
Successfully joined room | Initialize game state |
OnJoinDeclined |
Join rejected (room full) | Show error message |
OnClientConnected |
Player joins room | Spawn player avatar |
OnClientDisconnected |
Player leaves room | Remove player avatar |
OnDisconnected |
You are kicked/disconnected | Return to main menu |
OnHostMigration |
New host selected | Update UI, pause game |
OnPositionUpdated |
Position broadcast received | Update player position |
OnGamePacket |
Custom packet received | Chat, items, game events |
OnFullGameStateRequested |
Client/spectator requests game state | Send full state to late-joining player |
// Example: Send a chat message
NiFiPacket packet;
NiFi_SetPacket(&packet, "CHAT_MSG");
strcpy(packet.data[0], playerName);
strcpy(packet.data[1], "Hello!");
NiFi_SendBroadcast(&packet, NULL);
// Example: Handle in OnGamePacket
void OnGamePacket(NiFiPacket packet) {
if (strcmp(packet.command, "CHAT_MSG") == 0) {
printf("%s: %s\n", packet.data[0], packet.data[1]);
}
}// Send position (called from main loop)
Position pos = {touchX, touchY, 0};
NiFi_BroadcastPosition(pos);
// Receive position (automatic via OnPositionUpdated hook)
void OnPositionUpdated(Position pos, u8 clientIndex, NiFiClient client) {
players[clientIndex].x = pos.x;
players[clientIndex].y = pos.y;
}// Enable spectator mode (call after NiFi_Init)
NiFi_SetSpectatorMode(true);
// Scan for active games
NiFi_ScanRooms(); // Triggers OnRoomAnnounced for all rooms
// Watch a specific room (uses same function as joining)
NiFi_JoinRoom(room); // In spectator mode, joins passively without sending packets
// Request full game state from host (for late-joining spectators)
NiFi_RequestFullGameState();
// Host responds to state request
void OnFullGameStateRequested(char macAddress[MAC_ADDRESS_LENGTH]) {
// Send game state using custom packets
NiFiPacket packet;
NiFi_SetPacket(&packet, "STATE");
// ... populate with your game state data
NiFi_SendBroadcast(&packet, NULL);
}
// Leave current room (returns to scanning, stays in spectator mode)
NiFi_LeaveRoom();
// Disable spectator mode
NiFi_SetSpectatorMode(false);Spectator Features:
- Passive observation: Spectators receive all packets but cannot participate
- Full game scanning: Can discover all rooms regardless of status (open/locked/in-game)
- State synchronization: Request full game state to catch up on late joins
- No slot consumption: Spectators don't count toward room capacity
- All event handlers run in interrupt context (from hardware timer)
- Keep handler code fast and simple to avoid blocking network updates
- Access to
clients[]array for all connected player info localClientpointer for your own client data- Packet data: up to 6 parameters, 32 chars each
For deeper understanding of the protocol and implementation:
- ARCHITECTURE.md - Complete protocol specification, packet format, circular buffer architecture, design decisions, and performance characteristics
- LESSONS_LEARNED.md - Development journey, critical bugs encountered, debugging techniques, and general embedded networking wisdom
- IMPLEMENTATION_PLAN_ROOM_STATUS.md - Planned room status system for lobby management and player reconnection