use crate::{
MAGIC_NUMBER, SUPPORTED_VERSIONS,
banner::Banner,
entity::{Entity, EntityType},
error::{Error, OffsetType, Result},
remap, round_division_i64,
tile::{Liquid, LiquidType, Paint, Tile, TileForeground, TileShape, TileType, Wall, WallType},
world::{
AnglerQuest, BestiaryEntry, Chest, Difficulty, DisplayDollPose, Evil, Home, Invasion,
InvasionType, ItemDyeRow, LanternNight, LogicCheck, MoonPhase, MoonType, Party, Sandstorm,
Sign, TILE_IS_IMPORTANT, TILE_SIZE, TeamSpawns, TileEntity, TileEntityType, World,
},
};
use bounded_vector::{BoundedVec, bvec};
use byteorder::LittleEndian;
use byteorder::ReadBytesExt;
use cs_datetime_parse::DateTimeCs;
use cs_string_rw::ReadCsStrExt;
use grid::Grid;
use std::{
collections::{HashMap, HashSet},
io::{self, Read, Seek},
num::NonZeroU8,
};
use terra_items::{Item, Prefix, WorldItemSlot};
use terra_types::{InPixels, InTiles, PositiveI16, Rain, TerrariaFileType, Unit, Value, Vec2};
use time::{Duration, Time, ext::NumericalDuration};
use uuid::Uuid;
impl World {
pub fn read<R: Read + Seek>(reader: R) -> Result<World> {
WorldReader::new(reader).read_world()
}
pub fn try_read<R: Read + Seek>(&mut self, reader: R) -> Result<()> {
WorldReader::new(reader).try_read_world(self, false)
}
pub fn try_read_go_to_offsets<R: Read + Seek>(&mut self, reader: R) -> Result<()> {
WorldReader::new(reader).try_read_world(self, true)
}
}
pub struct WorldReader<R: Read + Seek> {
version: i32,
reader: R,
}
impl<R: Read + Seek> WorldReader<R> {
pub fn new(reader: R) -> WorldReader<R> {
WorldReader { version: 0, reader }
}
pub fn test_offset(&mut self, offsets_file: &[u64], idx: usize) -> Result<()> {
let Some(file_offset) = offsets_file.get(idx) else {
return Ok(());
};
let real_offset = self.reader.stream_position()?;
if real_offset == *file_offset {
return Ok(());
}
Err(Error::InvalidOffset {
offset_type: OffsetType::ALL[idx],
expected: *file_offset,
found: real_offset,
})
}
pub fn read_world(&mut self) -> Result<World> {
let mut wld = World::default();
self.read_first_header(&mut wld)?;
let offsets_file = self.read_offsets()?;
let tile_is_important = self.read_tile_is_important()?;
self.test_offset(&offsets_file, 0)?;
self.read_second_header(&mut wld)?;
self.test_offset(&offsets_file, 1)?;
self.read_tiles(&mut wld.tiles, &tile_is_important)?;
self.test_offset(&offsets_file, 2)?;
wld.chests = self.read_chests()?;
self.test_offset(&offsets_file, 3)?;
wld.signs = self.read_signs()?;
self.test_offset(&offsets_file, 4)?;
self.read_npcs_is_shimmer(&mut wld)?;
wld.entities = self.read_entities()?;
self.test_offset(&offsets_file, 5)?;
wld.tile_entities = self.read_tile_entities()?;
self.test_offset(&offsets_file, 6)?;
wld.weighted_pressure_plates = self.read_weighted_pressure_plates()?;
self.test_offset(&offsets_file, 7)?;
wld.homes = self.read_homes()?;
self.test_offset(&offsets_file, 8)?;
wld.bestiary = self.read_bestiary()?;
self.test_offset(&offsets_file, 9)?;
self.read_journey_powers(&mut wld)?;
self.test_offset(&offsets_file, 10)?;
self.test_footer(&wld)?;
Ok(wld)
}
pub fn try_read<T, E, E2: Into<E>>(
&mut self,
val: &mut T,
error: &mut std::result::Result<(), E>,
reader: impl FnOnce(&mut Self) -> std::result::Result<T, E2>,
) {
let res = reader(self);
match res {
Ok(v) => *val = v,
Err(e) if error.is_ok() => *error = Err(e.into()),
Err(_) => {}
}
}
fn get_position(&mut self) -> io::Result<u64> {
self.reader.stream_position()
}
fn seek_to(
&mut self,
error: &mut std::result::Result<(), impl From<io::Error>>,
is_seek_to_offsets: bool,
offset: Option<&u64>,
) {
if !is_seek_to_offsets {
return;
}
let Some(offset) = offset else {
return;
};
let res = self.reader.seek(io::SeekFrom::Start(*offset));
if error.is_ok() {
*error = res.map(|_| ()).map_err(|x| x.into())
}
}
pub fn try_read_world(&mut self, wld: &mut World, is_seek_to_offsets: bool) -> Result<()> {
let mut error = self.read_first_header(wld);
let mut offsets = [0; 11];
let mut offsets_file = vec![];
self.try_read(&mut offsets_file, &mut error, Self::read_offsets);
let mut tile_is_important = vec![];
self.try_read(
&mut tile_is_important,
&mut error,
Self::read_tile_is_important,
);
self.try_read(&mut offsets[0], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.first());
error = error.and(self.read_second_header(wld));
self.try_read(&mut offsets[1], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(1));
error = error.and(
self.read_tiles(&mut wld.tiles, &tile_is_important)
.map_err(|x| x.into()),
);
self.try_read(&mut offsets[2], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(2));
self.try_read(&mut wld.chests, &mut error, Self::read_chests);
self.try_read(&mut offsets[3], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(3));
self.try_read(&mut wld.signs, &mut error, Self::read_signs);
self.try_read(&mut offsets[4], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(4));
error = error.and(self.read_npcs_is_shimmer(wld).map_err(|x| x.into()));
self.try_read(&mut wld.entities, &mut error, Self::read_entities);
self.try_read(&mut offsets[5], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(5));
self.try_read(&mut wld.tile_entities, &mut error, Self::read_tile_entities);
self.try_read(&mut offsets[6], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(6));
self.try_read(
&mut wld.weighted_pressure_plates,
&mut error,
Self::read_weighted_pressure_plates,
);
self.try_read(&mut offsets[7], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(7));
self.try_read(&mut wld.homes, &mut error, Self::read_homes);
self.try_read(&mut offsets[8], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(8));
self.try_read(&mut wld.bestiary, &mut error, Self::read_bestiary);
self.try_read(&mut offsets[9], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(9));
error = error.and(self.read_journey_powers(wld).map_err(|x| x.into()));
self.try_read(&mut offsets[10], &mut error, Self::get_position);
self.seek_to(&mut error, is_seek_to_offsets, offsets_file.get(10));
error.and(self.test_footer(wld))?;
for ((offset_type, real_offset), offset_file) in
OffsetType::ALL.into_iter().zip(offsets).zip(offsets_file)
{
if real_offset != offset_file {
return Err(Error::InvalidOffset {
offset_type,
expected: offset_file,
found: real_offset,
});
}
}
Ok(())
}
fn read_tiles(&mut self, tiles: &mut Grid<Tile>, tile_is_important: &[bool]) -> io::Result<()> {
if self.version >= 88 {
self.read_tiles_new(tiles, tile_is_important)
} else {
self.read_tiles_old(tiles)
}
}
fn read_tile_texture_coords_old(
&mut self,
tile_id: u16,
) -> io::Result<Option<Vec2<InPixels, i16>>> {
if self.version < 72
&& let Some(idx) = [34, 35, 36, 170, 171, 172]
.iter()
.enumerate()
.find(|x| *x.1 == tile_id)
.map(|x| x.0)
{
return Ok(Some(Vec2::new(
self.read_i16()?,
self.read_i16()? + TILE_SIZE * 3 * idx as i16,
)));
}
if !TILE_IS_IMPORTANT.get(tile_id as usize).is_some_and(|x| *x) {
return Ok(None);
}
if self.version < 28 && tile_id == 4 {
return Ok(Some(Vec2::new(0, 0)));
}
if self.version < 40 && tile_id == 19 {
return Ok(Some(Vec2::new(0, 0)));
}
if self.version < 195 && tile_id == 49 {
return Ok(Some(Vec2::new(0, 0)));
}
Ok(Some(Vec2::new(self.read_i16()?, self.read_i16()?)))
}
fn read_tiles_old(&mut self, tiles: &mut Grid<Tile>) -> io::Result<()> {
for col_idx in 0..tiles.size().1 {
let mut col = tiles.iter_col_mut(col_idx);
while let Some(world_tile) = col.next() {
let mut tile_type = None;
let mut texture_coords = None;
let mut tile_paint_id = 0;
if self.read_bool()? {
let tile_id = if self.version >= 78 {
self.read_u16()?
} else {
self.read_u8()? as u16
};
texture_coords = self.read_tile_texture_coords_old(tile_id)?;
tile_type = if tile_id == 34
|| tile_id == 35 || tile_id == 36
|| tile_id == 170 || tile_id == 171
|| tile_id == 172
{
Some(TileType::Chandeliers)
} else {
TileType::from_id(tile_id)
};
if self.version >= 48 && self.read_bool()? {
tile_paint_id = self.read_u8()?;
}
}
if self.version <= 25 {
let _is_lighted = self.read_bool()?;
}
let mut wall_id = 0;
let mut wall_paint_id = 0;
if self.read_bool()? {
wall_id = self.read_u8()?;
if self.version >= 48 && self.read_bool()? {
wall_paint_id = self.read_u8()?;
}
}
let liquid = if self.read_bool()? {
let level = NonZeroU8::new(self.read_u8()?);
let mut liquid_type = LiquidType::Water;
if self.read_bool()? {
liquid_type = LiquidType::Lava;
}
if self.version >= 51 && self.read_bool()? {
liquid_type = LiquidType::Honey;
}
level.map(|x| Liquid {
liquid: liquid_type,
level: x,
})
} else {
None
};
let is_red_wire = self.version >= 33 && self.read_bool()?;
let is_blue_wire = self.version >= 43 && self.read_bool()?;
let is_green_wire = self.version >= 43 && self.read_bool()?;
let shape = match self.version {
..=40 => TileShape::Full,
41..=48 => {
if self.read_bool()? {
TileShape::Half
} else {
TileShape::Full
}
}
49.. => match (self.read_bool()?, self.read_u8()?) {
(true, _) => TileShape::Half,
(false, 0) | (false, 5..) => TileShape::Full,
(false, 1) => TileShape::SlopeDownRight,
(false, 2) => TileShape::SlopeDownLeft,
(false, 3) => TileShape::SlopeUpRight,
(false, 4) => TileShape::SlopeUpLeft,
},
};
let is_actuator = self.version >= 42 && self.read_bool()?;
let is_actuated = self.version >= 42 && self.read_bool()?;
let repeats = if self.version >= 25 {
self.read_i16()?
} else {
0
};
let foreground = tile_type.map(|tile| TileForeground {
tile,
shape,
paint: Paint::from_id(tile_paint_id),
is_invisible: false,
is_full_bright: false,
is_actuated,
texture_coords,
});
let wall = WallType::from_id(wall_id as u16).map(|wall| Wall {
wall,
paint: Paint::from_id(wall_paint_id),
is_invisible: false,
is_full_bright: false,
});
let tile = Tile {
tile: foreground,
wall,
liquid,
is_actuator,
is_blue_wire,
is_green_wire,
is_red_wire,
is_yellow_wire: false,
};
*world_tile = tile;
for _ in 0..repeats {
let Some(world_tile) = col.next() else {
break;
};
*world_tile = tile;
}
}
}
Ok(())
}
fn read_tiles_new(
&mut self,
tiles: &mut Grid<Tile>,
tile_is_important: &[bool],
) -> io::Result<()> {
let is_flag4_enabled = self.version > 194;
for col_idx in 0..tiles.size().1 {
let mut col = tiles.iter_col_mut(col_idx);
while let Some(world_tile) = col.next() {
let flag = self.read_u8()?;
let flag2 = if flag & 1 == 1 { self.read_u8()? } else { 0 };
let flag3 = if flag2 & 1 == 1 { self.read_u8()? } else { 0 };
let flag4 = if is_flag4_enabled && flag3 & 1 == 1 {
self.read_u8()?
} else {
0
};
let mut tile_type = None;
let mut texture_coords = None;
let mut tile_paint_id = 0;
let mut is_tile_full_bright = (flag4 & 0x8) == 0x8;
if flag & 0x2 == 0x2 {
let tile_id = if flag & 0x20 == 0x20 {
self.read_u16()?
} else {
self.read_u8()? as u16
};
tile_type = TileType::from_id(tile_id);
if tile_is_important.get(tile_id as usize).is_some_and(|x| *x) {
texture_coords = Some(Vec2::new(self.read_i16()?, self.read_i16()?));
}
if flag3 & 0x8 == 0x8 {
tile_paint_id = self.read_u8()?;
is_tile_full_bright = self.version < 258 && tile_paint_id == 31;
}
}
let mut wall_id = 0;
let mut wall_paint_id = 0;
let mut is_wall_full_bright = (flag4 & 0x10) == 0x10;
if flag & 0x4 == 0x4 {
wall_id = self.read_u8()? as u16;
if flag3 & 0x10 == 0x10 {
wall_paint_id = self.read_u8()?;
is_wall_full_bright = self.version < 258 && wall_paint_id == 31;
}
}
let liquid_id = (flag & 0x18) >> 3;
let liquid = if liquid_id != 0 {
let liquid = NonZeroU8::new(self.read_u8()?);
let liquid_type = match liquid_id {
_ if flag3 & 0x80 == 0x80 => LiquidType::Shimmer,
1 => LiquidType::Water,
2 => LiquidType::Lava,
3 => LiquidType::Honey,
_ => unreachable!(),
};
liquid.map(|x| Liquid {
liquid: liquid_type,
level: x,
})
} else {
None
};
if flag3 & 0x40 == 0x40 {
wall_id |= (self.read_u8()? as u16) << 8;
}
let repeats = match (flag & 0xC0) >> 6 {
0 => 0,
1 => self.read_u8()? as i16,
_ => self.read_i16()?,
};
let foreground = tile_type.map(|tile_type| TileForeground {
tile: tile_type,
paint: Paint::from_id(tile_paint_id),
texture_coords,
is_invisible: (flag4 & 0x2) == 0x2,
is_full_bright: is_tile_full_bright,
is_actuated: (flag3 & 0x4) == 0x4,
shape: match (flag2 & 0x70) >> 4 {
0 | 6 | 7 => TileShape::Full,
1 => TileShape::Half,
2 => TileShape::SlopeDownRight,
3 => TileShape::SlopeDownLeft,
4 => TileShape::SlopeUpRight,
5 => TileShape::SlopeUpLeft,
_ => unreachable!(),
},
});
let wall = WallType::from_id(wall_id).map(|wall| Wall {
wall,
paint: Paint::from_id(wall_paint_id),
is_invisible: (flag4 & 0x4) == 0x4,
is_full_bright: is_wall_full_bright,
});
let tile = Tile {
tile: foreground,
wall,
liquid,
is_red_wire: (flag2 & 0x2) == 0x2,
is_blue_wire: (flag2 & 0x4) == 0x4,
is_green_wire: (flag2 & 0x8) == 0x8,
is_yellow_wire: (flag3 & 0x20) == 0x20,
is_actuator: (flag3 & 0x2) == 0x2,
};
*world_tile = tile;
for _ in 0..repeats {
let Some(world_tile) = col.next() else {
break;
};
*world_tile = tile;
}
}
}
Ok(())
}
fn read_tile_entities(&mut self) -> Result<Vec<TileEntity>> {
if self.version < 116 {
return Ok(vec![]);
}
if self.version < 122 {
for _ in 0..self.read_i32()? {
self.read_i32()?;
}
return Ok(vec![]);
}
let count = self.read_i32()?;
let mut entities = Vec::with_capacity(count.clamp(0, 8000) as usize);
for _ in 0..count {
entities.push(self.read_tile_entity()?)
}
Ok(entities)
}
fn read_tile_entity(&mut self) -> Result<TileEntity> {
let type_id = self.read_u8()?;
let _tile_entity_id = self.read_i32()?;
let pos = Vec2::new(self.read_i16()?, self.read_i16()?);
let entity_type = match type_id {
0 => TileEntityType::TrainingDummy {
entity_idx: self.read_i16()?,
},
1 => TileEntityType::ItemFrame(self.read_item()?),
2 => TileEntityType::LogicSensor {
check: LogicCheck::from_id(self.read_u8()?),
is_on: self.read_bool()?,
},
3 => self.read_display_doll()?,
4 => TileEntityType::WeaponRack(self.read_item()?),
5 => {
let mut flags = self.read_u8()?;
let mut items = [None; 4];
for item in items.iter_mut() {
if flags & 1 == 1 {
*item = self.read_item()?;
}
flags >>= 1;
}
TileEntityType::HatRack {
upper: ItemDyeRow {
item: items[0],
dye: items[2],
},
lower: ItemDyeRow {
item: items[1],
dye: items[3],
},
}
}
6 => TileEntityType::FoodPlatter(self.read_item()?),
7 => TileEntityType::Pylon,
8 => TileEntityType::DisplayJar(self.read_item()?),
9 => TileEntityType::KiteAnchor(Item::from_id(self.read_i16()? as i32)),
10 => TileEntityType::CritterAnchor(Item::from_id(self.read_i16()? as i32)),
_ => return Err(Error::UnknownTileEntity { id: type_id }),
};
Ok(TileEntity {
entity: entity_type,
position: pos,
})
}
fn read_display_doll(&mut self) -> io::Result<TileEntityType> {
let mut equipable = [None; 9];
let mut dyes = [None; 9];
let mut equipable_flags = self.read_u8()? as u16;
let mut dye_flags = self.read_u8()? as u16;
let pose_id = if self.version >= 307 {
self.read_u8()?
} else {
0
};
let flags = if self.version >= 308 {
self.read_u8()?
} else {
0
};
if self.version > 311 {
equipable_flags |= ((flags & 0x02 == 0x02) as u16) << 8;
}
dye_flags |= ((flags & 0x04 == 0x04) as u16) << 8;
for item in equipable.iter_mut() {
if equipable_flags & 1 == 1 {
*item = self.read_item()?;
}
equipable_flags >>= 1;
}
for item in dyes.iter_mut() {
if dye_flags & 1 == 1 {
*item = self.read_item()?;
}
dye_flags >>= 1;
}
let hand = if flags & 1 == 1 {
self.read_item()?
} else {
None
};
if self.version == 311 && flags & 0x02 == 0x02 {
equipable[8] = self.read_item()?;
}
let display_doll = TileEntityType::DisplayDoll {
pose: DisplayDollPose::from_id(pose_id).unwrap_or_default(),
helmet: ItemDyeRow {
item: equipable[0],
dye: dyes[0],
},
breastplate: ItemDyeRow {
item: equipable[1],
dye: dyes[1],
},
pants: ItemDyeRow {
item: equipable[2],
dye: dyes[2],
},
accessories: [
ItemDyeRow {
item: equipable[3],
dye: dyes[3],
},
ItemDyeRow {
item: equipable[4],
dye: dyes[4],
},
ItemDyeRow {
item: equipable[5],
dye: dyes[5],
},
ItemDyeRow {
item: equipable[6],
dye: dyes[6],
},
ItemDyeRow {
item: equipable[7],
dye: dyes[7],
},
],
mount: ItemDyeRow {
item: equipable[8],
dye: dyes[8],
},
hand,
};
Ok(display_doll)
}
fn read_item(&mut self) -> io::Result<Option<WorldItemSlot>> {
let item_id = self.read_i16()?;
let prefix = self.read_u8()?;
let stack = self.read_i16()?;
let Some(item) = Item::from_id(item_id as i32) else {
return Ok(None);
};
let Some(stack) = PositiveI16::new(stack) else {
return Ok(None);
};
Ok(Some(WorldItemSlot {
item,
prefix: Prefix::from_id(prefix),
count: stack,
}))
}
fn read_journey_powers(&mut self, wld: &mut World) -> io::Result<()> {
if self.version < 220 {
return Ok(());
}
while self.read_bool()? {
match self.read_u16()? {
0 => wld.is_time_frozen = self.read_bool()?,
8 => wld.time_rate_power = remap(self.read_f32()?, 0.0..=1.0, 1.0..=24.0),
9 => wld.is_rain_frozen = self.read_bool()?,
10 => wld.is_wind_frozen = self.read_bool()?,
12 => {
let d = self.read_f32()?;
if d <= 0.33 {
wld.difficulty_power = remap(d, 0.0..=0.33, 0.5..=1.0)
} else {
wld.difficulty_power = remap(d, 0.33..=1.0, 1.0..=3.0)
}
}
13 => wld.is_biome_spread_disabled = self.read_bool()?,
d => {
panic!("{d}")
}
}
}
Ok(())
}
fn test_footer(&mut self, wld: &World) -> Result<()> {
if self.version < 7 {
return Ok(());
}
if !self.read_bool()? {
return Err(Error::InvalidFooter);
}
if self.read_string()? != wld.name {
return Err(Error::InvalidFooter);
}
if self.read_i32()? != wld.id {
return Err(Error::InvalidFooter);
}
Ok(())
}
fn read_bestiary(&mut self) -> io::Result<HashMap<EntityType, BestiaryEntry>> {
if self.version < 210 {
return Ok(HashMap::new());
}
let mut bestiary = HashMap::new();
for _ in 0..self.read_i32()? {
let id = self.read_string()?;
let count = self.read_i32()?;
if let Some(entity) = EntityType::from_bestiary_id(&id) {
bestiary.entry(entity).or_insert(BestiaryEntry {
killed_count: count,
..Default::default()
});
}
}
for _ in 0..self.read_i32()? {
if let Some(entity) = EntityType::from_bestiary_id(&self.read_string()?) {
bestiary
.entry(entity)
.and_modify(|x| x.is_seen = true)
.or_insert(BestiaryEntry {
is_seen: true,
..Default::default()
});
}
}
for _ in 0..self.read_i32()? {
if let Some(entity) = EntityType::from_bestiary_id(&self.read_string()?) {
bestiary
.entry(entity)
.and_modify(|x| x.is_chatted_with = true)
.or_insert(BestiaryEntry {
is_chatted_with: true,
..Default::default()
});
}
}
Ok(bestiary)
}
fn read_homes(&mut self) -> io::Result<Vec<Home>> {
if self.version < 189 {
return Ok(vec![]);
}
let home_count = self.read_i32()?;
let mut homes = Vec::with_capacity(home_count.clamp(0, 8000) as usize);
for _ in 0..home_count {
let entity_id = self.read_i32()?;
let pos = Vec2::new(self.read_i32()?, self.read_i32()?);
if let Some(entity) = EntityType::from_id(entity_id) {
homes.push(Home {
resident: entity,
position: pos,
});
}
}
Ok(homes)
}
fn read_weighted_pressure_plates(&mut self) -> io::Result<HashSet<Vec2<InTiles, i32>>> {
let mut plates = HashSet::new();
if self.version < 170 {
return Ok(plates);
}
for _ in 0..self.read_i32()? {
plates.insert(self.read_vec2_i32()?);
}
Ok(plates)
}
fn read_chests(&mut self) -> io::Result<BoundedVec<Chest, 0, 8000>> {
if self.version >= 88 {
self.read_chests_new_version()
} else {
self.read_chests_old_version().map(|x| x.into_bounded_vec())
}
}
fn read_chests_new_version(&mut self) -> io::Result<BoundedVec<Chest, 0, 8000>> {
let chest_count = self.read_i16()?.max(0) as usize;
let mut chests = BoundedVec::with_capacity(chest_count.min(8000));
let mut slot_count = if self.version < 294 {
self.read_i16()?.max(0) as usize
} else {
0
};
for _ in 0..chest_count {
let position = Vec2::new(self.read_i32()?, self.read_i32()?);
let name = self.read_string()?;
if self.version >= 294 {
slot_count = self.read_i32()?.max(0) as usize;
}
let mut slots = Vec::with_capacity(slot_count.min(100));
for _ in 0..slot_count {
let count = self.read_i16()?;
if count == 0 {
continue;
}
let item_id = self.read_i32()?;
let prefix = Prefix::from_id(self.read_u8()?);
let Some(item) = Item::from_id(item_id) else {
continue;
};
slots.push(Some(WorldItemSlot {
item,
prefix,
count: PositiveI16::new(count.max(1)).expect("In range"),
}));
}
_ = chests.push(Chest {
name,
position,
slots,
});
}
Ok(chests)
}
fn read_chests_old_version(&mut self) -> io::Result<BoundedVec<Chest, 0, 1000>> {
let mut chests = bvec![];
let slot_count = if self.version >= 58 { 40 } else { 20 };
for _ in 0..1000 {
if !self.read_bool()? {
continue;
}
let pos = Vec2::new(self.read_i32()?, self.read_i32()?);
let name = if self.version >= 85 {
self.read_string()?
} else {
"".to_owned()
};
let mut slots = vec![None; 40];
for slot in slots.iter_mut().take(slot_count) {
let count = if self.version >= 59 {
self.read_i16()?
} else {
self.read_u8()? as i16
};
let Some(count) = PositiveI16::new(count) else {
continue;
};
let item = if self.version >= 38 {
Item::from_id(self.read_i32()?)
} else {
Item::from_old_id(&self.read_string()?, self.version)
};
let prefix = if self.version >= 36 {
Prefix::from_id(self.read_u8()?)
} else {
None
};
if let Some(item) = item {
*slot = Some(WorldItemSlot {
item,
prefix,
count,
})
}
}
chests
.push(Chest {
name,
position: pos,
slots,
})
.expect("In range")
}
Ok(chests)
}
fn read_npcs_is_shimmer(&mut self, wld: &mut World) -> io::Result<()> {
if self.version < 268 {
return Ok(());
}
for _ in 0..self.read_i32()? {
let Some(entity_type) = EntityType::from_id(self.read_i32()?) else {
continue;
};
match entity_type {
EntityType::Merchant => wld.is_merchant_shimmered = true,
EntityType::Nurse => wld.is_nurse_shimmered = true,
EntityType::ArmsDealer => wld.is_arms_dealer_shimmered = true,
EntityType::Dryad => wld.is_dryad_shimmered = true,
EntityType::Guide => wld.is_guide_shimmered = true,
EntityType::OldMan => wld.is_old_man_shimmered = true,
EntityType::Demolitionist => wld.is_demolitionist_shimmered = true,
EntityType::Clothier => wld.is_clothier_shimmered = true,
EntityType::GoblinTinkerer => wld.is_goblin_tinkerer_shimmered = true,
EntityType::Wizard => wld.is_wizard_shimmered = true,
EntityType::Mechanic => wld.is_mechanic_shimmered = true,
EntityType::SantaClaus => wld.is_santa_claus_shimmered = true,
EntityType::Truffle => wld.is_truffle_shimmered = true,
EntityType::Steampunker => wld.is_steampunker_shimmered = true,
EntityType::DyeTrader => wld.is_dye_trader_shimmered = true,
EntityType::PartyGirl => wld.is_party_girl_shimmered = true,
EntityType::Cyborg => wld.is_cyborg_shimmered = true,
EntityType::Painter => wld.is_painter_shimmered = true,
EntityType::WitchDoctor => wld.is_witch_doctor_shimmered = true,
EntityType::Pirate => wld.is_pirate_shimmered = true,
EntityType::Stylist => wld.is_stylist_shimmered = true,
EntityType::TravellingMerchant => wld.is_travelling_merchant_shimmered = true,
EntityType::Angler => wld.is_angler_shimmered = true,
EntityType::TaxCollector => wld.is_tax_collector_shimmered = true,
EntityType::Dd2Bartender => wld.is_bartender_shimmered = true,
EntityType::Golfer => wld.is_golfer_shimmered = true,
EntityType::BestiaryGirl => wld.is_zoologist_shimmered = true,
EntityType::TownCat => wld.is_town_cat_shimmered = true,
EntityType::TownDog => wld.is_town_dog_shimmered = true,
EntityType::TownBunny => wld.is_town_bunny_shimmered = true,
EntityType::Princess => wld.is_princess_shimmered = true,
EntityType::TownSlimeBlue => wld.is_town_slime_blue_shimmered = true,
EntityType::TownSlimeGreen => wld.is_town_slime_green_shimmered = true,
EntityType::TownSlimeOld => wld.is_town_slime_old_shimmered = true,
EntityType::TownSlimePurple => wld.is_town_slime_purple_shimmered = true,
EntityType::TownSlimeRainbow => wld.is_town_slime_rainbow_shimmered = true,
EntityType::TownSlimeRed => wld.is_town_slime_red_shimmered = true,
EntityType::TownSlimeYellow => wld.is_town_slime_yellow_shimmered = true,
EntityType::TownSlimeCopper => wld.is_town_slime_copper_shimmered = true,
EntityType::SkeletonMerchant => wld.is_skeleton_merchant_shimmered = true,
_ => {}
}
}
Ok(())
}
fn read_entity_type(&mut self) -> io::Result<Option<EntityType>> {
Ok(if self.version >= 190 {
EntityType::from_id(self.read_i32()?)
} else {
EntityType::from_old_id(&self.read_string()?)
})
}
fn read_entities(&mut self) -> io::Result<BoundedVec<Entity, 0, 201>> {
let mut entities = bvec![];
while self.read_bool()? {
let entity_type = self.read_entity_type()?;
let name = if self.version >= 83 {
self.read_string()?
} else {
"".to_owned()
};
let pos = Vec2::new(self.read_f32()?, self.read_f32()?);
let is_home = !self.read_bool()?;
let home = Vec2::new(self.read_i32()?, self.read_i32()?);
let is_shimmered = if self.version >= 213 && self.read_u8()? & 0x1 == 0x1 {
self.read_i32()? != 0
} else {
false
};
let is_homeless_despawn = if self.version >= 315 {
self.read_bool()?
} else {
false
};
let Some(entity_type) = entity_type else {
continue;
};
let _ = entities.push(Entity::Npc {
entity_type,
name,
position: pos,
home: if is_home { Some(home) } else { None },
is_shimmered,
is_homeless_despawn,
});
}
if self.version >= 140 {
while self.read_bool()? {
let Some(entity_type) = self.read_entity_type()? else {
continue;
};
let _ = entities.push(Entity::NonNpc {
entity_type,
position: Vec2::new(self.read_f32()?, self.read_f32()?),
});
}
}
if !(31..=83).contains(&self.version) {
return Ok(entities);
}
let mut names = HashMap::new();
names.insert(EntityType::Merchant, self.read_string()?);
names.insert(EntityType::Nurse, self.read_string()?);
names.insert(EntityType::ArmsDealer, self.read_string()?);
names.insert(EntityType::Dryad, self.read_string()?);
names.insert(EntityType::Guide, self.read_string()?);
names.insert(EntityType::Clothier, self.read_string()?);
names.insert(EntityType::Demolitionist, self.read_string()?);
names.insert(EntityType::GoblinTinkerer, self.read_string()?);
names.insert(EntityType::Wizard, self.read_string()?);
if self.version >= 35 {
names.insert(EntityType::Mechanic, self.read_string()?);
}
if self.version >= 65 {
names.insert(EntityType::Truffle, self.read_string()?);
names.insert(EntityType::Steampunker, self.read_string()?);
names.insert(EntityType::DyeTrader, self.read_string()?);
names.insert(EntityType::PartyGirl, self.read_string()?);
names.insert(EntityType::Cyborg, self.read_string()?);
names.insert(EntityType::Painter, self.read_string()?);
names.insert(EntityType::WitchDoctor, self.read_string()?);
names.insert(EntityType::Pirate, self.read_string()?);
}
if self.version >= 79 {
names.insert(EntityType::Stylist, self.read_string()?);
}
for entity in entities.iter_mut() {
let Entity::Npc {
entity_type,
name: npc_name,
..
} = entity
else {
continue;
};
if let Some(name) = names.get(entity_type) {
*npc_name = name.clone();
}
}
Ok(entities)
}
fn read_signs(&mut self) -> io::Result<BoundedVec<Sign, 0, 32000>> {
if self.version >= 88 {
self.read_signs_new_version()
} else {
self.read_signs_old_version().map(|x| x.into_bounded_vec())
}
}
fn read_signs_new_version(&mut self) -> io::Result<BoundedVec<Sign, 0, 32000>> {
let sign_count = self.read_i16()?;
let mut signs = BoundedVec::with_capacity(sign_count.clamp(0, 1000) as usize);
for _ in 0..sign_count {
let sign = Sign {
text: self.read_string()?,
position: Vec2::new(self.read_i32()?, self.read_i32()?),
};
let _ = signs.push(sign);
}
Ok(signs)
}
fn read_signs_old_version(&mut self) -> io::Result<BoundedVec<Sign, 0, 1000>> {
let mut signs = bvec![];
for _ in 0..1000 {
if !self.read_bool()? {
continue;
}
let sign = Sign {
text: self.read_string()?,
position: Vec2::new(self.read_i32()?, self.read_i32()?),
};
let _ = signs.push(sign);
}
Ok(signs)
}
fn read_first_header(&mut self, wld: &mut World) -> Result<()> {
self.version = self.read_i32()?;
if !SUPPORTED_VERSIONS.contains(&self.version) {
return Err(Error::UnsupportedVersion(self.version));
}
if self.version >= 135 {
self.verify_terraria_file()?;
let file_type = self.read_terraria_file_type()?;
if file_type != TerrariaFileType::World {
return Err(Error::InvalidFileType(file_type));
}
wld.times_saved = self.read_u32()?;
wld.is_favorite = self.read_u64()? & 1 == 1;
}
Ok(())
}
fn read_offsets(&mut self) -> io::Result<Vec<u64>> {
if self.version < 88 {
return Ok(Vec::new());
}
let offset_count = self.read_i16()?.max(0) as usize;
let mut offsets = Vec::with_capacity(offset_count.min(20));
for _ in 0..offset_count {
let offset = self.read_i32()?.max(0) as u64;
offsets.push(offset);
}
Ok(offsets)
}
fn read_tile_is_important(&mut self) -> io::Result<Vec<bool>> {
if self.version < 88 {
return Ok(Vec::new());
}
let tiles_id_count = self.read_i16()?.max(0) as usize;
let mut tile_is_important =
Vec::with_capacity(tiles_id_count.min(TileType::MAX_ID as usize));
let mut x = 0;
for i in 0..tiles_id_count {
if i % 8 == 0 {
x = self.read_u8()?;
}
tile_is_important.push(x & 1 == 1);
x >>= 1;
}
Ok(tile_is_important)
}
fn read_second_header(&mut self, wld: &mut World) -> Result<()> {
wld.name = self.read_string()?;
if self.version >= 179 {
wld.seed = Some(if self.version == 179 {
self.read_i32()?.to_string()
} else {
self.read_string()?
});
wld.generator_version = Some(self.read_u64()?);
}
if self.version >= 181 {
let mut bytes = [0; 16];
self.reader.read_exact(&mut bytes)?;
wld.uuid = Some(Uuid::from_bytes_le(bytes));
}
wld.id = self.read_i32()?;
wld.left_border = self.read_i32()?.into();
wld.right_border = self.read_i32()?.into();
wld.top_border = self.read_i32()?.into();
wld.bottom_border = self.read_i32()?.into();
let size_y = self.read_i32()?.max(0);
let size_x = self.read_i32()?.max(0);
wld.tiles = Grid::init_with_order(
size_y as usize,
size_x as usize,
grid::Order::ColumnMajor,
Tile::default(),
);
wld.difficulty = match self.version {
209.. => Difficulty::from_id(self.read_i32()?).unwrap_or_default(),
208 => match (self.read_bool()?, self.read_bool()?) {
(false, false) => Difficulty::Normal,
(true, false) => Difficulty::Expert,
(_, true) => Difficulty::Master,
},
112..208 => {
if self.read_bool()? {
Difficulty::Expert
} else {
Difficulty::Normal
}
}
0..112 => Difficulty::Normal,
_ => Difficulty::default(),
};
if self.version >= 222 {
wld.is_drunk_world = self.read_bool()?;
}
if self.version >= 227 {
wld.is_for_the_worthy_world = self.read_bool()?;
}
if self.version >= 238 {
wld.is_tenth_anniversary_world = self.read_bool()?;
}
if self.version >= 239 {
wld.is_dont_starve_world = self.read_bool()?;
}
if self.version >= 241 {
wld.is_not_the_bees_world = self.read_bool()?;
}
if self.version >= 249 {
wld.is_remix_world = self.read_bool()?;
}
if self.version >= 266 {
wld.is_no_traps_world = self.read_bool()?;
}
if self.version >= 267 {
wld.is_zenith_world = self.read_bool()?;
} else {
wld.is_zenith_world = wld.is_remix_world && wld.is_drunk_world;
}
if self.version >= 302 {
wld.is_skyblock_world = self.read_bool()?;
}
if self.version >= 141 {
wld.creation_time =
Some(DateTimeCs::from_binary(self.read_i64()?).map_err(Error::CreationTimeRead)?);
}
if self.version >= 284 {
wld.last_played =
Some(DateTimeCs::from_binary(self.read_i64()?).map_err(Error::LastPlayedRead)?);
}
if self.version >= 63 {
wld.moon_type = MoonType::from_id(self.read_u8()?).unwrap_or_default();
}
if self.version >= 44 {
for i in 0..3 {
wld.tree_style_changes[i] = self.read_i32()?.into();
}
for i in 0..4 {
wld.tree_styles[i] = self.read_i32()?;
}
}
if self.version >= 60 {
for i in 0..3 {
wld.cave_back_style_changes[i] = self.read_i32()?.into();
}
for i in 0..4 {
wld.cave_back_styles[i] = self.read_i32()?;
}
wld.ice_cave_background = self.read_i32()?;
}
if self.version >= 61 {
wld.jungle_cave_background = self.read_i32()?;
wld.hell_background = self.read_i32()?;
}
wld.spawn = self.read_vec2_i32()?;
wld.surface_level = self.read_f64()?.into();
wld.cavern_level = self.read_f64()?.into();
let (time, moon_phase) = self.read_in_game_time_and_moon_phase()?;
wld.time = time;
wld.moon_phase = moon_phase;
wld.is_blood_moon = self.read_bool()?;
if self.version >= 70 {
wld.is_solar_eclipse = self.read_bool()?;
}
wld.dungeon = self.read_vec2_i32()?;
if self.version >= 56 {
wld.evil = if self.read_bool()? {
Evil::Crimson
} else {
Evil::Corruption
};
}
wld.is_eye_of_cthulhu_downed = self.read_bool()?;
wld.is_eater_of_worlds_or_brain_of_cthulhu_downed = self.read_bool()?;
wld.is_skeleton_downed = self.read_bool()?;
if self.version >= 66 {
wld.is_queen_bee_downed = self.read_bool()?;
}
if self.version >= 44 {
wld.is_destroyer_downed = self.read_bool()?;
wld.is_twins_downed = self.read_bool()?;
wld.is_skeletron_prime_downed = self.read_bool()?;
wld.is_any_mechanical_boss_downed = self.read_bool()?;
}
if self.version >= 64 {
wld.is_plantera_downed = self.read_bool()?;
wld.is_golem_downed = self.read_bool()?;
}
if self.version >= 118 {
wld.is_king_slime_downed = self.read_bool()?;
}
if self.version >= 29 {
wld.is_goblin_tinkerer_saved = self.read_bool()?;
wld.is_wizard_saved = self.read_bool()?;
}
if self.version >= 34 {
wld.is_mechanic_saved = self.read_bool()?;
}
if self.version >= 80 && self.version <= 87 {
wld.is_stylist_saved = self.read_bool()?;
}
if self.version >= 29 {
wld.is_goblin_army_downed = self.read_bool()?;
}
if self.version >= 32 {
wld.is_clown_downed = self.read_bool()?;
}
if self.version >= 37 {
wld.is_frost_legion_downed = self.read_bool()?;
}
if self.version >= 56 {
wld.is_pirate_invasion_downed = self.read_bool()?;
}
wld.is_shadow_orb_or_crimson_heart_destroyed = self.read_bool()?;
wld.is_meteor_to_spawn = self.read_bool()?;
wld.destroyed_shadow_orb_or_crimson_heart_count = self.read_u8()?;
if self.version >= 23 {
wld.destroyed_altar_count = self.read_i32()?;
wld.is_hardmode = self.read_bool()?;
}
if self.version >= 257 {
wld.is_after_party_of_doom = self.read_bool()?;
}
wld.invasion_cooldown = self.read_i32()?.into();
wld.invasion = self.read_invasion()?;
if self.version >= 118 {
wld.slime_rain_time = self.read_duration_terra_ticks_f64()?;
}
if self.version >= 113 {
wld.sundial_cooldown = Value::new(self.read_u8()?);
}
if self.version >= 53 {
wld.rain = self.read_rain()?;
}
if self.version >= 54 {
wld.tier_1_hardmode_ore =
TileType::from_id(self.read_i32()?.clamp(0, u16::MAX as i32) as u16);
wld.tier_2_hardmode_ore =
TileType::from_id(self.read_i32()?.clamp(0, u16::MAX as i32) as u16);
wld.tier_3_hardmode_ore =
TileType::from_id(self.read_i32()?.clamp(0, u16::MAX as i32) as u16);
} else if self.version >= 23 || wld.destroyed_altar_count != 0 {
wld.tier_1_hardmode_ore = Some(TileType::Cobalt);
wld.tier_2_hardmode_ore = Some(TileType::Mythril);
wld.tier_3_hardmode_ore = Some(TileType::Adamantite);
}
if self.version >= 55 {
wld.forest_background = self.read_u8()?;
wld.corruption_background = self.read_u8()?;
wld.jungle_background = self.read_u8()?;
}
if self.version >= 60 {
wld.snow_background = self.read_u8()?;
wld.hallow_background = self.read_u8()?;
wld.crimson_background = self.read_u8()?;
wld.desert_background = self.read_u8()?;
wld.ocean_background = self.read_u8()?;
}
if self.version >= 60 {
wld.cloud_bg_active = self.read_i32()?;
}
if self.version >= 62 {
wld.clouds_count = self.read_i16()?;
wld.wind = (self.read_f32()? * 50.0).into();
}
if self.version >= 95 {
let count = self.read_i32()?.max(0) as usize;
let mut arr = Vec::with_capacity(count.min(100));
for _ in 0..count {
arr.push(self.read_string()?);
}
wld.players_finished_angler_quest_today = arr;
}
if self.version >= 99 {
wld.is_angler_saved = self.read_bool()?;
}
if self.version >= 101 {
wld.angler_quest =
AnglerQuest::from_id(self.read_i32()?).unwrap_or(AnglerQuest::Batfish);
}
if self.version >= 104 {
wld.is_stylist_saved = self.read_bool()?;
}
if self.version >= 129 {
wld.is_tax_collector_saved = self.read_bool()?;
}
if self.version >= 201 {
wld.is_golfer_saved = self.read_bool()?;
}
if self.version >= 107 {
let invasion_size_start = self.read_i32()?;
if let Some(invasion) = &mut wld.invasion {
invasion.start_size = invasion_size_start;
}
}
if self.version >= 108 {
wld.cultist_delay = self.read_duration_terra_ticks_i32()?;
}
if self.version >= 109 {
for i in 0..self.read_i16()? {
let count = self.read_i32()?;
let Some(entity_type) = Banner::from_id(i) else {
continue;
};
wld.kill_count.insert(entity_type, count);
}
}
if self.version >= 289 {
for i in 0..self.read_i16()? {
let count = self.read_u16()?;
let Some(entity_type) = Banner::from_id(i) else {
continue;
};
wld.claimable_banners.insert(entity_type, count);
}
}
if self.version >= 128 {
wld.is_sundial_forward_time_active = self.read_bool()?;
}
if self.version >= 131 {
wld.is_fishron_downed = self.read_bool()?;
wld.is_martian_madness_downed = self.read_bool()?;
wld.is_ancient_cultist_downed = self.read_bool()?;
wld.is_moonlord_downed = self.read_bool()?;
wld.is_pumpking_downed = self.read_bool()?;
wld.is_mourning_wood_downed = self.read_bool()?;
wld.is_ice_queen_downed = self.read_bool()?;
wld.is_santa_nk1_downed = self.read_bool()?;
wld.is_everscream_downed = self.read_bool()?;
}
if self.version >= 140 {
wld.is_solar_pillar_downed = self.read_bool()?;
wld.is_vortex_pillar_downed = self.read_bool()?;
wld.is_nebula_pillar_downed = self.read_bool()?;
wld.is_stardust_pillar_downed = self.read_bool()?;
wld.is_solar_pillar_active = self.read_bool()?;
wld.is_vortex_pillar_active = self.read_bool()?;
wld.is_nebula_pillar_active = self.read_bool()?;
wld.is_stardust_pillar_active = self.read_bool()?;
wld.is_lunar_apocalypse = self.read_bool()?;
}
if self.version >= 170 {
let is_party_mannual = self.read_bool()?;
let is_party_not_mannual = self.read_bool()?;
wld.party_cooldown = self.read_duration_terra_ticks_i32()?;
let celebrating_nps_count = self.read_i32()?.max(0);
if is_party_mannual || is_party_not_mannual {
let mut celebrating_npcs =
Vec::with_capacity(celebrating_nps_count.min(30) as usize);
for _ in 0..celebrating_nps_count {
let Some(entity) = EntityType::from_id(self.read_i32()?) else {
continue;
};
celebrating_npcs.push(entity);
}
wld.party = Some(Party {
is_mannual: is_party_mannual,
celebrating_npcs,
})
} else {
io::copy(
&mut (&mut self.reader).take(celebrating_nps_count as u64 * 4),
&mut io::sink(),
)?;
}
}
if self.version >= 174 {
let is_sandstorm = self.read_bool()?;
let time = self.read_duration_terra_ticks_i32()?;
let current_severity = self.read_f32()?;
let intended_severity = self.read_f32()?;
if is_sandstorm {
wld.sandstorm = Some(Sandstorm {
time,
current_severity,
intended_severity,
})
}
}
if self.version >= 178 {
wld.is_bartender_saved = self.read_bool()?;
wld.is_dd2_t1_downed = self.read_bool()?;
wld.is_dd2_t2_downed = self.read_bool()?;
wld.is_dd2_t3_downed = self.read_bool()?;
}
if self.version >= 195 {
wld.mushroom_background = self.read_u8()?;
}
if self.version >= 215 {
wld.underworld_background = self.read_u8()?;
}
if self.version >= 196 {
self.reader.read_exact(&mut wld.tree_backgrounds)?;
}
if self.version >= 204 {
wld.is_combat_book_used = self.read_bool()?;
}
if self.version >= 207 {
wld.lantern_night_cooldown = self.read_i32()?.into();
let is_lantern_night_not_mannual = self.read_bool()?;
let is_lantern_night_mannual = self.read_bool()?;
if is_lantern_night_mannual || is_lantern_night_not_mannual {
wld.lantern_night = Some(LanternNight {
is_mannual: is_lantern_night_mannual,
})
}
wld.is_next_night_lantern_night = self.read_bool()?;
}
if self.version >= 211 {
let mut tree_tops_forest = wld.tree_tops_forest.iter_mut();
let mut tree_tops = [
tree_tops_forest.next().expect("Some"),
tree_tops_forest.next().expect("Some"),
tree_tops_forest.next().expect("Some"),
tree_tops_forest.next().expect("Some"),
&mut wld.tree_tops_corruption,
&mut wld.tree_tops_jungle,
&mut wld.tree_tops_snow,
&mut wld.tree_tops_hallow,
&mut wld.tree_tops_crimson,
&mut wld.tree_tops_desert,
&mut wld.tree_tops_ocean,
&mut wld.tree_tops_glowing_mushroom,
&mut wld.tree_tops_underworld,
];
let tree_tops_count = self.read_i32()?.max(0) as usize;
for i in 0..tree_tops_count {
let v = self.read_i32()?;
let Some(tree_tops) = tree_tops.get_mut(i) else {
continue;
};
**tree_tops = v;
}
}
if self.version >= 212 {
wld.is_hallowen_forced_today = self.read_bool()?;
wld.is_xmas_forced_today = self.read_bool()?;
}
if self.version >= 216 {
wld.tier_1_ore = u16::try_from(self.read_i32()?)
.ok()
.and_then(TileType::from_id);
wld.tier_2_ore = u16::try_from(self.read_i32()?)
.ok()
.and_then(TileType::from_id);
wld.tier_3_ore = u16::try_from(self.read_i32()?)
.ok()
.and_then(TileType::from_id);
wld.tier_4_ore = u16::try_from(self.read_i32()?)
.ok()
.and_then(TileType::from_id);
}
if self.version >= 217 {
wld.is_cat_bought = self.read_bool()?;
wld.is_dog_bought = self.read_bool()?;
wld.is_bunny_bought = self.read_bool()?;
}
if self.version >= 223 {
wld.is_empress_of_light_downed = self.read_bool()?;
wld.is_queen_slime_downed = self.read_bool()?;
}
if self.version >= 240 {
wld.is_deerclops_downed = self.read_bool()?;
}
if self.version >= 250 {
wld.is_blue_slime_spawn_unlocked = self.read_bool()?;
}
if self.version >= 251 {
wld.is_merchant_spawn_unlocked = self.read_bool()?;
wld.is_demolitionist_spawn_unlocked = self.read_bool()?;
wld.is_party_girl_spawn_unlocked = self.read_bool()?;
wld.is_dye_trader_spawn_unlocked = self.read_bool()?;
wld.is_truffle_spawn_unlocked = self.read_bool()?;
wld.is_arms_dealer_spawn_unlocked = self.read_bool()?;
wld.is_nurse_spawn_unlocked = self.read_bool()?;
wld.is_princess_spawn_unlocked = self.read_bool()?;
}
if self.version >= 259 {
wld.is_combat_book_volume_two_used = self.read_bool()?;
}
if self.version >= 260 {
wld.is_peddlers_satchel_used = self.read_bool()?;
}
if self.version >= 261 {
wld.is_green_slime_spawn_unlocked = self.read_bool()?;
wld.is_old_slime_spawn_unlocked = self.read_bool()?;
wld.is_purple_slime_spawn_unlocked = self.read_bool()?;
wld.is_rainbow_slime_spawn_unlocked = self.read_bool()?;
wld.is_red_slime_unlocked = self.read_bool()?;
wld.is_yellow_slime_spawn_unlocked = self.read_bool()?;
wld.is_copper_slime_spawn_unlocked = self.read_bool()?;
}
if self.version >= 264 {
wld.is_moondial_forward_time_active = self.read_bool()?;
wld.moondial_cooldown = Value::new(self.read_u8()?);
}
if self.version >= 287 {
wld.is_hallowen_forced_forever = self.read_bool()?;
wld.is_xmas_forced_forever = self.read_bool()?;
}
if self.version >= 288 {
wld.is_vampire_seed = self.read_bool()?;
}
if self.version >= 296 {
wld.is_infected_seed = self.read_bool()?;
}
if self.version >= 291 {
wld.meteor_shower_remaining = self.read_i32()?;
wld.coin_rain_remaining = self.read_i32()?;
}
if self.version >= 297 {
let is_team_spawns = self.read_bool()?;
let len = self.read_u8()?;
let spawn = Vec2::new(
wld.spawn.x.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
wld.spawn.y.clamp(i16::MIN as i32, i16::MAX as i32) as i16,
);
let mut teams = TeamSpawns {
no_team: spawn,
red: spawn,
green: spawn,
blue: spawn,
yellow: spawn,
pink: spawn,
};
let mut team_array = [
&mut teams.no_team,
&mut teams.red,
&mut teams.blue,
&mut teams.green,
&mut teams.yellow,
&mut teams.pink,
]
.into_iter();
for _ in 0..len {
let pos = self.read_vec2_i16()?;
if let Some(team) = team_array.next() {
*team = pos;
}
}
drop(team_array);
if is_team_spawns {
wld.team_spawns = Some(teams);
}
}
if self.version >= 304 {
wld.is_dual_dungeon_seed = self.read_bool()?;
}
if self.version >= 299 && self.version < 313 {
self.read_u32()?; }
if self.version >= 299 {
wld.manifest =
serde_json::from_str(self.read_string()?.as_str()).map_err(Error::SerdeError)?;
}
Ok(())
}
fn verify_terraria_file(&mut self) -> Result<()> {
let mut magic_number = [0u8; 7];
self.reader.read_exact(&mut magic_number)?;
if &magic_number != MAGIC_NUMBER {
return Err(Error::InvalidMagicNumber {
found: magic_number.to_vec(),
});
}
Ok(())
}
fn read_in_game_time_and_moon_phase(&mut self) -> io::Result<(Time, MoonPhase)> {
let mut game_time = self.read_f64()?.max(0.0);
let mut is_day_time = self.read_bool()?;
let mut moon_phase = u8::try_from(self.read_i32()?)
.ok()
.and_then(MoonPhase::from_id);
if is_day_time {
if game_time > 15.0 * 60.0 * 60.0 {
game_time = 0.0;
is_day_time = false;
}
} else if game_time > 9.0 * 60.0 * 60.0 {
game_time = 0.0;
is_day_time = true;
moon_phase = moon_phase.map(MoonPhase::next);
}
if !is_day_time {
game_time += 15.0 * 60.0 * 60.0;
}
Ok((
Time::from_hms(4, 30, 0).expect("Checked")
+ (game_time * 1_000_000_000.0).round().nanoseconds(),
moon_phase.unwrap_or_default(),
))
}
fn read_vec2_i16<U: Unit>(&mut self) -> io::Result<Vec2<U, i16>> {
Ok(Vec2::new(self.read_i16()?, self.read_i16()?))
}
fn read_vec2_i32<U: Unit>(&mut self) -> io::Result<Vec2<U, i32>> {
Ok(Vec2::new(self.read_i32()?, self.read_i32()?))
}
fn read_i64(&mut self) -> io::Result<i64> {
self.reader.read_i64::<LittleEndian>()
}
fn read_u64(&mut self) -> io::Result<u64> {
self.reader.read_u64::<LittleEndian>()
}
fn read_i32(&mut self) -> io::Result<i32> {
self.reader.read_i32::<LittleEndian>()
}
fn read_u32(&mut self) -> io::Result<u32> {
self.reader.read_u32::<LittleEndian>()
}
fn read_i16(&mut self) -> io::Result<i16> {
self.reader.read_i16::<LittleEndian>()
}
fn read_u16(&mut self) -> io::Result<u16> {
self.reader.read_u16::<LittleEndian>()
}
fn read_u8(&mut self) -> io::Result<u8> {
self.reader.read_u8()
}
fn read_f64(&mut self) -> io::Result<f64> {
self.reader.read_f64::<LittleEndian>()
}
fn read_f32(&mut self) -> io::Result<f32> {
self.reader.read_f32::<LittleEndian>()
}
fn read_bool(&mut self) -> io::Result<bool> {
Ok(self.read_u8()? != 0)
}
fn read_string(&mut self) -> io::Result<String> {
self.reader.read_cs_string()
}
fn read_terraria_file_type(&mut self) -> io::Result<TerrariaFileType> {
Ok(TerrariaFileType::from_id(self.read_u8()?))
}
fn read_duration_terra_ticks_f64(&mut self) -> io::Result<Duration> {
Ok(Duration::seconds_f64(self.read_f64()? / 60.0))
}
fn read_duration_terra_ticks_i32(&mut self) -> io::Result<Duration> {
let nano_seconds = round_division_i64(self.read_i32()? as i64 * 1000000000, 60);
Ok(nano_seconds.nanoseconds())
}
fn read_invasion(&mut self) -> io::Result<Option<Invasion>> {
let invasion_size = self.read_i32()?;
let invasion_type = self.read_i32()?;
let invasion_x = self.read_f64()?;
let Some(invasion_type) = InvasionType::from_id(invasion_type) else {
return Ok(None);
};
Ok(Some(Invasion {
size: invasion_size,
start_size: start_invasion_size(invasion_size, invasion_type),
invasion_type,
position: invasion_x.into(),
}))
}
fn read_rain(&mut self) -> io::Result<Option<Rain>> {
let is_raining = self.read_bool()?;
let rain_time = self.read_duration_terra_ticks_i32()?;
let max_rain = self.read_f32()?;
Ok(if is_raining {
Some(Rain {
time: rain_time,
max: max_rain,
})
} else {
None
})
}
}
pub fn start_invasion_size(invasion_size: i32, invasion_type: InvasionType) -> i32 {
let (start_invasion_size, steps) = match invasion_type {
InvasionType::GoblinArmy | InvasionType::SnowLegion => (80, 40),
InvasionType::PirateInvasion => (120, 60),
InvasionType::MartianMadness => (160, 40),
};
if start_invasion_size >= invasion_size {
return invasion_size;
}
let diff = invasion_size - start_invasion_size;
let add = (diff + (steps - 1)) / steps * steps;
start_invasion_size + add
}