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 rollback::{RollbackAuthority, RollbackMultiplayer, RollbackPlayerRole},
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 peer::{Peer, PeerId, TypedPeerRole},
50 replication::primitives::RepF32,
51 },
52 tehuti_timeline::time::TimeStamp,
53 time::Duration,
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::HashMap, error::Error, sync::Arc};
64
65const ADDRESS: &str = "127.0.0.1:12345";
66const AUTHORITY_CLOCK_CHANNEL: u64 = 10;
67const PLAYER_ROLE: u64 = 1;
68const HISTORY_CAPACITY: usize = 16;
69const SERVER_SEND_STATE_INTERVAL: Duration = Duration::from_millis(1000 / 30);
70const CLIENT_SEND_INPUT_WINDOW: u64 = 4;
71const CLIENT_LEAD_TICKS: u64 = 2;
72const CLIENT_PING_INTERVAL: Duration = Duration::from_millis(250);
73const SPEED: f32 = 100.0;
74const COLOR_WHITE: Color = Color {
75 r: 1.0,
76 g: 1.0,
77 b: 1.0,
78 a: 1.0,
79};
80const COLOR_BLACK: Color = Color {
81 r: 0.0,
82 g: 0.0,
83 b: 0.0,
84 a: 1.0,
85};
86
87type PlayerRole = RollbackPlayerRole<PLAYER_ROLE, HISTORY_CAPACITY, InputSnapshot, StateSnapshot>;
88type Authority = RollbackAuthority<AUTHORITY_CLOCK_CHANNEL>;
89type Multiplayer = RollbackMultiplayer<AUTHORITY_CLOCK_CHANNEL>;
90
91fn main() -> Result<(), Box<dyn Error>> {
92 registry()
93 .with(
94 layer()
95 .with_writer(std::io::stdout)
96 .with_filter(LevelFilter::DEBUG),
97 )
98 .init();
99
100 GameLauncher::new(GameInstance::new(Preloader).setup_assets(|assets| {
101 *assets = make_directory_database("./resources/").unwrap();
102 }))
103 .title("Netcode: Authoritative rollback")
104 .config(Config::load_from_file("./resources/GameConfig.toml")?)
105 .run();
106 Ok(())
107}
108
109#[derive(Default)]
110struct Preloader;
111
112impl GameState for Preloader {
113 fn enter(&mut self, context: GameContext) {
114 context.graphics.state.color = [0.2, 0.2, 0.2, 1.0];
115 context.graphics.state.main_camera.screen_alignment = 0.5.into();
116 context.graphics.state.main_camera.scaling = CameraScaling::FitVertical(500.0);
117 context.gui.coords_map_scaling = CoordsMappingScaling::FitVertical(500.0);
118
119 context
120 .assets
121 .spawn(
122 "shader://color",
123 (ShaderAsset::new(
124 Shader::COLORED_VERTEX_2D,
125 Shader::PASS_FRAGMENT,
126 ),),
127 )
128 .unwrap();
129 context
130 .assets
131 .spawn(
132 "shader://image",
133 (ShaderAsset::new(
134 Shader::TEXTURED_VERTEX_2D,
135 Shader::TEXTURED_FRAGMENT,
136 ),),
137 )
138 .unwrap();
139 context
140 .assets
141 .spawn(
142 "shader://text",
143 (ShaderAsset::new(Shader::TEXT_VERTEX, Shader::TEXT_FRAGMENT),),
144 )
145 .unwrap();
146
147 context.assets.ensure("font://roboto.ttf").unwrap();
148 context.assets.ensure("texture://ferris.png").unwrap();
149 }
150
151 fn update(&mut self, context: GameContext, _: f32) {
152 if !context.assets.is_busy() {
153 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
154 }
155 }
156}
157
158#[derive(Default)]
159struct Lobby {
160 exit: InputActionRef,
161}
162
163impl GameState for Lobby {
164 fn enter(&mut self, context: GameContext) {
165 let pointer_x = InputAxisRef::default();
166 let pointer_y = InputAxisRef::default();
167 let pointer_trigger = InputActionRef::default();
168 self.exit = InputActionRef::default();
169
170 context.gui.interactions.inputs.pointer_position =
171 ArrayInputCombinator::new([pointer_x.clone(), pointer_y.clone()]);
172 context.gui.interactions.inputs.pointer_trigger = pointer_trigger.clone();
173
174 context.input.push_mapping(
175 InputMapping::default()
176 .consume(InputConsume::Hit)
177 .axis(VirtualAxis::MousePositionX, pointer_x)
178 .axis(VirtualAxis::MousePositionY, pointer_y)
179 .action(
180 VirtualAction::MouseButton(MouseButton::Left),
181 pointer_trigger,
182 )
183 .action(
184 VirtualAction::KeyButton(VirtualKeyCode::Escape),
185 self.exit.clone(),
186 ),
187 );
188 }
189
190 fn exit(&mut self, context: GameContext) {
191 context.input.pop_mapping();
192 }
193
194 fn update(&mut self, context: GameContext, _delta_time: f32) {
195 if let Some(multiplayer) = context.multiplayer::<Multiplayer>()
196 && multiplayer.is_initialized()
197 {
198 *context.state_change = GameStateChange::Swap(Box::new(State::default()));
199 }
200 }
201
202 fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
203 if self.exit.get().is_pressed() {
204 *context.state_change = GameStateChange::Pop;
205 }
206 }
207
208 fn draw_gui(&mut self, context: GameContext) {
209 nav_horizontal_box(NavItemActive, || {
210 if context.multiplayer::<Multiplayer>().is_some() {
211 text_box(TextBoxProps {
212 text: "ESTABLISHING CONNECTION...".to_owned(),
213 font: TextBoxFont {
214 name: "roboto.ttf".to_owned(),
215 size: 24.0,
216 },
217 color: COLOR_WHITE,
218 horizontal_align: TextBoxHorizontalAlign::Center,
219 vertical_align: TextBoxVerticalAlign::Middle,
220 ..Default::default()
221 });
222 } else {
223 let host = lobby_button("HOST");
224 let join = lobby_button("JOIN");
225
226 let is_server = if host.trigger_start() {
227 Some(true)
228 } else if join.trigger_start() {
229 Some(false)
230 } else {
231 None
232 };
233
234 if let Some(is_server) = is_server {
235 debug!(
236 "Starting {}...",
237 if is_server { "server" } else { "client" }
238 );
239
240 context.network.clear_connections();
241 let mut network = GameNetwork::new(Arc::new(
242 Authority::peer_factory(is_server)
243 .unwrap()
244 .with_typed::<PlayerRole>(),
245 ));
246 if is_server {
247 network.add_connection(TcpServerConnection::listen(ADDRESS).unwrap());
248 } else {
249 network.add_connection(TcpClientConnection::connect(ADDRESS).unwrap());
250 }
251 if let Some(multiplayer) = Multiplayer::new(&network) {
252 debug!("{} started", if is_server { "Server" } else { "Client" });
253
254 *context.network = network;
255 *context.multiplayer_change = GameMultiplayerChange::Set(Box::new(
256 multiplayer
257 .with_ticks_per_second(30)
258 .with_server_send_state_interval(SERVER_SEND_STATE_INTERVAL)
259 .with_client_lead_ticks(CLIENT_LEAD_TICKS)
260 .with_client_ping_interval(CLIENT_PING_INTERVAL),
261 ));
262 }
263 }
264 }
265 });
266 }
267}
268
269fn lobby_button(label: &str) -> ImmediateButton {
270 button(NavItemActive, |state| {
271 let (bg_color, text_color) = if state.state.selected {
272 (COLOR_WHITE, COLOR_BLACK)
273 } else {
274 (COLOR_BLACK, COLOR_WHITE)
275 };
276
277 content_box((), || {
278 image_box(ImageBoxProps::colored(bg_color));
279
280 text_box(TextBoxProps {
281 text: label.to_owned(),
282 font: TextBoxFont {
283 name: "roboto.ttf".to_owned(),
284 size: 24.0,
285 },
286 color: text_color,
287 horizontal_align: TextBoxHorizontalAlign::Center,
288 vertical_align: TextBoxVerticalAlign::Middle,
289 ..Default::default()
290 });
291 });
292 })
293}
294
295#[derive(Default)]
296struct State {
297 players: HashMap<PeerId, PlayerCharacter>,
298 move_up: InputActionRef,
299 move_down: InputActionRef,
300 move_left: InputActionRef,
301 move_right: InputActionRef,
302 exit: InputActionRef,
303}
304
305impl State {
306 fn local_player_mut(&mut self) -> Option<&mut PlayerCharacter> {
307 self.players
308 .values_mut()
309 .find(|player| !player.role.info().remote)
310 }
311}
312
313impl GameState for State {
314 fn enter(&mut self, mut context: GameContext) {
315 self.move_left = InputActionRef::default();
316 self.move_right = InputActionRef::default();
317 self.move_up = InputActionRef::default();
318 self.move_down = InputActionRef::default();
319 self.exit = InputActionRef::default();
320 context.input.push_mapping(
321 InputMapping::default()
322 .consume(InputConsume::Hit)
323 .action(
324 VirtualAction::KeyButton(VirtualKeyCode::A),
325 self.move_left.clone(),
326 )
327 .action(
328 VirtualAction::KeyButton(VirtualKeyCode::D),
329 self.move_right.clone(),
330 )
331 .action(
332 VirtualAction::KeyButton(VirtualKeyCode::W),
333 self.move_up.clone(),
334 )
335 .action(
336 VirtualAction::KeyButton(VirtualKeyCode::S),
337 self.move_down.clone(),
338 )
339 .action(
340 VirtualAction::KeyButton(VirtualKeyCode::Left),
341 self.move_left.clone(),
342 )
343 .action(
344 VirtualAction::KeyButton(VirtualKeyCode::Right),
345 self.move_right.clone(),
346 )
347 .action(
348 VirtualAction::KeyButton(VirtualKeyCode::Up),
349 self.move_up.clone(),
350 )
351 .action(
352 VirtualAction::KeyButton(VirtualKeyCode::Down),
353 self.move_down.clone(),
354 )
355 .action(
356 VirtualAction::KeyButton(VirtualKeyCode::Escape),
357 self.exit.clone(),
358 ),
359 );
360
361 if let Some(multiplayer) = context.multiplayer_mut::<Multiplayer>() {
362 multiplayer.process_lifecycle_events = true;
363 let _ = multiplayer.authority.create_peer(PlayerRole::ROLE_ID);
364 } else {
365 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
366 }
367 }
368
369 fn exit(&mut self, context: GameContext) {
370 context.input.pop_mapping();
371
372 *context.network = GameNetwork::default();
373 *context.multiplayer_change = GameMultiplayerChange::Reset;
374 }
375
376 fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
377 if self.exit.get().is_pressed() {
378 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
379 }
380 }
381
382 fn draw(&mut self, context: GameContext) {
383 for player in self.players.values() {
384 player.draw(context.draw, context.graphics);
385 }
386 }
387
388 fn multiplayer_peer_added(&mut self, _context: GameContext, peer: Peer) {
389 let player = peer.into_typed::<PlayerRole>().unwrap();
390 self.players
391 .insert(player.info().peer_id, PlayerCharacter::new(player));
392 }
393
394 fn multiplayer_peer_removed(&mut self, _context: GameContext, peer_id: PeerId) {
395 self.players.remove(&peer_id);
396 }
397
398 game_state_custom_event! {
399 trait(UniversalMultiplayerGameState)
400 }
401}
402
403impl UniversalMultiplayerGameState for State {
404 fn prepare_frame(&mut self, _context: GameContext, current_tick: TimeStamp) {
405 for player in self.players.values_mut() {
406 player
407 .role
408 .inputs_mut()
409 .ensure_timestamp(current_tick, Default::default);
410 player
411 .role
412 .state_mut()
413 .ensure_timestamp(current_tick, Default::default);
414 }
415 }
416
417 fn time_travel(&mut self, _context: GameContext, target_tick: TimeStamp) {
418 for player in self.players.values_mut() {
419 player.role.inputs_mut().time_travel_to(target_tick);
420 player.role.state_mut().time_travel_to(target_tick);
421 }
422 }
423
424 fn handle_inputs(&mut self, _context: GameContext, current_tick: TimeStamp) {
425 let input = InputSnapshot::default()
426 .with_left(self.move_left.get().is_down())
427 .with_right(self.move_right.get().is_down())
428 .with_up(self.move_up.get().is_down())
429 .with_down(self.move_down.get().is_down());
430
431 if let Some(player) = self.local_player_mut() {
432 let (input_sender, input_history) = match &mut player.role {
433 PlayerRole::ServerLocal {
434 input_sender,
435 input_history,
436 ..
437 } => (input_sender, input_history),
438 PlayerRole::ClientLocal {
439 input_sender,
440 input_history,
441 ..
442 } => (input_sender, input_history),
443 _ => return,
444 };
445
446 input_history.set(current_tick, input);
447 let since = current_tick - CLIENT_SEND_INPUT_WINDOW;
448 if let Some(event) = input_history.collect_history(since..=current_tick) {
449 input_sender.send(event.into()).ok();
450 }
451 }
452 }
453
454 fn find_input_divergence(
455 &mut self,
456 _context: GameContext,
457 _current_tick: TimeStamp,
458 _authority: UniversalMultiplayerAuthority,
459 ) -> Option<TimeStamp> {
460 let mut divergence = None;
461
462 for player in self.players.values_mut() {
463 let (input_receiver, input_history) = match &mut player.role {
464 PlayerRole::ServerRemote {
465 input_receiver,
466 input_history,
467 ..
468 } => (input_receiver, input_history),
469 PlayerRole::ClientRemote {
470 input_receiver,
471 input_history,
472 ..
473 } => (input_receiver, input_history),
474 _ => continue,
475 };
476
477 if let Some(Dispatch { message, .. }) = input_receiver.last() {
478 match input_history.apply_history_divergence(&message) {
479 Ok(div) => {
480 divergence = TimeStamp::possibly_oldest(divergence, div);
481 }
482 Err(error) => debug!(
483 "Failed to apply input history divergence for player {}: {}",
484 player.role.info().peer_id,
485 error
486 ),
487 }
488 }
489 }
490
491 divergence
492 }
493
494 fn find_state_divergence(
495 &mut self,
496 context: GameContext,
497 current_tick: TimeStamp,
498 authority: UniversalMultiplayerAuthority,
499 ) -> Option<TimeStamp> {
500 if !authority.is_client() {
501 return None;
502 }
503
504 let delta_time = context
505 .multiplayer::<Multiplayer>()
506 .unwrap()
507 .tick_delta_time;
508
509 let mut divergence = None;
510
511 for player in self.players.values_mut() {
512 match &mut player.role {
513 PlayerRole::ClientLocal {
514 state_receiver,
515 state_history,
516 ..
517 } => {
518 if let Some(Dispatch { message, .. }) = state_receiver.last() {
519 match state_history.apply_history_divergence(&message) {
520 Ok(div) => {
521 divergence = TimeStamp::possibly_oldest(divergence, div);
522 }
523 Err(error) => debug!(
524 "Failed to apply state history divergence for player {}: {}",
525 player.role.info().peer_id,
526 error
527 ),
528 }
529 }
530 }
531 PlayerRole::ClientRemote {
532 state_receiver,
533 state_history,
534 ..
535 } => {
536 if let Some(Dispatch { message, .. }) = state_receiver.last() {
537 match state_history.apply_history_divergence(&message) {
538 Ok(Some(divergence)) => {
539 if let Err(error) =
540 state_history.evolve(divergence, current_tick, |prev| {
541 let mut state = *prev;
542 state.position_x.0 += state.velocity_x.0 * delta_time;
543 state.position_y.0 += state.velocity_y.0 * delta_time;
544 Ok(state)
545 })
546 {
547 debug!(
548 "Failed to evolve state history for player {}: {}",
549 player.role.info().peer_id,
550 error
551 );
552 }
553 }
554 Ok(None) => {}
555 Err(error) => {
556 debug!(
557 "Failed to apply state history for player {}: {}",
558 player.role.info().peer_id,
559 error
560 );
561 }
562 }
563 }
564 }
565 _ => {}
566 }
567 }
568
569 divergence
570 }
571
572 fn send_state(
573 &mut self,
574 _context: GameContext,
575 current_tick: TimeStamp,
576 authority: UniversalMultiplayerAuthority,
577 ) {
578 if !authority.is_server() {
579 return;
580 }
581
582 for player in self.players.values() {
583 let (sender, history) = match &player.role {
584 PlayerRole::ServerLocal {
585 state_sender,
586 state_history,
587 ..
588 } => (state_sender, state_history),
589 PlayerRole::ServerRemote {
590 state_sender,
591 state_history,
592 ..
593 } => (state_sender, state_history),
594 _ => {
595 continue;
596 }
597 };
598
599 if let Some(event) = history.collect_snapshot(current_tick)
600 && let Err(error) = sender.send(event.into())
601 {
602 debug!(
603 "Failed to send state snapshot for player {}: {}",
604 player.role.info().peer_id,
605 error
606 );
607 }
608 }
609 }
610
611 fn tick(
612 &mut self,
613 _context: GameContext,
614 current_tick: TimeStamp,
615 delta_time: f32,
616 _resimulating: bool,
617 ) {
618 for player in self.players.values_mut() {
619 let input = player
620 .role
621 .inputs()
622 .get_extrapolated(current_tick)
623 .copied()
624 .unwrap_or_default();
625
626 let states = player.role.state_mut();
627
628 let mut state = states
629 .get_extrapolated(current_tick)
630 .copied()
631 .unwrap_or_default();
632
633 state.velocity_x.0 = match (input.left(), input.right()) {
634 (true, false) => -SPEED,
635 (false, true) => SPEED,
636 _ => 0.0,
637 };
638 state.velocity_y.0 = match (input.up(), input.down()) {
639 (true, false) => -SPEED,
640 (false, true) => SPEED,
641 _ => 0.0,
642 };
643
644 state.position_x.0 += state.velocity_x.0 * delta_time;
645 state.position_y.0 += state.velocity_y.0 * delta_time;
646
647 states.set(current_tick, state);
648 }
649 }
650
651 fn commit_frame(&mut self, _context: GameContext, current_tick: TimeStamp) {
652 for player in self.players.values_mut() {
653 let state = player
654 .role
655 .state_mut()
656 .get_extrapolated(current_tick)
657 .copied()
658 .unwrap_or_default();
659 player.sprite.transform.position.x = state.position_x.0;
660 player.sprite.transform.position.y = state.position_y.0;
661 }
662 }
663}
664
665inputs_bitstruct! {
668 #[repr(transparent)]
669 #[derive(Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
670 struct InputSnapshot(u8) {
671 left: 0,
672 right: 1,
673 up: 2,
674 down: 3,
675 }
676}
677
678#[derive(Debug, Default, Clone, Copy, PartialEq, Serialize, Deserialize)]
683struct StateSnapshot {
684 position_x: RepF32,
685 position_y: RepF32,
686 velocity_x: RepF32,
687 velocity_y: RepF32,
688}
689
690struct PlayerCharacter {
691 role: PlayerRole,
692 sprite: Sprite,
693}
694
695impl PlayerCharacter {
696 fn new(role: PlayerRole) -> Self {
697 Self {
698 role,
699 sprite: Sprite::single(SpriteTexture {
700 sampler: "u_image".into(),
701 texture: TextureRef::name("ferris.png"),
702 filtering: GlowTextureFiltering::Linear,
703 })
704 .pivot(0.5.into())
705 .scale(0.25.into()),
706 }
707 }
708}
709
710impl Drawable for PlayerCharacter {
711 fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
712 self.sprite.draw(context, graphics);
713
714 Text::new(ShaderRef::name("text"))
715 .text(self.role.info().peer_id.id().to_string())
716 .font("roboto.ttf")
717 .size(20.0)
718 .position(Vec2::from(self.sprite.transform.position) - Vec2::new(0.0, 60.0))
719 .tint(if self.role.info().remote {
720 Rgba::red()
721 } else {
722 Rgba::white()
723 })
724 .horizontal_align(HorizontalAlign::Center)
725 .vertical_align(VerticalAlign::Bottom)
726 .draw(context, graphics);
727 }
728}