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/msh-core/src/lib.rs | 434 --------------------------------------------- 1 file changed, 434 deletions(-) delete mode 100644 crates/msh-core/src/lib.rs (limited to 'crates/msh-core/src/lib.rs') diff --git a/crates/msh-core/src/lib.rs b/crates/msh-core/src/lib.rs deleted file mode 100644 index bc51357..0000000 --- a/crates/msh-core/src/lib.rs +++ /dev/null @@ -1,434 +0,0 @@ -pub mod error; - -use crate::error::Error; -use encoding_rs::WINDOWS_1251; -use std::sync::Arc; - -pub type Result = core::result::Result; - -pub const RES1_NODE_TABLE: u32 = 1; -pub const RES2_SLOTS: u32 = 2; -pub const RES3_POSITIONS: u32 = 3; -pub const RES4_NORMALS: u32 = 4; -pub const RES5_UV0: u32 = 5; -pub const RES6_INDICES: u32 = 6; -pub const RES10_NAMES: u32 = 10; -pub const RES13_BATCHES: u32 = 13; - -#[derive(Clone, Debug)] -pub struct Slot { - pub tri_start: u16, - pub tri_count: u16, - pub batch_start: u16, - pub batch_count: u16, - pub aabb_min: [f32; 3], - pub aabb_max: [f32; 3], - pub sphere_center: [f32; 3], - pub sphere_radius: f32, - pub opaque: [u32; 5], -} - -#[derive(Clone, Debug)] -pub struct Batch { - pub batch_flags: u16, - pub material_index: u16, - pub opaque4: u16, - pub opaque6: u16, - pub index_count: u16, - pub index_start: u32, - pub opaque14: u16, - pub base_vertex: u32, -} - -#[derive(Clone, Debug)] -pub struct Model { - pub node_stride: usize, - pub node_count: usize, - pub nodes_raw: Vec, - pub slots: Vec, - pub positions: Vec<[f32; 3]>, - pub normals: Option>, - pub uv0: Option>, - pub indices: Vec, - pub batches: Vec, - pub node_names: Option>>, -} - -impl Model { - pub fn slot_index(&self, node_index: usize, lod: usize, group: usize) -> Option { - if node_index >= self.node_count || lod >= 3 || group >= 5 { - return None; - } - if self.node_stride != 38 { - return None; - } - let node_off = node_index.checked_mul(self.node_stride)?; - let matrix_off = node_off.checked_add(8)?; - let word_off = matrix_off.checked_add((lod * 5 + group) * 2)?; - let raw = read_u16(&self.nodes_raw, word_off).ok()?; - if raw == u16::MAX { - return None; - } - let idx = usize::from(raw); - if idx >= self.slots.len() { - return None; - } - Some(idx) - } -} - -pub fn parse_model_payload(payload: &[u8]) -> Result { - let archive = nres::Archive::open_bytes( - Arc::from(payload.to_vec().into_boxed_slice()), - nres::OpenOptions::default(), - )?; - - let res1 = read_required(&archive, RES1_NODE_TABLE, "Res1")?; - let res2 = read_required(&archive, RES2_SLOTS, "Res2")?; - let res3 = read_required(&archive, RES3_POSITIONS, "Res3")?; - let res6 = read_required(&archive, RES6_INDICES, "Res6")?; - let res13 = read_required(&archive, RES13_BATCHES, "Res13")?; - - let res4 = read_optional(&archive, RES4_NORMALS)?; - let res5 = read_optional(&archive, RES5_UV0)?; - let res10 = read_optional(&archive, RES10_NAMES)?; - - let node_stride = usize::try_from(res1.meta.attr3).map_err(|_| Error::IntegerOverflow)?; - if node_stride != 38 && node_stride != 24 { - return Err(Error::UnsupportedNodeStride { - stride: node_stride, - }); - } - if res1.bytes.len() % node_stride != 0 { - return Err(Error::InvalidResourceSize { - label: "Res1", - size: res1.bytes.len(), - stride: node_stride, - }); - } - let node_count = res1.bytes.len() / node_stride; - - if res2.bytes.len() < 0x8C { - return Err(Error::InvalidRes2Size { - size: res2.bytes.len(), - }); - } - let slot_blob = res2 - .bytes - .len() - .checked_sub(0x8C) - .ok_or(Error::IntegerOverflow)?; - if slot_blob % 68 != 0 { - return Err(Error::InvalidResourceSize { - label: "Res2.slots", - size: slot_blob, - stride: 68, - }); - } - let slot_count = slot_blob / 68; - let mut slots = Vec::with_capacity(slot_count); - for i in 0..slot_count { - let off = 0x8Cusize - .checked_add(i.checked_mul(68).ok_or(Error::IntegerOverflow)?) - .ok_or(Error::IntegerOverflow)?; - slots.push(Slot { - tri_start: read_u16(&res2.bytes, off)?, - tri_count: read_u16(&res2.bytes, off + 2)?, - batch_start: read_u16(&res2.bytes, off + 4)?, - batch_count: read_u16(&res2.bytes, off + 6)?, - aabb_min: [ - read_f32(&res2.bytes, off + 8)?, - read_f32(&res2.bytes, off + 12)?, - read_f32(&res2.bytes, off + 16)?, - ], - aabb_max: [ - read_f32(&res2.bytes, off + 20)?, - read_f32(&res2.bytes, off + 24)?, - read_f32(&res2.bytes, off + 28)?, - ], - sphere_center: [ - read_f32(&res2.bytes, off + 32)?, - read_f32(&res2.bytes, off + 36)?, - read_f32(&res2.bytes, off + 40)?, - ], - sphere_radius: read_f32(&res2.bytes, off + 44)?, - opaque: [ - read_u32(&res2.bytes, off + 48)?, - read_u32(&res2.bytes, off + 52)?, - read_u32(&res2.bytes, off + 56)?, - read_u32(&res2.bytes, off + 60)?, - read_u32(&res2.bytes, off + 64)?, - ], - }); - } - - let positions = parse_positions(&res3.bytes)?; - let indices = parse_u16_array(&res6.bytes, "Res6")?; - let batches = parse_batches(&res13.bytes)?; - validate_slot_batch_ranges(&slots, batches.len())?; - validate_batch_index_ranges(&batches, indices.len())?; - - let normals = match res4 { - Some(raw) => Some(parse_i8x4_array(&raw.bytes, "Res4")?), - None => None, - }; - let uv0 = match res5 { - Some(raw) => Some(parse_i16x2_array(&raw.bytes, "Res5")?), - None => None, - }; - let node_names = match res10 { - Some(raw) => Some(parse_res10_names(&raw.bytes, node_count)?), - None => None, - }; - - Ok(Model { - node_stride, - node_count, - nodes_raw: res1.bytes, - slots, - positions, - normals, - uv0, - indices, - batches, - node_names, - }) -} - -fn validate_slot_batch_ranges(slots: &[Slot], batch_count: usize) -> Result<()> { - for slot in slots { - let start = usize::from(slot.batch_start); - let end = start - .checked_add(usize::from(slot.batch_count)) - .ok_or(Error::IntegerOverflow)?; - if end > batch_count { - return Err(Error::IndexOutOfBounds { - label: "Res2.batch_range", - index: end, - limit: batch_count, - }); - } - } - Ok(()) -} - -fn validate_batch_index_ranges(batches: &[Batch], index_count: usize) -> Result<()> { - for batch in batches { - let start = usize::try_from(batch.index_start).map_err(|_| Error::IntegerOverflow)?; - let end = start - .checked_add(usize::from(batch.index_count)) - .ok_or(Error::IntegerOverflow)?; - if end > index_count { - return Err(Error::IndexOutOfBounds { - label: "Res13.index_range", - index: end, - limit: index_count, - }); - } - } - Ok(()) -} - -fn parse_positions(data: &[u8]) -> Result> { - if !data.len().is_multiple_of(12) { - return Err(Error::InvalidResourceSize { - label: "Res3", - size: data.len(), - stride: 12, - }); - } - let count = data.len() / 12; - let mut out = Vec::with_capacity(count); - for i in 0..count { - let off = i * 12; - out.push([ - read_f32(data, off)?, - read_f32(data, off + 4)?, - read_f32(data, off + 8)?, - ]); - } - Ok(out) -} - -fn parse_batches(data: &[u8]) -> Result> { - if !data.len().is_multiple_of(20) { - return Err(Error::InvalidResourceSize { - label: "Res13", - size: data.len(), - stride: 20, - }); - } - let count = data.len() / 20; - let mut out = Vec::with_capacity(count); - for i in 0..count { - let off = i * 20; - out.push(Batch { - batch_flags: read_u16(data, off)?, - material_index: read_u16(data, off + 2)?, - opaque4: read_u16(data, off + 4)?, - opaque6: read_u16(data, off + 6)?, - index_count: read_u16(data, off + 8)?, - index_start: read_u32(data, off + 10)?, - opaque14: read_u16(data, off + 14)?, - base_vertex: read_u32(data, off + 16)?, - }); - } - Ok(out) -} - -fn parse_u16_array(data: &[u8], label: &'static str) -> Result> { - if !data.len().is_multiple_of(2) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 2, - }); - } - let mut out = Vec::with_capacity(data.len() / 2); - for i in (0..data.len()).step_by(2) { - out.push(read_u16(data, i)?); - } - Ok(out) -} - -fn parse_i8x4_array(data: &[u8], label: &'static str) -> Result> { - if !data.len().is_multiple_of(4) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 4, - }); - } - let mut out = Vec::with_capacity(data.len() / 4); - for i in (0..data.len()).step_by(4) { - out.push([ - read_i8(data, i)?, - read_i8(data, i + 1)?, - read_i8(data, i + 2)?, - read_i8(data, i + 3)?, - ]); - } - Ok(out) -} - -fn parse_i16x2_array(data: &[u8], label: &'static str) -> Result> { - if !data.len().is_multiple_of(4) { - return Err(Error::InvalidResourceSize { - label, - size: data.len(), - stride: 4, - }); - } - let mut out = Vec::with_capacity(data.len() / 4); - for i in (0..data.len()).step_by(4) { - out.push([read_i16(data, i)?, read_i16(data, i + 2)?]); - } - Ok(out) -} - -fn parse_res10_names(data: &[u8], node_count: usize) -> Result>> { - let mut out = Vec::with_capacity(node_count); - let mut off = 0usize; - for _ in 0..node_count { - let len = usize::try_from(read_u32(data, off)?).map_err(|_| Error::IntegerOverflow)?; - off = off.checked_add(4).ok_or(Error::IntegerOverflow)?; - if len == 0 { - out.push(None); - continue; - } - let need = len.checked_add(1).ok_or(Error::IntegerOverflow)?; - let end = off.checked_add(need).ok_or(Error::IntegerOverflow)?; - let slice = data.get(off..end).ok_or(Error::InvalidResourceSize { - label: "Res10", - size: data.len(), - stride: 1, - })?; - let text = if slice.last().copied() == Some(0) { - &slice[..slice.len().saturating_sub(1)] - } else { - slice - }; - let decoded = decode_cp1251(text); - out.push(Some(decoded)); - off = end; - } - Ok(out) -} - -fn decode_cp1251(bytes: &[u8]) -> String { - let (decoded, _, _) = WINDOWS_1251.decode(bytes); - decoded.into_owned() -} - -struct RawResource { - meta: nres::EntryMeta, - bytes: Vec, -} - -fn read_required(archive: &nres::Archive, kind: u32, label: &'static str) -> Result { - let id = archive - .entries() - .find(|entry| entry.meta.kind == kind) - .map(|entry| entry.id) - .ok_or(Error::MissingResource { kind, label })?; - let entry = archive.get(id).ok_or(Error::IndexOutOfBounds { - label, - index: usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?, - limit: archive.entry_count(), - })?; - let data = archive.read(id)?.into_owned(); - Ok(RawResource { - meta: entry.meta.clone(), - bytes: data, - }) -} - -fn read_optional(archive: &nres::Archive, kind: u32) -> Result> { - let Some(id) = archive - .entries() - .find(|entry| entry.meta.kind == kind) - .map(|entry| entry.id) - else { - return Ok(None); - }; - let entry = archive.get(id).ok_or(Error::IndexOutOfBounds { - label: "optional", - index: usize::try_from(id.0).map_err(|_| Error::IntegerOverflow)?, - limit: archive.entry_count(), - })?; - let data = archive.read(id)?.into_owned(); - Ok(Some(RawResource { - meta: entry.meta.clone(), - bytes: data, - })) -} - -fn read_u16(data: &[u8], offset: usize) -> Result { - let bytes = data.get(offset..offset + 2).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 2] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(u16::from_le_bytes(arr)) -} - -fn read_i16(data: &[u8], offset: usize) -> Result { - let bytes = data.get(offset..offset + 2).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 2] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(i16::from_le_bytes(arr)) -} - -fn read_i8(data: &[u8], offset: usize) -> Result { - let byte = data.get(offset).copied().ok_or(Error::IntegerOverflow)?; - Ok(i8::from_le_bytes([byte])) -} - -fn read_u32(data: &[u8], offset: usize) -> Result { - let bytes = data.get(offset..offset + 4).ok_or(Error::IntegerOverflow)?; - let arr: [u8; 4] = bytes.try_into().map_err(|_| Error::IntegerOverflow)?; - Ok(u32::from_le_bytes(arr)) -} - -fn read_f32(data: &[u8], offset: usize) -> Result { - Ok(f32::from_bits(read_u32(data, offset)?)) -} - -#[cfg(test)] -mod tests; -- cgit v1.2.3