feat: implement FParkan architecture foundation
Docs Deploy / Build and Deploy MkDocs (push) Successful in 35s
Test / Lint (push) Failing after 1m14s
Test / Test (push) Has been skipped
Test / Render parity (push) Has been skipped

Add the modular fparkan workspace, domain crates, adapters, apps, xtask policy/CI, acceptance evidence, and licensed corpus gates for the macOS-focused roadmap foundation.
This commit is contained in:
2026-06-22 13:12:27 +04:00
parent 7416fdc7e9
commit d0bdbaa1ed
128 changed files with 26720 additions and 12137 deletions
+11
View File
@@ -0,0 +1,11 @@
[package]
name = "fparkan-world"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
[lints]
workspace = true
+840
View File
@@ -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());
}
}
}
}