Skip to main content

rollback/
rollback.rs

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
665// Inputs snapshot should only contain input down states, from which simulation
666// will deduce detailed changes between consecutive ticks.
667inputs_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// State snapshots should store information required for simulation evolution,
679// so not only positions, but also velocities for example.
680// If we will only send positions, players would not be able to predict
681// their movement!
682#[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}