physis/
tera.rs

1// SPDX-FileCopyrightText: 2024 Joshua Goins <josh@redstrate.com>
2// SPDX-License-Identifier: GPL-3.0-or-later
3
4use std::io::Cursor;
5
6use crate::ByteBuffer;
7use crate::ByteSpan;
8use binrw::BinRead;
9use binrw::BinWrite;
10use binrw::binrw;
11
12#[binrw]
13#[derive(Debug, Clone, Copy)]
14#[brw(little)]
15struct PlatePosition {
16    x: i16,
17    y: i16,
18}
19
20#[binrw]
21#[derive(Debug)]
22#[brw(little)]
23struct TerrainHeader {
24    // Example: 0x1000003
25    version: u32,
26    plate_count: u32,
27    plate_size: u32,
28    clip_distance: f32,
29
30    unknown: f32,
31
32    #[brw(pad_before = 32)]
33    #[br(count = plate_count)]
34    positions: Vec<PlatePosition>,
35}
36
37#[derive(Debug)]
38pub struct PlateModel {
39    pub position: (f32, f32),
40    pub filename: String,
41}
42
43#[derive(Debug)]
44pub struct Terrain {
45    pub plates: Vec<PlateModel>,
46}
47
48impl Terrain {
49    /// Reads an existing TERA file
50    pub fn from_existing(buffer: ByteSpan) -> Option<Terrain> {
51        let mut cursor = Cursor::new(buffer);
52        let header = TerrainHeader::read(&mut cursor).ok()?;
53
54        let mut plates = vec![];
55
56        for i in 0..header.plate_count {
57            plates.push(PlateModel {
58                position: (
59                    header.plate_size as f32 * (header.positions[i as usize].x as f32 + 0.5),
60                    header.plate_size as f32 * (header.positions[i as usize].y as f32 + 0.5),
61                ),
62                filename: format!("{:04}.mdl", i),
63            })
64        }
65
66        Some(Terrain { plates })
67    }
68
69    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
70        let mut buffer = ByteBuffer::new();
71
72        {
73            let mut cursor = Cursor::new(&mut buffer);
74
75            let plate_size = 128;
76
77            let header = TerrainHeader {
78                version: 0x1000003,
79                plate_count: self.plates.len() as u32,
80                plate_size,
81                clip_distance: 0.0, // TODO: make configurable
82                unknown: 1.0,       // TODO: what is this
83                positions: self
84                    .plates
85                    .iter()
86                    .map(|model| PlatePosition {
87                        x: ((model.position.0 / plate_size as f32) - 0.5) as i16,
88                        y: ((model.position.1 / plate_size as f32) - 0.5) as i16,
89                    })
90                    .collect(),
91            };
92            header.write_le(&mut cursor).ok()?;
93        }
94
95        Some(buffer)
96    }
97}
98
99#[cfg(test)]
100mod tests {
101    use std::fs::read;
102    use std::path::PathBuf;
103
104    use super::*;
105
106    #[test]
107    fn test_invalid() {
108        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
109        d.push("resources/tests");
110        d.push("random");
111
112        // Feeding it invalid data should not panic
113        Terrain::from_existing(&read(d).unwrap());
114    }
115}