bevygap_client_plugin/
lib.rs1use base64::prelude::*;
2use bevy::prelude::*;
3use bevy_nfws::prelude::*;
4use bevygap_shared::protocol::*;
5use lightyear::prelude::{client::*, *};
6use std::net::SocketAddr;
7
8pub mod prelude {
9 pub use super::traits::*;
10 pub use super::BevygapClientConfig;
11 pub use super::BevygapClientPlugin;
12 pub use super::BevygapClientState;
13}
14mod traits;
15
16#[derive(States, Debug, Clone, Default, Eq, PartialEq, Hash)]
17pub enum BevygapClientState {
18 #[default]
19 Dormant,
20 Request,
22 AwaitingResponse(String),
24 ReadyToConnect,
26 Finished,
28 Error(u16, String),
30}
31
32impl BevygapClientState {
33 fn pending_state() -> impl FnMut(Option<Res<State<BevygapClientState>>>) -> bool + Clone {
36 move |current_state: Option<Res<State<BevygapClientState>>>| match current_state {
37 Some(current_state) => {
38 matches!(current_state.get(), BevygapClientState::AwaitingResponse(_))
39 }
40 _ => false,
41 }
42 }
43}
44
45#[derive(Resource, Debug, Clone)]
47pub struct BevygapClientConfig {
48 pub matchmaker_url: String,
51 pub fake_client_ip: Option<String>,
54 pub game_name: String,
56 pub game_version: String,
58}
59
60impl Default for BevygapClientConfig {
61 fn default() -> Self {
62 Self {
63 matchmaker_url: "ws://localhost:3000/matchmaker/ws".to_string(),
64 fake_client_ip: None,
65 game_name: "bevygap-spaceships".to_string(),
66 game_version: "1".to_string(),
67 }
68 }
69}
70
71pub struct BevygapClientPlugin;
72
73impl Plugin for BevygapClientPlugin {
74 fn build(&self, app: &mut App) {
75 app.add_plugins(NfwsPlugin);
76 app.init_resource::<BevygapClientConfig>();
77 app.init_state::<BevygapClientState>();
78
79 app.add_systems(OnEnter(BevygapClientState::Request), request_token);
80
81 app.add_systems(
82 Update,
83 handle_matchmaker_response.run_if(BevygapClientState::pending_state()),
84 );
85
86 app.add_systems(OnEnter(BevygapClientState::ReadyToConnect), connect_client);
87 }
88}
89
90fn request_token(
91 mut next_state: ResMut<NextState<BevygapClientState>>,
92 config: Res<BevygapClientConfig>,
93 mut commands: Commands,
94) {
95 info!(
98 "Initiating matchmaker websocket connection: {}",
99 config.matchmaker_url
100 );
101 commands.spawn(NfwsHandle::new(config.matchmaker_url.clone()));
102
103 next_state.set(BevygapClientState::AwaitingResponse(
104 "Requesting ...".to_string(),
105 ));
106}
107
108fn handle_matchmaker_response(
109 mut q: Query<(Entity, &mut NfwsHandle)>,
110 mut commands: Commands,
111 mut client_config: ResMut<ClientConfig>,
112 mut next_state: ResMut<NextState<BevygapClientState>>,
113 config: Res<BevygapClientConfig>,
114) {
115 for (entity, mut nfws) in q.iter_mut() {
116 match nfws.next_event() {
117 NfwsPollResult::Closed => {
118 info!("EV None = closed, despawning");
119 commands.entity(entity).despawn();
120 continue;
121 }
122 NfwsPollResult::Empty => continue,
123 NfwsPollResult::Event(rec) => {
124 info!("EV: {rec:?}");
125 match rec {
126 NfwsEvent::Connecting => {
127 info!("Matchmaker: {rec:?}");
128 }
129 NfwsEvent::Connected => {
130 info!("Matchmaker: {rec:?}");
131 let req = RequestSession {
132 client_ip: config.fake_client_ip.clone(),
133 game: config.game_name.clone(),
134 version: config.game_version.clone(),
135 };
136 let payload = serde_json::to_string(&req).unwrap();
137 info!("Sending payload: {payload}");
138 nfws.send_text(payload);
139 }
140 NfwsEvent::Error(nfws_err) => match nfws_err {
141 NfwsErr::Connecting => next_state.set(BevygapClientState::Error(
142 0,
143 "Can't connect to matchmaker".to_string(),
144 )),
145 NfwsErr::Receiving(msg) => next_state.set(BevygapClientState::Error(
146 0,
147 format!("Rcv error from matchmaker: {msg}"),
148 )),
149 NfwsErr::Sending(msg) => next_state.set(BevygapClientState::Error(
150 0,
151 format!("Send error to matchmaker: {msg}"),
152 )),
153 },
154 NfwsEvent::Closed(frame) => {
155 info!("Matchmaker connection closed: {frame:?}");
156 }
157 NfwsEvent::BinaryMessage(_vec) => {
158 warn!("Matchmaker: binary msg received, unhandled.");
159 }
160 NfwsEvent::TextMessage(msg) => {
161 let Ok(feedback) =
162 serde_json::from_slice::<SessionRequestFeedback>(msg.as_bytes())
163 else {
164 warn!("Unhandled msg type from matchmaker: {msg:?}");
165 warn!("Despawning client entity");
166 next_state.set(BevygapClientState::Error(
167 0,
168 "Unhandled response from matchmaker".to_string(),
169 ));
170 commands.entity(entity).despawn();
171 continue;
172 };
173 info!(">>> {feedback:?}");
174 match feedback {
175 SessionRequestFeedback::Acknowledged => {
176 next_state.set(BevygapClientState::AwaitingResponse(
177 "Request acknowledged".to_string(),
178 ))
179 }
180 SessionRequestFeedback::SessionRequestAccepted(sess_id) => next_state
181 .set(BevygapClientState::AwaitingResponse(format!(
182 "Session created: {sess_id}"
183 ))),
184 SessionRequestFeedback::ProgressReport(prog_msg) => {
185 next_state.set(BevygapClientState::AwaitingResponse(format!(
186 "Progress: {prog_msg}"
187 )))
188 }
189 SessionRequestFeedback::Error(err_code, err_msg) => {
190 next_state.set(BevygapClientState::Error(err_code, err_msg))
191 }
192 SessionRequestFeedback::SessionReady {
193 token,
194 ip,
195 port,
196 cert_digest,
197 } => {
198 let cert_digest = cert_digest.replace(':', "");
199 info!("Using cert digest {cert_digest}");
200 let tok_bytes = BASE64_STANDARD.decode(&token).unwrap();
201 assert_eq!(
202 tok_bytes.len(),
203 2048,
204 "ConnectTokens should be 2048 bytes exactly"
205 );
206 let connect_token =
207 ConnectToken::try_from_bytes(tok_bytes.as_slice()).unwrap();
208
209 let server_addr: SocketAddr =
211 format!("{ip}:{port}").parse().expect(
212 "invalid gameserver addr/port from matchmaker response?",
213 );
214
215 info!("Got matchmaker response, game server: {server_addr:?}");
216
217 if let NetConfig::Netcode { auth, io, .. } = &mut client_config.net
218 {
219 info!("Setting Netcode connect token and server addr");
220 *auth = Authentication::Token(connect_token);
221 let client_addr = match &mut io.transport {
224 client::ClientTransport::WebTransportClient {
225 client_addr,
226 ..
227 } => client_addr,
228 _ => panic!("Unsupported transport: {:?}", io.transport),
229 };
230 io.transport = client::ClientTransport::WebTransportClient {
231 client_addr: *client_addr,
232 server_addr,
233 #[cfg(target_family = "wasm")]
234 certificate_digest: cert_digest,
235 };
236 } else {
237 panic!("Unsupported netconfig, only supports Netcode for now.");
238 }
239 next_state.set(BevygapClientState::ReadyToConnect);
240 }
241 }
242 }
243 }
244 }
245 }
246 }
247}
248
249fn connect_client(mut commands: Commands, mut next_state: ResMut<NextState<BevygapClientState>>) {
250 info!("Connecting to server...");
251 commands.connect_client();
252 next_state.set(BevygapClientState::Finished);
253}