aboutsummaryrefslogtreecommitdiff
path: root/crates/terrain-core/src/lib.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-06-22 12:12:27 +0300
committerValentin Popov <valentin@popov.link>2026-06-22 12:13:32 +0300
commitd0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448 (patch)
treea0bd35c3940be62a5b5de1acc2366af377ffd181 /crates/terrain-core/src/lib.rs
parent7416fdc7e9a48837fff5056e6dc8d0774e90964b (diff)
downloadfparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.tar.xz
fparkan-d0bdbaa1ed76dfbf3211bb43eee48c49cc4fd448.zip
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.
Diffstat (limited to 'crates/terrain-core/src/lib.rs')
-rw-r--r--crates/terrain-core/src/lib.rs281
1 files changed, 0 insertions, 281 deletions
diff --git a/crates/terrain-core/src/lib.rs b/crates/terrain-core/src/lib.rs
deleted file mode 100644
index 36a3e42..0000000
--- a/crates/terrain-core/src/lib.rs
+++ /dev/null
@@ -1,281 +0,0 @@
-use nres::Archive;
-use std::fmt;
-use std::path::Path;
-
-pub const TERRAIN_UV_SCALE: f32 = 1024.0;
-
-pub type Result<T> = core::result::Result<T, Error>;
-
-#[derive(Debug)]
-pub enum Error {
- Nres(nres::error::Error),
- MissingChunk(&'static str),
- InvalidChunkSize {
- label: &'static str,
- size: usize,
- stride: usize,
- },
- VertexCountOverflow {
- count: usize,
- },
-}
-
-impl fmt::Display for Error {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- match self {
- Self::Nres(err) => write!(f, "{err}"),
- Self::MissingChunk(label) => write!(f, "missing required terrain chunk: {label}"),
- Self::InvalidChunkSize {
- label,
- size,
- stride,
- } => write!(
- f,
- "invalid chunk size for {label}: {size} (must be divisible by {stride})"
- ),
- Self::VertexCountOverflow { count } => {
- write!(f, "terrain vertex count {count} exceeds u16 range")
- }
- }
- }
-}
-
-impl std::error::Error for Error {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Self::Nres(err) => Some(err),
- _ => None,
- }
- }
-}
-
-impl From<nres::error::Error> for Error {
- fn from(value: nres::error::Error) -> Self {
- Self::Nres(value)
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct TerrainMesh {
- pub positions: Vec<[f32; 3]>,
- pub uv0: Vec<[f32; 2]>,
- pub faces: Vec<TerrainFace>,
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct TerrainFace {
- pub indices: [u16; 3],
- pub flags: u32,
- pub material_tag: u16,
- pub aux_tag: u16,
-}
-
-#[derive(Clone, Debug)]
-pub struct TerrainRenderMesh {
- pub vertices: Vec<TerrainRenderVertex>,
- pub indices: Vec<u16>,
- pub face_count_raw: usize,
- pub face_count_kept: usize,
- pub face_count_dropped_invalid: usize,
-}
-
-#[derive(Copy, Clone, Debug)]
-pub struct TerrainRenderVertex {
- pub position: [f32; 3],
- pub uv0: [f32; 2],
-}
-
-pub fn load_land_mesh(path: impl AsRef<Path>) -> Result<TerrainMesh> {
- let archive = Archive::open_path(path.as_ref())?;
-
- let positions_entry = archive
- .entries()
- .find(|entry| entry.meta.kind == 3)
- .ok_or(Error::MissingChunk("type=3 (positions)"))?;
- let uv_entry = archive.entries().find(|entry| entry.meta.kind == 5);
- let faces_entry = archive
- .entries()
- .find(|entry| entry.meta.kind == 21)
- .ok_or(Error::MissingChunk("type=21 (faces)"))?;
-
- let positions_payload = archive.read(positions_entry.id)?.into_owned();
- if positions_payload.len() % 12 != 0 {
- return Err(Error::InvalidChunkSize {
- label: "type=3 (positions)",
- size: positions_payload.len(),
- stride: 12,
- });
- }
-
- let mut positions = Vec::with_capacity(positions_payload.len() / 12);
- for chunk in positions_payload.chunks_exact(12) {
- let x = f32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0; 4]));
- let y = f32::from_le_bytes(chunk[4..8].try_into().unwrap_or([0; 4]));
- let z = f32::from_le_bytes(chunk[8..12].try_into().unwrap_or([0; 4]));
- positions.push([x, y, z]);
- }
-
- let mut uv0 = vec![[0.0f32, 0.0f32]; positions.len()];
- if let Some(uv_entry) = uv_entry {
- let uv_payload = archive.read(uv_entry.id)?.into_owned();
- if uv_payload.len() % 4 != 0 {
- return Err(Error::InvalidChunkSize {
- label: "type=5 (uv)",
- size: uv_payload.len(),
- stride: 4,
- });
- }
- let uv_count = uv_payload.len() / 4;
- for idx in 0..uv_count.min(uv0.len()) {
- let off = idx * 4;
- let u = i16::from_le_bytes([uv_payload[off], uv_payload[off + 1]]) as f32;
- let v = i16::from_le_bytes([uv_payload[off + 2], uv_payload[off + 3]]) as f32;
- uv0[idx] = [u / TERRAIN_UV_SCALE, v / TERRAIN_UV_SCALE];
- }
- }
-
- let face_payload = archive.read(faces_entry.id)?.into_owned();
- if face_payload.len() % 28 != 0 {
- return Err(Error::InvalidChunkSize {
- label: "type=21 (faces)",
- size: face_payload.len(),
- stride: 28,
- });
- }
-
- let mut faces = Vec::with_capacity(face_payload.len() / 28);
- for chunk in face_payload.chunks_exact(28) {
- let flags = u32::from_le_bytes(chunk[0..4].try_into().unwrap_or([0; 4]));
- let material_tag = u16::from_le_bytes(chunk[4..6].try_into().unwrap_or([0; 2]));
- let aux_tag = u16::from_le_bytes(chunk[6..8].try_into().unwrap_or([0; 2]));
- let i0 = u16::from_le_bytes(chunk[8..10].try_into().unwrap_or([0; 2]));
- let i1 = u16::from_le_bytes(chunk[10..12].try_into().unwrap_or([0; 2]));
- let i2 = u16::from_le_bytes(chunk[12..14].try_into().unwrap_or([0; 2]));
- if usize::from(i0) >= positions.len()
- || usize::from(i1) >= positions.len()
- || usize::from(i2) >= positions.len()
- {
- continue;
- }
- faces.push(TerrainFace {
- indices: [i0, i1, i2],
- flags,
- material_tag,
- aux_tag,
- });
- }
-
- Ok(TerrainMesh {
- positions,
- uv0,
- faces,
- })
-}
-
-pub fn build_render_mesh(mesh: &TerrainMesh) -> Result<TerrainRenderMesh> {
- if mesh.positions.len() > usize::from(u16::MAX) + 1 {
- return Err(Error::VertexCountOverflow {
- count: mesh.positions.len(),
- });
- }
-
- let vertices = mesh
- .positions
- .iter()
- .enumerate()
- .map(|(idx, &position)| TerrainRenderVertex {
- position,
- uv0: mesh.uv0.get(idx).copied().unwrap_or([0.0, 0.0]),
- })
- .collect::<Vec<_>>();
-
- let mut indices = Vec::with_capacity(mesh.faces.len() * 3);
- for face in &mesh.faces {
- indices.extend_from_slice(&face.indices);
- }
-
- Ok(TerrainRenderMesh {
- vertices,
- indices,
- face_count_raw: mesh.faces.len(),
- face_count_kept: mesh.faces.len(),
- face_count_dropped_invalid: 0,
- })
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
- use common::collect_files_recursive;
- use std::path::{Path, PathBuf};
-
- fn game_root() -> Option<PathBuf> {
- let root = Path::new(env!("CARGO_MANIFEST_DIR"))
- .join("..")
- .join("..")
- .join("testdata")
- .join("Parkan - Iron Strategy");
- root.is_dir().then_some(root)
- }
-
- #[test]
- fn loads_known_land_mesh() {
- let Some(root) = game_root() else {
- eprintln!("skipping: game root missing");
- return;
- };
-
- let land = root
- .join("DATA")
- .join("MAPS")
- .join("Tut_1")
- .join("Land.msh");
- if !land.is_file() {
- eprintln!("skipping missing sample {}", land.display());
- return;
- }
-
- let mesh = load_land_mesh(&land)
- .unwrap_or_else(|err| panic!("failed to parse {}: {err}", land.display()));
- assert!(mesh.positions.len() > 1000);
- assert!(mesh.faces.len() > 1000);
-
- let render = build_render_mesh(&mesh).expect("failed to build render mesh");
- assert_eq!(render.vertices.len(), mesh.positions.len());
- assert_eq!(render.indices.len(), mesh.faces.len() * 3);
- }
-
- #[test]
- fn loads_all_retail_land_meshes() {
- let Some(root) = game_root() else {
- eprintln!("skipping: game root missing");
- return;
- };
-
- let maps_root = root.join("DATA").join("MAPS");
- let mut files = Vec::new();
- collect_files_recursive(&maps_root, &mut files);
- files.sort();
-
- let mut parsed = 0usize;
- for path in files {
- if !path
- .file_name()
- .and_then(|n| n.to_str())
- .is_some_and(|n| n.eq_ignore_ascii_case("Land.msh"))
- {
- continue;
- }
- let mesh = load_land_mesh(&path)
- .unwrap_or_else(|err| panic!("failed to parse {}: {err}", path.display()));
- assert!(
- !mesh.positions.is_empty() && !mesh.faces.is_empty(),
- "{} parsed but empty",
- path.display()
- );
- parsed += 1;
- }
-
- assert!(parsed > 0, "no Land.msh files parsed");
- }
-}