use std::hash::Hash;
use std::{
fmt::Debug,
io::{Cursor, Read, Seek, Write},
};
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use num_enum::{IntoPrimitive, TryFromPrimitive};
use ordered_float::OrderedFloat;
use crate::custom_version::{FEditorObjectVersion, FUE5ReleaseStreamObjectVersion};
use crate::properties::int_property::UInt64Property;
use crate::properties::struct_types::DateTime;
use crate::types::map::HashableIndexMap;
use crate::{
cursor_ext::{ReadExt, WriteExt},
error::Error,
};
use super::{impl_read, impl_read_header, impl_write, PropertyOptions, PropertyTrait};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct TextProperty {
#[cfg_attr(feature = "serde", serde(flatten))]
pub value: FText,
}
impl TextProperty {
pub fn new(value: FText) -> Self {
TextProperty { value }
}
#[inline]
pub(crate) fn read_body<R: Read + Seek>(
cursor: &mut R,
options: &mut PropertyOptions,
) -> Result<Self, Error> {
let value = FText::read(cursor, options)?;
Ok(TextProperty { value })
}
impl_read!(options);
impl_read_header!(options);
}
impl PropertyTrait for TextProperty {
impl_write!(TextProperty);
#[inline]
fn write_body<W: Write>(
&self,
cursor: &mut W,
options: &mut PropertyOptions,
) -> Result<usize, Error> {
let len = self.value.write(cursor, options)?;
Ok(len)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct FText {
#[cfg_attr(feature = "serde", serde(default, skip_serializing_if = "is_zero"))]
pub flags: u32,
#[cfg_attr(feature = "serde", serde(flatten))]
pub history: FTextHistory,
}
#[cfg(feature = "serde")]
#[inline]
fn is_zero(num: &u32) -> bool {
*num == 0
}
impl FText {
pub fn new_none(flags: u32, culture_invariant_string: Option<Option<String>>) -> Self {
FText {
flags,
history: match culture_invariant_string {
Some(culture_invariant_string) => FTextHistory::None {
culture_invariant_string,
},
None => FTextHistory::Empty {},
},
}
}
pub fn new_base(
flags: u32,
namespace: Option<String>,
key: Option<String>,
source_string: Option<String>,
) -> Self {
FText {
flags,
history: FTextHistory::Base {
namespace,
key,
source_string,
},
}
}
#[inline]
pub fn read<R: Read + Seek>(cursor: &mut R, options: &PropertyOptions) -> Result<Self, Error> {
let flags = cursor.read_u32::<LittleEndian>()?;
let history = FTextHistory::read(cursor, options)?;
Ok(FText { flags, history })
}
#[inline]
pub fn write<W: Write>(
&self,
cursor: &mut W,
options: &PropertyOptions,
) -> Result<usize, Error> {
let mut len = 4;
cursor.write_u32::<LittleEndian>(self.flags)?;
len += self.history.write(cursor, options)?;
Ok(len)
}
}
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[repr(i8)]
pub enum TextHistoryType {
#[default]
None = -1,
Base = 0,
NamedFormat,
OrderedFormat,
ArgumentFormat,
AsNumber,
AsPercent,
AsCurrency,
AsDate,
AsTime,
AsDateTime,
Transform,
StringTableEntry,
TextGenerator,
RawText,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", serde_with::skip_serializing_none)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "history"))]
pub enum FTextHistory {
Empty {},
None {
culture_invariant_string: Option<String>,
},
Base {
namespace: Option<String>,
key: Option<String>,
source_string: Option<String>,
},
NamedFormat {
source_format: Box<FText>,
arguments: HashableIndexMap<String, FormatArgumentValue>,
},
OrderedFormat {
source_format: Box<FText>,
arguments: Vec<FormatArgumentValue>,
},
ArgumentFormat {
source_format: Box<FText>,
arguments: HashableIndexMap<String, FormatArgumentValue>,
},
AsNumber {
source_value: Box<FormatArgumentValue>,
format_options: Option<NumberFormattingOptions>,
target_culture: Option<String>,
},
AsPercent {
source_value: Box<FormatArgumentValue>,
format_options: Option<NumberFormattingOptions>,
target_culture: Option<String>,
},
AsCurrency {
currency_code: Option<String>,
source_value: Box<FormatArgumentValue>,
format_options: Option<NumberFormattingOptions>,
target_culture: Option<String>,
},
AsDate {
date_time: DateTime,
date_style: DateTimeStyle,
target_culture: String,
},
AsTime {
source_date_time: DateTime,
time_style: DateTimeStyle,
time_zone: String,
target_culture: String,
},
AsDateTime {
source_date_time: DateTime,
date_style: DateTimeStyle,
time_style: DateTimeStyle,
time_zone: String,
target_culture: String,
},
Transform {
source_text: Box<FText>,
#[cfg_attr(feature = "serde", serde(flatten))]
transform_type: TransformType,
},
StringTableEntry {
table_id: Box<FText>,
key: String,
},
}
impl FTextHistory {
#[inline]
pub fn read<R: Read + Seek>(cursor: &mut R, options: &PropertyOptions) -> Result<Self, Error> {
let history_type = cursor.read_enum()?;
Ok(match history_type {
TextHistoryType::None => {
if options.supports_version(
FEditorObjectVersion::CultureInvariantTextSerializationKeyStability,
) {
let has_culture_invariant_string = cursor.read_b32()?;
if has_culture_invariant_string {
let culture_invariant_string = cursor.read_fstring()?;
FTextHistory::None {
culture_invariant_string,
}
} else {
FTextHistory::Empty {}
}
} else {
FTextHistory::Empty {}
}
}
TextHistoryType::Base => {
let namespace = cursor.read_fstring()?;
let key = cursor.read_fstring()?;
let source_string = cursor.read_fstring()?;
FTextHistory::Base {
namespace,
key,
source_string,
}
}
TextHistoryType::NamedFormat => {
let source_format = Box::new(FText::read(cursor, options)?);
let argument_count = cursor.read_i32::<LittleEndian>()?;
let mut arguments = HashableIndexMap::with_capacity(argument_count as usize);
for _ in 0..argument_count {
let key = cursor.read_string()?;
let value = FormatArgumentValue::read(cursor, options)?;
arguments.insert(key, value);
}
FTextHistory::NamedFormat {
source_format,
arguments,
}
}
TextHistoryType::OrderedFormat => {
let source_format = Box::new(FText::read(cursor, options)?);
let count = cursor.read_i32::<LittleEndian>()?;
let mut arguments = Vec::with_capacity(count as usize);
for _ in 0..count {
arguments.push(FormatArgumentValue::read(cursor, options)?);
}
FTextHistory::OrderedFormat {
source_format,
arguments,
}
}
TextHistoryType::ArgumentFormat => {
let source_format = Box::new(FText::read(cursor, options)?);
let count = cursor.read_i32::<LittleEndian>()?;
let mut arguments = HashableIndexMap::with_capacity(count as usize);
for _ in 0..count {
let key = cursor.read_string()?;
let value = FormatArgumentValue::read(cursor, options)?;
arguments.insert(key, value);
}
FTextHistory::ArgumentFormat {
source_format,
arguments,
}
}
TextHistoryType::AsNumber => {
let source_value = Box::new(FormatArgumentValue::read(cursor, options)?);
let has_format_options = cursor.read_b32()?;
let format_options = if has_format_options {
Some(NumberFormattingOptions::read(cursor)?)
} else {
None
};
let target_culture = cursor.read_fstring()?;
FTextHistory::AsNumber {
source_value,
format_options,
target_culture,
}
}
TextHistoryType::AsPercent => {
let source_value = Box::new(FormatArgumentValue::read(cursor, options)?);
let has_format_options = cursor.read_b32()?;
let format_options = if has_format_options {
Some(NumberFormattingOptions::read(cursor)?)
} else {
None
};
let target_culture = cursor.read_fstring()?;
FTextHistory::AsPercent {
source_value,
format_options,
target_culture,
}
}
TextHistoryType::AsCurrency => {
let currency_code = cursor.read_fstring()?;
let source_value = Box::new(FormatArgumentValue::read(cursor, options)?);
let has_format_options = cursor.read_b32()?;
let format_options = if has_format_options {
Some(NumberFormattingOptions::read(cursor)?)
} else {
None
};
let target_culture = cursor.read_fstring()?;
FTextHistory::AsCurrency {
currency_code,
source_value,
format_options,
target_culture,
}
}
TextHistoryType::AsDate => {
let date_time = DateTime {
ticks: UInt64Property::read(cursor, false)?.value,
};
let date_style = cursor.read_enum()?;
let target_culture = cursor.read_string()?;
FTextHistory::AsDate {
date_time,
date_style,
target_culture,
}
}
TextHistoryType::AsTime => {
let source_date_time = DateTime {
ticks: UInt64Property::read(cursor, false)?.value,
};
let time_style = cursor.read_enum()?;
let time_zone = cursor.read_string()?;
let target_culture = cursor.read_string()?;
FTextHistory::AsTime {
source_date_time,
time_style,
time_zone,
target_culture,
}
}
TextHistoryType::AsDateTime => {
let source_date_time = DateTime {
ticks: UInt64Property::read(cursor, false)?.value,
};
let date_style = cursor.read_enum()?;
let time_style = cursor.read_enum()?;
let time_zone = cursor.read_string()?;
let target_culture = cursor.read_string()?;
FTextHistory::AsDateTime {
source_date_time,
date_style,
time_style,
time_zone,
target_culture,
}
}
TextHistoryType::Transform => {
let source_text = Box::new(FText::read(cursor, options)?);
let transform_type = cursor.read_enum()?;
FTextHistory::Transform {
source_text,
transform_type,
}
}
TextHistoryType::StringTableEntry => {
let table_id = Box::new(FText::read(cursor, options)?);
let key = cursor.read_string()?;
FTextHistory::StringTableEntry { table_id, key }
}
_ => unimplemented!("unimplemented history type: {:?}", history_type),
})
}
#[inline]
pub fn write<W: Write>(
&self,
cursor: &mut W,
options: &PropertyOptions,
) -> Result<usize, Error> {
match self {
FTextHistory::Empty {} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::None)?;
if options.supports_version(
FEditorObjectVersion::CultureInvariantTextSerializationKeyStability,
) {
len += 4;
cursor.write_b32(false)?;
}
Ok(len)
}
FTextHistory::None {
culture_invariant_string,
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::None)?;
if options.supports_version(
FEditorObjectVersion::CultureInvariantTextSerializationKeyStability,
) {
len += 4;
cursor.write_b32(true)?;
len += cursor.write_fstring(culture_invariant_string.as_deref())?;
}
Ok(len)
}
FTextHistory::Base {
namespace,
key,
source_string,
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::Base)?;
len += cursor.write_fstring(namespace.as_deref())?;
len += cursor.write_fstring(key.as_deref())?;
len += cursor.write_fstring(source_string.as_deref())?;
Ok(len)
}
FTextHistory::NamedFormat {
source_format,
arguments: HashableIndexMap(arguments),
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::NamedFormat)?;
len += source_format.write(cursor, options)?;
len += 4;
cursor.write_i32::<LittleEndian>(arguments.len() as i32)?;
for (key, value) in arguments {
len += cursor.write_string(key)?;
len += value.write(cursor, options)?;
}
Ok(len)
}
FTextHistory::OrderedFormat {
source_format,
arguments,
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::OrderedFormat)?;
len += source_format.write(cursor, options)?;
len += 4;
cursor.write_i32::<LittleEndian>(arguments.len() as i32)?;
for argument in arguments {
len += argument.write(cursor, options)?;
}
Ok(len)
}
FTextHistory::ArgumentFormat {
source_format,
arguments: HashableIndexMap(arguments),
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::ArgumentFormat)?;
len += source_format.write(cursor, options)?;
len += 4;
cursor.write_i32::<LittleEndian>(arguments.len() as i32)?;
for (key, value) in arguments {
len += cursor.write_string(key)?;
len += value.write(cursor, options)?;
}
Ok(len)
}
FTextHistory::AsNumber {
source_value,
format_options,
target_culture,
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::AsNumber)?;
len += source_value.write(cursor, options)?;
len += 4;
cursor.write_b32(format_options.is_some())?;
if let Some(format_options) = format_options {
len += format_options.write(cursor)?;
};
len += cursor.write_fstring(target_culture.as_deref())?;
Ok(len)
}
FTextHistory::AsPercent {
source_value,
format_options,
target_culture,
} => {
let mut len = 1;
cursor.write_enum(TextHistoryType::AsPercent)?;
len += source_value.write(cursor, options)?;
len += 4;
cursor.write_b32(format_options.is_some())?;
if let Some(format_options) = format_options {
len += format_options.write(cursor)?;
}
len += cursor.write_fstring(target_culture.as_deref())?;
Ok(len)
}
FTextHistory::AsCurrency {
currency_code,
source_value,
format_options,
target_culture,
} => {
let mut len = 0;
len += cursor.write_fstring(currency_code.as_deref())?;
len += source_value.write(cursor, options)?;
len += 4;
cursor.write_b32(format_options.is_some())?;
if let Some(format_options) = format_options {
len += format_options.write(cursor)?;
}
len += cursor.write_fstring(target_culture.as_deref())?;
Ok(len)
}
FTextHistory::AsDate {
date_time,
date_style,
target_culture,
} => {
cursor.write_enum(TextHistoryType::AsDate)?;
cursor.write_u64::<LittleEndian>(date_time.ticks)?;
cursor.write_enum(*date_style)?;
let mut len = 10;
len += cursor.write_string(target_culture)?;
Ok(len)
}
FTextHistory::AsTime {
source_date_time,
time_style,
time_zone,
target_culture,
} => {
cursor.write_enum(TextHistoryType::AsTime)?;
cursor.write_u64::<LittleEndian>(source_date_time.ticks)?;
cursor.write_enum(*time_style)?;
let mut len = 10;
len += cursor.write_string(time_zone)?;
len += cursor.write_string(target_culture)?;
Ok(len)
}
FTextHistory::AsDateTime {
source_date_time,
date_style,
time_style,
time_zone,
target_culture,
} => {
cursor.write_enum(TextHistoryType::AsDateTime)?;
cursor.write_u64::<LittleEndian>(source_date_time.ticks)?;
cursor.write_enum(*date_style)?;
cursor.write_enum(*time_style)?;
let mut len = 11;
len += cursor.write_string(time_zone.as_str())?;
len += cursor.write_string(target_culture.as_str())?;
Ok(len)
}
FTextHistory::Transform {
source_text,
transform_type,
} => {
cursor.write_enum(TextHistoryType::Transform)?;
let mut len = 2;
len += source_text.write(cursor, options)?;
cursor.write_enum(*transform_type)?;
Ok(len)
}
FTextHistory::StringTableEntry { table_id, key } => {
let mut len = 0;
len += table_id.write(cursor, options)?;
len += cursor.write_string(key)?;
Ok(len)
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[repr(i8)]
pub enum FormatArgumentType {
Int,
UInt,
Float,
Double,
Text,
Gender,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum FormatArgumentValue {
Int(i32),
UInt(u32),
Float(OrderedFloat<f32>),
Double(OrderedFloat<f64>),
Text(FText),
Int64(i64),
UInt64(u64),
}
impl FormatArgumentValue {
#[inline]
pub(crate) fn read<R: Read + Seek>(
cursor: &mut R,
options: &PropertyOptions,
) -> Result<Self, Error> {
let format_argument_type = cursor.read_enum()?;
Ok(match format_argument_type {
FormatArgumentType::Int => match options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
) {
true => FormatArgumentValue::Int64(cursor.read_i64::<LittleEndian>()?),
false => FormatArgumentValue::Int(cursor.read_i32::<LittleEndian>()?),
},
FormatArgumentType::UInt => match options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
) {
true => FormatArgumentValue::UInt64(cursor.read_u64::<LittleEndian>()?),
false => FormatArgumentValue::UInt(cursor.read_u32::<LittleEndian>()?),
},
FormatArgumentType::Float => {
FormatArgumentValue::Float(cursor.read_f32::<LittleEndian>()?.into())
}
FormatArgumentType::Double => {
FormatArgumentValue::Double(cursor.read_f64::<LittleEndian>()?.into())
}
FormatArgumentType::Text => FormatArgumentValue::Text(FText::read(cursor, options)?),
FormatArgumentType::Gender => unimplemented!(),
})
}
#[inline]
pub fn write<W: Write>(
&self,
cursor: &mut W,
options: &PropertyOptions,
) -> Result<usize, Error> {
match self {
FormatArgumentValue::Int(value) => {
assert!(
!options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
),
"FormatArgumentValue::Int is not compatible with TextFormatArgumentData64bitSupport"
);
cursor.write_enum(FormatArgumentType::Int)?;
cursor.write_i32::<LittleEndian>(*value)?;
Ok(5)
}
FormatArgumentValue::Int64(value) => {
assert!(
options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
),
"FormatArgumentValue::Int64 requires TextFormatArgumentData64bitSupport"
);
cursor.write_enum(FormatArgumentType::Int)?;
cursor.write_i64::<LittleEndian>(*value)?;
Ok(9)
}
FormatArgumentValue::UInt(value) => {
assert!(
!options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
),
"FormatArgumentValue::UInt is not compatible with TextFormatArgumentData64bitSupport"
);
cursor.write_enum(FormatArgumentType::UInt)?;
cursor.write_u32::<LittleEndian>(*value)?;
Ok(5)
}
FormatArgumentValue::UInt64(value) => {
assert!(
options.supports_version(
FUE5ReleaseStreamObjectVersion::TextFormatArgumentData64bitSupport,
),
"FormatArgumentValue::UInt64 requires TextFormatArgumentData64bitSupport"
);
cursor.write_enum(FormatArgumentType::UInt)?;
cursor.write_u64::<LittleEndian>(*value)?;
Ok(9)
}
FormatArgumentValue::Float(value) => {
cursor.write_enum(FormatArgumentType::Float)?;
cursor.write_f32::<LittleEndian>(value.0)?;
Ok(5)
}
FormatArgumentValue::Double(value) => {
cursor.write_enum(FormatArgumentType::Double)?;
cursor.write_f64::<LittleEndian>(value.0)?;
Ok(9)
}
FormatArgumentValue::Text(value) => {
let mut len = 1;
cursor.write_enum(FormatArgumentType::Text)?;
len += value.write(cursor, options)?;
Ok(len)
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "rounding"))]
#[repr(i8)]
pub enum RoundingMode {
HalfToEven,
HalfFromZero,
HalfToZero,
FromZero,
ToZero,
ToNegativeInfinity,
ToPositiveInfinity,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NumberFormattingOptions {
pub always_include_sign: bool,
pub use_grouping: bool,
#[cfg_attr(feature = "serde", serde(flatten))]
pub rounding_mode: RoundingMode,
pub minimum_integral_digits: i32,
pub maximum_integral_digits: i32,
pub minimum_fractional_digits: i32,
pub maximum_fractional_digits: i32,
}
impl NumberFormattingOptions {
#[inline]
pub fn read<R: Read + Seek>(cursor: &mut R) -> Result<Self, Error> {
let always_include_sign = cursor.read_b32()?;
let use_grouping = cursor.read_b32()?;
let rounding_mode = cursor.read_enum()?;
let minimum_integral_digits = cursor.read_i32::<LittleEndian>()?;
let maximum_integral_digits = cursor.read_i32::<LittleEndian>()?;
let minimum_fractional_digits = cursor.read_i32::<LittleEndian>()?;
let maximum_fractional_digits = cursor.read_i32::<LittleEndian>()?;
Ok(NumberFormattingOptions {
always_include_sign,
use_grouping,
rounding_mode,
minimum_integral_digits,
maximum_integral_digits,
minimum_fractional_digits,
maximum_fractional_digits,
})
}
#[inline]
pub fn write<W: Write>(&self, cursor: &mut W) -> Result<usize, Error> {
cursor.write_b32(self.always_include_sign)?;
cursor.write_b32(self.use_grouping)?;
cursor.write_enum(self.rounding_mode)?;
cursor.write_i32::<LittleEndian>(self.minimum_integral_digits)?;
cursor.write_i32::<LittleEndian>(self.maximum_integral_digits)?;
cursor.write_i32::<LittleEndian>(self.minimum_fractional_digits)?;
cursor.write_i32::<LittleEndian>(self.maximum_fractional_digits)?;
Ok(25)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[repr(i8)]
pub enum DateTimeStyle {
Default,
Short,
Medium,
Long,
Full,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, IntoPrimitive, TryFromPrimitive)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(tag = "transform"))]
#[repr(i8)]
pub enum TransformType {
ToLower = 0,
ToUpper,
}