Skip to main content

rpfm_lib/files/rigidmodel/materials/
mod.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Material system for RigidModel files.
12//!
13//! # Overview
14//!
15//! Materials define how meshes are rendered in Total War games. Each material type
16//! has specific properties that control textures, shaders, vertex formats, and
17//! rendering behavior. The material system supports 40+ different material types
18//! for various rendering scenarios.
19//!
20//! # Material Types
21//!
22//! Materials are categorized by their rendering purpose:
23//!
24//! ## Standard Rendering
25//! - **DefaultMaterial (68)**: Full-featured material for most meshes
26//! - **Decal (71)**: Applied decals on surfaces
27//! - **Tree (74)**, **TreeLeaf (75)**: Vegetation rendering
28//! - **Grass (69)**: Grass
29//! - **Water (83)**: Water surfaces
30//! - **Unlit (84)**: No lighting calculations
31//!
32//! ## Skeletal Animation (Weighted)
33//! - **WeightedSkin (70)**: Character skin with bone weights
34//! - **WeightedCloth (58)**, **Cloth (60)**: Cloth simulation
35//! - **Weighted (65)**: Generic weighted material
36//!
37//! ## Terrain
38//! - **RsTerrain (66)**: Minimal terrain material
39//! - **CustomTerrain (49)**, **GlobalTerrain (98)**: Terrain variants
40//! - **WeightedTextureBlend (96)**, **TerrainBlend (86)**: Texture blending
41//! - **TiledDirtmap (63)**: Tiled dirt textures
42//!
43//! ## Special Effects
44//! - **Collision (61)**, **CollisionShape (62)**: Collision meshes (invisible)
45//! - **DebugGeometry (46)**: Debug visualization
46//! - **PointLight (38)**, **StaticPointLight (45)**: Light sources
47//! - **Rope (93)**: Rope rendering
48//!
49//! ## Projected Decals
50//! - **ProjectedDecal (67)**, **ProjectedDecalV2 (87)**, **ProjectedDecalV3 (95)**, **ProjectedDecalV4 (97)**
51//!
52//! # Material Data Structure
53//!
54//! Materials contain:
55//! - **Vertex format**: Determines vertex data layout
56//! - **Textures**: Diffuse, normal, specular, masks, etc.
57//! - **Transformation matrices**: 3x4 matrices for positioning/attachment
58//! - **Attachment points**: Named locations for effects/weapons
59//! - **Shader parameters**: Strings, floats, integers, vectors
60//! - **Cloth-specific data**: Physics simulation parameters (cloth materials only)
61//!
62//! # Texture Types
63//!
64//! Materials reference textures by type:
65//! - **Diffuse (0)**: Base color texture
66//! - **Normal (1)**: Normal mapping for surface detail
67//! - **Specular (11)**, **GlossMap (12)**: Reflectivity
68//! - **Mask (3)**: Alpha/transparency mask
69//! - **AmbientOcclusion (5)**: Baked ambient lighting
70//! - **Decal variants**: Special decal textures
71//!
72//! # Implementation Details
73//!
74//! Different material types have different data structures:
75//! - **Default materials**: Full data (textures, params, attachment points)
76//! - **RsTerrain**: Minimal (name + 5 unknown u32 fields)
77//! - **WeightedTextureBlend**: Name + 6 unknown u32 fields
78//! - **AlphaBlend**: Name only
79//! - **Cloth**: Default data + cloth simulation sections (uk_7, uk_8, uk_9)
80//!
81//! See individual material variant modules for format-specific details.
82
83use std::io::Cursor;
84
85use getset::{Getters, MutGetters, Setters};
86use nalgebra::{Matrix3x4, Vector3, Vector4};
87use serde::{Deserialize, Serialize};
88
89use crate::binary::{ReadBytes, WriteBytes};
90use crate::error::{Result, RLibError};
91
92use super::vertices::VertexFormat;
93use super::{PADDED_SIZE_32, PADDED_SIZE_256};
94
95mod alpha_blend;
96mod cloth;
97mod collision_shape;
98mod custom_terrain;
99mod default;
100mod rs_terrain;
101mod rope;
102mod rs_river;
103mod static_point_light;
104mod texture_combo;
105mod uk_27;
106mod uk_54;
107mod weighted_cloth;
108mod projected_decal;
109mod projected_decal_v4;
110mod weighted_texture_blend;
111
112#[cfg(test)] mod materials_test;
113
114//---------------------------------------------------------------------------//
115//                              Enum & Structs
116//---------------------------------------------------------------------------//
117
118/// Material type identifier determining rendering behavior and data structure.
119///
120/// Each material type corresponds to a specific shader and vertex format combination.
121/// The numeric value is stored in the RigidModel file format as a u16.
122///
123/// # Categories
124///
125/// - **22-46**: Effects and special materials (BowWave, PointLight, DebugGeometry)
126/// - **49-64**: Terrain and campaign materials
127/// - **65-82**: Weighted (skeletal) materials and variants
128/// - **83-100**: Water, unlit, terrain blends, projected decals
129#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
130#[repr(u16)]
131pub enum MaterialType {
132    /// Boat bow wave effect (water displacement).
133    BowWave = 22,
134    /// Non-rendered material (collision, occlusion, etc.).
135    NonRenderable = 26,
136    /// Unknown material, seen in an old test skybox.
137    Uk27 = 27,
138    /// Texture combo with vertex-based wind animation.
139    TextureComboVertexWind = 29,
140    /// Combined texture material.
141    TextureCombo = 30,
142    /// Waterfall decal effect.
143    DecalWaterfall = 31,
144    /// Simplified standard material.
145    StandardSimple = 32,
146    /// Campaign map tree rendering.
147    CampaignTrees = 34,
148    /// Dynamic point light source.
149    PointLight = 38,
150    /// Static baked point light.
151    StaticPointLight = 45,
152    /// Debug visualization geometry (wireframe, normals, etc.).
153    DebugGeometry = 46,
154    /// Outfield meshes of river tiles, with no material data.
155    RiverOutfield = 48,
156    /// Unknown material, seen in some statues. Has no vertex format.
157    Uk54 = 54,
158    /// Unknown material, seen in the burning variant of some buildings. Has no vertex data, only indices into the vertices of another mesh.
159    Uk56 = 56,
160    /// Unknown material, seen in the rain variant of some buildings. Has no vertex data, only indices into the vertices of another mesh.
161    Uk57 = 57,
162    /// Custom terrain material variant.
163    CustomTerrain = 49,
164    /// Cloth material with skeletal weights and physics.
165    WeightedCloth = 58,
166    /// Cloth material with physics simulation.
167    Cloth = 60,
168    /// Collision mesh (invisible, physics only).
169    Collision = 61,
170    /// Collision shape variant.
171    CollisionShape = 62,
172    /// Tiled dirt texture mapping.
173    TiledDirtmap = 63,
174    /// Ship ambient mapping (possibly incorrect identification).
175    ShipAmbientmap = 64,
176    /// Generic weighted material (skeletal animation).
177    Weighted = 65,
178    /// Minimal terrain material (Rome 2 style).
179    RsTerrain = 66,
180    /// Projected decal (first version).
181    ProjectedDecal = 67,
182    /// Default full-featured material (most common).
183    #[default]
184    DefaultMaterial = 68,
185    /// Grass and vegetation material.
186    Grass = 69,
187    /// Weighted skin material (characters with bone weights).
188    WeightedSkin = 70,
189    /// Surface decal material.
190    Decal = 71,
191    /// Decal with dirt mapping.
192    DecalDirtmap = 72,
193    /// Dirt map material.
194    Dirtmap = 73,
195    /// Tree trunk/branch material.
196    Tree = 74,
197    /// Tree leaf material (alpha transparency).
198    TreeLeaf = 75,
199    /// Weighted decal (animated decals on characters).
200    WeightedDecal = 77,
201    /// Weighted decal with dirt mapping.
202    WeightedDecalDirtmap = 78,
203    /// Weighted dirt mapping.
204    WeightedDirtmap = 79,
205    /// Weighted skin with decal overlay.
206    WeightedSkinDecal = 80,
207    /// Weighted skin with decal and dirt mapping.
208    WeightedSkinDecalDirtmap = 81,
209    /// Weighted skin with dirt mapping.
210    WeightedSkinDirtmap = 82,
211    /// Water surface material.
212    Water = 83,
213    /// Unlit material (no lighting calculations, full bright).
214    Unlit = 84,
215    /// Weighted unlit material.
216    WeightedUnlit = 85,
217    /// Terrain texture blending material.
218    TerrainBlend = 86,
219    /// Projected decal version 2.
220    ProjectedDecalV2 = 87,
221    /// Ignored/placeholder material.
222    Ignore = 88,
223    /// Billboard-style tree material (always faces camera).
224    TreeBillboardMaterial = 89,
225    /// River rendering material (Rome 2 style).
226    RsRiver = 90,
227    /// Water displacement volume (physics interaction).
228    WaterDisplaceVolume = 91,
229    /// Rope rendering material.
230    Rope = 93,
231    /// Campaign map vegetation.
232    CampaignVegetation = 94,
233    /// Projected decal version 3.
234    ProjectedDecalV3 = 95,
235    /// Weighted texture blending.
236    WeightedTextureBlend = 96,
237    /// Projected decal version 4 (latest).
238    ProjectedDecalV4 = 97,
239    /// Global terrain material.
240    GlobalTerrain = 98,
241    /// Overlay decal material.
242    DecalOverlay = 99,
243    /// Alpha-blended material.
244    AlphaBlend = 100,
245}
246
247/// Complete material definition with textures, transforms, and shader parameters.
248///
249/// Materials contain all rendering data except geometry. The exact fields present
250/// depend on the material type - some types use only a subset of these fields.
251///
252/// # Field Organization
253///
254/// - **Identification**: `vertex_format`, `name`
255/// - **Unknown fields**: `uk_1` through `uk_6` (only in certain material types)
256/// - **Textures**: `texture_directory`, `textures` list
257/// - **Transforms**: `v_pivot`, `matrix1/2/3`, matrix indices
258/// - **Attachments**: `attachment_points` for effects/weapons
259/// - **Shader params**: `params_string`, `params_f32`, `params_i32`, `params_vector4df32`
260/// - **Cloth physics**: `uk_7`, `uk_8`, `uk_9` (cloth materials only)
261///
262/// # Matrix Storage
263///
264/// 3x4 matrices are stored in the same format as skeleton bind pose matrices.
265/// The implicit fourth row `[0, 0, 0, 1]` must be added when converting to 4x4.
266#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
267#[getset(get = "pub", get_mut = "pub", set = "pub")]
268pub struct Material {
269    /// Vertex format determining vertex data layout.
270    vertex_format: VertexFormat,
271    
272    /// Material name (human-readable identifier).
273    name: String,
274
275    /// Second name, only in materials of types 56 and 57: the name of the mesh followed by a suffix, like `burning` or `rain`.
276    name2: String,
277
278    /// Bytes found after the end of `name` in its padded field, usually leftovers of the program that wrote the file.
279    /// They're kept so the file is written back as it was.
280    name_padding: Vec<u8>,
281
282    /// Unknown field 1 (only in RsTerrain and projected decal materials).
283    uk_1: u32,
284    /// Unknown field 2 (only in RsTerrain and projected decal materials).
285    uk_2: u32,
286    /// Unknown field 3 (only in RsTerrain and projected decal materials).
287    uk_3: u32,
288    /// Unknown field 4 (only in RsTerrain and projected decal materials).
289    uk_4: u32,
290    /// Unknown field 5 (only in RsTerrain and projected decal materials).
291    uk_5: u32,
292    /// Unknown field 6 (only in RsTerrain and projected decal materials).
293    uk_6: u32,
294
295    /// Directory path for texture files (relative to game data directory).
296    texture_directory: String,
297
298    /// Filter settings (NOT part of file format, runtime-only).
299    filters: String,
300
301    /// Padding byte 0 (alignment/reserved).
302    padding_byte0: u8,
303    /// Padding byte 1 (alignment/reserved).
304    padding_byte1: u8,
305
306    /// Pivot point for transformations.
307    v_pivot: Vector3<f32>,
308
309    /// Center of the box a projected decal is projected in.
310    decal_center: Vector3<f32>,
311
312    /// Size of the box a projected decal is projected in.
313    decal_size: Vector3<f32>,
314
315    /// Center of the sphere around a collision shape.
316    sphere_center: Vector3<f32>,
317
318    /// Radius of the sphere around a collision shape.
319    sphere_radius: f32,
320
321    /// Bones of the cloth simulation of weighted cloth materials.
322    cloth_bones: Vec<ClothBone>,
323
324    /// Simulation data of rope materials.
325    rope: Rope,
326    
327    /// Transform matrix 1 (3x4, implicit 4th row: [0, 0, 0, 1]).
328    matrix1: Matrix3x4<f32>,
329    /// Transform matrix 2 (3x4, implicit 4th row: [0, 0, 0, 1]).
330    matrix2: Matrix3x4<f32>,
331    /// Transform matrix 3 (3x4, implicit 4th row: [0, 0, 0, 1]).
332    matrix3: Matrix3x4<f32>,
333
334    /// Matrix index in hierarchy.
335    i_matrix_index: i32,
336    /// Parent matrix index for hierarchical transforms.
337    i_parent_matrix_index: i32,
338
339    /// Additional padding bytes (variable length).
340    sz_padding: Vec<u8>,
341
342    /// Named attachment points for weapons, effects, banners, etc.
343    attachment_points: Vec<AttachmentPointEntry>,
344    
345    /// Texture list with type and path for each texture.
346    textures: Vec<Texture>,
347    
348    /// String shader parameters (index, value pairs).
349    params_string: Vec<(i32, String)>,
350    /// Float shader parameters (index, value pairs).
351    params_f32: Vec<(i32, f32)>,
352    /// Integer shader parameters (index, value pairs).
353    params_i32: Vec<(i32, i32)>,
354    /// Vector4 shader parameters (index, value pairs).
355    params_vector4df32: Vec<(i32, Vector4<f32>)>,
356
357    /// Cloth physics data section 1 (cloth materials only, format undocumented).
358    uk_7: Vec<Uk7>,
359    /// Cloth physics data section 2 (cloth materials only, format undocumented).
360    uk_8: Vec<Uk8>,
361    /// Cloth physics data section 3 (cloth materials only, format undocumented).
362    uk_9: Vec<Uk9>,
363}
364
365/// Named attachment point for visual effects, weapons, or equipment.
366///
367/// Attachment points define locations on a model where other objects can be attached,
368/// such as weapon hardpoints, banner poles, or particle effect emitters.
369#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
370#[getset(get = "pub", get_mut = "pub", set = "pub")]
371pub struct AttachmentPointEntry {
372    /// Attachment point name (e.g., "weapon_1", "engine_exhaust").
373    name: String,
374    
375    /// 3x4 transform matrix positioning the attachment point (implicit 4th row: [0, 0, 0, 1]).
376    matrix: Matrix3x4<f32>,
377    
378    /// Bone ID for skeletal attachment (0 for non-skeletal).
379    bone_id: u32,
380}
381
382/// Texture reference with type and file path.
383/// The simulation data of a rope material: a chain of nodes joined by links.
384#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
385#[getset(get = "pub", get_mut = "pub", set = "pub")]
386pub struct Rope {
387
388    /// Unknown indices, or -1. Likely what the rope is attached to.
389    attachments: Vector4<i32>,
390
391    /// Unknown value, around 0.03.
392    uk_1: f32,
393
394    /// Unknown value, usually a power of two or 0. Likely flags.
395    uk_2: u32,
396
397    /// Nodes of the rope, from one end to the other.
398    nodes: Vec<RopeNode>,
399
400    /// Links between consecutive nodes. There's always one fewer than nodes.
401    links: Vec<RopeLink>,
402}
403
404/// A node of a rope.
405#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
406#[getset(get = "pub", get_mut = "pub", set = "pub")]
407pub struct RopeNode {
408
409    /// Position of the node.
410    position: Vector3<f32>,
411
412    /// Weight of the node in the simulation: 0 for the pinned ends, 1 for the rest.
413    weight: f32,
414
415    /// Unknown value, decreasing along the rope.
416    uk_1: f32,
417}
418
419/// A link between two nodes of a rope.
420#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
421#[getset(get = "pub", get_mut = "pub", set = "pub")]
422pub struct RopeLink {
423
424    /// Index of the first node.
425    node_a: u32,
426
427    /// Index of the second node.
428    node_b: u32,
429
430    /// Length of the link.
431    length: f32,
432}
433
434/// A bone of the cloth simulation of a weighted cloth material, attached to a vertex of its mesh.
435#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
436#[getset(get = "pub", get_mut = "pub", set = "pub")]
437pub struct ClothBone {
438
439    /// Name of the bone.
440    name: String,
441
442    /// Bytes found after the end of `name` in its padded field, usually leftovers of the program that wrote the file.
443    /// They're kept so the file is written back as it was.
444    name_padding: Vec<u8>,
445
446    /// Unknown values. (100, 200, 400) in every Attila file.
447    uk_1: Vector3<f32>,
448
449    /// Unknown value. 12 in every Attila file.
450    uk_2: u32,
451
452    /// Unknown value. 64 in every Attila file.
453    uk_3: u32,
454
455    /// Unknown value, the same for all the bones of a mesh.
456    uk_4: u32,
457
458    /// Unknown value. 24 or 25.
459    uk_5: u32,
460
461    /// Unknown value. 24 or 25.
462    uk_6: u32,
463
464    /// Unknown value. 0 or 1.
465    uk_7: f32,
466
467    /// Transform of the bone.
468    matrix: Matrix3x4<f32>,
469
470    /// Index of the parent bone, or -1.
471    parent_index: i32,
472
473    /// Index of the vertex of the mesh the bone is attached to.
474    vertex_index: u32,
475}
476
477#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
478#[getset(get = "pub", get_mut = "pub", set = "pub")]
479pub struct Texture {
480    /// Texture type/slot (diffuse, normal, specular, etc.).
481    tex_type: TextureType,
482    
483    /// Relative path to texture file (from `texture_directory`).
484    path: String,
485}
486
487/// Texture type identifier for shader texture slots.
488///
489/// Determines which shader texture slot this texture is bound to during rendering.
490/// The numeric value is stored as an i32 in the file format.
491#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
492#[repr(i32)]
493pub enum TextureType {
494    /// Base color/albedo texture.
495    #[default]
496    Diffuse = 0,
497    /// Normal map for surface detail (tangent space).
498    Normal = 1,
499    /// Alpha/transparency mask.
500    Mask = 3,
501    /// Material map of older games, like Rome II and Attila.
502    LegacyMaterialMap = 4,
503    /// Baked ambient occlusion.
504    AmbientOcclusion = 5,
505    /// Tessellation displacement map (red channel: the offset along the normal), bound as `t_tesselation_displacement_map` by the
506    /// Rome II / Attila / Thrones of Britannia engine when tessellation is enabled.
507    Displacement = 6,
508    /// Tiling dirt texture (UV set 2).
509    TilingDirtUV2 = 7,
510    /// Dirt alpha mask.
511    DirtAlphaMask = 8,
512    /// Skin mask texture.
513    SkinMask = 10,
514    /// Specular reflectivity map.
515    Specular = 11,
516    /// Gloss/smoothness map (specular roughness).
517    GlossMap = 12,
518    /// Decal dirt map.
519    DecalDirtmap = 13,
520    /// Decal dirt mask.
521    DecalDirtmask = 14,
522    /// Decal alpha mask.
523    DecalMask = 15,
524    /// Damaged diffuse texture variant.
525    DiffuseDamage = 17,
526    /// PBR base color.
527    BaseColor = 27,
528    /// PBR material properties (metallic/roughness/AO packed).
529    MaterialMap = 29,
530}
531
532/// Cloth physics data structure 1 (format undocumented).
533///
534/// This structure appears only in cloth materials and likely contains cloth simulation
535/// parameters. The exact meaning of the fields is not yet reverse-engineered.
536#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
537#[getset(get = "pub", get_mut = "pub", set = "pub")]
538pub struct Uk7 {
539    /// Unknown integer field 1 (possibly constraint/spring index).
540    uk1: i32,
541    /// Unknown integer field 2 (possibly vertex/node index).
542    uk2: i32,
543    /// Unknown float field (possibly stiffness/damping coefficient).
544    uk3: f32,
545}
546
547/// Cloth physics data structure 2 (format undocumented).
548///
549/// This structure appears only in cloth materials and likely contains cloth simulation
550/// parameters. The exact meaning of the field is not yet reverse-engineered.
551#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
552#[getset(get = "pub", get_mut = "pub", set = "pub")]
553pub struct Uk8 {
554    /// Unknown integer field (possibly fixed vertex/anchor point index).
555    uk1: i32,
556}
557
558/// Cloth physics data structure 3 (format undocumented).
559///
560/// This structure appears only in cloth materials and likely contains cloth simulation
561/// parameters. The exact meaning of the fields is not yet reverse-engineered.
562#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
563#[getset(get = "pub", get_mut = "pub", set = "pub")]
564pub struct Uk9 {
565    /// Unknown integer field 1 (possibly triangle/face index).
566    uk1: i32,
567    /// Unknown integer field 2 (possibly collision group).
568    uk2: i32,
569    /// Unknown integer field 3 (possibly property flags).
570    uk3: i32,
571}
572
573//---------------------------------------------------------------------------//
574//                            Implementation
575//---------------------------------------------------------------------------//
576
577
578impl Material {
579
580    /// Reads a material from binary data based on the material type.
581    ///
582    /// Different material types have different binary layouts, which may change between
583    /// versions of the RigidModel format. This function dispatches to the appropriate reader.
584    ///
585    /// # Errors
586    ///
587    /// Returns [`RLibError::DecodingRigidModelUnsupportedMaterialType`] if the
588    /// material type is not supported.
589    pub fn read<R: ReadBytes>(data: &mut R, version: u32, mtype: MaterialType) -> Result<Self> {
590        Ok(match mtype {
591            MaterialType::RsTerrain => Self::read_rs_terrain(data, version)?,
592            MaterialType::BowWave |
593            MaterialType::DebugGeometry |
594            MaterialType::RiverOutfield |
595            MaterialType::RsRiver => Self::read_rs_river(data, version)?,
596            MaterialType::WeightedTextureBlend => Self::read_weighted_texture_blend(data, version)?,
597            MaterialType::ProjectedDecalV4 if version <= 6 => Self::read_projected_decal_v4(data, version)?,
598            MaterialType::ProjectedDecal |
599            MaterialType::ProjectedDecalV2 |
600            MaterialType::ProjectedDecalV3 => Self::read_projected_decal(data, version, mtype)?,
601            MaterialType::CustomTerrain => Self::read_custom_terrain(data, version)?,
602            MaterialType::CollisionShape => Self::read_collision_shape(data, version)?,
603            MaterialType::WeightedCloth if version <= 6 => Self::read_weighted_cloth(data, version)?,
604            MaterialType::Rope if version <= 6 => Self::read_rope(data, version)?,
605            MaterialType::TextureCombo |
606            MaterialType::TextureComboVertexWind => Self::read_texture_combo(data, version)?,
607            MaterialType::StaticPointLight => Self::read_static_point_light(data, version)?,
608            MaterialType::Uk27 => Self::read_uk_27(data, version)?,
609            MaterialType::Uk54 |
610            MaterialType::Uk56 |
611            MaterialType::Uk57 => Self::read_uk_54(data, version, mtype)?,
612            MaterialType::AlphaBlend => Self::read_alpha_blend(data, version)?,
613            MaterialType::Cloth => Self::read_cloth(data, version)?,
614            MaterialType::ProjectedDecalV4 |
615            MaterialType::Water |
616            MaterialType::TiledDirtmap |
617            MaterialType::ShipAmbientmap |
618            MaterialType::TerrainBlend |
619            MaterialType::Weighted |
620            MaterialType::Grass |
621            MaterialType::WeightedSkin |
622            MaterialType::Decal |
623            MaterialType::DecalDirtmap |
624            MaterialType::Dirtmap |
625            MaterialType::Tree |
626            MaterialType::TreeLeaf |
627            MaterialType::WeightedDecal |
628            MaterialType::WeightedDecalDirtmap |
629            MaterialType::WeightedDirtmap |
630            MaterialType::WeightedSkinDirtmap |
631            MaterialType::Unlit |
632            MaterialType::TreeBillboardMaterial |
633            MaterialType::WaterDisplaceVolume |
634            MaterialType::CampaignVegetation |
635            MaterialType::DefaultMaterial => Self::read_default(data, version)?,
636            _ => return Err(RLibError::DecodingRigidModelUnsupportedMaterialType(mtype.into()))
637        })
638    }
639
640    /// Writes a material to binary data based on the material type.
641    ///
642    /// Different material types have different binary layouts, which may change between
643    /// versions of the RigidModel format. This function dispatches to the appropriate writer.
644    ///
645    /// # Errors
646    ///
647    /// Returns [`RLibError::DecodingRigidModelUnsupportedMaterialType`] if the
648    /// material type is not supported.
649    pub fn write<W: WriteBytes>(&self, buffer: &mut W, version: u32, mtype: MaterialType) -> Result<()> {
650        match mtype {
651            MaterialType::RsTerrain => self.write_rs_terrain(buffer, version)?,
652            MaterialType::BowWave |
653            MaterialType::DebugGeometry |
654            MaterialType::RiverOutfield |
655            MaterialType::RsRiver => self.write_rs_river(buffer, version)?,
656            MaterialType::WeightedTextureBlend => self.write_weighted_texture_blend(buffer, version)?,
657            MaterialType::ProjectedDecalV4 if version <= 6 => self.write_projected_decal_v4(buffer, version)?,
658            MaterialType::ProjectedDecal |
659            MaterialType::ProjectedDecalV2 |
660            MaterialType::ProjectedDecalV3 => self.write_projected_decal(buffer, version, mtype)?,
661            MaterialType::CustomTerrain => self.write_custom_terrain(buffer, version)?,
662            MaterialType::CollisionShape => self.write_collision_shape(buffer, version)?,
663            MaterialType::WeightedCloth if version <= 6 => self.write_weighted_cloth(buffer, version)?,
664            MaterialType::Rope if version <= 6 => self.write_rope(buffer, version)?,
665            MaterialType::TextureCombo |
666            MaterialType::TextureComboVertexWind => self.write_texture_combo(buffer, version)?,
667            MaterialType::StaticPointLight => self.write_static_point_light(buffer, version)?,
668            MaterialType::Uk27 => self.write_uk_27(buffer, version)?,
669            MaterialType::Uk54 |
670            MaterialType::Uk56 |
671            MaterialType::Uk57 => self.write_uk_54(buffer, version, mtype)?,
672            MaterialType::AlphaBlend => self.write_alpha_blend(buffer, version)?,
673            MaterialType::Cloth => self.write_cloth(buffer, version)?,
674            MaterialType::ProjectedDecalV4 |
675            MaterialType::Water |
676            MaterialType::TiledDirtmap |
677            MaterialType::ShipAmbientmap |
678            MaterialType::TerrainBlend |
679            MaterialType::Weighted |
680            MaterialType::Grass |
681            MaterialType::WeightedSkin |
682            MaterialType::Decal |
683            MaterialType::DecalDirtmap |
684            MaterialType::Dirtmap |
685            MaterialType::Tree |
686            MaterialType::TreeLeaf |
687            MaterialType::WeightedDecal |
688            MaterialType::WeightedDecalDirtmap |
689            MaterialType::WeightedDirtmap |
690            MaterialType::WeightedSkinDirtmap |
691            MaterialType::Unlit |
692            MaterialType::TreeBillboardMaterial |
693            MaterialType::WaterDisplaceVolume |
694            MaterialType::CampaignVegetation |
695            MaterialType::DefaultMaterial => self.write_default(buffer, version)?,
696            _ => return Err(RLibError::DecodingRigidModelUnsupportedMaterialType(mtype.into()))
697        }
698
699        Ok(())
700    }
701}
702
703impl TryFrom<u16> for MaterialType {
704    type Error = RLibError;
705    fn try_from(value: u16) -> Result<Self> {
706        match value {
707            _ if value == Self::BowWave as u16 => Ok(Self::BowWave),
708            _ if value == Self::NonRenderable as u16 => Ok(Self::NonRenderable),
709            _ if value == Self::Uk27 as u16 => Ok(Self::Uk27),
710            _ if value == Self::TextureComboVertexWind as u16 => Ok(Self::TextureComboVertexWind),
711            _ if value == Self::TextureCombo as u16 => Ok(Self::TextureCombo),
712            _ if value == Self::DecalWaterfall as u16 => Ok(Self::DecalWaterfall),
713            _ if value == Self::StandardSimple as u16 => Ok(Self::StandardSimple),
714            _ if value == Self::CampaignTrees as u16 => Ok(Self::CampaignTrees),
715            _ if value == Self::PointLight as u16 => Ok(Self::PointLight),
716            _ if value == Self::StaticPointLight as u16 => Ok(Self::StaticPointLight),
717            _ if value == Self::DebugGeometry as u16 => Ok(Self::DebugGeometry),
718            _ if value == Self::RiverOutfield as u16 => Ok(Self::RiverOutfield),
719            _ if value == Self::Uk54 as u16 => Ok(Self::Uk54),
720            _ if value == Self::Uk56 as u16 => Ok(Self::Uk56),
721            _ if value == Self::Uk57 as u16 => Ok(Self::Uk57),
722            _ if value == Self::CustomTerrain as u16 => Ok(Self::CustomTerrain),
723            _ if value == Self::WeightedCloth as u16 => Ok(Self::WeightedCloth),
724            _ if value == Self::Cloth as u16 => Ok(Self::Cloth),
725            _ if value == Self::Collision as u16 => Ok(Self::Collision),
726            _ if value == Self::CollisionShape as u16 => Ok(Self::CollisionShape),
727            _ if value == Self::TiledDirtmap as u16 => Ok(Self::TiledDirtmap),
728            _ if value == Self::ShipAmbientmap as u16 => Ok(Self::ShipAmbientmap),
729            _ if value == Self::Weighted as u16 => Ok(Self::Weighted),
730            _ if value == Self::RsTerrain as u16 => Ok(Self::RsTerrain),
731            _ if value == Self::ProjectedDecal as u16 => Ok(Self::ProjectedDecal),
732            _ if value == Self::DefaultMaterial as u16 => Ok(Self::DefaultMaterial),
733            _ if value == Self::Grass as u16 => Ok(Self::Grass),
734            _ if value == Self::WeightedSkin as u16 => Ok(Self::WeightedSkin),
735            _ if value == Self::Decal as u16 => Ok(Self::Decal),
736            _ if value == Self::DecalDirtmap as u16 => Ok(Self::DecalDirtmap),
737            _ if value == Self::Dirtmap as u16 => Ok(Self::Dirtmap),
738            _ if value == Self::Tree as u16 => Ok(Self::Tree),
739            _ if value == Self::TreeLeaf as u16 => Ok(Self::TreeLeaf),
740            _ if value == Self::WeightedDecal as u16 => Ok(Self::WeightedDecal),
741            _ if value == Self::WeightedDecalDirtmap as u16 => Ok(Self::WeightedDecalDirtmap),
742            _ if value == Self::WeightedDirtmap as u16 => Ok(Self::WeightedDirtmap),
743            _ if value == Self::WeightedSkinDecal as u16 => Ok(Self::WeightedSkinDecal),
744            _ if value == Self::WeightedSkinDecalDirtmap as u16 => Ok(Self::WeightedSkinDecalDirtmap),
745            _ if value == Self::WeightedSkinDirtmap as u16 => Ok(Self::WeightedSkinDirtmap),
746            _ if value == Self::Water as u16 => Ok(Self::Water),
747            _ if value == Self::Unlit as u16 => Ok(Self::Unlit),
748            _ if value == Self::WeightedUnlit as u16 => Ok(Self::WeightedUnlit),
749            _ if value == Self::TerrainBlend as u16 => Ok(Self::TerrainBlend),
750            _ if value == Self::ProjectedDecalV2 as u16 => Ok(Self::ProjectedDecalV2),
751            _ if value == Self::Ignore as u16 => Ok(Self::Ignore),
752            _ if value == Self::TreeBillboardMaterial as u16 => Ok(Self::TreeBillboardMaterial),
753            _ if value == Self::RsRiver as u16 => Ok(Self::RsRiver),
754            _ if value == Self::WaterDisplaceVolume as u16 => Ok(Self::WaterDisplaceVolume),
755            _ if value == Self::Rope as u16 => Ok(Self::Rope),
756            _ if value == Self::CampaignVegetation as u16 => Ok(Self::CampaignVegetation),
757            _ if value == Self::ProjectedDecalV3 as u16 => Ok(Self::ProjectedDecalV3),
758            _ if value == Self::WeightedTextureBlend as u16 => Ok(Self::WeightedTextureBlend),
759            _ if value == Self::ProjectedDecalV4 as u16 => Ok(Self::ProjectedDecalV4),
760            _ if value == Self::GlobalTerrain as u16 => Ok(Self::GlobalTerrain),
761            _ if value == Self::DecalOverlay as u16 => Ok(Self::DecalOverlay),
762            _ if value == Self::AlphaBlend as u16 => Ok(Self::AlphaBlend),
763            _ => Err(RLibError::DecodingRigidModelUnsupportedMaterialType(value))
764        }
765    }
766}
767
768impl From<MaterialType> for u16 {
769    fn from(value: MaterialType) -> u16 {
770        value as u16
771    }
772}
773
774impl TryFrom<i32> for TextureType {
775    type Error = RLibError;
776    fn try_from(value: i32) -> Result<Self> {
777        match value {
778            _ if value == Self::Diffuse as i32 => Ok(Self::Diffuse),
779            _ if value == Self::Normal as i32 => Ok(Self::Normal),
780            _ if value == Self::Mask as i32 => Ok(Self::Mask),
781            _ if value == Self::LegacyMaterialMap as i32 => Ok(Self::LegacyMaterialMap),
782            _ if value == Self::AmbientOcclusion as i32 => Ok(Self::AmbientOcclusion),
783            _ if value == Self::Displacement as i32 => Ok(Self::Displacement),
784            _ if value == Self::TilingDirtUV2 as i32 => Ok(Self::TilingDirtUV2),
785            _ if value == Self::DirtAlphaMask as i32 => Ok(Self::DirtAlphaMask),
786            _ if value == Self::SkinMask as i32 => Ok(Self::SkinMask),
787            _ if value == Self::Specular as i32 => Ok(Self::Specular),
788            _ if value == Self::GlossMap as i32 => Ok(Self::GlossMap),
789            _ if value == Self::DecalDirtmap as i32 => Ok(Self::DecalDirtmap),
790            _ if value == Self::DecalDirtmask as i32 => Ok(Self::DecalDirtmask),
791            _ if value == Self::DecalMask as i32 => Ok(Self::DecalMask),
792            _ if value == Self::DiffuseDamage as i32 => Ok(Self::DiffuseDamage),
793            _ if value == Self::BaseColor as i32 => Ok(Self::BaseColor),
794            _ if value == Self::MaterialMap as i32 => Ok(Self::MaterialMap),
795            _ => Err(RLibError::DecodingRigidModelUnknownTextureType(value))
796        }
797    }
798}
799
800impl TryFrom<TextureType> for i32 {
801    type Error = RLibError;
802    fn try_from(value: TextureType) -> Result<i32> {
803        Ok(value as i32)
804    }
805}
806
807/// Reads a 0-padded string of `size` bytes, also returning the bytes after its NUL up to the last non-zero one.
808///
809/// The string is UTF-8, or UTF-16 if `utf16` is set.
810fn read_string_0padded_with_tail<R: ReadBytes>(data: &mut R, size: usize, utf16: bool) -> Result<(String, Vec<u8>)> {
811    let bytes = data.read_slice(size, false)?;
812    let char_size = if utf16 { 2 } else { 1 };
813    let len = bytes.chunks_exact(char_size).position(|char| char.iter().all(|x| *x == 0)).map_or(size, |chars| chars * char_size);
814    let tail_end = bytes.iter().rposition(|x| *x != 0).map_or(0, |last| last + 1);
815    let tail = if tail_end > len + char_size { bytes[len + char_size..tail_end].to_vec() } else { vec![] };
816
817    let string = if utf16 { Cursor::new(&bytes[..len]).read_string_u16(len)? } else { String::from_utf8(bytes[..len].to_vec())? };
818    Ok((string, tail))
819}
820
821/// Writes a 0-padded string of `size` bytes, with `tail` right after its NUL.
822///
823/// The string is UTF-8, or UTF-16 if `utf16` is set.
824fn write_string_0padded_with_tail<W: WriteBytes>(data: &mut W, string: &str, tail: &[u8], size: usize, utf16: bool) -> Result<()> {
825    let char_size = if utf16 { 2 } else { 1 };
826    let string_size = if utf16 { string.encode_utf16().count() * 2 } else { string.len() };
827    let used = string_size + if tail.is_empty() { 0 } else { char_size + tail.len() };
828    if used > size {
829        return Err(RLibError::EncodingPaddedStringError("0-Padded String".to_owned(), string.to_owned(), used, size));
830    }
831
832    if utf16 {
833        data.write_string_u16(string)?;
834    } else {
835        data.write_string_u8(string)?;
836    }
837
838    if !tail.is_empty() {
839        data.write_all(&vec![0; char_size])?;
840        data.write_all(tail)?;
841    }
842
843    data.write_all(&vec![0; size - used]).map_err(From::from)
844}
845
846/// Reads a 0-padded string of `size` characters: one byte each, or two in version 5 RigidModels, which use UTF-16.
847fn read_padded_string<R: ReadBytes>(data: &mut R, version: u32, size: usize) -> Result<String> {
848    if version <= 5 {
849        data.read_string_u16_0padded(size * 2)
850    } else {
851        data.read_string_u8_0padded(size)
852    }
853}
854
855/// Writes a 0-padded string of `size` characters: one byte each, or two in version 5 RigidModels, which use UTF-16.
856fn write_padded_string<W: WriteBytes>(data: &mut W, version: u32, string: &str, size: usize) -> Result<()> {
857    if version <= 5 {
858        data.write_string_u16_0padded(string, size * 2, true)
859    } else {
860        data.write_string_u8_0padded(string, size, true)
861    }
862}