From d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Mon, 22 Jun 2026 13:12:27 +0400 Subject: 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. --- crates/fparkan-world/src/lib.rs | 840 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 840 insertions(+) create mode 100644 crates/fparkan-world/src/lib.rs (limited to 'crates/fparkan-world/src') 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, +} + +/// Distinct object identity metadata. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct IdentityMetadata { + /// Original mission object id. + pub original_id: Option, + /// Mirrored original id. + pub mirror_id: Option, + /// Local owner id. + pub owner_id: Option, +} + +/// World command. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WorldCommand { + /// Sequence. + pub sequence: u64, + /// Target. + pub target: Option, +} + +/// World event. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct WorldEvent { + /// Sequence. + pub sequence: u64, + /// Target object, if any. + pub target: Option, +} + +/// 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, + /// Commands processed during this step. + pub events: Vec, + /// 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, + /// Whether managers were released after objects. + pub managers_released: bool, +} + +#[derive(Clone, Debug)] +struct Slot { + generation: u32, + live: bool, + registered: bool, + original_id: Option, + owner_id: Option, + mirror_id: Option, + registration_sequence: Option, +} + +/// World. +#[derive(Clone, Debug)] +pub struct World { + slots: Vec, + queue: VecDeque, + deferred_delete: Vec, + 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 { + 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, +) -> 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, +) -> 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, 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 { + 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 { + 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( + world: &mut World, + _input: &InputSnapshot, + mut handler: F, +) -> Result +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 { + 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 { + 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) -> Vec { + 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 { + 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()); + } + } + } +} -- cgit v1.2.3