mirror of
https://github.com/redstrate/Kawari.git
synced 2025-06-30 11:47:45 +00:00
322 lines
10 KiB
Rust
322 lines
10 KiB
Rust
use icarus::Action::ActionSheet;
|
|
use icarus::Aetheryte::AetheryteSheet;
|
|
use icarus::ClassJob::ClassJobSheet;
|
|
use icarus::EquipSlotCategory::EquipSlotCategorySheet;
|
|
use icarus::PlaceName::PlaceNameSheet;
|
|
use icarus::TerritoryType::TerritoryTypeSheet;
|
|
use icarus::WeatherRate::WeatherRateSheet;
|
|
use icarus::World::WorldSheet;
|
|
use icarus::{Tribe::TribeSheet, Warp::WarpSheet};
|
|
use physis::common::{Language, Platform};
|
|
use physis::exd::{EXD, ExcelRowKind};
|
|
use physis::exh::EXH;
|
|
|
|
use crate::{common::Attributes, config::get_config};
|
|
|
|
use super::timestamp_secs;
|
|
|
|
/// Convenient methods built on top of Physis to access data relevant to the server
|
|
pub struct GameData {
|
|
pub game_data: physis::gamedata::GameData,
|
|
pub item_exh: EXH,
|
|
pub item_pages: Vec<EXD>,
|
|
}
|
|
|
|
impl Default for GameData {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl GameData {
|
|
pub fn new() -> Self {
|
|
let config = get_config();
|
|
|
|
let mut game_data =
|
|
physis::gamedata::GameData::from_existing(Platform::Win32, &config.game_location);
|
|
|
|
let mut item_pages = Vec::new();
|
|
|
|
let item_exh = game_data.read_excel_sheet_header("Item").unwrap();
|
|
for (i, _) in item_exh.pages.iter().enumerate() {
|
|
item_pages.push(
|
|
game_data
|
|
.read_excel_sheet("Item", &item_exh, Language::English, i)
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
Self {
|
|
game_data,
|
|
item_exh,
|
|
item_pages,
|
|
}
|
|
}
|
|
|
|
/// Gets the world name from an id into the World Excel sheet.
|
|
pub fn get_world_name(&mut self, world_id: u16) -> Option<String> {
|
|
let sheet = WorldSheet::read_from(&mut self.game_data, Language::None)?;
|
|
let row = sheet.get_row(world_id as u32)?;
|
|
|
|
row.Name().into_string().cloned()
|
|
}
|
|
|
|
/// Gets the starting city-state from a given class/job id.
|
|
pub fn get_citystate(&mut self, classjob_id: u16) -> Option<u8> {
|
|
let sheet = ClassJobSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(classjob_id as u32)?;
|
|
|
|
row.StartingTown().into_u8().copied()
|
|
}
|
|
|
|
pub fn get_racial_base_attributes(&mut self, tribe_id: u8) -> Option<Attributes> {
|
|
// The Tribe Excel sheet only has deltas (e.g. 2 or -2) which are applied to a base 20 number... from somewhere
|
|
let base_stat = 20;
|
|
|
|
let sheet = TribeSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(tribe_id as u32)?;
|
|
|
|
Some(Attributes {
|
|
strength: (base_stat + row.STR().into_i8()?) as u32,
|
|
dexterity: (base_stat + row.DEX().into_i8()?) as u32,
|
|
vitality: (base_stat + row.VIT().into_i8()?) as u32,
|
|
intelligence: (base_stat + row.INT().into_i8()?) as u32,
|
|
mind: (base_stat + row.MND().into_i8()?) as u32,
|
|
})
|
|
}
|
|
|
|
/// Gets the primary model ID for a given item ID
|
|
pub fn get_primary_model_id(&mut self, item_id: u32) -> Option<u64> {
|
|
for page in &self.item_pages {
|
|
if let Some(row) = page.get_row(item_id) {
|
|
let ExcelRowKind::SingleRow(item_row) = row else {
|
|
panic!("Expected a single row!")
|
|
};
|
|
|
|
let physis::exd::ColumnData::UInt64(id) = &item_row.columns[47] else {
|
|
panic!("Unexpected type!");
|
|
};
|
|
|
|
return Some(*id);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Returns the pop range object id that's associated with the warp id
|
|
pub fn get_warp(&mut self, warp_id: u32) -> Option<(u32, u16)> {
|
|
let sheet = WarpSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(warp_id)?;
|
|
|
|
let pop_range_id = row.PopRange().into_u32()?;
|
|
let zone_id = row.TerritoryType().into_u16()?;
|
|
|
|
Some((*pop_range_id, *zone_id))
|
|
}
|
|
|
|
pub fn get_aetheryte(&mut self, aetheryte_id: u32) -> Option<(u32, u16)> {
|
|
let sheet = AetheryteSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(aetheryte_id)?;
|
|
|
|
// TODO: just look in the level sheet?
|
|
let pop_range_id = row.Level()[0].into_u32()?;
|
|
let zone_id = row.Territory().into_u16()?;
|
|
|
|
Some((*pop_range_id, *zone_id))
|
|
}
|
|
|
|
// Retrieves a zone's internal name, place name or parent region name.
|
|
pub fn get_territory_name(&mut self, zone_id: u32, which: TerritoryNameKind) -> Option<String> {
|
|
let sheet = TerritoryTypeSheet::read_from(&mut self.game_data, Language::None)?;
|
|
let row = sheet.get_row(zone_id)?;
|
|
|
|
let offset = match which {
|
|
TerritoryNameKind::Internal => {
|
|
return row.Name().into_string().cloned();
|
|
}
|
|
TerritoryNameKind::Region => row.PlaceNameRegion().into_u16()?,
|
|
TerritoryNameKind::Place => row.PlaceName().into_u16()?,
|
|
};
|
|
|
|
let sheet = PlaceNameSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(*offset as u32)?;
|
|
|
|
let value = row.Name().into_string()?;
|
|
|
|
Some(value.clone())
|
|
}
|
|
|
|
/// Find an item's equip category and id by name, if it exists.
|
|
pub fn get_item_by_name(&mut self, name: &str) -> Option<(u8, u32)> {
|
|
for page in &self.item_pages {
|
|
for row in &page.rows {
|
|
let ExcelRowKind::SingleRow(single_row) = &row.kind else {
|
|
panic!("Expected a single row!")
|
|
};
|
|
|
|
let physis::exd::ColumnData::String(item_name) = &single_row.columns[9] else {
|
|
panic!("Unexpected type!");
|
|
};
|
|
|
|
if !item_name.to_lowercase().contains(&name.to_lowercase()) {
|
|
continue;
|
|
}
|
|
|
|
let physis::exd::ColumnData::UInt8(equip_category) = &single_row.columns[17] else {
|
|
panic!("Unexpected type!");
|
|
};
|
|
|
|
return Some((*equip_category, row.row_id));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
/// Turn an equip slot category id into a slot for the equipped inventory
|
|
pub fn get_equipslot_category(&mut self, equipslot_id: u8) -> Option<u16> {
|
|
let sheet = EquipSlotCategorySheet::read_from(&mut self.game_data, Language::None)?;
|
|
let row = sheet.get_row(equipslot_id as u32)?;
|
|
|
|
let main_hand = row.MainHand().into_i8()?;
|
|
if *main_hand == 1 {
|
|
return Some(0);
|
|
}
|
|
|
|
let off_hand = row.OffHand().into_i8()?;
|
|
if *off_hand == 1 {
|
|
return Some(1);
|
|
}
|
|
|
|
let head = row.Head().into_i8()?;
|
|
if *head == 1 {
|
|
return Some(2);
|
|
}
|
|
|
|
let body = row.Body().into_i8()?;
|
|
if *body == 1 {
|
|
return Some(3);
|
|
}
|
|
|
|
let gloves = row.Gloves().into_i8()?;
|
|
if *gloves == 1 {
|
|
return Some(4);
|
|
}
|
|
|
|
let legs = row.Legs().into_i8()?;
|
|
if *legs == 1 {
|
|
return Some(6);
|
|
}
|
|
|
|
let feet = row.Feet().into_i8()?;
|
|
if *feet == 1 {
|
|
return Some(7);
|
|
}
|
|
|
|
let ears = row.Ears().into_i8()?;
|
|
if *ears == 1 {
|
|
return Some(8);
|
|
}
|
|
|
|
let neck = row.Neck().into_i8()?;
|
|
if *neck == 1 {
|
|
return Some(9);
|
|
}
|
|
|
|
let wrists = row.Wrists().into_i8()?;
|
|
if *wrists == 1 {
|
|
return Some(10);
|
|
}
|
|
|
|
let right_finger = row.FingerR().into_i8()?;
|
|
if *right_finger == 1 {
|
|
return Some(11);
|
|
}
|
|
|
|
let left_finger = row.FingerL().into_i8()?;
|
|
if *left_finger == 1 {
|
|
return Some(12);
|
|
}
|
|
|
|
let soul_crystal = row.SoulCrystal().into_i8()?;
|
|
if *soul_crystal == 1 {
|
|
return Some(13);
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn get_casttime(&mut self, action_id: u32) -> Option<u16> {
|
|
let sheet = ActionSheet::read_from(&mut self.game_data, Language::English)?;
|
|
let row = sheet.get_row(action_id)?;
|
|
|
|
row.Cast100ms().into_u16().copied()
|
|
}
|
|
|
|
/// Calculates the current weather at the current time
|
|
// TODO: instead allow targetting a specific time to calculate forcecasts
|
|
pub fn get_weather_rate(&mut self, weather_rate_id: u32) -> Option<i32> {
|
|
let sheet = WeatherRateSheet::read_from(&mut self.game_data, Language::None)?;
|
|
let row = sheet.get_row(weather_rate_id)?;
|
|
|
|
let target = Self::calculate_target();
|
|
let weather_and_rates: Vec<(i32, i32)> = row
|
|
.Weather()
|
|
.iter()
|
|
.cloned()
|
|
.zip(row.Rate())
|
|
.map(|(x, y)| (*x.into_i32().unwrap(), *y.into_u8().unwrap() as i32))
|
|
.collect();
|
|
|
|
Some(
|
|
weather_and_rates
|
|
.iter()
|
|
.filter(|(_, rate)| target < *rate)
|
|
.take(1)
|
|
.collect::<Vec<&(i32, i32)>>()
|
|
.first()?
|
|
.0,
|
|
)
|
|
}
|
|
|
|
/// Calculate target window for weather calculations
|
|
fn calculate_target() -> i32 {
|
|
// Based off of https://github.com/Rogueadyn/SaintCoinach/blob/master/SaintCoinach/Xiv/WeatherRate.cs
|
|
// TODO: this isn't correct still and doesn't seem to match up with the retail server
|
|
|
|
let real_to_eorzean_factor = (60.0 * 24.0) / 70.0;
|
|
let unix = (timestamp_secs() as f32 / real_to_eorzean_factor) as u64;
|
|
// Get Eorzea hour for weather start
|
|
let bell = unix / 175;
|
|
// Do the magic 'cause for calculations 16:00 is 0, 00:00 is 8 and 08:00 is 16
|
|
let increment = ((bell + 8 - (bell % 8)) as u32) % 24;
|
|
|
|
// Take Eorzea days since unix epoch
|
|
let total_days = (unix / 4200) as u32;
|
|
|
|
let calc_base = (total_days * 0x64) + increment;
|
|
|
|
let step1 = (calc_base << 0xB) ^ calc_base;
|
|
let step2 = (step1 >> 8) ^ step1;
|
|
|
|
(step2 % 0x64) as i32
|
|
}
|
|
|
|
/// Gets the current weather for the given zone id
|
|
pub fn get_weather(&mut self, zone_id: u32) -> Option<i32> {
|
|
let sheet = TerritoryTypeSheet::read_from(&mut self.game_data, Language::None)?;
|
|
let row = sheet.get_row(zone_id)?;
|
|
|
|
let weather_rate_id = row.WeatherRate().into_u8()?;
|
|
|
|
self.get_weather_rate(*weather_rate_id as u32)
|
|
}
|
|
}
|
|
|
|
// Simple enum for GameData::get_territory_name
|
|
pub enum TerritoryNameKind {
|
|
Internal,
|
|
Region,
|
|
Place,
|
|
}
|