pub struct WorldSnapshot {
pub people: Vec<Person>,
pub governments: Vec<Government>,
pub territories: Vec<Territory>,
pub routes: Vec<Route>,
pub armies: Vec<Army>,
pub letters: Vec<LetterCargo>,
}Fields§
§people: Vec<Person>§governments: Vec<Government>§territories: Vec<Territory>§routes: Vec<Route>§armies: Vec<Army>§letters: Vec<LetterCargo>Implementations§
Source§impl WorldSnapshot
impl WorldSnapshot
pub fn entities(&self) -> Vec<EntityRef>
pub fn person(&self, id: PersonId) -> Option<&Person>
pub fn government(&self, id: GovernmentId) -> Option<&Government>
pub fn territory(&self, id: TerritoryId) -> Option<&Territory>
pub fn route(&self, id: RouteId) -> Option<&Route>
Sourcepub fn army(&self, id: ArmyId) -> Option<&Army>
pub fn army(&self, id: ArmyId) -> Option<&Army>
Examples found in repository?
examples/continuous_game_loop.rs (line 121)
120 fn from_world(world: &WorldSnapshot, army: ArmyId) -> Self {
121 let army = world.army(army).expect("the demo army must exist");
122 Self {
123 authoritative_location: army.location,
124 last_authoritative_transit: army.transit.clone(),
125 visual_progress: None,
126 }
127 }
128
129 fn refresh_from_authority(&mut self, world: &WorldSnapshot, army: ArmyId) {
130 let army = world.army(army).expect("the demo army must exist");
131 self.authoritative_location = army.location;
132 if let Some(transit) = &army.transit {
133 self.last_authoritative_transit = Some(transit.clone());
134 }
135 }
136
137 fn render(&mut self, presentation_time: PresentationTime) -> Option<f64> {
138 self.visual_progress = self.last_authoritative_transit.as_ref().map(|transit| {
139 if presentation_time.canonical < transit.departed_at {
140 return 0.0;
141 }
142 let elapsed = std_duration(presentation_time.canonical - transit.departed_at)
143 .checked_add(presentation_time.sub_minute)
144 .expect("presentation elapsed time must remain representable");
145 let travel = std_duration(transit.arrives_at - transit.departed_at);
146 (elapsed.as_secs_f64() / travel.as_secs_f64()).clamp(0.0, 1.0)
147 });
148 self.visual_progress
149 }
150
151 fn describe(&self) -> String {
152 match (&self.last_authoritative_transit, self.visual_progress) {
153 (Some(transit), Some(progress)) => format!(
154 "army visual progress={:.1}% ({} -> {}, authoritative_location={})",
155 progress * 100.0,
156 transit.from,
157 transit.to,
158 self.authoritative_location,
159 ),
160 _ => format!("army stationary at {}", self.authoritative_location),
161 }
162 }
163}
164
165/// The authoritative simulation clock and the render clock deliberately live
166/// in different fields. Only whole simulation minutes cross the Canwu API.
167struct GameHost {
168 authority: Canwu,
169 ids: DemoIds,
170 presentation: PresentationState,
171 accumulated_sim_nanos: u128,
172 wall_elapsed: Duration,
173 frame: u64,
174 reported_events: usize,
175 next_progress_milestone: u8,
176 canonical_boundaries: usize,
177 verbose: bool,
178}
179
180impl GameHost {
181 fn new(verbose: bool) -> Result<Self, CanwuError> {
182 let authority = Canwu::demo(35)?;
183 let ids = Canwu::demo_ids();
184 let presentation = PresentationState::from_world(&authority.world(), ids.army);
185 Ok(Self {
186 authority,
187 ids,
188 presentation,
189 accumulated_sim_nanos: 0,
190 wall_elapsed: Duration::ZERO,
191 frame: 0,
192 reported_events: 0,
193 next_progress_milestone: 25,
194 canonical_boundaries: 0,
195 verbose,
196 })
197 }
198
199 fn run_phase(&mut self, phase: SpeedPhase, profile: FrameProfile) -> Result<(), CanwuError> {
200 let mut remaining_millis = phase.wall_millis;
201 let mut pattern_index = 0;
202 let mut first_frame = true;
203
204 while remaining_millis > 0 {
205 let frame_millis = profile.frame_millis[pattern_index % profile.frame_millis.len()]
206 .min(remaining_millis);
207 remaining_millis -= frame_millis;
208 self.render_frame(
209 Duration::from_millis(frame_millis),
210 phase.speed,
211 first_frame || remaining_millis == 0,
212 )?;
213 first_frame = false;
214 pattern_index += 1;
215 }
216 Ok(())
217 }
218
219 fn render_frame(
220 &mut self,
221 wall_dt: Duration,
222 speed: GameSpeed,
223 phase_edge: bool,
224 ) -> Result<(), CanwuError> {
225 self.frame += 1;
226 self.wall_elapsed += wall_dt;
227
228 let converted_wall_nanos = wall_dt.as_nanos() * BASE_SIM_SECONDS_PER_WALL_SECOND;
229 self.accumulated_sim_nanos += converted_wall_nanos * speed.multiplier();
230
231 let mut authority_changed = false;
232 while self.accumulated_sim_nanos >= SIMULATION_MINUTE_NANOS {
233 let receipts = self.authority.advance_canonical(SIMULATION_QUANTUM)?;
234 self.canonical_boundaries += receipts.len();
235 authority_changed |= !receipts.is_empty();
236 self.accumulated_sim_nanos -= SIMULATION_MINUTE_NANOS;
237 }
238
239 if authority_changed {
240 self.refresh_presentation_from_authority();
241 }
242
243 let progress = self.presentation.render(self.presentation_time());
244 let crossed_milestone = self.crossed_progress_milestone(progress);
245 let has_new_events = self.reported_events < self.authority.events().len();
246
247 if self.verbose && (phase_edge || crossed_milestone || has_new_events) {
248 self.print_frame(speed);
249 }
250 self.report_new_events();
251 Ok(())
252 }
253
254 /// This host never backdates mid-quantum input. It deterministically rounds
255 /// up to the next representable Canwu minute; another host may choose a
256 /// different policy, but it must apply the same policy across render FPS.
257 fn submit_player_command(&mut self) -> Result<(), CanwuError> {
258 let captured_at = self.presentation_time();
259 let due_at = if self.accumulated_sim_nanos == 0 {
260 self.authority.time()
261 } else {
262 self.authority
263 .time()
264 .checked_add(SIMULATION_QUANTUM)
265 .expect("the scripted command time must remain representable")
266 };
267 let envelope = CommandEnvelope::new(
268 Issuer::Actor(self.ids.commander),
269 Command::OrderMovement {
270 subject: EntityRef::Army(self.ids.army),
271 destination: self.ids.eastern_territory,
272 cargo: Vec::new(),
273 },
274 )
275 .at_time(due_at);
276 let request = CommandRequest::new(
277 CommandRequestId::new(1),
278 self.authority.revision(),
279 envelope,
280 );
281 let ingress = self.authority.enqueue_command(due_at, 0, request)?;
282
283 if self.verbose {
284 println!(
285 "INPUT: queued commander move as {:?}, captured_at={captured_at}, quantized_due_at={due_at}",
286 ingress.ingress_id,
287 );
288 }
289
290 // This drains ingress due at the current canonical time only. The
291 // mid-quantum command remains queued until normal accumulation reaches
292 // its quantized minute; future scheduled work is never jumped to.
293 let receipts = self.authority.advance_canonical(SimDuration::ZERO)?;
294 self.canonical_boundaries += receipts.len();
295 self.refresh_presentation_from_authority();
296 self.presentation.render(self.presentation_time());
297 if self.verbose {
298 self.print_frame(GameSpeed::X1);
299 }
300 self.report_new_events();
301 Ok(())
302 }
303
304 fn refresh_presentation_from_authority(&mut self) {
305 self.presentation
306 .refresh_from_authority(&self.authority.world(), self.ids.army);
307 }
308
309 fn presentation_time(&self) -> PresentationTime {
310 PresentationTime {
311 canonical: self.authority.time(),
312 sub_minute: Duration::from_nanos(
313 u64::try_from(self.accumulated_sim_nanos)
314 .expect("the sub-minute accumulator must fit a Duration"),
315 ),
316 }
317 }
318
319 fn crossed_progress_milestone(&mut self, progress: Option<f64>) -> bool {
320 let Some(progress) = progress else {
321 return false;
322 };
323 let mut crossed = false;
324 while self.next_progress_milestone <= 100
325 && progress * 100.0 + f64::EPSILON >= f64::from(self.next_progress_milestone)
326 {
327 crossed = true;
328 self.next_progress_milestone += 25;
329 }
330 crossed
331 }
332
333 fn print_frame(&self, speed: GameSpeed) {
334 println!(
335 "frame={:>5} speed={:<6} wall={:>7.3}s visual_time={} canwu_time={} {}",
336 self.frame,
337 speed.label(),
338 self.wall_elapsed.as_secs_f64(),
339 self.presentation_time(),
340 self.authority.time(),
341 self.presentation.describe(),
342 );
343 }
344
345 fn report_new_events(&mut self) {
346 let events = self.authority.events();
347 if self.verbose {
348 for event in &events[self.reported_events..] {
349 println!(
350 "EVENT: at={} type={} {}",
351 event.timestamp,
352 event.kind.qualified_event_type(),
353 event.summary,
354 );
355 }
356 }
357 self.reported_events = events.len();
358 }
359
360 fn outcome(self, profile: FrameProfile) -> Result<RunOutcome, CanwuError> {
361 Ok(RunOutcome {
362 profile: profile.name,
363 frames: self.frame,
364 time: self.authority.time(),
365 world: self.authority.world(),
366 events: self.authority.events().to_vec(),
367 commands: self.authority.commands().to_vec(),
368 authoritative_state_hash: self.authority.authoritative_state_hash()?,
369 checkpoint_hash: self.authority.checkpoint_hash().to_owned(),
370 canonical_boundaries: self.canonical_boundaries,
371 })
372 }
373}
374
375struct RunOutcome {
376 profile: &'static str,
377 frames: u64,
378 time: SimTime,
379 world: WorldSnapshot,
380 events: Vec<SimEvent>,
381 commands: Vec<CommandRecord>,
382 authoritative_state_hash: String,
383 checkpoint_hash: String,
384 canonical_boundaries: usize,
385}
386
387fn run(profile: FrameProfile, verbose: bool) -> Result<RunOutcome, CanwuError> {
388 let mut host = GameHost::new(verbose)?;
389 if verbose {
390 println!("\n{} render loop", profile.name);
391 }
392
393 host.run_phase(SPEED_SCHEDULE[0], profile)?;
394 host.submit_player_command()?;
395 host.run_phase(SPEED_SCHEDULE[1], profile)?;
396 host.run_phase(SPEED_SCHEDULE[2], profile)?;
397
398 let time_before_pause = host.authority.time();
399 let remainder_before_pause = host.accumulated_sim_nanos;
400 let presentation_before_pause = host.presentation_time();
401 let frames_before_pause = host.frame;
402 assert!(
403 remainder_before_pause > 0,
404 "the pause fixture must preserve a fractional simulation minute"
405 );
406 host.run_phase(SPEED_SCHEDULE[3], profile)?;
407 assert!(
408 host.frame > frames_before_pause,
409 "pause must keep rendering"
410 );
411 assert_eq!(
412 host.authority.time(),
413 time_before_pause,
414 "pause must not advance Canwu"
415 );
416 assert_eq!(
417 host.accumulated_sim_nanos, remainder_before_pause,
418 "pause must not add desired simulation time"
419 );
420 assert_eq!(
421 host.presentation_time(),
422 presentation_before_pause,
423 "pause must freeze presentation time"
424 );
425
426 host.run_phase(SPEED_SCHEDULE[4], profile)?;
427 assert_eq!(
428 host.accumulated_sim_nanos, 0,
429 "the scripted schedule should end on a simulation-minute boundary"
430 );
431 host.outcome(profile)
432}
433
434fn std_duration(duration: SimDuration) -> Duration {
435 let minutes = u64::try_from(duration.as_minutes())
436 .expect("presentation interpolation requires a non-negative duration");
437 Duration::from_secs(
438 minutes
439 .checked_mul(60)
440 .expect("presentation duration must remain representable"),
441 )
442}
443
444fn assert_fps_independent(left: &RunOutcome, right: &RunOutcome) {
445 assert_ne!(
446 left.frames, right.frames,
447 "the render segmentations must differ"
448 );
449 assert_eq!(left.time, right.time, "Canwu time changed with render FPS");
450 assert_eq!(
451 left.world, right.world,
452 "world state changed with render FPS"
453 );
454 assert_eq!(left.events, right.events, "events changed with render FPS");
455 assert_eq!(
456 left.commands, right.commands,
457 "command ordering changed with render FPS"
458 );
459 assert_eq!(
460 left.authoritative_state_hash, right.authoritative_state_hash,
461 "authoritative state hash changed with render FPS"
462 );
463 assert_eq!(
464 left.checkpoint_hash, right.checkpoint_hash,
465 "checkpoint hash changed with render FPS"
466 );
467 assert_eq!(
468 left.canonical_boundaries, right.canonical_boundaries,
469 "canonical boundary count changed with render FPS"
470 );
471}
472
473fn assert_expected_outcome(outcome: &RunOutcome) {
474 let ids = Canwu::demo_ids();
475 let command_at = SimTime::from_minutes(1);
476 let arrival_at = SimTime::from_minutes(1_081);
477 let final_time = SimTime::from_minutes(1_082);
478
479 assert_eq!(outcome.time, final_time, "the scripted final time changed");
480 let army = outcome
481 .world
482 .army(ids.army)
483 .expect("the demo army must remain present");
484 assert_eq!(
485 army.location, ids.eastern_territory,
486 "the scheduled arrival must execute"
487 );
488 assert!(
489 army.transit.is_none(),
490 "the army must no longer be in authoritative transit"
491 );
492
493 assert_eq!(
494 outcome.commands.len(),
495 1,
496 "the move command must be accepted"
497 );
498 let command = &outcome.commands[0];
499 assert_eq!(command.accepted_at, command_at);
500 assert_eq!(command.envelope.expected_time, Some(command_at));
501 assert!(matches!(
502 &command.envelope.command,
503 Command::OrderMovement {
504 subject: EntityRef::Army(army),
505 destination,
506 cargo,
507 } if *army == ids.army
508 && *destination == ids.eastern_territory
509 && cargo.is_empty()
510 ));
511
512 let event_timeline = outcome
513 .events
514 .iter()
515 .map(|event| (event.kind.qualified_event_type(), event.timestamp))
516 .collect::<Vec<_>>();
517 assert_eq!(
518 event_timeline,
519 vec![
520 ("move_ordered".to_owned(), command_at),
521 ("army_arrived".to_owned(), arrival_at),
522 ("knowledge_updated".to_owned(), arrival_at),
523 ("report_dispatched".to_owned(), arrival_at),
524 ],
525 "the expected command and scheduled-arrival event timeline changed"
526 );
527 assert_eq!(
528 outcome.canonical_boundaries, 2,
529 "the command and arrival should settle at two canonical boundaries"
530 );
531}pub fn letter(&self, id: LetterId) -> Option<&LetterCargo>
pub fn route_between( &self, from: TerritoryId, to: TerritoryId, ) -> Option<&Route>
pub fn adjacent_territories( &self, territory: TerritoryId, ) -> impl Iterator<Item = TerritoryId>
pub fn travel_duration( &self, from: TerritoryId, to: TerritoryId, ) -> Option<SimDuration>
pub fn distance(&self, from: TerritoryId, to: TerritoryId) -> Option<f32>
pub fn territories_within( &self, center: TerritoryId, radius: f32, ) -> impl Iterator<Item = &Territory>
Trait Implementations§
Source§impl Clone for WorldSnapshot
impl Clone for WorldSnapshot
Source§fn clone(&self) -> WorldSnapshot
fn clone(&self) -> WorldSnapshot
Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§fn clone_from(&mut self, source: &Self)
fn clone_from(&mut self, source: &Self)
Performs copy-assignment from
source. Read moreSource§impl Debug for WorldSnapshot
impl Debug for WorldSnapshot
Source§impl Default for WorldSnapshot
impl Default for WorldSnapshot
Source§fn default() -> WorldSnapshot
fn default() -> WorldSnapshot
Returns the “default value” for a type. Read more
Source§impl<'de> Deserialize<'de> for WorldSnapshot
impl<'de> Deserialize<'de> for WorldSnapshot
Source§fn deserialize<__D>(
__deserializer: __D,
) -> Result<WorldSnapshot, <__D as Deserializer<'de>>::Error>where
__D: Deserializer<'de>,
fn deserialize<__D>(
__deserializer: __D,
) -> Result<WorldSnapshot, <__D as Deserializer<'de>>::Error>where
__D: Deserializer<'de>,
Deserialize this value from the given Serde deserializer. Read more
Source§impl PartialEq for WorldSnapshot
impl PartialEq for WorldSnapshot
Source§impl Serialize for WorldSnapshot
impl Serialize for WorldSnapshot
Source§fn serialize<__S>(
&self,
__serializer: __S,
) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>where
__S: Serializer,
fn serialize<__S>(
&self,
__serializer: __S,
) -> Result<<__S as Serializer>::Ok, <__S as Serializer>::Error>where
__S: Serializer,
Serialize this value into the given Serde serializer. Read more
impl StructuralPartialEq for WorldSnapshot
Auto Trait Implementations§
impl Freeze for WorldSnapshot
impl RefUnwindSafe for WorldSnapshot
impl Send for WorldSnapshot
impl Sync for WorldSnapshot
impl Unpin for WorldSnapshot
impl UnsafeUnpin for WorldSnapshot
impl UnwindSafe for WorldSnapshot
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
Mutably borrows from an owned value. Read more