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    /// Tiling dirt texture (UV set 2).
506    TilingDirtUV2 = 7,
507    /// Dirt alpha mask.
508    DirtAlphaMask = 8,
509    /// Skin mask texture.
510    SkinMask = 10,
511    /// Specular reflectivity map.
512    Specular = 11,
513    /// Gloss/smoothness map (specular roughness).
514    GlossMap = 12,
515    /// Decal dirt map.
516    DecalDirtmap = 13,
517    /// Decal dirt mask.
518    DecalDirtmask = 14,
519    /// Decal alpha mask.
520    DecalMask = 15,
521    /// Damaged diffuse texture variant.
522    DiffuseDamage = 17,
523    /// PBR base color.
524    BaseColor = 27,
525    /// PBR material properties (metallic/roughness/AO packed).
526    MaterialMap = 29,
527}
528
529/// Cloth physics data structure 1 (format undocumented).
530///
531/// This structure appears only in cloth materials and likely contains cloth simulation
532/// parameters. The exact meaning of the fields is not yet reverse-engineered.
533#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
534#[getset(get = "pub", get_mut = "pub", set = "pub")]
535pub struct Uk7 {
536    /// Unknown integer field 1 (possibly constraint/spring index).
537    uk1: i32,
538    /// Unknown integer field 2 (possibly vertex/node index).
539    uk2: i32,
540    /// Unknown float field (possibly stiffness/damping coefficient).
541    uk3: f32,
542}
543
544/// Cloth physics data structure 2 (format undocumented).
545///
546/// This structure appears only in cloth materials and likely contains cloth simulation
547/// parameters. The exact meaning of the field is not yet reverse-engineered.
548#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
549#[getset(get = "pub", get_mut = "pub", set = "pub")]
550pub struct Uk8 {
551    /// Unknown integer field (possibly fixed vertex/anchor point index).
552    uk1: i32,
553}
554
555/// Cloth physics data structure 3 (format undocumented).
556///
557/// This structure appears only in cloth materials and likely contains cloth simulation
558/// parameters. The exact meaning of the fields is not yet reverse-engineered.
559#[derive(Clone, Debug, Default, PartialEq, Getters, MutGetters, Setters, Serialize, Deserialize)]
560#[getset(get = "pub", get_mut = "pub", set = "pub")]
561pub struct Uk9 {
562    /// Unknown integer field 1 (possibly triangle/face index).
563    uk1: i32,
564    /// Unknown integer field 2 (possibly collision group).
565    uk2: i32,
566    /// Unknown integer field 3 (possibly property flags).
567    uk3: i32,
568}
569
570//---------------------------------------------------------------------------//
571//                            Implementation
572//---------------------------------------------------------------------------//
573
574
575impl Material {
576
577    /// Reads a material from binary data based on the material type.
578    ///
579    /// Different material types have different binary layouts, which may change between
580    /// versions of the RigidModel format. This function dispatches to the appropriate reader.
581    ///
582    /// # Errors
583    ///
584    /// Returns [`RLibError::DecodingRigidModelUnsupportedMaterialType`] if the
585    /// material type is not supported.
586    pub fn read<R: ReadBytes>(data: &mut R, version: u32, mtype: MaterialType) -> Result<Self> {
587        Ok(match mtype {
588            MaterialType::RsTerrain => Self::read_rs_terrain(data, version)?,
589            MaterialType::BowWave |
590            MaterialType::DebugGeometry |
591            MaterialType::RiverOutfield |
592            MaterialType::RsRiver => Self::read_rs_river(data, version)?,
593            MaterialType::WeightedTextureBlend => Self::read_weighted_texture_blend(data, version)?,
594            MaterialType::ProjectedDecalV4 if version <= 6 => Self::read_projected_decal_v4(data, version)?,
595            MaterialType::ProjectedDecal |
596            MaterialType::ProjectedDecalV2 |
597            MaterialType::ProjectedDecalV3 => Self::read_projected_decal(data, version, mtype)?,
598            MaterialType::CustomTerrain => Self::read_custom_terrain(data, version)?,
599            MaterialType::CollisionShape => Self::read_collision_shape(data, version)?,
600            MaterialType::WeightedCloth if version <= 6 => Self::read_weighted_cloth(data, version)?,
601            MaterialType::Rope if version <= 6 => Self::read_rope(data, version)?,
602            MaterialType::TextureCombo |
603            MaterialType::TextureComboVertexWind => Self::read_texture_combo(data, version)?,
604            MaterialType::StaticPointLight => Self::read_static_point_light(data, version)?,
605            MaterialType::Uk27 => Self::read_uk_27(data, version)?,
606            MaterialType::Uk54 |
607            MaterialType::Uk56 |
608            MaterialType::Uk57 => Self::read_uk_54(data, version, mtype)?,
609            MaterialType::AlphaBlend => Self::read_alpha_blend(data, version)?,
610            MaterialType::Cloth => Self::read_cloth(data, version)?,
611            MaterialType::ProjectedDecalV4 |
612            MaterialType::Water |
613            MaterialType::TiledDirtmap |
614            MaterialType::ShipAmbientmap |
615            MaterialType::TerrainBlend |
616            MaterialType::Weighted |
617            MaterialType::Grass |
618            MaterialType::WeightedSkin |
619            MaterialType::Decal |
620            MaterialType::DecalDirtmap |
621            MaterialType::Dirtmap |
622            MaterialType::Tree |
623            MaterialType::TreeLeaf |
624            MaterialType::WeightedDecal |
625            MaterialType::WeightedDecalDirtmap |
626            MaterialType::WeightedDirtmap |
627            MaterialType::WeightedSkinDirtmap |
628            MaterialType::Unlit |
629            MaterialType::TreeBillboardMaterial |
630            MaterialType::WaterDisplaceVolume |
631            MaterialType::CampaignVegetation |
632            MaterialType::DefaultMaterial => Self::read_default(data, version)?,
633            _ => return Err(RLibError::DecodingRigidModelUnsupportedMaterialType(mtype.into()))
634        })
635    }
636
637    /// Writes a material to binary data based on the material type.
638    ///
639    /// Different material types have different binary layouts, which may change between
640    /// versions of the RigidModel format. This function dispatches to the appropriate writer.
641    ///
642    /// # Errors
643    ///
644    /// Returns [`RLibError::DecodingRigidModelUnsupportedMaterialType`] if the
645    /// material type is not supported.
646    pub fn write<W: WriteBytes>(&self, buffer: &mut W, version: u32, mtype: MaterialType) -> Result<()> {
647        match mtype {
648            MaterialType::RsTerrain => self.write_rs_terrain(buffer, version)?,
649            MaterialType::BowWave |
650            MaterialType::DebugGeometry |
651            MaterialType::RiverOutfield |
652            MaterialType::RsRiver => self.write_rs_river(buffer, version)?,
653            MaterialType::WeightedTextureBlend => self.write_weighted_texture_blend(buffer, version)?,
654            MaterialType::ProjectedDecalV4 if version <= 6 => self.write_projected_decal_v4(buffer, version)?,
655            MaterialType::ProjectedDecal |
656            MaterialType::ProjectedDecalV2 |
657            MaterialType::ProjectedDecalV3 => self.write_projected_decal(buffer, version, mtype)?,
658            MaterialType::CustomTerrain => self.write_custom_terrain(buffer, version)?,
659            MaterialType::CollisionShape => self.write_collision_shape(buffer, version)?,
660            MaterialType::WeightedCloth if version <= 6 => self.write_weighted_cloth(buffer, version)?,
661            MaterialType::Rope if version <= 6 => self.write_rope(buffer, version)?,
662            MaterialType::TextureCombo |
663            MaterialType::TextureComboVertexWind => self.write_texture_combo(buffer, version)?,
664            MaterialType::StaticPointLight => self.write_static_point_light(buffer, version)?,
665            MaterialType::Uk27 => self.write_uk_27(buffer, version)?,
666            MaterialType::Uk54 |
667            MaterialType::Uk56 |
668            MaterialType::Uk57 => self.write_uk_54(buffer, version, mtype)?,
669            MaterialType::AlphaBlend => self.write_alpha_blend(buffer, version)?,
670            MaterialType::Cloth => self.write_cloth(buffer, version)?,
671            MaterialType::ProjectedDecalV4 |
672            MaterialType::Water |
673            MaterialType::TiledDirtmap |
674            MaterialType::ShipAmbientmap |
675            MaterialType::TerrainBlend |
676            MaterialType::Weighted |
677            MaterialType::Grass |
678            MaterialType::WeightedSkin |
679            MaterialType::Decal |
680            MaterialType::DecalDirtmap |
681            MaterialType::Dirtmap |
682            MaterialType::Tree |
683            MaterialType::TreeLeaf |
684            MaterialType::WeightedDecal |
685            MaterialType::WeightedDecalDirtmap |
686            MaterialType::WeightedDirtmap |
687            MaterialType::WeightedSkinDirtmap |
688            MaterialType::Unlit |
689            MaterialType::TreeBillboardMaterial |
690            MaterialType::WaterDisplaceVolume |
691            MaterialType::CampaignVegetation |
692            MaterialType::DefaultMaterial => self.write_default(buffer, version)?,
693            _ => return Err(RLibError::DecodingRigidModelUnsupportedMaterialType(mtype.into()))
694        }
695
696        Ok(())
697    }
698}
699
700impl TryFrom<u16> for MaterialType {
701    type Error = RLibError;
702    fn try_from(value: u16) -> Result<Self> {
703        match value {
704            _ if value == Self::BowWave as u16 => Ok(Self::BowWave),
705            _ if value == Self::NonRenderable as u16 => Ok(Self::NonRenderable),
706            _ if value == Self::Uk27 as u16 => Ok(Self::Uk27),
707            _ if value == Self::TextureComboVertexWind as u16 => Ok(Self::TextureComboVertexWind),
708            _ if value == Self::TextureCombo as u16 => Ok(Self::TextureCombo),
709            _ if value == Self::DecalWaterfall as u16 => Ok(Self::DecalWaterfall),
710            _ if value == Self::StandardSimple as u16 => Ok(Self::StandardSimple),
711            _ if value == Self::CampaignTrees as u16 => Ok(Self::CampaignTrees),
712            _ if value == Self::PointLight as u16 => Ok(Self::PointLight),
713            _ if value == Self::StaticPointLight as u16 => Ok(Self::StaticPointLight),
714            _ if value == Self::DebugGeometry as u16 => Ok(Self::DebugGeometry),
715            _ if value == Self::RiverOutfield as u16 => Ok(Self::RiverOutfield),
716            _ if value == Self::Uk54 as u16 => Ok(Self::Uk54),
717            _ if value == Self::Uk56 as u16 => Ok(Self::Uk56),
718            _ if value == Self::Uk57 as u16 => Ok(Self::Uk57),
719            _ if value == Self::CustomTerrain as u16 => Ok(Self::CustomTerrain),
720            _ if value == Self::WeightedCloth as u16 => Ok(Self::WeightedCloth),
721            _ if value == Self::Cloth as u16 => Ok(Self::Cloth),
722            _ if value == Self::Collision as u16 => Ok(Self::Collision),
723            _ if value == Self::CollisionShape as u16 => Ok(Self::CollisionShape),
724            _ if value == Self::TiledDirtmap as u16 => Ok(Self::TiledDirtmap),
725            _ if value == Self::ShipAmbientmap as u16 => Ok(Self::ShipAmbientmap),
726            _ if value == Self::Weighted as u16 => Ok(Self::Weighted),
727            _ if value == Self::RsTerrain as u16 => Ok(Self::RsTerrain),
728            _ if value == Self::ProjectedDecal as u16 => Ok(Self::ProjectedDecal),
729            _ if value == Self::DefaultMaterial as u16 => Ok(Self::DefaultMaterial),
730            _ if value == Self::Grass as u16 => Ok(Self::Grass),
731            _ if value == Self::WeightedSkin as u16 => Ok(Self::WeightedSkin),
732            _ if value == Self::Decal as u16 => Ok(Self::Decal),
733            _ if value == Self::DecalDirtmap as u16 => Ok(Self::DecalDirtmap),
734            _ if value == Self::Dirtmap as u16 => Ok(Self::Dirtmap),
735            _ if value == Self::Tree as u16 => Ok(Self::Tree),
736            _ if value == Self::TreeLeaf as u16 => Ok(Self::TreeLeaf),
737            _ if value == Self::WeightedDecal as u16 => Ok(Self::WeightedDecal),
738            _ if value == Self::WeightedDecalDirtmap as u16 => Ok(Self::WeightedDecalDirtmap),
739            _ if value == Self::WeightedDirtmap as u16 => Ok(Self::WeightedDirtmap),
740            _ if value == Self::WeightedSkinDecal as u16 => Ok(Self::WeightedSkinDecal),
741            _ if value == Self::WeightedSkinDecalDirtmap as u16 => Ok(Self::WeightedSkinDecalDirtmap),
742            _ if value == Self::WeightedSkinDirtmap as u16 => Ok(Self::WeightedSkinDirtmap),
743            _ if value == Self::Water as u16 => Ok(Self::Water),
744            _ if value == Self::Unlit as u16 => Ok(Self::Unlit),
745            _ if value == Self::WeightedUnlit as u16 => Ok(Self::WeightedUnlit),
746            _ if value == Self::TerrainBlend as u16 => Ok(Self::TerrainBlend),
747            _ if value == Self::ProjectedDecalV2 as u16 => Ok(Self::ProjectedDecalV2),
748            _ if value == Self::Ignore as u16 => Ok(Self::Ignore),
749            _ if value == Self::TreeBillboardMaterial as u16 => Ok(Self::TreeBillboardMaterial),
750            _ if value == Self::RsRiver as u16 => Ok(Self::RsRiver),
751            _ if value == Self::WaterDisplaceVolume as u16 => Ok(Self::WaterDisplaceVolume),
752            _ if value == Self::Rope as u16 => Ok(Self::Rope),
753            _ if value == Self::CampaignVegetation as u16 => Ok(Self::CampaignVegetation),
754            _ if value == Self::ProjectedDecalV3 as u16 => Ok(Self::ProjectedDecalV3),
755            _ if value == Self::WeightedTextureBlend as u16 => Ok(Self::WeightedTextureBlend),
756            _ if value == Self::ProjectedDecalV4 as u16 => Ok(Self::ProjectedDecalV4),
757            _ if value == Self::GlobalTerrain as u16 => Ok(Self::GlobalTerrain),
758            _ if value == Self::DecalOverlay as u16 => Ok(Self::DecalOverlay),
759            _ if value == Self::AlphaBlend as u16 => Ok(Self::AlphaBlend),
760            _ => Err(RLibError::DecodingRigidModelUnsupportedMaterialType(value))
761        }
762    }
763}
764
765impl From<MaterialType> for u16 {
766    fn from(value: MaterialType) -> u16 {
767        value as u16
768    }
769}
770
771impl TryFrom<i32> for TextureType {
772    type Error = RLibError;
773    fn try_from(value: i32) -> Result<Self> {
774        match value {
775            _ if value == Self::Diffuse as i32 => Ok(Self::Diffuse),
776            _ if value == Self::Normal as i32 => Ok(Self::Normal),
777            _ if value == Self::Mask as i32 => Ok(Self::Mask),
778            _ if value == Self::LegacyMaterialMap as i32 => Ok(Self::LegacyMaterialMap),
779            _ if value == Self::AmbientOcclusion as i32 => Ok(Self::AmbientOcclusion),
780            _ if value == Self::TilingDirtUV2 as i32 => Ok(Self::TilingDirtUV2),
781            _ if value == Self::DirtAlphaMask as i32 => Ok(Self::DirtAlphaMask),
782            _ if value == Self::SkinMask as i32 => Ok(Self::SkinMask),
783            _ if value == Self::Specular as i32 => Ok(Self::Specular),
784            _ if value == Self::GlossMap as i32 => Ok(Self::GlossMap),
785            _ if value == Self::DecalDirtmap as i32 => Ok(Self::DecalDirtmap),
786            _ if value == Self::DecalDirtmask as i32 => Ok(Self::DecalDirtmask),
787            _ if value == Self::DecalMask as i32 => Ok(Self::DecalMask),
788            _ if value == Self::DiffuseDamage as i32 => Ok(Self::DiffuseDamage),
789            _ if value == Self::BaseColor as i32 => Ok(Self::BaseColor),
790            _ if value == Self::MaterialMap as i32 => Ok(Self::MaterialMap),
791            _ => Err(RLibError::DecodingRigidModelUnknownTextureType(value))
792        }
793    }
794}
795
796impl TryFrom<TextureType> for i32 {
797    type Error = RLibError;
798    fn try_from(value: TextureType) -> Result<i32> {
799        Ok(value as i32)
800    }
801}
802
803/// Reads a 0-padded string of `size` bytes, also returning the bytes after its NUL up to the last non-zero one.
804///
805/// The string is UTF-8, or UTF-16 if `utf16` is set.
806fn read_string_0padded_with_tail<R: ReadBytes>(data: &mut R, size: usize, utf16: bool) -> Result<(String, Vec<u8>)> {
807    let bytes = data.read_slice(size, false)?;
808    let char_size = if utf16 { 2 } else { 1 };
809    let len = bytes.chunks_exact(char_size).position(|char| char.iter().all(|x| *x == 0)).map_or(size, |chars| chars * char_size);
810    let tail_end = bytes.iter().rposition(|x| *x != 0).map_or(0, |last| last + 1);
811    let tail = if tail_end > len + char_size { bytes[len + char_size..tail_end].to_vec() } else { vec![] };
812
813    let string = if utf16 { Cursor::new(&bytes[..len]).read_string_u16(len)? } else { String::from_utf8(bytes[..len].to_vec())? };
814    Ok((string, tail))
815}
816
817/// Writes a 0-padded string of `size` bytes, with `tail` right after its NUL.
818///
819/// The string is UTF-8, or UTF-16 if `utf16` is set.
820fn write_string_0padded_with_tail<W: WriteBytes>(data: &mut W, string: &str, tail: &[u8], size: usize, utf16: bool) -> Result<()> {
821    let char_size = if utf16 { 2 } else { 1 };
822    let string_size = if utf16 { string.encode_utf16().count() * 2 } else { string.len() };
823    let used = string_size + if tail.is_empty() { 0 } else { char_size + tail.len() };
824    if used > size {
825        return Err(RLibError::EncodingPaddedStringError("0-Padded String".to_owned(), string.to_owned(), used, size));
826    }
827
828    if utf16 {
829        data.write_string_u16(string)?;
830    } else {
831        data.write_string_u8(string)?;
832    }
833
834    if !tail.is_empty() {
835        data.write_all(&vec![0; char_size])?;
836        data.write_all(tail)?;
837    }
838
839    data.write_all(&vec![0; size - used]).map_err(From::from)
840}
841
842/// Reads a 0-padded string of `size` characters: one byte each, or two in version 5 RigidModels, which use UTF-16.
843fn read_padded_string<R: ReadBytes>(data: &mut R, version: u32, size: usize) -> Result<String> {
844    if version <= 5 {
845        data.read_string_u16_0padded(size * 2)
846    } else {
847        data.read_string_u8_0padded(size)
848    }
849}
850
851/// Writes a 0-padded string of `size` characters: one byte each, or two in version 5 RigidModels, which use UTF-16.
852fn write_padded_string<W: WriteBytes>(data: &mut W, version: u32, string: &str, size: usize) -> Result<()> {
853    if version <= 5 {
854        data.write_string_u16_0padded(string, size * 2, true)
855    } else {
856        data.write_string_u8_0padded(string, size, true)
857    }
858}