pub struct EventKind { /* private fields */ }Expand description
Domain-neutral event identity and structured fields.
The type tag and flattened field layout deliberately preserve the wire
shape used by earlier concrete event variants. Domain crates and runtime
compatibility modules own typed payloads and encode them through
Self::from_payload; this crate does not own their vocabulary.
Implementations§
Source§impl EventKind
impl EventKind
Sourcepub fn from_fields(
event_type: impl Into<String>,
fields: BTreeMap<String, Value>,
) -> Result<EventKind, EventKindError>
pub fn from_fields( event_type: impl Into<String>, fields: BTreeMap<String, Value>, ) -> Result<EventKind, EventKindError>
Builds an event record from an explicit type and flattened fields.
§Errors
Returns an error when the type is empty or the fields contain the
reserved type key.
Sourcepub fn from_payload<T>(
event_type: impl Into<String>,
payload: &T,
) -> Result<EventKind, EventKindError>where
T: Serialize,
pub fn from_payload<T>(
event_type: impl Into<String>,
payload: &T,
) -> Result<EventKind, EventKindError>where
T: Serialize,
Serializes a typed domain payload into an event record.
§Errors
Returns an error when serialization fails, the payload is not an
object, the type is empty, or the payload contains the reserved type
key.
Sourcepub fn plugin(
plugin: impl Into<String>,
event_type: impl Into<String>,
) -> EventKind
pub fn plugin( plugin: impl Into<String>, event_type: impl Into<String>, ) -> EventKind
Constructs the compatibility wire identity for a plugin event.
pub fn event_type(&self) -> &str
pub fn is_type(&self, event_type: &str) -> bool
pub fn fields(&self) -> BTreeMap<String, Value>
pub fn field(&self, name: &str) -> Option<Value>
Sourcepub fn set_field<T>(
&mut self,
name: &str,
value: &T,
) -> Result<(), EventKindError>where
T: Serialize,
pub fn set_field<T>(
&mut self,
name: &str,
value: &T,
) -> Result<(), EventKindError>where
T: Serialize,
Replaces one existing field with a serializable value.
§Errors
Returns an error when the field is absent or the value cannot be represented as structured JSON data.
Sourcepub fn decode_payload<T>(&self) -> Result<T, EventKindError>where
T: DeserializeOwned,
pub fn decode_payload<T>(&self) -> Result<T, EventKindError>where
T: DeserializeOwned,
Decodes all flattened fields into a domain-owned payload type.
§Errors
Returns an error when the fields do not deserialize as T.
Sourcepub fn decode_field<T>(&self, name: &str) -> Result<T, EventKindError>where
T: DeserializeOwned,
pub fn decode_field<T>(&self, name: &str) -> Result<T, EventKindError>where
T: DeserializeOwned,
Decodes one named field.
§Errors
Returns an error when the field is absent or does not deserialize as
T.
pub fn plugin_identity(&self) -> Option<(&str, &str)>
Sourcepub fn qualified_event_type(&self) -> String
pub fn qualified_event_type(&self) -> String
Returns the stable, display-oriented type identity for this event.
Built-in event kinds retain the same snake-case labels returned by
Self::event_type. Plugin events are qualified with their registered
plugin name and event type, separated by a dot. The two plugin-provided
components are preserved as registered; no case folding or additional
normalization is applied.
Examples found in repository?
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}