serde_hkx/bytes/ser/
mod.rs

1//! Bytes Serialization
2mod sub_ser;
3mod trait_impls;
4
5use crate::{align, lib::*, tri};
6
7use self::sub_ser::structs::StructSerializer;
8use self::trait_impls::{Align as _, ClassNamesWriter, ClassStartsMap};
9use super::serde::{hkx_header::HkxHeader, section_header::SectionHeader};
10use crate::errors::ser::{
11    Error, InvalidEndianSnafu, MissingClassInClassnamesSectionSnafu, MissingGlobalFixupClassSnafu,
12    Result, UnsupportedPtrSizeSnafu,
13};
14use byteorder::{BigEndian, ByteOrder, LittleEndian, WriteBytesExt as _};
15use havok_serde::ser::{Serialize, Serializer};
16use havok_types::*;
17use indexmap::IndexMap;
18use std::borrow::Cow;
19use std::collections::HashMap;
20use std::ffi::CString as StdCString;
21use std::io::{Cursor, Write as _};
22
23/// To hkx binary file data.
24///
25/// # Errors
26/// - When information necessary for binary data conversion is missing.
27/// - When a write to the wrong write position is requested.
28///
29/// # NOTE
30/// - To reproduce the original hkx, it is necessary to arrange them in dependency order using `HavokSort::sort_for_bytes` or similar.
31/// - If you convert in the order of hkx => XML => hkx, you may not be able to restore the original hkx due to data loss during XML conversion.
32pub fn to_bytes<V>(value: &V, header: &HkxHeader) -> Result<Vec<u8>>
33where
34    V: Serialize + ClassNamesWriter,
35{
36    to_bytes_with_opt(
37        value,
38        header,
39        ByteSerializer {
40            is_little_endian: match header.endian {
41                0 => false, // big endian
42                1 => true,  // little endian
43                invalid => InvalidEndianSnafu { invalid }.fail()?,
44            },
45            is_x86: match header.pointer_size {
46                4 => true,
47                8 => false,
48                invalid => UnsupportedPtrSizeSnafu { invalid }.fail()?,
49            },
50            ..Default::default()
51        },
52    )
53}
54
55/// Serialize to bytes with custom `BytesSerializer` settings.
56///
57/// # Errors
58/// - When information necessary for binary data conversion is missing.
59/// - When a write to the wrong write position is requested.
60pub fn to_bytes_with_opt<V>(value: &V, header: &HkxHeader, ser: ByteSerializer) -> Result<Vec<u8>>
61where
62    V: Serialize + ClassNamesWriter,
63{
64    let mut serializer = ser;
65
66    // 1/5: root header
67    serializer.output.write_all(&header.to_bytes())?;
68    serializer.contents_class_name_section_index = header.contents_class_name_section_index;
69    serializer.contents_section_index = header.contents_section_index;
70
71    // 2/5: Get Section headers positions.(because the values of the fixups are not yet known.)
72    let classnames_header_start = 64 + header.padding_size() as u64; // 64: Root header size
73    let types_header_start = classnames_header_start + 48; // next `SectionHeader`(size 48bytes) position.
74    let data_header_start = types_header_start + 48;
75    serializer.output.set_position(data_header_start + 48);
76
77    // 3/5: section contents
78    // - `__classnames__` section
79    if serializer.is_little_endian {
80        serializer.class_starts =
81            value.write_classnames_section::<LittleEndian>(&mut serializer.output)?;
82    } else {
83        serializer.class_starts =
84            value.write_classnames_section::<BigEndian>(&mut serializer.output)?;
85    };
86    #[cfg(feature = "tracing")]
87    tracing::trace!("class_starts: {:#?}", serializer.class_starts);
88
89    // - `__data__` section
90    serializer.abs_data_offset = header.padding_size() + serializer.output.position() as u32;
91    serializer.current_last_local_dst = serializer.abs_data_offset as u64;
92    value.serialize(&mut serializer)?;
93
94    // 4/5: Write fixups_offsets of `__data__` section header.
95    serializer.output.zero_fill_align(16)?; // Always make the start of fixups a multiple of 16.
96    let (local_offset, global_offset, virtual_offset) = serializer.write_data_fixups()?; // Write local, global and virtual fixups
97    let exports_offset = serializer.relative_position()?; // This is where the exports_offset is finally obtained.
98
99    // 5/5 Write remain offsets for `__types__` & `__data__` section header.
100    let abs_data_start = serializer.abs_data_offset;
101    let data_section_header = SectionHeader {
102        section_tag: SectionHeader::DATA_SECTION_HEADER_TAG,
103        section_tag_separator: 0xff,
104        absolute_data_start: abs_data_start,
105        local_fixups_offset: local_offset,
106        global_fixups_offset: global_offset,
107        virtual_fixups_offset: virtual_offset,
108        exports_offset,
109        imports_offset: exports_offset,
110        end_offset: exports_offset,
111    };
112    #[cfg(feature = "tracing")]
113    tracing::trace!("data_section_header = {data_section_header}");
114
115    serializer.output.set_position(classnames_header_start);
116    let section_offset = header.section_offset;
117
118    if serializer.is_little_endian {
119        tri!(serializer.write_section_headers::<LittleEndian>(
120            section_offset,
121            types_header_start,
122            data_header_start,
123            &data_section_header,
124        ));
125    } else {
126        tri!(serializer.write_section_headers::<BigEndian>(
127            section_offset,
128            types_header_start,
129            data_header_start,
130            &data_section_header,
131        ));
132    };
133
134    Ok(serializer.output.into_inner())
135}
136
137/// Binary data serializer
138///
139/// # Note
140/// - All of these fixups are from the `__data__` section.
141#[derive(Debug, Default)]
142pub struct ByteSerializer {
143    /// Endianness of serialization target
144    is_little_endian: bool,
145    /// Ptr size of serialization target.
146    /// 32bit: x86
147    is_x86: bool,
148
149    /// Bytes
150    output: Cursor<Vec<u8>>,
151
152    /// This is cached to find the relative position of the binary.
153    abs_data_offset: u32,
154
155    /// Flag to branch special align16 conditions such as the following.
156    /// Switch only in `SerializeStruct::serialize_array` and read only in `serialize_cow`.
157    /// - `hkArray<CString>` / `hkArray<StringPtr>` do not align16 each string.
158    /// - For a single field, align16 is done after string serialization.
159    is_in_str_array: bool,
160
161    // ---- local fixup information
162    /// The position to which the pointer returns after writing the end of the pointer.
163    /// Holds positional information for writing the destination of the nested array pointers in the field,
164    /// where the Nth element means N hierarchically nested.
165    /// - Example: `u32` <- `ClassB.b: u32` <- `ClassA.a: Array<ClassB>` <- `Array<ClassA>`
166    pointed_pos: Vec<u64>,
167    /// each Root class ptr pointed data position.
168    current_last_local_dst: u64,
169    /// Coordination information to associate a pointer of a pointer type of a field in a class with the data location to which it points.
170    ///
171    /// # Note
172    /// All of these fixups are from the DATA SECTION.
173    ///
174    /// The following are not recorded in `local_fixup`.
175    /// - If `Array<T>` is empty.
176    /// - `CString`, `StringPtr` points to null ptr.
177    local_fixups: Vec<u8>,
178
179    // ---- Global fixup information
180    /// A map that holds the src of global_fixups until the dst of virtual_fixups is known.
181    /// -   key: Starting point of the binary for which the pointer class write is requested.
182    /// - value: Unique class pointer.(e.g. XML: #0050 -> 50)
183    global_fixups_ptr_src: IndexMap<u32, Pointer>,
184    /// The `global_fixup.dst` == `virtual_fixup.src`.
185    ///
186    /// Therefore, the write start position must be retained.
187    /// The position of dst of global_fixup will be known after all the binary data of all classes are written.
188    /// - key: Unique class pointer.(e.g. XML: #0050 -> 50)
189    /// - value: Starting point where Havok Class binary data is written.
190    ///
191    /// # Note
192    /// - This is used as a key since no duplicate ptr is required at the same relative position.
193    ///   The ptr may be shared-referenced and cannot be keyed.
194    virtual_fixups_ptr_src: HashMap<Pointer, u32>,
195    /// Index of the contents section.(To write `global_fixups` index)
196    ///
197    /// It is usually `2` and refers to the index of `__data__`.
198    contents_section_index: i32,
199
200    // ---- Virtual fixup information
201    /// C++ Class constructor positions map binary temporally buffer.
202    ///
203    /// Finally, write to the data for output.
204    virtual_fixups: Vec<u8>,
205    /// This information is needed in `virtual_fixup.name_offset`.
206    ///
207    /// This is created by writing the `__classnames__` section.
208    /// - key: class name
209    /// - value: class name start position
210    class_starts: ClassStartsMap,
211    /// Index of the contents class name section.(To write virtual fixups index)
212    ///
213    /// It is usually `0` and refers to the index of `__classnames__`.
214    contents_class_name_section_index: i32,
215}
216
217impl ByteSerializer {
218    /// Get the position relative to the start of the `__data__` section.
219    #[inline]
220    fn relative_position(&self) -> Result<u32> {
221        let position = self.output.position() as u32;
222        let abs_data_offset = self.abs_data_offset;
223        position
224            .checked_sub(abs_data_offset)
225            .ok_or(Error::OverflowSubtractAbs {
226                position,
227                abs_data_offset,
228            })
229    }
230
231    /// Write `global_fixups` of data section bytes to writer.
232    ///
233    /// # Info
234    /// If all virtual_fixups are not obtained, references may not be available?
235    ///
236    /// # Note
237    /// `global_fixup.dst` == `virtual_fixup.src`
238    fn write_global_fixups(&mut self) -> Result<()> {
239        for (&src, ptr) in &self.global_fixups_ptr_src {
240            if let Some(&dst) = self.virtual_fixups_ptr_src.get(ptr) {
241                #[cfg(feature = "tracing")]
242                {
243                    let src_abs = self.abs_data_offset + src;
244                    let dst_abs = self.abs_data_offset + dst;
245                    tracing::debug!(
246                        "[global_fixups]({:#x}) src({src}:{src:#x}/abs {src_abs:#x}) -> {ptr} dst({dst}:{dst:#x}/abs: {dst_abs:#x})",
247                        self.output.position()
248                    );
249                }
250
251                if self.is_little_endian {
252                    self.output.write_u32::<LittleEndian>(src)?; // src
253                    self.output
254                        .write_i32::<LittleEndian>(self.contents_section_index)?; // dst_section_index
255                    self.output.write_u32::<LittleEndian>(dst)?; // dst(virtual_fixup.dst)
256                } else {
257                    self.output.write_u32::<BigEndian>(src)?; // src
258                    self.output
259                        .write_i32::<BigEndian>(self.contents_section_index)?; // dst_section_index
260                    self.output.write_u32::<BigEndian>(dst)?; // dst(virtual_fixup.dst)
261                }
262            } else {
263                return MissingGlobalFixupClassSnafu { ptr: *ptr }.fail();
264            }
265        }
266        Ok(())
267    }
268
269    /// Write to temporary virtual_fixup data.
270    ///
271    /// # Info
272    /// Since the `class_name` and its location are known when the `__classnames__` section is written, the pair can be
273    /// written the moment virtual_fixup.src is available.
274    fn write_virtual_fixups_pair(
275        &mut self,
276        class_name: &'static str,
277        virtual_src: u32,
278    ) -> Result<()> {
279        if let Some(class_name_offset) = self.class_starts.get(class_name) {
280            if self.is_little_endian {
281                self.virtual_fixups.write_u32::<LittleEndian>(virtual_src)?; // src
282                self.virtual_fixups
283                    .write_i32::<LittleEndian>(self.contents_class_name_section_index)?; // dst_section_index, `__classnames__` section is 0
284                self.virtual_fixups
285                    .write_u32::<LittleEndian>(*class_name_offset)?; // dst(virtual_fixup.dst)
286            } else {
287                self.virtual_fixups.write_u32::<BigEndian>(virtual_src)?; // src
288                self.virtual_fixups
289                    .write_i32::<BigEndian>(self.contents_class_name_section_index)?; // dst_section_index, `__classnames__` section is 0
290                self.virtual_fixups
291                    .write_u32::<BigEndian>(*class_name_offset)?; // dst(virtual_fixup.dst)
292            };
293            Ok(())
294        } else {
295            MissingClassInClassnamesSectionSnafu { class_name }.fail()
296        }
297    }
298
299    /// Write all(`local`, `global` and `virtual`) fixups of data section.
300    ///
301    /// # Returns
302    /// (`local_offset`, `global_offset`, `virtual_offset`)
303    fn write_data_fixups(&mut self) -> Result<(u32, u32, u32)> {
304        let local_offset = self.relative_position()?;
305        self.output.write_all(&self.local_fixups)?;
306        self.output.align(16, 0xff)?;
307
308        #[cfg(feature = "tracing")]
309        tracing::debug!(
310            "[global_fixups pointers]\nsrc: {:#?},\ndest(same as virtual.src): {:#?}",
311            self.global_fixups_ptr_src,
312            self.virtual_fixups_ptr_src
313        );
314        let global_offset = self.relative_position()?;
315        self.write_global_fixups()?;
316        self.output.align(16, 0xff)?;
317
318        let virtual_offset = self.relative_position()?;
319        self.output.write_all(&self.virtual_fixups)?;
320        self.output.align(16, 0xff)?;
321        Ok((local_offset, global_offset, virtual_offset))
322    }
323
324    /// Write all section headers.
325    #[inline]
326    fn write_section_headers<B>(
327        &mut self,
328        section_offset: i16,
329        types_header_start: u64,
330        data_header_start: u64,
331        data_section_header: &SectionHeader,
332    ) -> Result<()>
333    where
334        B: ByteOrder,
335    {
336        // `__classnames__` header`
337        SectionHeader::write_classnames::<B>(
338            &mut self.output,
339            section_offset,
340            self.abs_data_offset,
341        )?;
342        // `__types__` header`
343        self.output.set_position(types_header_start);
344        SectionHeader::write_types::<B>(&mut self.output, self.abs_data_offset)?;
345        // `__data__` header`
346        self.output.set_position(data_header_start);
347        data_section_header.write_bytes::<B>(&mut self.output)?;
348        Ok(())
349    }
350
351    /// The data position pointed to by ptr.
352    /// And return destination position.
353    #[inline]
354    fn goto_local_dst(&mut self) -> Result<u32> {
355        let &dest_abs_pos = tri!(
356            self.pointed_pos
357                .last()
358                .ok_or(Error::NotFoundPointedPosition)
359        );
360        self.output.set_position(dest_abs_pos);
361        self.relative_position()
362    }
363
364    /// Write a pair of local_fixups.
365    #[inline]
366    fn write_local_fixup_pair(&mut self, local_src: u32, local_dst: u32) -> Result<()> {
367        #[cfg(feature = "tracing")]
368        {
369            let src_abs = self.abs_data_offset + local_src;
370            let dst_abs = self.abs_data_offset + local_dst;
371            tracing::debug!(
372                "[local_fixup] src({local_src}/abs: {src_abs:#x}), dst({local_dst}/abs: {dst_abs:#x})"
373            );
374        }
375        if self.is_little_endian {
376            self.local_fixups.write_u32::<LittleEndian>(local_src)?;
377            self.local_fixups.write_u32::<LittleEndian>(local_dst)?;
378        } else {
379            self.local_fixups.write_u32::<BigEndian>(local_src)?;
380            self.local_fixups.write_u32::<BigEndian>(local_dst)?;
381        }
382        Ok(())
383    }
384
385    /// Write the internal data pointed to by the pointer of `CString` or `StringPtr`.
386    fn serialize_cow(&mut self, v: &Option<Cow<'_, str>>) -> Result<()> {
387        #[cfg(feature = "tracing")]
388        tracing::trace!("pointed_pos:({:#x?})", self.pointed_pos);
389
390        // Skip if `Option::None`(null pointer).
391        if let Some(v) = v {
392            let ptr_start = self.relative_position()?;
393            tri!(self.serialize_ulong(Ulong::new(0))); // ptr size
394            let next_ser_pos = self.output.position();
395
396            #[cfg(feature = "tracing")]
397            tracing::trace!("Serialize `CString`/`StringPtr` ({next_ser_pos:#x}): (\"{v}\")",);
398
399            // local dst
400            let pointed_pos = tri!(self.goto_local_dst());
401            tri!(self.write_local_fixup_pair(ptr_start, pointed_pos));
402
403            let c_string = StdCString::new(v.as_bytes())?;
404            let _ = self.output.write(c_string.as_bytes_with_nul())?;
405            if self.is_in_str_array {
406                self.output.zero_fill_align(2)?;
407            } else {
408                self.output.zero_fill_align(16)?;
409            }
410
411            let next_pointed_ser_pos = self.output.position();
412            self.current_last_local_dst = next_pointed_ser_pos;
413            if let Some(last) = self.pointed_pos.last_mut() {
414                *last = next_pointed_ser_pos; // Update to serialize the next pointed data.
415            };
416
417            self.output.set_position(next_ser_pos);
418        } else {
419            tri!(self.serialize_ulong(Ulong::new(0))); // ptr size
420        };
421        Ok(())
422    }
423}
424
425/// Endianness and a common write process that takes into account whether the array is being serialized or not.
426macro_rules! impl_serialize_primitive {
427    ($method:ident, $value_type:ty, $write:ident) => {
428        #[inline]
429        fn $method(self, v: $value_type) -> Result<Self::Ok, Self::Error> {
430            match self.is_little_endian {
431                true => self.output.$write::<LittleEndian>(v),
432                false => self.output.$write::<BigEndian>(v),
433            }?;
434            Ok(())
435        }
436    };
437}
438
439/// Endianness and a common write process that takes into account whether the array is being serialized or not.
440macro_rules! impl_serialize_math {
441    ($method:ident, $value_type:ty) => {
442        fn $method(self, v: &$value_type) -> Result<Self::Ok, Self::Error> {
443            match self.is_little_endian {
444                true => self.output.write(v.to_le_bytes().as_slice()),
445                false => self.output.write(v.to_be_bytes().as_slice()),
446            }?;
447            Ok(())
448        }
449    };
450}
451
452impl<'a> Serializer for &'a mut ByteSerializer {
453    type Ok = ();
454    type Error = Error;
455
456    type SerializeSeq = Self;
457    type SerializeStruct = StructSerializer<'a>;
458    type SerializeFlags = Self;
459
460    #[inline]
461    fn serialize_void(self, _: ()) -> Result<Self::Ok, Self::Error> {
462        Ok(())
463    }
464
465    #[inline]
466    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
467        self.serialize_uint8(v as u8)
468    }
469
470    #[inline]
471    /// Assume that the characters are ASCII characters`c_char`. In that case, i8 is used to fit into 128 characters.
472    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
473        self.serialize_int8(v as i8)
474    }
475
476    #[inline]
477    fn serialize_int8(self, v: i8) -> Result<Self::Ok, Self::Error> {
478        self.output.write_i8(v)?;
479        Ok(())
480    }
481
482    #[inline]
483    fn serialize_uint8(self, v: u8) -> Result<Self::Ok, Self::Error> {
484        self.output.write_u8(v)?;
485        Ok(())
486    }
487
488    impl_serialize_primitive!(serialize_int16, i16, write_i16);
489    impl_serialize_primitive!(serialize_uint16, u16, write_u16);
490
491    impl_serialize_primitive!(serialize_int32, i32, write_i32);
492    impl_serialize_primitive!(serialize_uint32, u32, write_u32);
493
494    impl_serialize_primitive!(serialize_int64, i64, write_i64);
495    impl_serialize_primitive!(serialize_uint64, u64, write_u64);
496
497    impl_serialize_primitive!(serialize_real, f32, write_f32);
498
499    impl_serialize_math!(serialize_vector4, Vector4);
500    impl_serialize_math!(serialize_quaternion, Quaternion);
501    impl_serialize_math!(serialize_matrix3, Matrix3);
502    impl_serialize_math!(serialize_rotation, Rotation);
503    impl_serialize_math!(serialize_matrix4, Matrix4);
504    impl_serialize_math!(serialize_qstransform, QsTransform);
505    impl_serialize_math!(serialize_transform, Transform);
506
507    // Register data to be written to global_fixups.
508    // Until the data in virtual_fixups is determined (until all C++ classes are written), temporarily write to `IndexMap` as the value of dst is unknown.
509    fn serialize_pointer(self, ptr: Pointer) -> Result<Self::Ok, Self::Error> {
510        #[allow(clippy::needless_else)]
511        if !ptr.is_null() {
512            // Write global_fixup src(write start) position.
513            let start = self.relative_position()?;
514            #[cfg(feature = "tracing")]
515            tracing::debug!(
516                "Insert `global_fixup.src`({start}:{start:#x}/abs {:#x}): {ptr}",
517                self.output.position()
518            );
519            self.global_fixups_ptr_src.insert(start, ptr);
520        } else {
521            #[cfg(feature = "tracing")]
522            tracing::debug!("Skip global_fixup.src writing, because it's null ptr.");
523        };
524        self.serialize_ulong(Ulong::new(0))
525    }
526
527    #[inline]
528    fn serialize_array(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
529        Ok(self)
530    }
531
532    /// This is called in the Havok Class array or HashMap, or in a serializer in a field.
533    /// Classes in the field may be inlined, in which case `class_meta` will be [`None`].
534    ///
535    /// # what's `class_meta`?
536    /// If `class_meta` exists, it is when writing an `hkobject` with the `name` attribute in XML.
537    /// This must be written to virtual_fixup so that the constructor can be called.
538    fn serialize_struct(
539        self,
540        name: &'static str,
541        class_meta: Option<(Pointer, Signature)>,
542        sizes: (u64, u64),
543    ) -> Result<Self::SerializeStruct, Self::Error> {
544        let size = if self.is_x86 { sizes.0 } else { sizes.1 };
545
546        #[allow(clippy::needless_else)]
547        let is_root = if let Some((ptr, _sig)) = class_meta {
548            self.output.zero_fill_align(16)?; // Make sure `virtual_fixup.src`(each Class) is `align16`.
549
550            let virtual_fixup_abs = self.output.position();
551            #[cfg(feature = "tracing")]
552            tracing::debug!(
553                "serialize struct {name}(index = {ptr}, signature = {_sig}, abs_position = {virtual_fixup_abs:#x})"
554            );
555
556            let virtual_src = self.relative_position()?;
557            self.write_virtual_fixups_pair(name, virtual_src)?; // Ok, `virtual_fixup` is known.
558            self.virtual_fixups_ptr_src.insert(ptr, virtual_src); // Backup to write `global_fixups`
559
560            // The data pointed to by the pointer (`T* m_data`) must first be aligned 16 bytes before it is written.
561            let last_local_dst = align!(virtual_fixup_abs + size, 16_u64);
562            self.current_last_local_dst = last_local_dst;
563            self.pointed_pos.push(last_local_dst);
564            true
565        } else {
566            // if let Some(last) = self.pointed_pos.last_mut() {
567            //     *last = self.output.position() + size;
568            // }
569            #[cfg(feature = "tracing")]
570            tracing::debug!("serialize struct {name}(A class within a field.)");
571            false
572        };
573
574        Ok(Self::SerializeStruct::new(self, is_root))
575    }
576
577    #[inline]
578    fn serialize_variant(self, v: &Variant) -> Result<Self::Ok, Self::Error> {
579        self.serialize_pointer(v.object)?;
580        self.serialize_pointer(v.class)
581    }
582
583    #[inline]
584    fn serialize_cstring(self, v: &CString) -> Result<Self::Ok, Self::Error> {
585        self.serialize_cow(v.get_ref())
586    }
587
588    #[inline]
589    fn serialize_ulong(self, v: Ulong) -> Result<Self::Ok, Self::Error> {
590        match self.is_x86 {
591            true => self.serialize_uint32(v.get() as u32),
592            false => self.serialize_uint64(v.get()),
593        }
594    }
595
596    #[inline]
597    fn serialize_enum_flags(self) -> Result<Self::SerializeFlags, Self::Error> {
598        Ok(self)
599    }
600
601    #[inline]
602    fn serialize_half(self, v: f16) -> Result<Self::Ok, Self::Error> {
603        let bytes = if self.is_little_endian {
604            v.to_le_bytes()
605        } else {
606            v.to_be_bytes()
607        };
608        self.output.write_all(&bytes)?;
609        Ok(())
610    }
611
612    /// In the binary serialization of hkx, this is the actual data writing process beyond
613    /// the pointer that is called only after all fields of the structure have been written.
614    #[inline]
615    fn serialize_stringptr(self, v: &StringPtr) -> Result<Self::Ok, Self::Error> {
616        self.serialize_cow(v.get_ref())
617    }
618}
619
620#[cfg(test)]
621mod tests {
622    use super::*;
623    use crate::{HavokSort as _, bytes::hexdump, tests::mocks::new_defaultmale};
624    use havok_classes::{
625        BlendCurve, EndMode, EventMode, FlagBits, hkbBlendingTransitionEffect, hkbGenerator,
626        hkbModifierGenerator,
627    };
628    // use pretty_assertions::assert_eq;
629
630    fn partial_parse_assert<T>(s: T, expected: &[u8], ser: Option<ByteSerializer>)
631    where
632        T: Serialize + PartialEq,
633    {
634        let mut ser = ser.unwrap_or(ByteSerializer {
635            is_little_endian: true,
636            ..Default::default()
637        });
638        match <T as Serialize>::serialize(&s, &mut ser) {
639            Ok(_) => assert_eq!(ser.output.into_inner(), expected),
640            Err(err) => {
641                tracing::error!(?err);
642                panic!("{err}")
643            }
644        }
645    }
646
647    #[test]
648    fn test_serialize_primitive() {
649        assert_eq!(FlagBits::empty().bits(), 0);
650        assert_eq!(FlagBits::from_bits_retain(0).bits(), 0);
651        partial_parse_assert(FlagBits::empty().bits(), &[0, 0], None);
652        partial_parse_assert(FlagBits::empty(), &[0, 0], None);
653        partial_parse_assert(FlagBits::FLAG_SYNC, &[2, 0], None);
654        partial_parse_assert(EventMode::EVENT_MODE_DEFAULT, &[0], None);
655    }
656
657    #[test]
658    fn test_serialize_hkb_blending_transition_effect() {
659        #[rustfmt::skip]
660        let expected = &[
661// parent: Default::default(), // - size: ` 44`(x86)/` 80`(x86_64)
662// 40 bytes + 40 bytes
6630, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
6640, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
665
6660, 0, 128, 63,
6670, 0, 128, 63,
6681, 0, // flags: FlagBits(u16)
6692, // endMode: EndMode(u8)
6701,
6710,
6720, 0, 0, 0, 0, 0, 0, 0,
6730, 0, 0, 0, 0, 0, 0, 0,
6740, 0, 0, 0, 0, 0, 0, 0,
6750, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128,
6760, 0, 0, 64, // timeRemaining(le_f32): 0x00 0x00 0x00 0x40 => 2.0
6770, 0, 128, 63, // timeInTransition(le_f32): 0x80(128) 0x3f(63) 0x00 0x00 => 1.0
6781,
6791, // applySelfTransition
6800, 0, 0, 0, 0, 0 // offset 138, but class size is 144 => padding 6
681];
682
683        partial_parse_assert(
684            hkbBlendingTransitionEffect {
685                __ptr: None,
686                parent: Default::default(), // - size: ` 44`(x86)/` 80`(x86_64)
687                m_duration: 1.0,            // - f32 bits: 0x3F800000
688                m_toGeneratorStartTimeFraction: 1.0, // - f32 bits: 0x3F800000
689                // m_flags: FlagBits::FLAG_NONE, // [0, 0]
690                m_flags: FlagBits::FLAG_IGNORE_FROM_WORLD_FROM_MODEL, // [1, 0]
691                m_endMode: EndMode::END_MODE_CAP_DURATION_AT_END_OF_FROM_GENERATOR, // [2]
692                m_blendCurve: BlendCurve::BLEND_CURVE_LINEAR,         // [1]
693                m_fromGenerator: Pointer::new(0),                     // 0 fill 8bytes(x86_64)
694                m_toGenerator: Pointer::new(0),                       // 0 fill 8bytes(x86_64)
695                m_characterPoseAtBeginningOfTransition: Vec::new(),   // hkArray 16bytes
696                m_timeRemaining: 2.0,                                 // - f32 bits: 0x40000000
697                m_timeInTransition: 1.0,                              // - f32 bits: 0x3F800000
698                m_applySelfTransition: true,                          // [1]
699                m_initializeCharacterPose: true,                      // [1]
700            },
701            expected,
702            None,
703        );
704    }
705
706    #[test]
707    // #[quick_tracing::init] // NOTE: tracing cannot be used in miri test.
708    fn test_serialize_hkb_modifier_generator() {
709        let ser = ByteSerializer {
710            class_starts: {
711                let mut class_starts = IndexMap::new();
712                class_starts.insert("hkbModifierGenerator", 0);
713                class_starts
714            },
715            is_little_endian: true,
716            ..Default::default()
717        };
718
719        // parent.parent.parent: hkbBindable 48bytes
720        let parent_parent_parent: [u8; 48] = [
721            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
722            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 128, 0, 0, 0, 0, 0, 0, 0, 0,
723        ];
724        // parent.parent: hkbNode 72bytes - 48bytes = 24bytes
725        #[rustfmt::skip]
726        let parent_parent = [
7271, 0, 0, 0, 0, 0, 0, 0,   // userData: ulong
7280, 0, 0, 0, 0, 0, 0, 0,  // name: StringPtr
7290, 0, // id: i16
7300, // cloneState: i8
7310, // padNode: [bool;1]
7320, 0, 0, 0 // _pad: [u8;4]
733];
734
735        // hkbModifierGenerator 88bytes - (72) = 16bytes
736        #[rustfmt::skip]
737        let current = [
7380, 0, 0, 0, 0, 0, 0, 0, // modifier: Pointer
7390, 0, 0, 0, 0, 0, 0, 0, // generator: Pointer
740];
741        #[rustfmt::skip]
742        let string_ptr = [
7430, 0, 0, 0, 0, 0, 0, 0, // align 16: 88 to 96bytes => 96 / 16 = 6
74478, 111, 100, 101, 78, 97, 109, 101, 0,  // name: "NodeName\0" -> 9bytes
7450, 0, 0, 0, 0, 0, 0,  // align 16: 105 + 7bytes => 112 / 16 = 7
746];
747
748        const ARRAY_LEN: usize = 88 + 24;
749        let mut expected: [u8; ARRAY_LEN] = [0; ARRAY_LEN];
750        expected[0..48].clone_from_slice(&parent_parent_parent);
751        expected[48..72].clone_from_slice(&parent_parent);
752        // expected.extend_from_slice(&parent); parent: hkbGenerator 72bytes - (48 + 24) = 0
753        expected[72..88].clone_from_slice(&current);
754        expected[88..ARRAY_LEN].clone_from_slice(&string_ptr);
755
756        partial_parse_assert(
757            hkbModifierGenerator {
758                __ptr: Some(Pointer::new(1)), // Root class must have a pointer.
759                parent: hkbGenerator {
760                    __ptr: None,
761                    parent: havok_classes::hkbNode {
762                        __ptr: None,
763                        parent: Default::default(),
764                        m_userData: Ulong::new(1),
765                        m_name: "NodeName".into(),
766                        m_id: 0,
767                        m_cloneState: 0,
768                        m_padNode: [false],
769                    },
770                },
771                m_modifier: Pointer::new(0),
772                m_generator: Pointer::new(0),
773            },
774            &expected,
775            Some(ser),
776        );
777    }
778
779    #[test]
780    fn test_serialize() -> Result<()> {
781        let mut classes = new_defaultmale();
782        classes.sort_for_bytes();
783        tracing::debug!("{classes:#?}");
784
785        let bytes = to_bytes(&classes, &HkxHeader::new_skyrim_se())?;
786        let actual = hexdump::to_string(&bytes);
787        tracing::debug!("\n{actual}");
788
789        let expected = hexdump::to_string(include_bytes!(
790            "../../../../docs/handson_hex_dump/defaultmale/defaultmale.hkx"
791        ));
792        pretty_assertions::assert_eq!(actual, expected);
793        Ok(())
794    }
795}