physis 0.6.0

Library for reading and writing FFXIV data.
Documentation
// SPDX-FileCopyrightText: 2023 Joshua Goins <josh@redstrate.com>
// SPDX-License-Identifier: GPL-3.0-or-later

#![allow(clippy::ptr_arg)] // binrw trips up another false positive

use crate::ByteBuffer;
use crate::ByteSpan;
use crate::equipment::EquipSlot;
use crate::savedata::dat::DatHeader;
use binrw::NullString;
use binrw::binrw;
use binrw::{BinRead, BinWrite};
use std::collections::HashMap;
use std::io::Cursor;
use std::io::Read;

// FIXME: unclear what this is
const UNKNOWN_FLAG: u32 = 1_000_000;

fn convert_from_gear_id(id: u32) -> u32 {
    id & !UNKNOWN_FLAG
}

fn convert_to_gear_id(id: &u32) -> u32 {
    id | UNKNOWN_FLAG
}

fn convert_to_string(s: NullString) -> String {
    s.to_string()
}

fn convert_from_string(s: &String) -> NullString {
    NullString::from(s.as_str())
}

const NUMBER_OF_GEARSETS: usize = 100;

fn convert_from_gearsets(gearsets: [GearSet; NUMBER_OF_GEARSETS]) -> Vec<Option<GearSet>> {
    gearsets
        .iter()
        .cloned()
        .map(|x| if !x.name.is_empty() { Some(x) } else { None })
        .collect()
}

fn convert_to_gearsets(gearsets: &Vec<Option<GearSet>>) -> Vec<GearSet> {
    let mut result = vec![GearSet::default(); NUMBER_OF_GEARSETS];
    for (i, gearset) in gearsets.iter().enumerate() {
        if i >= NUMBER_OF_GEARSETS {
            break;
        }
        if let Some(gearset) = gearset {
            result[i] = gearset.clone();
        }
    }
    result
}

const NUMBER_OF_GEARSLOTS: usize = 14;

fn convert_from_slots(slots: [GearSlot; NUMBER_OF_GEARSLOTS]) -> HashMap<EquipSlot, GearSlot> {
    slots
        .iter()
        .cloned()
        .enumerate()
        .filter_map(|(i, x)| match x.id {
            0 => None,
            _ => Some((EquipSlot::from_repr(i as u16)?, x)),
        })
        .collect()
}

fn convert_to_slots(slots: &HashMap<EquipSlot, GearSlot>) -> Vec<GearSlot> {
    let mut result = vec![GearSlot::default(); NUMBER_OF_GEARSLOTS];
    for (idx, slot) in slots.iter() {
        result[*idx as usize] = slot.clone();
    }
    result
}

fn convert_id_opt(id: u32) -> Option<u32> {
    if id == 0 { None } else { Some(id) }
}

fn convert_opt_id(id: &Option<u32>) -> u32 {
    id.unwrap_or(0)
}

#[binrw]
#[derive(Debug, Clone, Default)]
pub struct GearSlot {
    #[br(map = convert_from_gear_id)]
    #[bw(map = convert_to_gear_id)]
    /// The ID of the item.
    pub id: u32,
    #[br(map = convert_id_opt)]
    #[bw(map = convert_opt_id)]
    /// The ID of the item used as glamour.
    pub glamour_id: Option<u32>,
    // FIXME: one of those is most likely dyes, no idea about the rest
    unknown1: u32,
    unknown2: u32,
    unknown3: u32,
    unknown4: u32,
    unknown5: u32,
}

#[binrw]
#[derive(Debug, Clone, Default)]
pub struct GearSet {
    /// The index of the gear set in the list of gear sets.
    pub index: u8,
    #[brw(pad_size_to = 47)]
    #[br(map = convert_to_string)]
    #[bw(map = convert_from_string)]
    /// The name of the gear set.
    pub name: String,
    // FIXME: no clue what this is
    unknown1: u64,
    #[br(map = convert_from_slots)]
    #[bw(map = convert_to_slots)]
    /// The slots of the gear set.
    pub slots: HashMap<EquipSlot, GearSlot>,
    #[br(map = convert_id_opt)]
    #[bw(map = convert_opt_id)]
    /// The ID of the facewear item.
    pub facewear: Option<u32>,
}

#[binrw]
#[br(little)]
#[derive(Debug, Clone)]
pub struct GearSets {
    // FIXME: can't be a version because it's always 0
    unknown1: u8,
    /// The index of the current active geat set.
    pub current_gearset: u8,
    // FIXME: no clue what this is
    unknown3: u16,
    #[br(map = convert_from_gearsets)]
    #[bw(map = convert_to_gearsets)]
    /// The list of gear sets.
    pub gearsets: Vec<Option<GearSet>>,
}

const GEARSET_KEY: u8 = 0x73;

impl GearSets {
    /// Read an existing file.
    pub fn from_existing(buffer: ByteSpan) -> Option<GearSets> {
        let mut cursor = Cursor::new(buffer);

        let header = DatHeader::read(&mut cursor).ok()?;

        let mut buffer = vec![0; header.content_size as usize - 1];
        cursor.read_exact(&mut buffer).ok()?;

        let decoded = buffer.iter().map(|x| *x ^ GEARSET_KEY).collect::<Vec<_>>();
        let mut cursor = Cursor::new(decoded);

        GearSets::read(&mut cursor).ok()
    }

    /// Writes data back to a buffer.
    pub fn write_to_buffer(&self) -> Option<ByteBuffer> {
        let mut buffer = ByteBuffer::new();

        // header
        {
            let mut cursor = Cursor::new(&mut buffer);

            let header = DatHeader {
                file_type: crate::savedata::dat::DatFileType::Gearset,
                file_version: 109,
                max_size: 45205,
                content_size: 45205,
            };
            header.write_le(&mut cursor).ok()?
        }

        // buffer contents encoded
        {
            let mut cursor = Cursor::new(ByteBuffer::new());
            self.write_le(&mut cursor).ok()?;

            buffer.extend_from_slice(
                &cursor
                    .into_inner()
                    .iter()
                    .map(|x| *x ^ GEARSET_KEY)
                    .collect::<Vec<_>>(),
            );
        }

        Some(buffer)
    }
}

#[cfg(test)]
mod tests {
    use std::fs::read;
    use std::path::PathBuf;

    use super::*;

    #[test]
    fn test_invalid() {
        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        d.push("resources/tests");
        d.push("random");

        // Feeding it invalid data should not panic
        GearSets::from_existing(&read(d).unwrap());
    }

    fn common_setup(name: &str) -> GearSets {
        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        d.push("resources/tests/gearsets");
        d.push(name);

        GearSets::from_existing(&read(&d).unwrap()).unwrap()
    }

    #[test]
    fn read_simple() {
        let gearsets = common_setup("simple.dat");

        assert_eq!(gearsets.current_gearset, 0);
        let gearset = gearsets.gearsets[0].as_ref().unwrap();
        for i in 1..gearsets.gearsets.len() {
            assert!(gearsets.gearsets[i].is_none());
        }

        assert_eq!(gearset.index, 0);
        assert_eq!(gearset.name, "White Mage");
        assert!(gearset.facewear.is_none());
        assert_eq!(gearset.slots.len(), 2);
        let slot = gearset.slots.get(&EquipSlot::MainHand).unwrap();
        assert_eq!(slot.id, 5269);
        assert_eq!(slot.glamour_id, Some(2453));
        let slot = gearset.slots.get(&EquipSlot::Body).unwrap();
        assert_eq!(slot.id, 8395913);
        assert_eq!(slot.glamour_id, None);
    }

    #[test]
    fn write_simple() {
        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
        d.push("resources/tests/gearsets");
        d.push("simple.dat");

        let gearset_bytes = &read(d).unwrap();
        let gearset = GearSets::from_existing(gearset_bytes).unwrap();
        assert_eq!(*gearset_bytes, gearset.write_to_buffer().unwrap());
    }
}