nimble_assent/lib.rs
1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/nimble-rust/nimble
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5/*!
6
7`nimble-assent` is a library designed for deterministic simulation of game logic based on player input.
8It operates on the concept of "steps" (or actions) taken by players and ensures these steps are applied
9in a specific, predictable order. This library integrates smoothly with deterministic simulations, ensuring
10that all participants in a networked game that receive and process the same steps in the same order, yield
11identical results.
12
13## Why "Assent"?
14
15The name "Assent" was chosen because it reflects the concept of agreement or concurrence.
16In a deterministic simulation, especially for multiplayer games, it is crucial that all parties
17(the players and the host) are in complete agreement on the sequence of steps or actions taken.
18In this context, "assent" represents the system's role in
19enforcing an authoritative and agreed-upon sequence of events, ensuring that everyone shares
20the same view of the game state at every step.
21
22## Overview
23
24The main structure in this crate is the `Assent` struct, which handles the simulation of player input
25(called "steps") over a series of game ticks. The crate is designed to:
26
27- Queue player inputs (steps) with associated tick IDs.
28- Apply these inputs consistently across all participants in the simulation.
29- Limit the number of ticks processed per update to avoid overloading the system.
30
31The crate also provides a customizable callback mechanism ([`AssentCallback`]) that allows developers
32to hook into different stages of the update cycle, enabling detailed control over how steps are processed.
33
34*/
35
36pub mod prelude;
37
38use std::fmt::{Debug, Display};
39use std::marker::PhantomData;
40
41use log::trace;
42use tick_id::TickId;
43use tick_queue::{Queue, QueueError};
44
45/// A trait representing callbacks for the `Assent` simulation.
46///
47/// This trait defines hooks for handling steps at different stages of a game update cycle.
48pub trait AssentCallback<CombinedStepT> {
49 /// Called before any ticks are processed.
50 fn on_pre_ticks(&mut self) {}
51
52 /// Called for each tick with the corresponding step.
53 fn on_tick(&mut self, step: &CombinedStepT);
54
55 /// Called after all ticks have been processed.
56 fn on_post_ticks(&mut self) {}
57}
58
59/// Enum representing the state of an update cycle in the `Assent` simulation.
60#[derive(Debug, PartialEq, Eq)]
61pub enum UpdateState {
62 ConsumedAllKnowledge,
63 DidNotConsumeAllKnowledge,
64 NoKnowledge,
65}
66
67/// Configuration settings for controlling the behavior of the `Assent` simulation.
68#[derive(Debug, Copy, Clone)]
69pub struct Settings {
70 pub max_tick_count_per_update: usize,
71}
72
73impl Default for Settings {
74 fn default() -> Self {
75 Self {
76 max_tick_count_per_update: 5,
77 }
78 }
79}
80
81/// Main struct for managing and processing player steps (actions) in a deterministic simulation.
82#[derive(Debug)]
83pub struct Assent<C, CombinedStepT>
84where
85 C: AssentCallback<CombinedStepT>,
86{
87 phantom: PhantomData<C>,
88 settings: Settings,
89 steps: Queue<CombinedStepT>,
90}
91
92impl<C, CombinedStepT> Default for Assent<C, CombinedStepT>
93where
94 C: AssentCallback<CombinedStepT>,
95 CombinedStepT: Clone + Debug + Display,
96{
97 fn default() -> Self {
98 Self::new(Settings::default())
99 }
100}
101
102impl<C, CombinedStepT> Assent<C, CombinedStepT>
103where
104 C: AssentCallback<CombinedStepT>,
105 CombinedStepT: Clone + Debug + Display,
106{
107 #[must_use]
108 pub fn new(settings: Settings) -> Self {
109 Self {
110 phantom: PhantomData {},
111 steps: Queue::default(),
112 settings,
113 }
114 }
115
116 /// Adds a new step to be processed for the given `tick_id`.
117 ///
118 /// # Errors
119 ///
120 /// Returns an error if the step cannot be added.
121 pub fn push(&mut self, tick_id: TickId, steps: CombinedStepT) -> Result<(), QueueError> {
122 self.steps.push(tick_id, steps)
123 }
124
125 /// Returns the next expected `TickId` for inserting new steps.
126 #[must_use]
127 pub const fn next_expected_tick_id(&self) -> TickId {
128 self.steps.expected_write_tick_id()
129 }
130
131 /// Returns the most recent `TickId`, or `None` if no steps have been added.
132 #[must_use]
133 pub fn end_tick_id(&self) -> Option<TickId> {
134 self.steps.back_tick_id()
135 }
136
137 /// Returns a reference to the underlying steps for debugging purposes.
138 #[must_use]
139 pub const fn debug_steps(&self) -> &Queue<CombinedStepT> {
140 &self.steps
141 }
142
143 /// Processes available steps, invoking the provided callback for each step.
144 ///
145 /// This method processes up to `max_ticks_per_update` steps (ticks) and returns an
146 /// `UpdateState` indicating whether all steps were processed or if some remain.
147 #[must_use]
148 pub fn update(&mut self, callback: &mut C) -> UpdateState {
149 if self.steps.is_empty() {
150 trace!("notice: assent steps are empty");
151 return UpdateState::NoKnowledge;
152 }
153
154 callback.on_pre_ticks();
155 trace!("tick start. {} steps in queue.", self.steps.len());
156 let mut count = 0;
157 while let Some(combined_step_info) = self.steps.pop() {
158 trace!("tick: {}", &combined_step_info);
159 callback.on_tick(&combined_step_info.item);
160 count += 1;
161 if count >= self.settings.max_tick_count_per_update {
162 trace!("encountered threshold, not simulating all ticks");
163 return UpdateState::DidNotConsumeAllKnowledge;
164 }
165 }
166
167 trace!("consumed all knowledge");
168 UpdateState::ConsumedAllKnowledge
169 }
170}