aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-world/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/fparkan-world/src/lib.rs
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
feat: implement FParkan architecture foundation
Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
Diffstat (limited to 'crates/fparkan-world/src/lib.rs')
-rw-r--r--crates/fparkan-world/src/lib.rs840
1 files changed, 840 insertions, 0 deletions
diff --git a/crates/fparkan-world/src/lib.rs b/crates/fparkan-world/src/lib.rs
new file mode 100644
index 0000000..58412d9
--- /dev/null
+++ b/crates/fparkan-world/src/lib.rs
@@ -0,0 +1,840 @@
+#![forbid(unsafe_code)]
+//! Deterministic world identity, queue, lifecycle, and snapshots.
+
+use std::collections::VecDeque;
+
+/// Object handle with generation.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct ObjectHandle {
+ /// Generation.
+ pub generation: u32,
+ /// Slot.
+ pub slot: u32,
+}
+
+/// Original mission object id.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct OriginalObjectId(pub u32);
+
+/// Owner id.
+#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
+pub struct OwnerId(pub u16);
+
+/// Tick.
+#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
+pub struct Tick(pub u64);
+
+/// State hash.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct StateHash(pub [u8; 32]);
+
+/// World phase.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum WorldPhase {
+ /// Idle.
+ Idle,
+ /// Calculating.
+ Calculating,
+ /// Applying deferred operations.
+ ApplyingDeferred,
+ /// Publishing snapshot.
+ PublishingSnapshot,
+}
+
+/// Object draft.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct ObjectDraft {
+ /// Original id.
+ pub original_id: Option<OriginalObjectId>,
+}
+
+/// Distinct object identity metadata.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct IdentityMetadata {
+ /// Original mission object id.
+ pub original_id: Option<OriginalObjectId>,
+ /// Mirrored original id.
+ pub mirror_id: Option<OriginalObjectId>,
+ /// Local owner id.
+ pub owner_id: Option<OwnerId>,
+}
+
+/// World command.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct WorldCommand {
+ /// Sequence.
+ pub sequence: u64,
+ /// Target.
+ pub target: Option<ObjectHandle>,
+}
+
+/// World event.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct WorldEvent {
+ /// Sequence.
+ pub sequence: u64,
+ /// Target object, if any.
+ pub target: Option<ObjectHandle>,
+}
+
+/// Input snapshot.
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
+pub struct InputSnapshot;
+
+/// World snapshot.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct WorldSnapshot {
+ /// Tick.
+ pub tick: Tick,
+ /// Live object handles.
+ pub objects: Vec<ObjectHandle>,
+ /// Commands processed during this step.
+ pub events: Vec<WorldEvent>,
+ /// State hash.
+ pub hash: StateHash,
+}
+
+/// World configuration.
+#[derive(Clone, Debug, Default, Eq, PartialEq)]
+pub struct WorldConfig;
+
+/// Fixed-step clock state.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct FixedStepClock {
+ accumulated_millis: u64,
+ tick: Tick,
+ paused: bool,
+ platform_event_collections: u64,
+}
+
+/// Fixed-step configuration.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub struct FixedStepConfig {
+ /// Milliseconds per simulation tick.
+ pub step_millis: u32,
+}
+
+impl Default for FixedStepConfig {
+ fn default() -> Self {
+ Self { step_millis: 16 }
+ }
+}
+
+/// Shutdown ordering report.
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct ShutdownReport {
+ /// Object handles released before managers.
+ pub released_objects: Vec<ObjectHandle>,
+ /// Whether managers were released after objects.
+ pub managers_released: bool,
+}
+
+#[derive(Clone, Debug)]
+struct Slot {
+ generation: u32,
+ live: bool,
+ registered: bool,
+ original_id: Option<OriginalObjectId>,
+ owner_id: Option<OwnerId>,
+ mirror_id: Option<OriginalObjectId>,
+ registration_sequence: Option<u64>,
+}
+
+/// World.
+#[derive(Clone, Debug)]
+pub struct World {
+ slots: Vec<Slot>,
+ queue: VecDeque<WorldCommand>,
+ deferred_delete: Vec<ObjectHandle>,
+ phase: WorldPhase,
+ tick: Tick,
+ next_sequence: u64,
+ next_registration_sequence: u64,
+}
+
+/// World error.
+#[derive(Debug, Eq, PartialEq)]
+pub enum WorldError {
+ /// Invalid handle.
+ InvalidHandle,
+ /// Stale handle.
+ StaleHandle,
+ /// Object already deleted.
+ Deleted,
+ /// Duplicate original object id.
+ DuplicateOriginalObjectId(OriginalObjectId),
+ /// Invalid fixed-step configuration.
+ InvalidFixedStep,
+}
+
+impl std::fmt::Display for WorldError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{self:?}")
+ }
+}
+
+impl std::error::Error for WorldError {}
+
+/// Creates a world.
+#[must_use]
+pub fn new(_config: WorldConfig) -> World {
+ World {
+ slots: Vec::new(),
+ queue: VecDeque::new(),
+ deferred_delete: Vec::new(),
+ phase: WorldPhase::Idle,
+ tick: Tick(0),
+ next_sequence: 0,
+ next_registration_sequence: 0,
+ }
+}
+
+/// Constructs an object without registering it.
+///
+/// # Errors
+///
+/// Returns [`WorldError::InvalidHandle`] if the slot index cannot be
+/// represented by an [`ObjectHandle`].
+pub fn construct_object(world: &mut World, draft: ObjectDraft) -> Result<ObjectHandle, WorldError> {
+ let slot = u32::try_from(world.slots.len()).map_err(|_| WorldError::InvalidHandle)?;
+ let handle = ObjectHandle {
+ generation: 1,
+ slot,
+ };
+ world.slots.push(Slot {
+ generation: 1,
+ live: true,
+ registered: false,
+ original_id: draft.original_id,
+ owner_id: None,
+ mirror_id: None,
+ registration_sequence: None,
+ });
+ Ok(handle)
+}
+
+/// Registers a constructed object.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn register_object(world: &mut World, handle: ObjectHandle) -> Result<(), WorldError> {
+ let original_id = checked_slot(world, handle)?.original_id;
+ if let Some(original_id) = original_id {
+ let duplicate = world.slots.iter().enumerate().any(|(idx, slot)| {
+ u32::try_from(idx).is_ok_and(|slot_index| slot_index != handle.slot)
+ && slot.live
+ && slot.registered
+ && slot.original_id == Some(original_id)
+ });
+ if duplicate {
+ return Err(WorldError::DuplicateOriginalObjectId(original_id));
+ }
+ }
+ let sequence = world.next_registration_sequence;
+ world.next_registration_sequence = world.next_registration_sequence.saturating_add(1);
+ let slot = checked_slot_mut(world, handle)?;
+ slot.registered = true;
+ slot.registration_sequence = Some(sequence);
+ Ok(())
+}
+
+/// Attaches local ownership metadata to an object.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn set_owner(
+ world: &mut World,
+ handle: ObjectHandle,
+ owner_id: Option<OwnerId>,
+) -> Result<(), WorldError> {
+ checked_slot_mut(world, handle)?.owner_id = owner_id;
+ Ok(())
+}
+
+/// Attaches mirror metadata to an object without changing its original id.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn set_mirror_original(
+ world: &mut World,
+ handle: ObjectHandle,
+ mirror_id: Option<OriginalObjectId>,
+) -> Result<(), WorldError> {
+ checked_slot_mut(world, handle)?.mirror_id = mirror_id;
+ Ok(())
+}
+
+/// Returns registration sequence for a live object.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn registration_sequence(
+ world: &World,
+ handle: ObjectHandle,
+) -> Result<Option<u64>, WorldError> {
+ Ok(checked_slot(world, handle)?.registration_sequence)
+}
+
+/// Returns object identity metadata.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn identity_metadata(
+ world: &World,
+ handle: ObjectHandle,
+) -> Result<IdentityMetadata, WorldError> {
+ let slot = checked_slot(world, handle)?;
+ Ok(IdentityMetadata {
+ original_id: slot.original_id,
+ mirror_id: slot.mirror_id,
+ owner_id: slot.owner_id,
+ })
+}
+
+/// Requests deletion.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
+pub fn request_delete(world: &mut World, handle: ObjectHandle) -> Result<(), WorldError> {
+ checked_slot(world, handle)?;
+ if world.phase == WorldPhase::Calculating {
+ if !world.deferred_delete.contains(&handle) {
+ world.deferred_delete.push(handle);
+ }
+ Ok(())
+ } else {
+ delete_now(world, handle)
+ }
+}
+
+/// Enqueues a command.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] when a targeted command references an invalid
+/// handle.
+pub fn enqueue(world: &mut World, mut command: WorldCommand) -> Result<(), WorldError> {
+ if let Some(handle) = command.target {
+ checked_slot(world, handle)?;
+ }
+ command.sequence = world.next_sequence;
+ world.next_sequence = world.next_sequence.saturating_add(1);
+ world.queue.push_back(command);
+ Ok(())
+}
+
+/// Advances one deterministic step.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if a queued command references a stale, deleted, or
+/// out-of-range handle.
+pub fn step(world: &mut World, input: &InputSnapshot) -> Result<WorldSnapshot, WorldError> {
+ step_with_handler(world, input, |_, _| Ok(()))
+}
+
+/// Advances one deterministic step with a command callback.
+///
+/// The callback runs while the world is in the calculating phase, which allows
+/// tests and adapters to exercise deferred deletion semantics without exposing
+/// mutable slot internals.
+///
+/// # Errors
+///
+/// Returns [`WorldError`] if a queued command references a stale, deleted, or
+/// out-of-range handle, or if the callback reports a world error.
+pub fn step_with_handler<F>(
+ world: &mut World,
+ _input: &InputSnapshot,
+ mut handler: F,
+) -> Result<WorldSnapshot, WorldError>
+where
+ F: FnMut(&mut World, &WorldCommand) -> Result<(), WorldError>,
+{
+ world.phase = WorldPhase::Calculating;
+ let mut events = Vec::new();
+ while let Some(command) = world.queue.pop_front() {
+ if let Some(handle) = command.target {
+ if world.deferred_delete.contains(&handle) {
+ continue;
+ }
+ checked_slot(world, handle)?;
+ }
+ handler(world, &command)?;
+ events.push(WorldEvent {
+ sequence: command.sequence,
+ target: command.target,
+ });
+ }
+ world.phase = WorldPhase::ApplyingDeferred;
+ let deletes = std::mem::take(&mut world.deferred_delete);
+ for handle in deletes {
+ let _ = delete_now(world, handle);
+ }
+ world.tick.0 = world.tick.0.saturating_add(1);
+ world.phase = WorldPhase::PublishingSnapshot;
+ let snapshot = WorldSnapshot {
+ tick: world.tick,
+ objects: live_registered(world),
+ events,
+ hash: canonical_state_hash(world),
+ };
+ world.phase = WorldPhase::Idle;
+ Ok(snapshot)
+}
+
+/// Computes canonical state hash.
+#[must_use]
+pub fn canonical_state_hash(world: &World) -> StateHash {
+ let mut state = 0xcbf2_9ce4_8422_2325_u64;
+ hash_u64(&mut state, world.tick.0);
+ for (idx, slot) in world.slots.iter().enumerate() {
+ hash_u64(&mut state, idx as u64);
+ hash_u64(&mut state, u64::from(slot.generation));
+ hash_u64(&mut state, u64::from(u8::from(slot.live)));
+ hash_u64(&mut state, u64::from(u8::from(slot.registered)));
+ hash_u64(&mut state, slot.original_id.map_or(0, |id| u64::from(id.0)));
+ hash_u64(&mut state, slot.mirror_id.map_or(0, |id| u64::from(id.0)));
+ hash_u64(&mut state, slot.owner_id.map_or(0, |id| u64::from(id.0)));
+ hash_u64(&mut state, slot.registration_sequence.unwrap_or(u64::MAX));
+ }
+ let mut out = [0; 32];
+ out[..8].copy_from_slice(&state.to_le_bytes());
+ out[8..16].copy_from_slice(&state.rotate_left(13).to_le_bytes());
+ out[16..24].copy_from_slice(&state.rotate_left(29).to_le_bytes());
+ out[24..32].copy_from_slice(&state.rotate_left(47).to_le_bytes());
+ StateHash(out)
+}
+
+/// Creates a fixed-step clock.
+///
+/// # Errors
+///
+/// Returns [`WorldError::InvalidFixedStep`] when the configured step is zero.
+pub fn fixed_step_clock(config: FixedStepConfig) -> Result<FixedStepClock, WorldError> {
+ if config.step_millis == 0 {
+ return Err(WorldError::InvalidFixedStep);
+ }
+ Ok(FixedStepClock {
+ accumulated_millis: 0,
+ tick: Tick(0),
+ paused: false,
+ platform_event_collections: 0,
+ })
+}
+
+/// Records platform event collection independently of game time.
+pub fn collect_platform_events(clock: &mut FixedStepClock) {
+ clock.platform_event_collections = clock.platform_event_collections.saturating_add(1);
+}
+
+/// Sets pause state.
+pub fn set_paused(clock: &mut FixedStepClock, paused: bool) {
+ clock.paused = paused;
+}
+
+/// Advances fixed-step game time.
+///
+/// Returns the number of simulation ticks that should be executed.
+///
+/// # Errors
+///
+/// Returns [`WorldError::InvalidFixedStep`] when the configured step is zero.
+pub fn advance_fixed_step(
+ clock: &mut FixedStepClock,
+ config: FixedStepConfig,
+ elapsed_millis: u64,
+) -> Result<u32, WorldError> {
+ if config.step_millis == 0 {
+ return Err(WorldError::InvalidFixedStep);
+ }
+ if clock.paused {
+ return Ok(0);
+ }
+ clock.accumulated_millis = clock.accumulated_millis.saturating_add(elapsed_millis);
+ let step = u64::from(config.step_millis);
+ let mut ticks = 0_u32;
+ while clock.accumulated_millis >= step {
+ clock.accumulated_millis -= step;
+ clock.tick.0 = clock.tick.0.saturating_add(1);
+ ticks = ticks.saturating_add(1);
+ }
+ Ok(ticks)
+}
+
+/// Returns fixed-step clock tick.
+#[must_use]
+pub fn fixed_step_tick(clock: &FixedStepClock) -> Tick {
+ clock.tick
+}
+
+/// Returns platform event collection count.
+#[must_use]
+pub fn platform_event_collections(clock: &FixedStepClock) -> u64 {
+ clock.platform_event_collections
+}
+
+/// Runs end-frame callbacks in stable sequence order.
+#[must_use]
+pub fn end_frame_callback_order(mut callbacks: Vec<WorldEvent>) -> Vec<u64> {
+ callbacks.sort_by_key(|event| event.sequence);
+ callbacks.into_iter().map(|event| event.sequence).collect()
+}
+
+/// Releases live objects before managers.
+#[must_use]
+pub fn shutdown(mut world: World) -> ShutdownReport {
+ let released_objects = live_registered(&world);
+ for slot in &mut world.slots {
+ slot.live = false;
+ slot.registered = false;
+ slot.generation = slot.generation.saturating_add(1);
+ }
+ ShutdownReport {
+ released_objects,
+ managers_released: true,
+ }
+}
+
+fn hash_u64(state: &mut u64, value: u64) {
+ for byte in value.to_le_bytes() {
+ *state ^= u64::from(byte);
+ *state = state.wrapping_mul(0x0000_0100_0000_01b3);
+ }
+}
+
+fn checked_slot(world: &World, handle: ObjectHandle) -> Result<&Slot, WorldError> {
+ let slot = world
+ .slots
+ .get(handle.slot as usize)
+ .ok_or(WorldError::InvalidHandle)?;
+ if slot.generation != handle.generation {
+ return Err(WorldError::StaleHandle);
+ }
+ if !slot.live {
+ return Err(WorldError::Deleted);
+ }
+ Ok(slot)
+}
+
+fn checked_slot_mut(world: &mut World, handle: ObjectHandle) -> Result<&mut Slot, WorldError> {
+ let slot = world
+ .slots
+ .get_mut(handle.slot as usize)
+ .ok_or(WorldError::InvalidHandle)?;
+ if slot.generation != handle.generation {
+ return Err(WorldError::StaleHandle);
+ }
+ if !slot.live {
+ return Err(WorldError::Deleted);
+ }
+ Ok(slot)
+}
+
+fn delete_now(world: &mut World, handle: ObjectHandle) -> Result<(), WorldError> {
+ let slot = checked_slot_mut(world, handle)?;
+ slot.live = false;
+ slot.generation = slot.generation.saturating_add(1);
+ Ok(())
+}
+
+fn live_registered(world: &World) -> Vec<ObjectHandle> {
+ world
+ .slots
+ .iter()
+ .enumerate()
+ .filter_map(|(idx, slot)| {
+ let slot_index = u32::try_from(idx).ok()?;
+ (slot.live && slot.registered).then_some(ObjectHandle {
+ generation: slot.generation,
+ slot: slot_index,
+ })
+ })
+ .collect()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn construct_register_and_hash_are_stable() {
+ let mut world = new(WorldConfig);
+ let handle = construct_object(&mut world, ObjectDraft { original_id: None }).expect("obj");
+ let before = step(&mut world, &InputSnapshot).expect("step");
+ assert!(before.objects.is_empty());
+ register_object(&mut world, handle).expect("register");
+ let after = step(&mut world, &InputSnapshot).expect("step");
+ assert_eq!(after.objects, vec![handle]);
+ }
+
+ #[test]
+ fn registration_sequence_stale_and_duplicate_original_contracts() {
+ let mut world = new(WorldConfig);
+ let first = construct_object(
+ &mut world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(7)),
+ },
+ )
+ .expect("first");
+ let second = construct_object(
+ &mut world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(8)),
+ },
+ )
+ .expect("second");
+ register_object(&mut world, first).expect("register first");
+ register_object(&mut world, second).expect("register second");
+ assert_eq!(registration_sequence(&world, first), Ok(Some(0)));
+ assert_eq!(registration_sequence(&world, second), Ok(Some(1)));
+
+ request_delete(&mut world, first).expect("delete");
+ assert_eq!(
+ register_object(&mut world, first),
+ Err(WorldError::StaleHandle)
+ );
+ let recycled = ObjectHandle {
+ generation: first.generation,
+ slot: first.slot,
+ };
+ assert_eq!(
+ register_object(&mut world, recycled),
+ Err(WorldError::StaleHandle)
+ );
+
+ let duplicate = construct_object(
+ &mut world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(8)),
+ },
+ )
+ .expect("duplicate");
+ assert_eq!(
+ register_object(&mut world, duplicate),
+ Err(WorldError::DuplicateOriginalObjectId(OriginalObjectId(8)))
+ );
+ }
+
+ #[test]
+ fn identity_metadata_keeps_original_mirror_and_owner_distinct() {
+ let mut world = new(WorldConfig);
+ let handle = construct_object(
+ &mut world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(10)),
+ },
+ )
+ .expect("object");
+ set_mirror_original(&mut world, handle, Some(OriginalObjectId(20))).expect("mirror");
+ set_owner(&mut world, handle, Some(OwnerId(3))).expect("owner");
+ assert_eq!(
+ identity_metadata(&world, handle),
+ Ok(IdentityMetadata {
+ original_id: Some(OriginalObjectId(10)),
+ mirror_id: Some(OriginalObjectId(20)),
+ owner_id: Some(OwnerId(3))
+ })
+ );
+ }
+
+ #[test]
+ fn command_fifo_and_deferred_delete_during_calculation() {
+ let mut world = new(WorldConfig);
+ let first = construct_object(&mut world, ObjectDraft { original_id: None }).expect("first");
+ let second =
+ construct_object(&mut world, ObjectDraft { original_id: None }).expect("second");
+ register_object(&mut world, first).expect("register first");
+ register_object(&mut world, second).expect("register second");
+ enqueue(
+ &mut world,
+ WorldCommand {
+ sequence: 99,
+ target: Some(first),
+ },
+ )
+ .expect("enqueue first");
+ enqueue(
+ &mut world,
+ WorldCommand {
+ sequence: 99,
+ target: Some(second),
+ },
+ )
+ .expect("enqueue second");
+ enqueue(
+ &mut world,
+ WorldCommand {
+ sequence: 99,
+ target: Some(first),
+ },
+ )
+ .expect("enqueue first again");
+
+ let snapshot = step_with_handler(&mut world, &InputSnapshot, |world, command| {
+ if command.target == Some(first) {
+ request_delete(world, first)?;
+ request_delete(world, first)?;
+ }
+ Ok(())
+ })
+ .expect("step");
+
+ assert_eq!(
+ snapshot.events,
+ vec![
+ WorldEvent {
+ sequence: 0,
+ target: Some(first)
+ },
+ WorldEvent {
+ sequence: 1,
+ target: Some(second)
+ }
+ ]
+ );
+ assert_eq!(
+ request_delete(&mut world, first),
+ Err(WorldError::StaleHandle)
+ );
+ assert_eq!(
+ step(&mut world, &InputSnapshot).expect("step").objects,
+ vec![second]
+ );
+ }
+
+ #[test]
+ fn snapshot_hash_determinism_and_immutability() {
+ let mut left = new(WorldConfig);
+ let mut right = new(WorldConfig);
+ for world in [&mut left, &mut right] {
+ let handle = construct_object(
+ world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(1)),
+ },
+ )
+ .expect("object");
+ register_object(world, handle).expect("register");
+ }
+ let snapshot = step(&mut left, &InputSnapshot).expect("snapshot");
+ let clone = snapshot.clone();
+ let extra = construct_object(&mut left, ObjectDraft { original_id: None }).expect("extra");
+ register_object(&mut left, extra).expect("register extra");
+
+ assert_eq!(snapshot, clone);
+ assert_eq!(
+ clone.hash,
+ step(&mut right, &InputSnapshot).expect("right").hash
+ );
+ }
+
+ #[test]
+ fn fixed_step_pause_and_long_determinism_are_stable() {
+ let config = FixedStepConfig { step_millis: 20 };
+ let mut clock = fixed_step_clock(config).expect("clock");
+ collect_platform_events(&mut clock);
+ set_paused(&mut clock, true);
+ assert_eq!(advance_fixed_step(&mut clock, config, 100), Ok(0));
+ collect_platform_events(&mut clock);
+ assert_eq!(fixed_step_tick(&clock), Tick(0));
+ assert_eq!(platform_event_collections(&clock), 2);
+
+ set_paused(&mut clock, false);
+ assert_eq!(advance_fixed_step(&mut clock, config, 45), Ok(2));
+ assert_eq!(fixed_step_tick(&clock), Tick(2));
+
+ let mut first = new(WorldConfig);
+ let mut second = new(WorldConfig);
+ let mut first_hashes = Vec::new();
+ let mut second_hashes = Vec::new();
+ for _ in 0..10_000 {
+ first_hashes.push(step(&mut first, &InputSnapshot).expect("first").hash);
+ second_hashes.push(step(&mut second, &InputSnapshot).expect("second").hash);
+ }
+ assert_eq!(first_hashes, second_hashes);
+ }
+
+ #[test]
+ fn render_disabled_does_not_change_hash_end_callbacks_and_shutdown_order() {
+ let callbacks = vec![
+ WorldEvent {
+ sequence: 3,
+ target: None,
+ },
+ WorldEvent {
+ sequence: 1,
+ target: None,
+ },
+ WorldEvent {
+ sequence: 2,
+ target: None,
+ },
+ ];
+ assert_eq!(end_frame_callback_order(callbacks), vec![1, 2, 3]);
+
+ let mut rendered = new(WorldConfig);
+ let mut headless = rendered.clone();
+ assert_eq!(
+ step(&mut rendered, &InputSnapshot).expect("rendered").hash,
+ step(&mut headless, &InputSnapshot).expect("headless").hash
+ );
+
+ let handle =
+ construct_object(&mut rendered, ObjectDraft { original_id: None }).expect("object");
+ register_object(&mut rendered, handle).expect("register");
+ assert_eq!(
+ shutdown(rendered),
+ ShutdownReport {
+ released_objects: vec![handle],
+ managers_released: true
+ }
+ );
+ }
+
+ #[test]
+ fn generated_command_delete_sequences_preserve_registry_invariants() {
+ for seed in 0_u32..64 {
+ let mut world = new(WorldConfig);
+ let mut handles = Vec::new();
+ for index in 0..8 {
+ let handle = construct_object(
+ &mut world,
+ ObjectDraft {
+ original_id: Some(OriginalObjectId(seed * 100 + index)),
+ },
+ )
+ .expect("object");
+ register_object(&mut world, handle).expect("register");
+ handles.push(handle);
+ }
+ for (index, handle) in handles.iter().copied().enumerate() {
+ if (seed as usize + index) % 3 == 0 {
+ request_delete(&mut world, handle).expect("delete");
+ } else {
+ enqueue(
+ &mut world,
+ WorldCommand {
+ sequence: 0,
+ target: Some(handle),
+ },
+ )
+ .expect("enqueue");
+ }
+ }
+ let snapshot = step(&mut world, &InputSnapshot).expect("step");
+ for handle in snapshot.objects {
+ assert!(registration_sequence(&world, handle)
+ .expect("sequence")
+ .is_some());
+ }
+ }
+ }
+}