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