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,
8 multiplayer::{
9 GameMultiplayerChange, GameNetwork,
10 client_server::ClientServerMultiplayer,
11 tcp::{TcpClientConnection, TcpServerConnection},
12 universal::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, CardinalInputCombinator, InputActionRef, InputAxisRef,
45 InputConsume, InputMapping, MouseButton, VirtualAction, VirtualAxis,
46 },
47 tehuti::{
48 channel::{ChannelId, ChannelMode},
49 codec::postcard::PostcardCodec,
50 event::{Receiver, unbounded},
51 peer::{Peer, PeerBuilder, PeerId, PeerInfo, PeerRoleId, TypedPeer, TypedPeerRole},
52 replica::{
53 Replica, ReplicaApplyChanges, ReplicaCollectChanges, ReplicaId, ReplicationBuffer,
54 },
55 replication::{HashReplicated, primitives::RepF32},
56 },
57 tehuti_client_server::{
58 authority::PureAuthority,
59 controller::{Controller, ControllerEvent},
60 puppet::{Puppet, Puppetable},
61 },
62 tehuti_timeline::time::TimeStamp,
63 tracing::{debug, level_filters::LevelFilter},
64 tracing_subscriber::{
65 Layer, fmt::layer, layer::SubscriberExt, registry, util::SubscriberInitExt,
66 },
67 vek::{Rgba, Vec2},
68 windowing::event::VirtualKeyCode,
69 },
70};
71use std::{collections::HashMap, error::Error, sync::Arc};
72
73const ADDRESS: &str = "127.0.0.1:12345";
74const PLAYER_ROLE: PeerRoleId = PeerRoleId::new(1);
75const PLAYER_EVENT_CHANNEL: ChannelId = ChannelId::new(0);
76const PLAYER_CHANGE_CHANNEL: ChannelId = ChannelId::new(1);
77const SPEED: f32 = 100.0;
78const COLOR_WHITE: Color = Color {
79 r: 1.0,
80 g: 1.0,
81 b: 1.0,
82 a: 1.0,
83};
84const COLOR_BLACK: Color = Color {
85 r: 0.0,
86 g: 0.0,
87 b: 0.0,
88 a: 1.0,
89};
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: Replicated Client-Server")
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::<ClientServerMultiplayer>()
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::<ClientServerMultiplayer>().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 PureAuthority::peer_factory(is_server)
243 .unwrap()
244 .with_typed::<PlayerController>(),
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) = ClientServerMultiplayer::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.with_ticks_per_second(30),
257 ));
258 }
259 }
260 }
261 });
262 }
263}
264
265fn lobby_button(label: &str) -> ImmediateButton {
266 button(NavItemActive, |state| {
267 let (bg_color, text_color) = if state.state.selected {
268 (COLOR_WHITE, COLOR_BLACK)
269 } else {
270 (COLOR_BLACK, COLOR_WHITE)
271 };
272
273 content_box((), || {
274 image_box(ImageBoxProps::colored(bg_color));
275
276 text_box(TextBoxProps {
277 text: label.to_owned(),
278 font: TextBoxFont {
279 name: "roboto.ttf".to_owned(),
280 size: 24.0,
281 },
282 color: text_color,
283 horizontal_align: TextBoxHorizontalAlign::Center,
284 vertical_align: TextBoxVerticalAlign::Middle,
285 ..Default::default()
286 });
287 });
288 })
289}
290
291#[derive(Default)]
292struct State {
293 players: HashMap<PeerId, Player>,
294 movement: CardinalInputCombinator,
295 exit: InputActionRef,
296}
297
298impl GameState for State {
299 fn enter(&mut self, mut context: GameContext) {
300 let move_left = InputActionRef::default();
301 let move_right = InputActionRef::default();
302 let move_up = InputActionRef::default();
303 let move_down = InputActionRef::default();
304 self.movement = CardinalInputCombinator::new(
305 move_left.clone(),
306 move_right.clone(),
307 move_up.clone(),
308 move_down.clone(),
309 );
310 context.input.push_mapping(
311 InputMapping::default()
312 .consume(InputConsume::Hit)
313 .action(
314 VirtualAction::KeyButton(VirtualKeyCode::A),
315 move_left.clone(),
316 )
317 .action(
318 VirtualAction::KeyButton(VirtualKeyCode::D),
319 move_right.clone(),
320 )
321 .action(VirtualAction::KeyButton(VirtualKeyCode::W), move_up.clone())
322 .action(
323 VirtualAction::KeyButton(VirtualKeyCode::S),
324 move_down.clone(),
325 )
326 .action(VirtualAction::KeyButton(VirtualKeyCode::Left), move_left)
327 .action(VirtualAction::KeyButton(VirtualKeyCode::Right), move_right)
328 .action(VirtualAction::KeyButton(VirtualKeyCode::Up), move_up)
329 .action(VirtualAction::KeyButton(VirtualKeyCode::Down), move_down)
330 .action(
331 VirtualAction::KeyButton(VirtualKeyCode::Escape),
332 self.exit.clone(),
333 ),
334 );
335
336 if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
337 multiplayer.process_lifecycle_events = true;
338 let _ = multiplayer.authority.create_peer(PlayerController::ROLE_ID);
339 } else {
340 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
341 }
342 }
343
344 fn exit(&mut self, context: GameContext) {
345 context.input.pop_mapping();
346
347 *context.network = GameNetwork::default();
348 *context.multiplayer_change = GameMultiplayerChange::Reset;
349 }
350
351 fn fixed_update(&mut self, context: GameContext, _delta_time: f32) {
352 if self.exit.get().is_pressed() {
353 *context.state_change = GameStateChange::Swap(Box::new(Lobby::default()));
354 }
355 }
356
357 fn draw(&mut self, context: GameContext) {
358 for player in self.players.values() {
359 if let Some(character) = &player.character {
360 character.draw(context.draw, context.graphics);
361 }
362 }
363 }
364
365 fn multiplayer_peer_added(&mut self, mut context: GameContext, peer: Peer) {
366 let (replica_added_sender, replica_added_receiver) = unbounded();
367 let (replica_removed_sender, replica_removed_receiver) = unbounded();
368
369 let mut controller = Controller::new(
370 peer,
371 PLAYER_EVENT_CHANNEL,
372 Some(PLAYER_CHANGE_CHANNEL),
373 None,
374 replica_added_sender,
375 replica_removed_sender,
376 )
377 .unwrap();
378
379 if let Some(multiplayer) = context.multiplayer_mut::<ClientServerMultiplayer>() {
380 let _ = controller.create_replica(&mut multiplayer.authority);
381 }
382
383 for player in self.players.values_mut() {
384 if let Some(character) = &mut player.character {
385 character.request_full_snapshot();
386 }
387 }
388
389 self.players.insert(
390 controller.info().peer_id,
391 Player {
392 controller,
393 character: None,
394 replica_added_receiver,
395 replica_removed_receiver,
396 },
397 );
398 }
399
400 fn multiplayer_peer_removed(&mut self, _context: GameContext, peer_id: PeerId) {
401 self.players.remove(&peer_id);
402 }
403
404 game_state_custom_event! {
405 trait(UniversalMultiplayerGameState)
406 }
407}
408
409impl UniversalMultiplayerGameState for State {
410 fn handle_inputs(&mut self, _context: GameContext, _current_tick: TimeStamp) {
411 let [x, y] = self.movement.get();
412 let movement = Vec2::new(x, y).try_normalized().unwrap_or_default() * SPEED;
413
414 for player in self.players.values_mut() {
415 if !player.controller.info().remote
416 && let Some(character) = &mut player.character
417 {
418 character.velocity_x.0 = movement.x;
419 character.velocity_y.0 = movement.y;
420 }
421 }
422 }
423
424 fn tick(
425 &mut self,
426 _context: GameContext,
427 _current_tick: TimeStamp,
428 delta_time: f32,
429 _resimulating: bool,
430 ) {
431 for player in self.players.values_mut() {
432 player.tick(delta_time);
433 }
434 }
435}
436
437struct Character {
438 peer_info: PeerInfo,
439 sprite: Sprite,
440 position_x: HashReplicated<RepF32>,
441 position_y: HashReplicated<RepF32>,
442 velocity_x: HashReplicated<RepF32>,
443 velocity_y: HashReplicated<RepF32>,
444}
445
446impl Character {
447 fn new(peer_info: PeerInfo, position: Vec2<f32>) -> Self {
448 Self {
449 peer_info,
450 sprite: Sprite::single(SpriteTexture {
451 sampler: "u_image".into(),
452 texture: TextureRef::name("ferris.png"),
453 filtering: GlowTextureFiltering::Linear,
454 })
455 .pivot(0.5.into())
456 .position(position)
457 .scale(0.25.into()),
458 position_x: HashReplicated::new(RepF32(position.x)),
459 position_y: HashReplicated::new(RepF32(position.y)),
460 velocity_x: Default::default(),
461 velocity_y: Default::default(),
462 }
463 }
464
465 fn tick_before(&mut self, delta_time: f32) {
466 self.position_x.0 += self.velocity_x.0 * delta_time;
467 self.position_y.0 += self.velocity_y.0 * delta_time;
468 }
469
470 fn tick_after(&mut self) {
471 self.sprite.transform.position.x = self.position_x.0;
472 self.sprite.transform.position.y = self.position_y.0;
473 }
474}
475
476impl Drawable for Character {
477 fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
478 self.sprite.draw(context, graphics);
479
480 Text::new(ShaderRef::name("text"))
481 .text(self.peer_info.peer_id.id().to_string())
482 .font("roboto.ttf")
483 .size(20.0)
484 .position(Vec2::from(self.sprite.transform.position) - Vec2::new(0.0, 60.0))
485 .tint(if self.peer_info.remote {
486 Rgba::red()
487 } else {
488 Rgba::white()
489 })
490 .horizontal_align(HorizontalAlign::Center)
491 .vertical_align(VerticalAlign::Bottom)
492 .draw(context, graphics);
493 }
494}
495
496impl Puppetable for Character {
497 fn collect_changes(
498 &mut self,
499 mut collector: ReplicaCollectChanges,
500 full_snapshot: bool,
501 ) -> Result<(), Box<dyn Error>> {
502 if full_snapshot {
503 HashReplicated::mark_changed(&mut self.position_x);
504 HashReplicated::mark_changed(&mut self.position_y);
505 }
506
507 collector
508 .scope()
509 .maybe_collect_replicated::<0, _, _>(&self.position_x)?;
510 collector
511 .scope()
512 .maybe_collect_replicated::<1, _, _>(&self.position_y)?;
513 collector
514 .scope()
515 .maybe_collect_replicated::<2, _, _>(&self.velocity_x)?;
516 collector
517 .scope()
518 .maybe_collect_replicated::<3, _, _>(&self.velocity_y)?;
519
520 Ok(())
521 }
522
523 fn apply_changes(&mut self, mut applicator: ReplicaApplyChanges) -> Result<(), Box<dyn Error>> {
524 applicator
525 .scope()
526 .maybe_apply_replicated::<0, _, _>(&mut self.position_x)?;
527 applicator
528 .scope()
529 .maybe_apply_replicated::<1, _, _>(&mut self.position_y)?;
530 applicator
531 .scope()
532 .maybe_apply_replicated::<2, _, _>(&mut self.velocity_x)?;
533 applicator
534 .scope()
535 .maybe_apply_replicated::<3, _, _>(&mut self.velocity_y)?;
536
537 self.sprite.transform.position.x = self.position_x.0;
538 self.sprite.transform.position.y = self.position_y.0;
539
540 Ok(())
541 }
542}
543
544struct Player {
545 controller: Controller,
546 character: Option<Puppet<Character>>,
547 replica_added_receiver: Receiver<Replica>,
548 replica_removed_receiver: Receiver<ReplicaId>,
549}
550
551impl Player {
552 fn tick(&mut self, delta_time: f32) {
553 let _ = self.controller.maintain();
554
555 for replica in self.replica_added_receiver.iter() {
556 self.character = Some(Puppet::new(
557 replica,
558 Character::new(*self.controller.info(), 0.0.into()),
559 ));
560 }
561
562 for _ in self.replica_removed_receiver.iter() {
563 self.character = None;
564 }
565
566 if let Some(character) = &mut self.character {
567 character.tick_before(delta_time);
568 }
569
570 if let Some(puppet) = &mut self.character {
571 let _ = puppet.replicate();
572 }
573
574 if let Some(character) = &mut self.character {
575 character.tick_after();
576 }
577 }
578}
579
580struct PlayerController;
581
582impl TypedPeerRole for PlayerController {
583 const ROLE_ID: PeerRoleId = PLAYER_ROLE;
584}
585
586impl TypedPeer for PlayerController {
587 fn builder(builder: PeerBuilder) -> Result<PeerBuilder, Box<dyn Error>> {
588 if builder.info().remote {
589 Ok(builder
590 .bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
591 PLAYER_EVENT_CHANNEL,
592 ChannelMode::ReliableOrdered,
593 None,
594 )
595 .bind_read::<ReplicationBuffer, ReplicationBuffer>(
596 PLAYER_CHANGE_CHANNEL,
597 ChannelMode::ReliableOrdered,
598 None,
599 ))
600 } else {
601 Ok(builder
602 .bind_read_write::<PostcardCodec<ControllerEvent>, ControllerEvent>(
603 PLAYER_EVENT_CHANNEL,
604 ChannelMode::ReliableOrdered,
605 None,
606 )
607 .bind_write::<ReplicationBuffer, ReplicationBuffer>(
608 PLAYER_CHANGE_CHANNEL,
609 ChannelMode::ReliableOrdered,
610 None,
611 ))
612 }
613 }
614}