feat(world): retain mission transform state
This commit is contained in:
@@ -1702,6 +1702,7 @@ mod tests {
|
|||||||
slot: 5,
|
slot: 5,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
transforms: Vec::new(),
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
hash: StateHash([0; 32]),
|
hash: StateHash([0; 32]),
|
||||||
};
|
};
|
||||||
@@ -1733,6 +1734,7 @@ mod tests {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
slot: 7,
|
slot: 7,
|
||||||
}],
|
}],
|
||||||
|
transforms: Vec::new(),
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
hash: StateHash([0; 32]),
|
hash: StateHash([0; 32]),
|
||||||
};
|
};
|
||||||
@@ -1787,6 +1789,7 @@ mod tests {
|
|||||||
generation: 1,
|
generation: 1,
|
||||||
slot: 0,
|
slot: 0,
|
||||||
}],
|
}],
|
||||||
|
transforms: Vec::new(),
|
||||||
events: Vec::new(),
|
events: Vec::new(),
|
||||||
hash: StateHash([0; 32]),
|
hash: StateHash([0; 32]),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ use fparkan_prototype::{
|
|||||||
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
use fparkan_resource::{resource_name, CachedResourceRepository, ResourceRepository};
|
||||||
use fparkan_vfs::{Vfs, VfsError};
|
use fparkan_vfs::{Vfs, VfsError};
|
||||||
use fparkan_world::{
|
use fparkan_world::{
|
||||||
construct_object, new as new_world, register_object, step, InputSnapshot, ObjectDraft,
|
construct_object, new as new_world, register_object, set_transform, step, InputSnapshot,
|
||||||
OriginalObjectId, World, WorldConfig, WorldSnapshot,
|
ObjectDraft, OriginalObjectId, TransformState, World, WorldConfig, WorldSnapshot,
|
||||||
};
|
};
|
||||||
use std::num::NonZeroUsize;
|
use std::num::NonZeroUsize;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
@@ -852,9 +852,18 @@ fn load_mission_with_options_and_progress(
|
|||||||
let mut new_runtime_world = new_world(WorldConfig);
|
let mut new_runtime_world = new_world(WorldConfig);
|
||||||
let mut handles = Vec::with_capacity(mission.objects.len());
|
let mut handles = Vec::with_capacity(mission.objects.len());
|
||||||
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Construct);
|
record_load_phase(&mut trace, &mut on_phase, MissionLoadPhase::Construct);
|
||||||
for (index, _object) in mission.objects.iter().enumerate() {
|
for (index, object) in mission.objects.iter().enumerate() {
|
||||||
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
|
let original_id = u32::try_from(index).ok().map(OriginalObjectId);
|
||||||
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
|
let handle = construct_object(&mut new_runtime_world, ObjectDraft { original_id })?;
|
||||||
|
set_transform(
|
||||||
|
&mut new_runtime_world,
|
||||||
|
handle,
|
||||||
|
TransformState {
|
||||||
|
position: object.position.map(f32::to_bits),
|
||||||
|
orientation: object.orientation.map(f32::to_bits),
|
||||||
|
scale: object.scale.map(f32::to_bits),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
handles.push(handle);
|
handles.push(handle);
|
||||||
}
|
}
|
||||||
trace.drafts_before_registration = handles.len();
|
trace.drafts_before_registration = handles.len();
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
use fparkan_binary::sha256;
|
use fparkan_binary::sha256;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
const WORLD_STATE_HASH_SCHEMA: &[u8] = b"fparkan-world-state-v2\0";
|
const WORLD_STATE_HASH_SCHEMA: &[u8] = b"fparkan-world-state-v3\0";
|
||||||
|
|
||||||
/// Object handle with generation.
|
/// Object handle with generation.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||||
@@ -50,6 +50,20 @@ pub struct Tick(pub u64);
|
|||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub struct StateHash(pub [u8; 32]);
|
pub struct StateHash(pub [u8; 32]);
|
||||||
|
|
||||||
|
/// Exact source transform carried by one world object.
|
||||||
|
///
|
||||||
|
/// Values are IEEE-754 bit patterns so mission transforms retain their input
|
||||||
|
/// identity in deterministic state before a controller interprets them.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||||
|
pub struct TransformState {
|
||||||
|
/// Position words in source axis order.
|
||||||
|
pub position: [u32; 3],
|
||||||
|
/// Orientation words in source axis order.
|
||||||
|
pub orientation: [u32; 3],
|
||||||
|
/// Non-uniform scale words in source axis order.
|
||||||
|
pub scale: [u32; 3],
|
||||||
|
}
|
||||||
|
|
||||||
/// World phase.
|
/// World phase.
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum WorldPhase {
|
pub enum WorldPhase {
|
||||||
@@ -110,6 +124,8 @@ pub struct WorldSnapshot {
|
|||||||
pub tick: Tick,
|
pub tick: Tick,
|
||||||
/// Live object handles.
|
/// Live object handles.
|
||||||
pub objects: Vec<ObjectHandle>,
|
pub objects: Vec<ObjectHandle>,
|
||||||
|
/// Exact transforms for live registered objects in object-handle order.
|
||||||
|
pub transforms: Vec<(ObjectHandle, TransformState)>,
|
||||||
/// Commands processed during this step.
|
/// Commands processed during this step.
|
||||||
pub events: Vec<WorldEvent>,
|
pub events: Vec<WorldEvent>,
|
||||||
/// State hash.
|
/// State hash.
|
||||||
@@ -167,6 +183,7 @@ struct Slot {
|
|||||||
owner_id: Option<OwnerId>,
|
owner_id: Option<OwnerId>,
|
||||||
mirror_id: Option<OriginalObjectId>,
|
mirror_id: Option<OriginalObjectId>,
|
||||||
registration_sequence: Option<u64>,
|
registration_sequence: Option<u64>,
|
||||||
|
transform: TransformState,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// World.
|
/// World.
|
||||||
@@ -248,6 +265,7 @@ pub fn construct_object(world: &mut World, draft: ObjectDraft) -> Result<ObjectH
|
|||||||
owner_id: None,
|
owner_id: None,
|
||||||
mirror_id: None,
|
mirror_id: None,
|
||||||
registration_sequence: None,
|
registration_sequence: None,
|
||||||
|
transform: TransformState::default(),
|
||||||
});
|
});
|
||||||
Ok(handle)
|
Ok(handle)
|
||||||
}
|
}
|
||||||
@@ -335,6 +353,29 @@ pub fn identity_metadata(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the exact source transform for a live object.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
|
||||||
|
pub fn set_transform(
|
||||||
|
world: &mut World,
|
||||||
|
handle: ObjectHandle,
|
||||||
|
transform: TransformState,
|
||||||
|
) -> Result<(), WorldError> {
|
||||||
|
checked_slot_mut(world, handle)?.transform = transform;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the exact source transform for a live object.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// Returns [`WorldError`] if the handle is stale, deleted, or out of range.
|
||||||
|
pub fn transform_state(world: &World, handle: ObjectHandle) -> Result<TransformState, WorldError> {
|
||||||
|
Ok(checked_slot(world, handle)?.transform)
|
||||||
|
}
|
||||||
|
|
||||||
/// Requests deletion.
|
/// Requests deletion.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -423,6 +464,7 @@ where
|
|||||||
let snapshot = WorldSnapshot {
|
let snapshot = WorldSnapshot {
|
||||||
tick: world.tick,
|
tick: world.tick,
|
||||||
objects: live_registered(world),
|
objects: live_registered(world),
|
||||||
|
transforms: live_registered_with_transforms(world),
|
||||||
events,
|
events,
|
||||||
hash: canonical_state_hash(world),
|
hash: canonical_state_hash(world),
|
||||||
};
|
};
|
||||||
@@ -459,6 +501,7 @@ fn canonical_state_bytes(world: &World) -> Vec<u8> {
|
|||||||
push_optional_u32(&mut out, slot.mirror_id.map(|id| id.0));
|
push_optional_u32(&mut out, slot.mirror_id.map(|id| id.0));
|
||||||
push_optional_u16(&mut out, slot.owner_id.map(|id| id.0));
|
push_optional_u16(&mut out, slot.owner_id.map(|id| id.0));
|
||||||
push_optional_u64(&mut out, slot.registration_sequence);
|
push_optional_u64(&mut out, slot.registration_sequence);
|
||||||
|
push_transform(&mut out, slot.transform);
|
||||||
}
|
}
|
||||||
push_len(&mut out, world.queue.len());
|
push_len(&mut out, world.queue.len());
|
||||||
for command in &world.queue {
|
for command in &world.queue {
|
||||||
@@ -639,6 +682,17 @@ fn push_handle(out: &mut Vec<u8>, handle: ObjectHandle) {
|
|||||||
push_u32(out, handle.slot);
|
push_u32(out, handle.slot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn push_transform(out: &mut Vec<u8>, transform: TransformState) {
|
||||||
|
for word in transform
|
||||||
|
.position
|
||||||
|
.into_iter()
|
||||||
|
.chain(transform.orientation)
|
||||||
|
.chain(transform.scale)
|
||||||
|
{
|
||||||
|
push_u32(out, word);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn checked_slot(world: &World, handle: ObjectHandle) -> Result<&Slot, WorldError> {
|
fn checked_slot(world: &World, handle: ObjectHandle) -> Result<&Slot, WorldError> {
|
||||||
let slot = world
|
let slot = world
|
||||||
.slots
|
.slots
|
||||||
@@ -689,6 +743,24 @@ fn live_registered(world: &World) -> Vec<ObjectHandle> {
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn live_registered_with_transforms(world: &World) -> Vec<(ObjectHandle, TransformState)> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
slot.transform,
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -704,6 +776,25 @@ mod tests {
|
|||||||
assert_eq!(after.objects, vec![handle]);
|
assert_eq!(after.objects, vec![handle]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn transform_is_exactly_snapshotted_and_changes_state_hash() {
|
||||||
|
let mut world = new(WorldConfig);
|
||||||
|
let handle =
|
||||||
|
construct_object(&mut world, ObjectDraft { original_id: None }).expect("object");
|
||||||
|
register_object(&mut world, handle).expect("register");
|
||||||
|
let before = step(&mut world, &InputSnapshot).expect("before");
|
||||||
|
let transform = TransformState {
|
||||||
|
position: [1.0_f32.to_bits(), (-2.0_f32).to_bits(), 3.0_f32.to_bits()],
|
||||||
|
orientation: [0.0_f32.to_bits(), 0.5_f32.to_bits(), (-0.25_f32).to_bits()],
|
||||||
|
scale: [1.0_f32.to_bits(), 2.0_f32.to_bits(), 0.5_f32.to_bits()],
|
||||||
|
};
|
||||||
|
set_transform(&mut world, handle, transform).expect("set transform");
|
||||||
|
let after = step(&mut world, &InputSnapshot).expect("after");
|
||||||
|
assert_eq!(transform_state(&world, handle), Ok(transform));
|
||||||
|
assert_eq!(after.transforms, vec![(handle, transform)]);
|
||||||
|
assert_ne!(before.hash, after.hash);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn registration_sequence_stale_and_duplicate_original_contracts() {
|
fn registration_sequence_stale_and_duplicate_original_contracts() {
|
||||||
let mut world = new(WorldConfig);
|
let mut world = new(WorldConfig);
|
||||||
|
|||||||
@@ -374,6 +374,22 @@ Demo mission total: 201 objects -> 501 prototypes -> 501 object MSH/WEAR.
|
|||||||
|
|
||||||
### Deterministic simulation replay
|
### Deterministic simulation replay
|
||||||
|
|
||||||
|
#### Mission transform state in the world contract
|
||||||
|
|
||||||
|
`fparkan-world` now carries a `TransformState` for every live object: the
|
||||||
|
three TMA position words, three orientation words and three scale words are
|
||||||
|
preserved as exact IEEE-754 bit patterns. This stores source identity before a
|
||||||
|
movement or physics controller interprets axes, units or Euler order.
|
||||||
|
`WorldSnapshot` publishes transforms in stable object-handle order and the
|
||||||
|
canonical SHA-256 state hash includes every transform word.
|
||||||
|
|
||||||
|
Mission loading assigns this state after construction and before registration.
|
||||||
|
A headless licensed GOG AutoDemo run on 2026-07-18 loaded eight objects, 343
|
||||||
|
areals and 3,174 terrain surfaces with zero graph failures, then completed two
|
||||||
|
deterministic ticks. This is the state foundation for a future route/movement
|
||||||
|
controller; it does not claim recovered velocity, collision or original
|
||||||
|
behavior-controller semantics.
|
||||||
|
|
||||||
Записывается начальная миссия, seed, input events, network messages и значения
|
Записывается начальная миссия, seed, input events, network messages и значения
|
||||||
внешних часов. На контрольных ticks сохраняется canonical state hash:
|
внешних часов. На контрольных ticks сохраняется canonical state hash:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user