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}