use crate::bits::{BitReader, BitWriter};
use crate::data::ExternalData;
use crate::enums::{BodyLocation, ItemMode, StashTabType};
use crate::error::{Error, Result};
use crate::model::sections::{list_newtype, ItemsSection};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
pub struct D2StashSave(pub Vec<D2StashTab>);
list_newtype!(D2StashSave, D2StashTab);
impl D2StashSave {
#[cfg(feature = "embedded-data")]
pub fn from_bytes(data: &[u8]) -> Result<Self> {
Self::from_bytes_with(data, crate::TxtData::embedded())
}
pub fn from_bytes_with(data: &[u8], external: &(impl ExternalData + ?Sized)) -> Result<Self> {
Self::read(&mut BitReader::new(data), external)
}
pub fn read(r: &mut BitReader<'_>, data: &(impl ExternalData + ?Sized)) -> Result<Self> {
let mut tabs = Vec::new();
while r.bits_remaining() > D2StashTab::HEADER_SIZE * 8 {
tabs.push(D2StashTab::read(r, data)?);
}
Ok(D2StashSave(tabs))
}
pub fn convert_to(&mut self, version: u32) {
for tab in &mut self.0 {
tab.convert_to(version);
}
}
#[cfg(feature = "embedded-data")]
pub fn to_bytes(&self) -> Result<Vec<u8>> {
self.to_bytes_with(crate::TxtData::embedded())
}
pub fn to_bytes_with(&self, data: &(impl ExternalData + ?Sized)) -> Result<Vec<u8>> {
let mut buf = Vec::with_capacity(self.estimate_size());
self.write(&mut BitWriter::new(&mut buf), data)?;
Ok(buf)
}
pub fn write(&self, w: &mut BitWriter<'_>, data: &(impl ExternalData + ?Sized)) -> Result<()> {
for tab in &self.0 {
tab.write(w, data)?;
}
Ok(())
}
pub fn estimate_size(&self) -> usize {
self.0.iter().map(D2StashTab::estimate_size).sum()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct D2StashTab {
pub stash_format: u32,
pub save_version: u32,
pub gold: u32,
pub season: u16,
pub tab_type: StashTabType,
pub padding_21: [u8; 3],
pub withdraw_only_data: [u8; 20],
pub padding_44: [u8; 20],
pub items: ItemsSection,
pub chronicle: Option<ChronicleSection>,
}
impl Default for D2StashTab {
fn default() -> Self {
D2StashTab {
stash_format: 0,
save_version: 0,
gold: 0,
season: 0,
tab_type: StashTabType::Normal,
padding_21: [0; 3],
withdraw_only_data: [0; 20],
padding_44: [0; 20],
items: ItemsSection::default(),
chronicle: None,
}
}
}
impl D2StashTab {
pub const HEADER_SIZE: usize = 64;
pub const MAGIC: u32 = 0xAA55AA55;
pub fn is_withdraw_only(&self) -> bool {
self.withdraw_only_data[..4] != [0; 4]
}
#[cfg(feature = "embedded-data")]
pub fn from_bytes(data: &[u8]) -> Result<Self> {
Self::read(&mut BitReader::new(data), crate::TxtData::embedded())
}
pub fn read(r: &mut BitReader<'_>, data: &(impl ExternalData + ?Sized)) -> Result<Self> {
let start = r.byte_position();
let magic = r.read_u32()?;
if magic != Self::MAGIC {
return Err(Error::InvalidMagic { section: "stash tab", found: magic });
}
let mut tab = D2StashTab {
stash_format: r.read_u32()?,
save_version: r.read_u32()?,
gold: r.read_u32()?,
..Default::default()
};
let size = r.read_u16()? as usize;
tab.season = r.read_u16()?;
tab.tab_type = StashTabType::from_raw(r.read_u8()?);
tab.padding_21 = r.read_array()?;
tab.withdraw_only_data = r.read_array()?;
tab.padding_44 = r.read_array()?;
if tab.stash_format < 2 {
tab.tab_type = StashTabType::Normal;
}
let body_size = size as isize - Self::HEADER_SIZE as isize;
match tab.tab_type {
StashTabType::Chronicle => tab.chronicle = Some(ChronicleSection::read(r, body_size)?),
_ => tab.items = ItemsSection::read(r, data, tab.save_version)?,
}
if size == 0 {
return Err(Error::invalid_data("stash tab size is zero"));
}
r.set_byte_position(start + size);
Ok(tab)
}
pub fn convert_to(&mut self, version: u32) {
if self.save_version <= 96 && version > 96 {
for item in &mut self.items {
if item.position.mode != ItemMode::Equipped {
item.position.body_location = BodyLocation::None;
}
}
}
(self.stash_format, self.tab_type) = Self::normalized_format(version, self.stash_format, self.tab_type);
self.save_version = version;
}
fn normalized_format(version: u32, stash_format: u32, tab_type: StashTabType) -> (u32, StashTabType) {
if version >= 100 {
(stash_format.max(2), tab_type)
} else if version > 96 && stash_format >= 2 {
(1, StashTabType::Normal)
} else {
(stash_format, tab_type)
}
}
#[cfg(feature = "embedded-data")]
pub fn to_bytes(&self) -> Result<Vec<u8>> {
self.to_bytes_with(crate::TxtData::embedded())
}
pub fn to_bytes_with(&self, data: &(impl ExternalData + ?Sized)) -> Result<Vec<u8>> {
let mut buf = Vec::with_capacity(self.estimate_size());
self.write(&mut BitWriter::new(&mut buf), data)?;
Ok(buf)
}
pub fn write(&self, w: &mut BitWriter<'_>, data: &(impl ExternalData + ?Sized)) -> Result<()> {
let (stash_format, tab_type) = Self::normalized_format(self.save_version, self.stash_format, self.tab_type);
if tab_type == StashTabType::Chronicle && stash_format < 2 {
return Ok(());
}
let start = w.bytes_written();
w.write_u32(Self::MAGIC)?;
w.write_u32(stash_format)?;
w.write_u32(self.save_version)?;
w.write_u32(self.gold)?;
let size_offset = w.byte_position();
w.write_u16(0)?;
w.write_u16(self.season)?;
w.write_u8(tab_type.to_raw())?;
w.write_bytes(&self.padding_21)?;
w.write_bytes(&self.withdraw_only_data)?;
w.write_bytes(&self.padding_44)?;
match tab_type {
StashTabType::Chronicle => {
if let Some(chronicle) = &self.chronicle {
chronicle.write(w)?;
}
}
_ => self.items.write(w, data, self.save_version)?,
}
let size = w.bytes_written() - start;
w.patch_u16(size_offset, size as u16);
Ok(())
}
pub fn estimate_size(&self) -> usize {
let mut size = Self::HEADER_SIZE;
match (&self.tab_type, &self.chronicle) {
(StashTabType::Chronicle, Some(c)) => {
size += 4 + 2 + 6 + 8;
size += (c.set_entries.len() + c.unique_entries.len() + c.runeword_entries.len()) * 10;
size += c.trailing_data.len();
}
_ => size += self.items.estimate_size(),
}
(size as f64 * 1.2) as usize
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChronicleSection {
pub version: u16,
pub set_entries: Vec<ChronicleEntry>,
pub unique_entries: Vec<ChronicleEntry>,
pub runeword_entries: Vec<ChronicleEntry>,
pub reserved: [u8; 8],
pub trailing_data: Vec<u8>,
}
impl Default for ChronicleSection {
fn default() -> Self {
ChronicleSection {
version: 1,
set_entries: Vec::new(),
unique_entries: Vec::new(),
runeword_entries: Vec::new(),
reserved: [0; 8],
trailing_data: Vec::new(),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ChronicleEntry {
pub item_id: u32,
pub source: u16,
pub timestamp: u32,
}
impl ChronicleEntry {
pub fn time(&self) -> std::time::SystemTime {
std::time::UNIX_EPOCH + std::time::Duration::from_secs(self.timestamp as u64 * 60)
}
}
impl ChronicleSection {
pub const MAGIC: u32 = 0xC0EAEDC0;
pub fn read(r: &mut BitReader<'_>, body_size: isize) -> Result<Self> {
let start = r.byte_position();
let magic = r.read_u32()?;
if magic != Self::MAGIC {
return Err(Error::InvalidMagic { section: "chronicle", found: magic });
}
let version = r.read_u16()?;
let set_count = r.read_u16()?;
let unique_count = r.read_u16()?;
let runeword_count = r.read_u16()?;
let reserved = r.read_array()?;
let mut read_entries = |count: u16| -> Result<Vec<ChronicleEntry>> {
(0..count)
.map(|_| Ok(ChronicleEntry { item_id: r.read_u32()?, source: r.read_u16()?, timestamp: r.read_u32()? }))
.collect()
};
let set_entries = read_entries(set_count)?;
let unique_entries = read_entries(unique_count)?;
let runeword_entries = read_entries(runeword_count)?;
let consumed = (r.byte_position() - start) as isize;
let trailing = body_size - consumed;
let trailing_data = if trailing > 0 { r.read_slice(trailing as usize)?.to_vec() } else { Vec::new() };
Ok(ChronicleSection { version, set_entries, unique_entries, runeword_entries, reserved, trailing_data })
}
pub fn write(&self, w: &mut BitWriter<'_>) -> Result<()> {
w.write_u32(Self::MAGIC)?;
w.write_u16(self.version)?;
w.write_u16(self.set_entries.len() as u16)?;
w.write_u16(self.unique_entries.len() as u16)?;
w.write_u16(self.runeword_entries.len() as u16)?;
w.write_bytes(&self.reserved)?;
for e in self.set_entries.iter().chain(&self.unique_entries).chain(&self.runeword_entries) {
w.write_u32(e.item_id)?;
w.write_u16(e.source)?;
w.write_u32(e.timestamp)?;
}
w.write_bytes(&self.trailing_data)
}
}