1use quaso::{
2 GameLauncher,
3 assets::{make_directory_database, shader::ShaderAsset},
4 config::Config,
5 context::GameContext,
6 game::{GameInstance, GameState, GameStateChange},
7 game_state_custom_event, inputs_bitstruct,
8 multiplayer::{
9 GameMultiplayerChange, GameNetwork,
10 ggpo::{GgpoMultiplayer, GgpoPlayerCommunication, GgpoPlayerRole},
11 tcp::{TcpClientConnection, TcpServerConnection},
12 universal::{UniversalMultiplayerAuthority, UniversalMultiplayerGameState},
13 },
14 third_party::{
15 fontdue::layout::{HorizontalAlign, VerticalAlign},
16 raui_core::{
17 layout::CoordsMappingScaling,
18 widget::{
19 component::{
20 image_box::ImageBoxProps, interactive::navigation::NavItemActive,
21 text_box::TextBoxProps,
22 },
23 unit::text::{TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
24 utils::Color,
25 },
26 },
27 raui_immediate_widgets::core::{
28 containers::{content_box, nav_horizontal_box},
29 image_box,
30 interactive::{ImmediateButton, button},
31 text_box,
32 },
33 spitfire_draw::{
34 context::DrawContext,
35 sprite::{Sprite, SpriteTexture},
36 text::Text,
37 utils::{Drawable, ShaderRef, TextureRef, Vertex},
38 },
39 spitfire_glow::{
40 graphics::{CameraScaling, GraphicsTarget, Shader},
41 renderer::GlowTextureFiltering,
42 },
43 spitfire_input::{
44 ArrayInputCombinator, InputActionRef, InputAxisRef, InputConsume, InputMapping,
45 MouseButton, VirtualAction, VirtualAxis,
46 },
47 tehuti::{
48 channel::Dispatch,
49 hash,
50 peer::{Peer, PeerFactory, PeerId, TypedPeerRole},
51 },
52 tehuti_timeline::time::TimeStamp,
53 time::{Duration, Instant},
54 tracing::{debug, level_filters::LevelFilter},
55 tracing_subscriber::{
56 Layer, fmt::layer, layer::SubscriberExt, registry, util::SubscriberInitExt,
57 },
58 vek::{Rgba, Vec2},
59 windowing::event::VirtualKeyCode,
60 },
61};
62use serde::{Deserialize, Serialize};
63use std::{collections::BTreeMap, error::Error, sync::Arc};
64use tehuti::fixed::Fixed;
65
66type Number = Fixed<6>;
67
68const ADDRESS: &str = "127.0.0.1:12345";
69const PLAYER_ROLE: u64 = 1;
70const HISTORY_CAPACITY: usize = 16;
71const SEND_STATE_HASH_INTERVAL: Duration = Duration::from_millis(100);
72const SEND_INPUT_WINDOW: u64 = 8;
73const SEND_STATE_HASH_WINDOW: u64 = 8;
74const INPUT_DELAY_TICKS: u64 = 3;
75const MAX_PREDICTION_TICKS: u64 = 6;
76const SPEED: f32 = 100.0;
77const COLOR_WHITE: Color = Color {
78 r: 1.0,
79 g: 1.0,
80 b: 1.0,
81 a: 1.0,
82};
83const COLOR_BLACK: Color = Color {
84 r: 0.0,
85 g: 0.0,
86 b: 0.0,
87 a: 1.0,
88};
89
90type PlayerRole = GgpoPlayerRole<PLAYER_ROLE, HISTORY_CAPACITY, InputSnapshot, StateSnapshot>;
91type Multiplayer = GgpoMultiplayer;
92
93fn main() -> Result<(), Box<dyn Error>> {
94 registry()
95 .with(
96 layer()
97 .with_writer(std::io::stdout)
98 .with_filter(LevelFilter::DEBUG),
99 )
100 .init();
101
102 GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
103 *assets = make_directory_database("./resources/").unwrap();
104 }))
105 .title("Netcode: GGPO (P2P rollback)")
106 .config(Config::load_from_file("./resources/GameConfig.toml")?)
107 .run();
108 Ok(())
109}
110
111#[derive(Default)]
112struct Preloader;
113
114impl GameState for Preloader {
115 fn enter(&mut self, context: GameContext) {
116 context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
117 context.graphics.state.main_camera.screen_alignment = 0.5.into();
118 context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(500.0);
119 context.gui.coords_map_scaling = CoordsMappingScaling::FitVertical(500.0);
120
121 context
122 .assets
123 .spawn(
124 "shader://color",
125 (ShaderAsset::new(
126 Shader::COLORED_VERTEX_2D,
127 Shader::PASS_FRAGMENT,
128 ),),
129 )
130 .unwrap();
131 context
132 .assets
133 .spawn(
134 "shader://image",
135 (ShaderAsset::new(
136 Shader::TEXTURED_VERTEX_2D,
137 Shader::TEXTURED_FRAGMENT,
138 ),),
139 )
140 .unwrap();
141 context
142 .assets
143 .spawn(
144 "shader://text",
145 (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
146 )
147 .unwrap();
148
149 context.assets.ensure("font://roboto.ttf").unwrap();
150 context.assets.ensure("texture://ferris.png").unwrap();
151
152 *context.network =
153 GameNetwork::new(Arc::new(PeerFactory::default().with_typed::<PlayerRole>()));
154 }
155
156 fn update(&mut self, context: GameContext, _: f32) {
157 if !context.assets.is_busy() {
158 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
159 }
160 }
161}
162
163#[derive(Default)]
164struct Lobby {
165 exit: InputActionRef,
166}
167
168impl GameState for Lobby {
169 fn enter(&mut self, context: GameContext) {
170 let pointer_x = InputAxisRef::default();
171 let pointer_y = InputAxisRef::default();
172 let pointer_trigger = InputActionRef::default();
173 self.exit = InputActionRef::default();
174
175 context.gui.interactions.inputs.pointer_position =
176 ArrayInputCombinator::new([pointer_x.clone(), pointer_y.clone()]);
177 context.gui.interactions.inputs.pointer_trigger = pointer_trigger.clone();
178
179 context.input.push_mapping(
180 InputMapping::default()
181 .consume(InputConsume::Hit)
182 .axis(VirtualAxis::MousePositionX, pointer_x)
183 .axis(VirtualAxis::MousePositionY, pointer_y)
184 .action(
185 VirtualAction::MouseButton(MouseButton::Left),
186 pointer_trigger,
187 )
188 .action(
189 VirtualAction::KeyButton(VirtualKeyCode::Escape),
190 self.exit.clone(),
191 ),
192 );
193 }
194
195 fn exit(&mut self, context: GameContext) {
196 context.input.pop_mapping();
197 }
198
199 fn update(&mut self, context: GameContext, _delta_time: f32) {
200 if let Some(multiplayer) = context.multiplayer::<Multiplayer>()
201 && multiplayer.peers_in_queue().count() == 2
202 {
203 debug!("All players connected, starting game...");
204 *context.state_change = GameStateChange::Swap(Box::new(State::default()));
205 }
206 }
207
208 fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
209 if self.exit.get().is_pressed() {
210 *context.state_change = GameStateChange::Pop;
211 }
212 }
213
214 fn draw_gui(&mut self, context: GameContext) {
215 nav_horizontal_box(NavItemActive, || {
216 if let Some(multiplayer) = context.multiplayer::<Multiplayer>() {
217 text_box(TextBoxProps {
218 text: format!(
219 "WAITING FOR PLAYERS... ({}/2)",
220 multiplayer.peers_in_queue().count()
221 ),
222 font: TextBoxFont {
223 name: "roboto.ttf".to_owned(),
224 size: 24.0,
225 },
226 color: COLOR_WHITE,
227 horizontal_align: TextBoxHorizontalAlign::Center,
228 vertical_align: TextBoxVerticalAlign::Middle,
229 ..Default::default()
230 });
231 } else {
232 let host = lobby_button("HOST");
233 let join = lobby_button("JOIN");
234
235 let is_host = if host.trigger_start() {
236 Some(true)
237 } else if join.trigger_start() {
238 Some(false)
239 } else {
240 None
241 };
242
243 if let Some(is_host) = is_host {
244 debug!("Starting {}...", if is_host { "server" } else { "client" });
245
246 context.network.clear_connections();
247 let id = if is_host {
248 context
249 .network
250 .add_connection(TcpServerConnection::listen(ADDRESS).unwrap());
251 PeerId::new(0)
252 } else {
253 context
254 .network
255 .add_connection(TcpClientConnection::connect(ADDRESS).unwrap());
256 PeerId::new(1)
257 };
258
259 let multiplayer = Multiplayer::new(context.network);
260 multiplayer.create_peer(id, PlayerRole::ROLE_ID);
261 *context.multiplayer_change = GameMultiplayerChange::Set(Box::new(
262 multiplayer
263 .with_ticks_per_second(30)
264 .with_max_prediction_ticks(MAX_PREDICTION_TICKS),
265 ));
266 }
267 }
268 });
269 }
270}
271
272fn lobby_button(label: &str) -> ImmediateButton {
273 button(NavItemActive, |state| {
274 let (bg_color, text_color) = if state.state.selected {
275 (COLOR_WHITE, COLOR_BLACK)
276 } else {
277 (COLOR_BLACK, COLOR_WHITE)
278 };
279
280 content_box((), || {
281 image_box(ImageBoxProps::colored(bg_color));
282
283 text_box(TextBoxProps {
284 text: label.to_owned(),
285 font: TextBoxFont {
286 name: "roboto.ttf".to_owned(),
287 size: 24.0,
288 },
289 color: text_color,
290 horizontal_align: TextBoxHorizontalAlign::Center,
291 vertical_align: TextBoxVerticalAlign::Middle,
292 ..Default::default()
293 });
294 });
295 })
296}
297
298struct State {
299 players: BTreeMap<PeerId, PlayerCharacter>,
300 move_up: InputActionRef,
301 move_down: InputActionRef,
302 move_left: InputActionRef,
303 move_right: InputActionRef,
304 exit: InputActionRef,
305 send_state_timer: Instant,
306}
307
308impl Default for State {
309 fn default() -> Self {
310 Self {
311 players: Default::default(),
312 move_up: Default::default(),
313 move_down: Default::default(),
314 move_left: Default::default(),
315 move_right: Default::default(),
316 exit: Default::default(),
317 send_state_timer: Instant::now(),
318 }
319 }
320}
321
322impl State {
323 fn local_player_mut(&mut self) -> Option<&mut PlayerCharacter> {
324 self.players
325 .values_mut()
326 .find(|player| !player.role.info.remote)
327 }
328}
329
330impl GameState for State {
331 fn enter(&mut self, mut context: GameContext) {
332 self.move_left = InputActionRef::default();
333 self.move_right = InputActionRef::default();
334 self.move_up = InputActionRef::default();
335 self.move_down = InputActionRef::default();
336 self.exit = InputActionRef::default();
337 context.input.push_mapping(
338 InputMapping::default()
339 .consume(InputConsume::Hit)
340 .action(
341 VirtualAction::KeyButton(VirtualKeyCode::A),
342 self.move_left.clone(),
343 )
344 .action(
345 VirtualAction::KeyButton(VirtualKeyCode::D),
346 self.move_right.clone(),
347 )
348 .action(
349 VirtualAction::KeyButton(VirtualKeyCode::W),
350 self.move_up.clone(),
351 )
352 .action(
353 VirtualAction::KeyButton(VirtualKeyCode::S),
354 self.move_down.clone(),
355 )
356 .action(
357 VirtualAction::KeyButton(VirtualKeyCode::Left),
358 self.move_left.clone(),
359 )
360 .action(
361 VirtualAction::KeyButton(VirtualKeyCode::Right),
362 self.move_right.clone(),
363 )
364 .action(
365 VirtualAction::KeyButton(VirtualKeyCode::Up),
366 self.move_up.clone(),
367 )
368 .action(
369 VirtualAction::KeyButton(VirtualKeyCode::Down),
370 self.move_down.clone(),
371 )
372 .action(
373 VirtualAction::KeyButton(VirtualKeyCode::Escape),
374 self.exit.clone(),
375 ),
376 );
377
378 if let Some(multiplayer) = context.multiplayer_mut::<Multiplayer>() {
379 multiplayer.request_progressing(true);
380 } else {
381 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
382 }
383 }
384
385 fn exit(&mut self, context: GameContext) {
386 context.input.pop_mapping();
387
388 context.network.clear_connections();
389 *context.multiplayer_change = GameMultiplayerChange::Reset;
390 }
391
392 fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
393 if self.exit.get().is_pressed() {
394 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
395 }
396 }
397
398 fn draw(&mut self, context: GameContext) {
399 for player in self.players.values() {
400 player.draw(context.draw, context.graphics);
401 }
402 }
403
404 fn multiplayer_peer_added(&mut self, _context: GameContext, peer: Peer) {
405 let player = peer.into_typed::<PlayerRole>().unwrap();
406 self.players
407 .insert(player.info.peer_id, PlayerCharacter::new(player));
408 }
409
410 fn multiplayer_peer_removed(&mut self, _context: GameContext, peer_id: PeerId) {
411 self.players.remove(&peer_id);
412 }
413
414 game_state_custom_event! {
415 trait(UniversalMultiplayerGameState)
416 }
417}
418
419impl UniversalMultiplayerGameState for State {
420 fn on_stop(&mut self, context: GameContext, _current_tick: TimeStamp) {
421 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
422 }
423
424 fn prepare_frame(&mut self, _context: GameContext, current_tick: TimeStamp) {
425 for player in self.players.values_mut() {
426 player
427 .role
428 .input_history
429 .ensure_timestamp(current_tick, Default::default);
430 player
431 .role
432 .state_history
433 .ensure_timestamp(current_tick, Default::default);
434 player
435 .role
436 .state_hash_history
437 .ensure_timestamp(current_tick, Default::default);
438 }
439 }
440
441 fn time_travel(&mut self, _context: GameContext, target_tick: TimeStamp) {
442 for player in self.players.values_mut() {
443 player.role.input_history.time_travel_to(target_tick + 1);
444 player.role.state_history.time_travel_to(target_tick);
445 player.role.state_hash_history.time_travel_to(target_tick);
446 }
447 }
448
449 fn handle_inputs(&mut self, _context: GameContext, current_tick: TimeStamp) {
450 let input_tick = current_tick + INPUT_DELAY_TICKS;
451 let input = InputSnapshot::default()
452 .with_left(self.move_left.get().is_down())
453 .with_right(self.move_right.get().is_down())
454 .with_up(self.move_up.get().is_down())
455 .with_down(self.move_down.get().is_down());
456
457 if let Some(player) = self.local_player_mut() {
458 let GgpoPlayerCommunication::Local { input_sender, .. } =
459 &mut player.role.communication
460 else {
461 return;
462 };
463
464 player.role.input_history.set(input_tick, input);
465 let since = current_tick - SEND_INPUT_WINDOW;
466 if let Some(event) = player
467 .role
468 .input_history
469 .collect_history(since..=input_tick)
470 {
471 input_sender.send(event.into()).ok();
472 }
473 }
474 }
475
476 fn find_input_divergence(
477 &mut self,
478 _context: GameContext,
479 _current_tick: TimeStamp,
480 _authority: UniversalMultiplayerAuthority,
481 ) -> Option<TimeStamp> {
482 let mut divergence = None;
483
484 for player in self.players.values_mut() {
485 if let GgpoPlayerCommunication::Remote { input_receiver, .. } =
486 &player.role.communication
487 {
488 for Dispatch { message, .. } in input_receiver.iter() {
489 player.confirmed_tick = player.confirmed_tick.max(message.now());
490 let div = player
491 .role
492 .input_history
493 .apply_history_divergence(&message)
494 .unwrap();
495 divergence = TimeStamp::possibly_oldest(divergence, div);
496 }
497 }
498 }
499
500 divergence
501 }
502
503 fn min_confirmed_tick(&mut self, _context: GameContext, current_tick: TimeStamp) -> TimeStamp {
504 self.players
505 .values()
506 .filter(|player| player.role.info.remote)
507 .map(|player| player.confirmed_tick)
508 .min()
509 .unwrap_or(current_tick)
510 }
511
512 fn detect_state_desync(&mut self, _context: GameContext, current_tick: TimeStamp) -> bool {
513 let mut desync = false;
514 let since = current_tick - SEND_STATE_HASH_WINDOW;
515 for player in self.players.values_mut() {
516 if self.send_state_timer.elapsed() >= SEND_STATE_HASH_INTERVAL
517 && let GgpoPlayerCommunication::Local {
518 state_hash_sender, ..
519 } = &player.role.communication
520 && let Some(event) = player
521 .role
522 .state_hash_history
523 .collect_history(since..=current_tick)
524 {
525 self.send_state_timer = Instant::now();
526 state_hash_sender.send(event.into()).ok();
527 }
528
529 if let GgpoPlayerCommunication::Remote {
530 state_hash_receiver,
531 ..
532 } = &player.role.communication
533 {
534 for Dispatch { message, .. } in state_hash_receiver.iter() {
535 for (tick, hash) in message.iter() {
536 if let Some(local_hash) = player.role.state_hash_history.get(tick).copied()
537 && *hash != local_hash
538 {
539 debug!(
540 "State hash mismatch at tick {:?}: local = {}, remote = {}",
541 tick, local_hash, hash
542 );
543 desync = true;
544 break;
545 }
546 }
547 }
548 }
549 }
550
551 desync
552 }
553
554 fn tick(
555 &mut self,
556 _context: GameContext,
557 current_tick: TimeStamp,
558 delta_time: f32,
559 _resimulating: bool,
560 ) {
561 let delta_time = Number::from_f32(delta_time);
562 let prev_tick = current_tick - 1;
563
564 for player in self.players.values_mut() {
565 let input = player
566 .role
567 .input_history
568 .get_extrapolated(current_tick)
569 .copied()
570 .unwrap_or_default();
571 let mut state = player
572 .role
573 .state_history
574 .get_extrapolated(prev_tick)
575 .copied()
576 .unwrap_or_default();
577
578 state.velocity_x = match (input.left(), input.right()) {
579 (true, false) => Number::from_f32(-SPEED),
580 (false, true) => Number::from_f32(SPEED),
581 _ => Number::from_f32(0.0),
582 };
583 state.velocity_y = match (input.up(), input.down()) {
584 (true, false) => Number::from_f32(-SPEED),
585 (false, true) => Number::from_f32(SPEED),
586 _ => Number::from_f32(0.0),
587 };
588
589 state.position_x += state.velocity_x * delta_time;
590 state.position_y += state.velocity_y * delta_time;
591
592 player.role.input_history.set(current_tick, input);
593 player.role.state_history.set(current_tick, state);
594 player
595 .role
596 .state_hash_history
597 .set(current_tick, hash(&state));
598 }
599 }
600
601 fn commit_frame(&mut self, _context: GameContext, current_tick: TimeStamp) {
602 for player in self.players.values_mut() {
603 let state = player
604 .role
605 .state_history
606 .get_extrapolated(current_tick)
607 .copied()
608 .unwrap_or_default();
609 player.sprite.transform.position.x = state.position_x.into_f32();
610 player.sprite.transform.position.y = state.position_y.into_f32();
611 }
612 }
613}
614
615inputs_bitstruct! {
618 #[repr(transparent)]
619 #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
620 struct InputSnapshot(u8) {
621 left: 0,
622 right: 1,
623 up: 2,
624 down: 3,
625 }
626}
627
628#[derive(Debug, Default, Clone, Copy, PartialEq, Hash)]
633struct StateSnapshot {
634 position_x: Number,
635 position_y: Number,
636 velocity_x: Number,
637 velocity_y: Number,
638}
639
640struct PlayerCharacter {
641 role: PlayerRole,
642 confirmed_tick: TimeStamp,
643 sprite: Sprite,
644}
645
646impl PlayerCharacter {
647 fn new(role: PlayerRole) -> Self {
648 Self {
649 role,
650 confirmed_tick: TimeStamp::default(),
651 sprite: Sprite::single(SpriteTexture {
652 sampler: "u_image".into(),
653 texture: TextureRef::name("ferris.png"),
654 filtering: GlowTextureFiltering::Linear,
655 })
656 .pivot(0.5.into())
657 .scale(0.25.into()),
658 }
659 }
660}
661
662impl Drawable for PlayerCharacter {
663 fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
664 self.sprite.draw(context, graphics);
665
666 Text::new(ShaderRef::name("text"))
667 .text(self.role.info.peer_id.id().to_string())
668 .font("roboto.ttf")
669 .size(20.0)
670 .position(Vec2::from(self.sprite.transform.position) - Vec2::new(0.0, 60.0))
671 .tint(if self.role.info.remote {
672 Rgba::red()
673 } else {
674 Rgba::white()
675 })
676 .horizontal_align(HorizontalAlign::Center)
677 .vertical_align(VerticalAlign::Bottom)
678 .draw(context, graphics);
679 }
680}