use super::{
buff::BuffEffect,
error::Error,
inventory::{InventorySlot, ItemSlot, Loadout, SingleItemSlot},
Difficulty, Player, SpawnPoint, Visibility, MAGIC_NUMBER, PLAYER_ENCRYPTION_KEY,
SUPPORTED_VERSIONS,
};
use crate::{round_division_i128, Style, TerrariaFileType};
use aes::{
cipher::{BlockEncryptMut, KeyIvInit},
Aes128Enc,
};
use bounded_vector::BoundedVec;
use byteorder::LittleEndian;
use byteorder::WriteBytesExt;
use cs_datetime_parse::DateTimeCs;
use cs_string_rw::WriteCsStrExt;
use inout::block_padding::Pkcs7;
use std::io::{self, Cursor, Write};
use terra_items::{Item, Prefix};
use terra_types::Color;
use time::{Duration, OffsetDateTime};
type Aes128CbcEnc = cbc::Encryptor<Aes128Enc>;
impl super::Player {
pub fn write_player_unencrypted<W: Write + ?Sized>(
&self,
writer: &mut W,
version: i32,
) -> Result<(), Error> {
let mut player_writer = PlayerWriter::new(writer, version)?;
player_writer.write_player(self)?;
Ok(())
}
pub fn write_player<W: Write + ?Sized>(
&self,
writer: &mut W,
version: i32,
) -> Result<(), Error> {
let enc = Aes128CbcEnc::new(&PLAYER_ENCRYPTION_KEY.into(), &PLAYER_ENCRYPTION_KEY.into());
let mut cursor: Cursor<Vec<u8>> = Cursor::new(Vec::new());
self.write_player_unencrypted(&mut cursor, version)?;
let mut player_bytes = cursor.into_inner();
let len = player_bytes.len();
player_bytes.resize(len + 16 - len % 16, 0);
let encrypted_player_bytes = enc
.encrypt_padded_mut::<Pkcs7>(&mut player_bytes, len)
.expect("Buf len should by valid because it is resized");
writer.write_all(encrypted_player_bytes)?;
Ok(())
}
}
struct PlayerWriter<'a, W: Write + ?Sized> {
version: i32,
writer: &'a mut W,
}
impl<'a, W: Write + ?Sized> PlayerWriter<'a, W> {
fn new(writer: &'a mut W, version: i32) -> Result<PlayerWriter<'a, W>, Error> {
if !SUPPORTED_VERSIONS.contains(&version) {
return Err(Error::UnsupportedVersion {
version,
version_bounds: SUPPORTED_VERSIONS,
});
}
Ok(Self { writer, version })
}
fn write_i64(&mut self, value: i64) -> io::Result<()> {
self.writer.write_i64::<LittleEndian>(value)
}
fn write_i32(&mut self, value: i32) -> io::Result<()> {
self.writer.write_i32::<LittleEndian>(value)
}
fn write_i16(&mut self, value: i16) -> io::Result<()> {
self.writer.write_i16::<LittleEndian>(value)
}
fn write_u8(&mut self, value: u8) -> io::Result<()> {
self.writer.write_u8(value)
}
fn write_f32(&mut self, value: f32) -> io::Result<()> {
self.writer.write_f32::<LittleEndian>(value)
}
fn write_bool(&mut self, value: bool) -> io::Result<()> {
self.write_u8(value as u8)
}
fn write_string(&mut self, value: &str) -> io::Result<()> {
self.writer.write_cs_string(value)
}
fn write_item(&mut self, item: Option<Item>) -> io::Result<()> {
if self.version < 38 {
let id = item
.map(|x| x.to_old_id(self.version).unwrap_or(""))
.unwrap_or("");
return self.write_string(id);
}
let id = item.map(|x| x.to_id(self.version)).unwrap_or(0);
self.write_i32(id)
}
fn write_datetime(&mut self, value: DateTimeCs) -> Result<(), Error> {
self.write_i64(value.to_binary()?)?;
Ok(())
}
fn write_item_slot(&mut self, item_slot: Option<ItemSlot>) -> io::Result<()> {
let Some(item_slot) = item_slot else {
self.write_item(None)?;
self.write_i32(0)?;
self.write_prefix(None)?;
return Ok(());
};
self.write_item(Some(item_slot.item))?;
self.write_i32(item_slot.count.into())?;
self.write_prefix(item_slot.prefix)
}
fn write_inventory_slot(
&mut self,
inventory_slot: Option<InventorySlot>,
is_read_is_favorite: bool,
) -> io::Result<()> {
let Some(inventory_slot) = inventory_slot else {
self.write_item_slot(None)?;
if is_read_is_favorite {
self.write_bool(false)?;
}
return Ok(());
};
self.write_item_slot(Some(ItemSlot {
item: inventory_slot.item,
prefix: inventory_slot.prefix,
count: inventory_slot.count,
}))?;
if is_read_is_favorite {
self.write_bool(inventory_slot.is_favorite)?;
}
Ok(())
}
fn write_single_item_slot(&mut self, equipable_slot: Option<SingleItemSlot>) -> io::Result<()> {
let Some(equipable_slot) = equipable_slot else {
self.write_item(None)?;
self.write_prefix(None)?;
return Ok(());
};
self.write_item(Some(equipable_slot.item))?;
self.write_prefix(equipable_slot.prefix)
}
fn write_magic_number(&mut self) -> io::Result<()> {
self.writer.write_all(MAGIC_NUMBER)
}
fn write_terraria_file_type(&mut self, file_type: TerrariaFileType) -> io::Result<()> {
self.write_u8(file_type.to_id().unwrap_or(255))
}
fn write_duration_cs_ticks(&mut self, value: Duration) -> io::Result<()> {
let cs_ticks = round_division_i128(value.whole_nanoseconds(), 100);
let cs_ticks = if cs_ticks > i64::MAX as i128 {
i64::MAX
} else if cs_ticks < i64::MIN as i128 {
i64::MIN
} else {
cs_ticks as i64
};
self.write_i64(cs_ticks)
}
fn write_duration_terra_ticks(&mut self, value: Duration) -> io::Result<()> {
let nano_seconds = value.whole_nanoseconds();
let terra_ticks = if nano_seconds > 35791394116666666 {
i32::MAX
} else if nano_seconds < -35791394133333333 {
i32::MIN
} else {
round_division_i128(nano_seconds * 60, 1000000000) as i32
};
self.write_i32(terra_ticks)
}
fn write_color(&mut self, value: Color) -> io::Result<()> {
self.write_u8(value.red)?;
self.write_u8(value.green)?;
self.write_u8(value.blue)
}
fn write_prefix(&mut self, prefix: Option<Prefix>) -> io::Result<()> {
if self.version < 36 {
return Ok(());
}
self.write_u8(prefix.map(|x| x.to_id()).unwrap_or(0))
}
fn write_difficulty(&mut self, value: Difficulty) -> io::Result<()> {
match self.version {
..=10 => Ok(()),
11..=17 if value == Difficulty::Hardcore => self.write_bool(true),
11..=17 => self.write_bool(false),
18.. => self.write_u8(value.to_id()),
}
}
fn write_inventory(&mut self, inventory: &[[Option<InventorySlot>; 10]; 5]) -> io::Result<()> {
for row in inventory.iter().take(if self.version < 58 { 4 } else { 5 }) {
for item in row {
self.write_inventory_slot(*item, self.version >= 114)?;
}
}
Ok(())
}
fn write_small_inventory(
&mut self,
small_inventory: &[Option<InventorySlot>; 4],
) -> io::Result<()> {
for item in small_inventory {
self.write_inventory_slot(*item, self.version >= 114)?;
}
Ok(())
}
fn write_container(&mut self, container: &[[Option<ItemSlot>; 10]; 4]) -> io::Result<()> {
for row in container.iter() {
for item in row.iter().take(if self.version >= 58 { 10 } else { 5 }) {
self.write_item_slot(*item)?;
}
}
Ok(())
}
fn write_void_vault(
&mut self,
vault: &Option<[[Option<InventorySlot>; 10]; 4]>,
) -> io::Result<()> {
if self.version < 198 {
return Ok(());
}
let Some(vault) = vault else {
for _ in 0..40 {
self.write_inventory_slot(None, self.version >= 255)?;
}
if self.version >= 199 {
self.write_bool(false)?;
}
return Ok(());
};
if self.version >= 198 {
for row in vault.iter() {
for item in row {
self.write_inventory_slot(*item, self.version >= 255)?;
}
}
}
if self.version >= 199 {
self.write_bool(true)?;
}
Ok(())
}
fn write_visibility(&mut self, value: Visibility) -> io::Result<()> {
self.write_i32(value.to_id())
}
fn write_buffs(&mut self, buffs: &BoundedVec<BuffEffect, 0, 44>) -> io::Result<()> {
if self.version < 11 {
return Ok(());
}
for i in 0..(match self.version {
..=73 => 10,
74..=251 => 22,
252.. => 44,
}) {
let buff = buffs.get(i);
self.write_i32(buff.map(|x| x.buff.to_id()).unwrap_or(0))?;
self.write_duration_terra_ticks(
buff.map(|x| x.time).unwrap_or_else(|| Duration::new(0, 0)),
)?;
}
Ok(())
}
fn write_spawn_points(
&mut self,
spawn_points: &BoundedVec<SpawnPoint, 0, 200>,
) -> io::Result<()> {
for spawn_point in spawn_points.iter() {
self.write_i32(spawn_point.coords.x.into())?;
self.write_i32(spawn_point.coords.y.into())?;
self.write_i32(spawn_point.world_id)?;
self.write_string(&spawn_point.world_name)?;
}
self.write_i32(-1)
}
fn write_loadout(&mut self, loadout: &Loadout) -> io::Result<()> {
self.write_item_slot(loadout.helmet.armor.map(|x| x.into()))?;
self.write_item_slot(loadout.breastplate.armor.map(|x| x.into()))?;
self.write_item_slot(loadout.pants.armor.map(|x| x.into()))?;
for accessory in loadout.accessories.iter() {
self.write_item_slot(accessory.accessory.map(|x| x.into()))?;
}
self.write_item_slot(loadout.helmet.vanity_armor.map(|x| x.into()))?;
self.write_item_slot(loadout.breastplate.vanity_armor.map(|x| x.into()))?;
self.write_item_slot(loadout.pants.vanity_armor.map(|x| x.into()))?;
for accessory in loadout.accessories.iter() {
self.write_item_slot(accessory.vanity_accessory.map(|x| x.into()))?;
}
self.write_item_slot(loadout.helmet.dye.map(|x| x.into()))?;
self.write_item_slot(loadout.breastplate.dye.map(|x| x.into()))?;
self.write_item_slot(loadout.pants.dye.map(|x| x.into()))?;
for accessory in loadout.accessories.iter() {
self.write_item_slot(accessory.dye.map(|x| x.into()))?;
}
for _ in 0..3 {
self.write_bool(false)?;
}
for accessory in loadout.accessories.iter() {
self.write_bool(!accessory.is_accessory_shown)?;
}
Ok(())
}
fn write_hair_style(&mut self, hair_style: i32, style: Style) -> io::Result<()> {
let mut hair_style = hair_style.max(0);
let max_hair_style = match self.version {
..=17 => 16,
18..=67 => 35,
68..=92 => 50,
93..=145 => 122,
146..=224 => 133,
225..=231 => 161,
232..=241 => 162,
242.. => 163,
};
if hair_style > max_hair_style {
hair_style = 0;
}
if self.version <= 17 {
hair_style = match hair_style {
5 | 6 | 9 | 11 if !style.is_male_style() => hair_style,
5 | 6 | 9 | 11 => 0,
_ if style.is_male_style() => hair_style,
_ => 5,
}
}
self.write_i32(hair_style)
}
fn write_player(&mut self, player: &Player) -> Result<(), Error> {
let version = self.version;
let plr = player;
self.write_i32(version)?;
if version >= 135 {
self.write_magic_number()?;
self.write_terraria_file_type(TerrariaFileType::Player)?;
self.write_i32(0)?; self.write_i64(plr.is_favorite as i64)?;
}
self.write_string(&plr.name)?;
self.write_difficulty(plr.difficulty)?;
if version >= 138 {
self.write_duration_cs_ticks(plr.playtime.unwrap_or_else(|| Duration::new(0, 0)))?;
}
self.write_hair_style(plr.hair_style, plr.style)?;
if version >= 83 {
self.write_u8(plr.hair_dye.map(|x| x.to_id()).unwrap_or(0))?;
}
if version >= 83 {
let mut bytes = 0i16;
for accessory_row in plr.loadouts.loadouts[0].accessories.iter().rev() {
bytes <<= 1;
bytes |= !accessory_row.is_accessory_shown as i16;
}
bytes <<= 3;
if version >= 124 {
self.write_i16(bytes)?;
} else {
self.write_u8(bytes as u8)?;
}
}
if version >= 119 {
let mut byte = !plr.pet.is_shown as u8;
byte |= (!plr.light_pet.is_shown as u8) << 1;
self.write_u8(byte)?;
}
match version {
..=17 => {}
18..=107 => self.write_bool(plr.style.is_male_style())?,
108.. => self.write_u8(plr.style.to_id(self.version))?,
};
self.write_i32(plr.life)?;
self.write_i32(plr.max_life)?;
self.write_i32(plr.mana)?;
self.write_i32(plr.max_mana)?;
if version >= 125 {
self.write_bool(plr.is_demon_heart_accessory_slot_unlocked)?;
}
if version >= 229 {
self.write_bool(plr.is_using_biome_torches.is_some())?
}
if version >= 229 {
self.write_bool(plr.is_using_biome_torches.unwrap_or(false))?;
}
if version >= 256 {
self.write_bool(plr.is_artisan_bread_eaten)?;
}
if version >= 260 {
self.write_bool(plr.is_aegis_crystal_used)?;
self.write_bool(plr.is_aegis_fruit_used)?;
self.write_bool(plr.is_arcane_crystal_used)?;
self.write_bool(plr.is_galaxy_pearl_used)?;
self.write_bool(plr.is_gummy_worm_used)?;
self.write_bool(plr.is_ambrosia_used)?;
}
if version >= 182 {
self.write_bool(plr.is_dd2_event_downed)?;
}
if version >= 128 {
self.write_i32(plr.tax_money_in_copper_coins)?;
}
if version >= 254 {
self.write_i32(plr.deaths_not_caused_by_player_count)?;
}
if version >= 254 {
self.write_i32(plr.deaths_caused_by_player_count)?;
}
self.write_color(plr.hair_color)?;
self.write_color(plr.skin_color)?;
self.write_color(plr.eye_color)?;
self.write_color(plr.shirt_color)?;
self.write_color(plr.under_shirt_color)?;
self.write_color(plr.pants_color)?;
self.write_color(plr.shoe_color)?;
let loadout = &plr.loadouts.loadouts[0];
self.write_single_item_slot(loadout.helmet.armor)?;
self.write_single_item_slot(loadout.breastplate.armor)?;
self.write_single_item_slot(loadout.pants.armor)?;
for accessory in loadout
.accessories
.iter()
.take(if version >= 124 { 7 } else { 5 })
{
self.write_single_item_slot(accessory.accessory)?;
}
if version >= 6 {
self.write_single_item_slot(loadout.helmet.vanity_armor)?;
self.write_single_item_slot(loadout.breastplate.vanity_armor)?;
self.write_single_item_slot(loadout.pants.vanity_armor)?;
}
for accessory in loadout.accessories.iter().take(match version {
..=80 => 0,
81..=123 => 5,
124.. => 7,
}) {
self.write_single_item_slot(accessory.vanity_accessory)?;
}
if version >= 47 {
self.write_single_item_slot(loadout.helmet.dye)?;
self.write_single_item_slot(loadout.breastplate.dye)?;
self.write_single_item_slot(loadout.pants.dye)?;
}
for accessory in loadout.accessories.iter().take(match version {
..=80 => 0,
81..=123 => 5,
124.. => 7,
}) {
self.write_single_item_slot(accessory.dye)?;
}
self.write_inventory(&plr.inventory)?;
self.write_small_inventory(&plr.coins)?;
if version >= 15 {
self.write_small_inventory(&plr.ammo)?;
}
if version >= 136 {
self.write_single_item_slot(plr.pet.item)?;
self.write_single_item_slot(plr.pet.dye)?;
}
if version >= 117 {
self.write_single_item_slot(plr.light_pet.item)?;
self.write_single_item_slot(plr.light_pet.dye)?;
self.write_single_item_slot(plr.minecart.item)?;
self.write_single_item_slot(plr.minecart.dye)?;
self.write_single_item_slot(plr.mount.item)?;
self.write_single_item_slot(plr.mount.dye)?;
self.write_single_item_slot(plr.hook.item)?;
self.write_single_item_slot(plr.hook.dye)?;
}
self.write_container(&plr.piggy_bank)?;
if version >= 20 {
self.write_container(&plr.safe)?;
}
if version >= 182 {
self.write_container(&plr.defenders_forge)?;
}
self.write_void_vault(&plr.void_vault)?;
self.write_buffs(&plr.buffs)?;
self.write_spawn_points(&plr.spawn_points)?;
if version >= 16 {
self.write_bool(plr.is_hotbar_locked)?;
}
if version >= 115 {
self.write_bool(!plr.is_time_info_shown)?;
self.write_bool(!plr.is_weather_info_shown)?;
self.write_bool(!plr.is_fishing_power_info_shown)?;
self.write_bool(!plr.is_position_info_shown)?;
self.write_bool(!plr.is_depth_info_shown)?;
self.write_bool(!plr.is_creature_count_info_shown)?;
self.write_bool(!plr.is_kill_count_info_shown)?;
self.write_bool(!plr.is_moon_phase_info_shown)?;
self.write_bool(false)?; self.write_bool(!plr.is_movement_speed_info_shown)?;
self.write_bool(!plr.is_treasure_finder_info_shown)?;
self.write_bool(!plr.is_rare_creatures_finder_info_shown)?;
self.write_bool(!plr.is_damage_per_second_info_shown)?;
self.write_i32(plr.finished_angler_quests_count)?;
}
if version >= 164 {
self.write_i32(plr.dpad_shortcuts.up.map(|x| x.into()).unwrap_or(-1))?;
self.write_i32(plr.dpad_shortcuts.right.map(|x| x.into()).unwrap_or(-1))?;
self.write_i32(plr.dpad_shortcuts.down.map(|x| x.into()).unwrap_or(-1))?;
self.write_i32(plr.dpad_shortcuts.left.map(|x| x.into()).unwrap_or(-1))?;
self.write_i32(!plr.is_ruler_enabled as i32)?;
self.write_i32(!plr.is_mechanical_ruler_enabled as i32)?;
self.write_i32(!plr.is_auto_paint_enabled as i32)?;
self.write_i32(!plr.is_auto_paint_enabled as i32)?;
self.write_visibility(plr.red_wires_visibility)?;
self.write_visibility(plr.blue_wires_visibility)?;
self.write_visibility(plr.green_wires_visibility)?;
self.write_visibility(plr.yellow_wires_visibility)?;
}
if version >= 167 {
self.write_i32(!plr.is_always_showing_wires_and_actuators as i32)?;
self.write_visibility(plr.actuators_visibility)?;
}
if version >= 197 {
self.write_i32(!plr.is_tile_replacement_enabled as i32)?;
}
if version >= 230 {
self.write_i32(!plr.is_using_biome_torches.unwrap_or(false) as i32)?;
}
if version >= 181 {
self.write_i32(plr.is_talked_to_bartender as i32)?;
}
if version >= 200 {
self.write_bool(plr.time_to_respawn.is_some())?;
if let Some(duration) = plr.time_to_respawn {
self.write_duration_terra_ticks(duration)?;
}
}
if version >= 202 {
self.write_datetime(
plr.last_time_player_was_saved
.unwrap_or_else(|| OffsetDateTime::now_utc().into()),
)?;
}
if version >= 206 {
self.write_i32(plr.golfer_score_accumulated)?;
}
if version >= 218 {
self.write_i32(plr.item_researched_count.len() as i32)?;
for item_researched_count in plr.item_researched_count.iter() {
self.write_string(item_researched_count.0.to_code_name())?;
self.write_i32(*item_researched_count.1)?;
}
}
if version >= 214 {
let mut byte: u8 = plr.temporary_mouse_item.is_some() as u8;
byte |= (plr.temporary_research_item.is_some() as u8) << 1;
byte |= (plr.temporary_guide_item.is_some() as u8) << 2;
byte |= (plr.temporary_goblin_item.is_some() as u8) << 3;
self.write_u8(byte)?;
if plr.temporary_mouse_item.is_some() {
self.write_item_slot(plr.temporary_mouse_item)?;
}
if plr.temporary_research_item.is_some() {
self.write_item_slot(plr.temporary_research_item)?;
}
if plr.temporary_guide_item.is_some() {
self.write_item_slot(plr.temporary_guide_item)?;
}
if plr.temporary_goblin_item.is_some() {
self.write_item_slot(plr.temporary_goblin_item)?;
}
}
if version >= 220 {
self.write_bool(true)?;
self.write_i16(5)?;
self.write_bool(plr.is_godmode_enabled)?;
self.write_bool(true)?;
self.write_i16(11)?;
self.write_bool(plr.is_far_placement_enabled)?;
self.write_bool(true)?;
self.write_i16(14)?;
self.write_f32(plr.spawn_rate_factor / 2f32)?;
self.write_bool(false)?;
}
if version >= 253 {
let byte = match plr.is_super_cart_enabled {
None => 0,
Some(false) => 2,
Some(true) => 3,
};
self.write_u8(byte)?;
}
if version >= 262 {
self.write_i32(plr.loadouts.selected_loadout_index)?;
self.write_loadout(&Loadout::default())?;
for loadout in plr.loadouts.loadouts.iter().skip(1) {
self.write_loadout(loadout)?;
}
}
Ok(())
}
}