sfn_tpn/lib.rs
1//! saffron's two-player networking code for turn-based games.
2//!
3//! The changelog is currently the commit log. A proper changelog may be added in the future.
4//!
5//! # What sfn-tpn is made for
6//!
7//! This crate provides a tiny interface for adding multiplayer to casual two-player turn-based games.
8//!
9//! "Casual" is important because we connect two players directly and trust the bytes they send.
10//! "Hacking" in these games is not an issue because you would simply not accept game invitations from
11//! people you do not want to play with.
12//!
13//! "Two-player turn-based" specifically means two-player games that have strict turns.
14//! That is, each player is allowed to take a turn if and only if it is not the other player's turn,
15//! and players alternate turns.
16//!
17//! Examples include chess, checkers, Connect 4, (two-player) Blokus, and such.
18//! Nonexamples could include games that allow actions on the other player's turn, like activating Trap Cards
19//! in Yu-Gi-Oh, though you might be able to define the concept of a turn such that it works with
20//! sfn-tpn.
21//!
22//! # What sfn-tpn can do
23//!
24//! This crate exposes a [`NetcodeInterface`](https://docs.rs/sfn_tpn/latest/sfn_tpn/struct.NetcodeInterface.html) with functionality for
25//!
26//! - connecting two game instances (peer-to-peer via [iroh](https://www.iroh.computer/))
27//! - sending byte buffers of a constant size between the two game instances
28//! - doing so in a strictly turn-based manner (as described above)
29//!
30//! # What sfn-tpn cannot do
31//!
32//! I promise to "do my best" regarding security and not leaking resources, but I do not
33//! guarantee everything is perfect. Please feel encouraged to read the source code to make sure
34//! any risks or inefficiencies are tolerable for your use case (they are for mine, else
35//! I'd have fixed the code). Issues and PRs are appreciated, if you'd like!
36//!
37//! Additionally, these features are currently considered out of scope for sfn-tpn:
38//!
39//! - connecting multiple game instances
40//! - anything not turn-based
41//! - wasm is probably not supported because we use threading
42//! - (I'd like it to be, to be able to use this with macroquad for wasm, but this spawns a host of issues :/)
43//!
44//! # Examples
45//!
46//! - See the examples directory at <https://github.com/wade-cheng/sfn-tpn>
47
48mod protocol;
49
50use tokio::{
51 sync::{
52 mpsc::{self, error::TryRecvError},
53 oneshot::{self},
54 },
55 task::{self, JoinHandle},
56};
57
58/// Config used to create a new [`NetcodeInterface`].
59///
60/// The user was either given a ticket, or is generating a new ticket.
61pub enum Config {
62 /// A ticket string obtained from the other player.
63 Ticket(String),
64 /// Holds a oneshot sender that will send a newly generated ticket.
65 TicketSender(oneshot::Sender<String>),
66}
67
68/// The interface for netcode.
69///
70/// Runs [Tokio](https://tokio.rs/) and [iroh](https://www.iroh.computer/)
71/// under the hood in a separate thread. So, methods must be called from the
72/// context of a Tokio runtime. The procedure for operation is as follows.
73///
74/// A [`new`][`NetcodeInterface::new`] `NetcodeInterface` should be created on
75/// the two players' machines. The first, the "server," must provide a oneshot
76/// sender that receives a newly generated ticket. The second, the "client,"
77/// must provide a ticket string from that server.
78///
79/// The server moves second and the client moves first.
80///
81/// If it is the user's turn, they may:
82///
83/// - [`send_turn`][`NetcodeInterface::send_turn`] once
84/// - it will no longer be the user's turn
85///
86/// If it is not the user's turn, they may:
87///
88/// - [`try_recv_turn`][`NetcodeInterface::try_recv_turn`] repeatedly
89/// - if it returns `Ok`, it will be the user's turn.
90///
91/// Turns are represented as byte buffers of a constant size. Both players'
92/// buffer sizes must be the same.
93///
94/// Deviations from this procedure are undefined behavior.
95pub struct NetcodeInterface<const SIZE: usize> {
96 is_my_turn: bool,
97 recv_from_iroh: mpsc::Receiver<[u8; SIZE]>,
98 send_to_iroh: mpsc::Sender<[u8; SIZE]>,
99 /// A handle to the thread running iroh under the hood.
100 ///
101 /// Might need to be dropped if we want to be pedantic about the code.
102 _iroh_handle: JoinHandle<()>,
103}
104
105impl<const SIZE: usize> NetcodeInterface<SIZE> {
106 /// Create a new interface.
107 ///
108 /// See the struct's [`docs`][`NetcodeInterface`] for invariants.
109 pub fn new(config: Config) -> Self {
110 // hand-coding a bidirectional channel, sorta :p
111 let (send_to_iroh, recv_from_game) = mpsc::channel(1);
112 let (send_to_game, recv_from_iroh) = mpsc::channel(1);
113 let is_my_turn = match &config {
114 Config::Ticket(_) => true,
115 Config::TicketSender(_) => false,
116 };
117 let _iroh_handle = task::spawn(protocol::start_iroh_protocol(
118 send_to_game,
119 recv_from_game,
120 config,
121 ));
122
123 Self {
124 is_my_turn,
125 _iroh_handle,
126 recv_from_iroh,
127 send_to_iroh,
128 }
129 }
130
131 /// Send a turn to the other player.
132 ///
133 /// See the struct's [`docs`][`NetcodeInterface`] for invariants.
134 pub fn send_turn(&mut self, turn: &[u8; SIZE]) {
135 assert!(self.is_my_turn);
136 self.send_to_iroh
137 .try_send(*turn)
138 .expect("we should never have a full buffer");
139 self.is_my_turn = false;
140 }
141
142 /// Check if the other player has sent a turn to the user.
143 ///
144 /// See the struct's [`docs`][`NetcodeInterface`] for invariants.
145 pub fn try_recv_turn(&mut self) -> Result<[u8; SIZE], ()> {
146 assert!(!self.is_my_turn);
147 match self.recv_from_iroh.try_recv() {
148 Ok(t) => {
149 self.is_my_turn = true;
150 Ok(t)
151 }
152 Err(TryRecvError::Empty) => Err(()),
153 Err(TryRecvError::Disconnected) => unreachable!("unreachable if all goes well"),
154 }
155 }
156
157 /// Return whether it is the user's turn.
158 pub fn my_turn(&self) -> bool {
159 self.is_my_turn
160 }
161}