From a01c21f8658e239fd6eaed06189fcbe9ae9bea52 Mon Sep 17 00:00:00 2001 From: Valentin Popov Date: Sat, 18 Jul 2026 19:30:27 +0400 Subject: [PATCH] feat(render): consume world snapshot transforms --- apps/fparkan-game/src/main.rs | 99 ++++++++++++++++++++++++++++++--- docs/tomes/07-implementation.md | 7 +++ 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/apps/fparkan-game/src/main.rs b/apps/fparkan-game/src/main.rs index 2ec347e..90effa7 100644 --- a/apps/fparkan-game/src/main.rs +++ b/apps/fparkan-game/src/main.rs @@ -54,7 +54,7 @@ use fparkan_terrain::TerrainWorld; use fparkan_vfs::DirectoryVfs; #[cfg(test)] use fparkan_world::OriginalObjectId; -use fparkan_world::WorldSnapshot; +use fparkan_world::{TransformState, WorldSnapshot}; use serde::Deserialize; use std::num::NonZeroUsize; use std::path::PathBuf; @@ -1024,12 +1024,14 @@ fn render_snapshot_with_assets( material_slots: vec![material], material_index: 0, pipeline_state: fparkan_render::LegacyPipelineState::default(), - transform: mission_drafts - .and_then(|drafts| drafts.get(index)) - .map_or_else( - || identity_transform(index_to_f32(index)), - mission_position_scale_transform, - ), + transform: snapshot_transform(snapshot, *handle).unwrap_or_else(|| { + mission_drafts + .and_then(|drafts| drafts.get(index)) + .map_or_else( + || identity_transform(index_to_f32(index)), + mission_position_scale_transform, + ) + }), range: IndexRange { start: 0, count: 3 }, stable_order, }); @@ -1041,6 +1043,40 @@ fn render_snapshot_with_assets( } } +fn snapshot_transform( + snapshot: &WorldSnapshot, + handle: fparkan_world::ObjectHandle, +) -> Option<[f32; 16]> { + snapshot + .transforms + .iter() + .find_map(|(candidate, transform)| (*candidate == handle).then_some(*transform)) + .map(transform_state_position_scale_transform) +} + +fn transform_state_position_scale_transform(transform: TransformState) -> [f32; 16] { + let position = transform.position.map(f32::from_bits); + let scale = transform.scale.map(f32::from_bits); + [ + scale[0], + 0.0, + 0.0, + position[0], + 0.0, + scale[1], + 0.0, + position[1], + 0.0, + 0.0, + scale[2], + position[2], + 0.0, + 0.0, + 0.0, + 1.0, + ] +} + fn mission_position_scale_transform(draft: &MissionObjectDraft) -> [f32; 16] { [ draft.scale[0], @@ -1781,6 +1817,55 @@ mod tests { ); } + #[test] + fn render_snapshot_prefers_world_transform_over_mission_draft() -> Result<(), String> { + let handle = ObjectHandle { + generation: 1, + slot: 0, + }; + let snapshot = WorldSnapshot { + tick: Tick(1), + objects: vec![handle], + transforms: vec![( + handle, + TransformState { + position: [ + 100.0_f32.to_bits(), + 200.0_f32.to_bits(), + 300.0_f32.to_bits(), + ], + orientation: [0; 3], + scale: [4.0_f32.to_bits(), 5.0_f32.to_bits(), 6.0_f32.to_bits()], + }, + )], + events: Vec::new(), + hash: StateHash([0; 32]), + }; + let drafts = vec![MissionObjectDraft { + original_id: None, + resource_name_raw: Vec::new(), + identity_or_clan_raw: 0, + position: [1.0; 3], + orientation_raw: [0.0; 3], + scale: [1.0; 3], + visual_ids: Vec::new(), + properties: Vec::new(), + }]; + + let commands = render_snapshot_commands_with_assets(&snapshot, None, Some(&drafts))?; + let RenderCommand::Draw(draw) = &commands.commands[1] else { + return Err("expected draw".to_string()); + }; + assert_eq!( + draw.transform, + [ + 4.0, 0.0, 0.0, 100.0, 0.0, 5.0, 0.0, 200.0, 0.0, 0.0, 6.0, 300.0, 0.0, 0.0, 0.0, + 1.0 + ] + ); + Ok(()) + } + #[test] fn render_snapshot_preserves_mission_original_object_id() -> Result<(), String> { let snapshot = WorldSnapshot { diff --git a/docs/tomes/07-implementation.md b/docs/tomes/07-implementation.md index d9c6f80..c48fea2 100644 --- a/docs/tomes/07-implementation.md +++ b/docs/tomes/07-implementation.md @@ -390,6 +390,13 @@ 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. +The ordinary planning renderer now consumes this snapshot state before falling +back to a mission draft. Its current transform bridge applies the preserved +position and non-uniform scale only; raw orientation remains uninterpreted in +this backend-neutral path. A GOG AutoDemo planning run on 2026-07-18 completed +two ticks with eight objects, 66 draws and state hash +`a54855a4f47ffa380911228f295dd49a9a7b88d6ff271a23db48ba318b1fbbb4`. + Записывается начальная миссия, seed, input events, network messages и значения внешних часов. На контрольных ticks сохраняется canonical state hash: