From 01697c8f62563fa032751c5735737f25c009e44a Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Tue, 24 Jun 2025 19:15:25 -0400 Subject: [PATCH] Begin implementing currency, add //gm gil command and more This doesn't work yet though , and I'm not sure why. I also fixed a bug where new characters doesn't get their inventories initialized properly. --- USAGE.md | 1 + resources/opcodes.json | 5 ++ src/bin/kawari-world.rs | 9 +++- src/inventory/currency.rs | 40 ++++++++++++++++ src/inventory/generic.rs | 10 +++- src/inventory/mod.rs | 7 +++ src/inventory/storage.rs | 2 + src/ipc/zone/currency_info.rs | 16 +++++++ src/ipc/zone/mod.rs | 9 +++- src/world/connection.rs | 88 ++++++++++++++++++++++++----------- 10 files changed, 156 insertions(+), 31 deletions(-) create mode 100644 src/inventory/currency.rs create mode 100644 src/ipc/zone/currency_info.rs diff --git a/USAGE.md b/USAGE.md index 7a0da6a..a70f74e 100644 --- a/USAGE.md +++ b/USAGE.md @@ -133,3 +133,4 @@ These GM commands are implemented in the FFXIV protocol, but only some of them a * `//gm orchestrion `: Unlock an Orchestrion song. * `//gm exp `: Adds the specified amount of EXP to the current class/job. * `//gm teri_info`: Displays information about the current zone. Currently displays zone id, weather, internal zone name, parent region name, and place/display name. +* `//gm gil `: Adds the specified amount of gil to the player diff --git a/resources/opcodes.json b/resources/opcodes.json index 0cea83a..86ac739 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -169,6 +169,11 @@ "name": "ActorControlTarget", "opcode": 593, "size": 28 + }, + { + "name": "CurrencyCrystalInfo", + "opcode": 548, + "size": 32 } ], "ClientZoneIpcType": [ diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 0550ee7..c456faf 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -6,7 +6,7 @@ use kawari::RECEIVE_BUFFER_SIZE; use kawari::common::Position; use kawari::common::{GameData, TerritoryNameKind, timestamp_secs}; use kawari::config::get_config; -use kawari::inventory::Item; +use kawari::inventory::{Item, Storage}; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::zone::{ ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList, @@ -716,7 +716,12 @@ async fn client_loop( "Internal name: {}\n", "Region name: {}\n", "Place name: {}"), id, weather_id, internal_name, region_name, place_name).as_str()).await; - } + }, + GameMasterCommandType::Gil => { + let amount = *arg0; + connection.player_data.inventory.currency.get_slot_mut(0).quantity += amount; + connection.send_inventory(false).await; + }, } } ClientZoneIpcData::ZoneJump { diff --git a/src/inventory/currency.rs b/src/inventory/currency.rs new file mode 100644 index 0000000..002d268 --- /dev/null +++ b/src/inventory/currency.rs @@ -0,0 +1,40 @@ +use serde::{Deserialize, Serialize}; + +use super::{Item, Storage}; + +#[derive(Clone, Copy, Deserialize, Serialize, Debug)] +pub struct CurrencyStorage { + pub gil: Item, +} + +impl Default for CurrencyStorage { + fn default() -> Self { + Self { + gil: Item { quantity: 0, id: 1 }, + } + } +} + +impl Storage for CurrencyStorage { + fn max_slots(&self) -> u32 { + 1 + } + + fn num_items(&self) -> u32 { + self.gil.quantity + } + + fn get_slot_mut(&mut self, index: u16) -> &mut Item { + match index { + 0 => &mut self.gil, + _ => panic!("{} is not a valid src_container_index?!?", index), + } + } + + fn get_slot(&self, index: u16) -> &Item { + match index { + 0 => &self.gil, + _ => panic!("{} is not a valid src_container_index?!?", index), + } + } +} diff --git a/src/inventory/generic.rs b/src/inventory/generic.rs index 531f8e8..c226c7b 100644 --- a/src/inventory/generic.rs +++ b/src/inventory/generic.rs @@ -2,11 +2,19 @@ use serde::{Deserialize, Serialize}; use super::{Item, Storage}; -#[derive(Debug, Clone, Default, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct GenericStorage { pub slots: Vec, } +impl GenericStorage { + pub fn default() -> Self { + Self { + slots: vec![Item::default(); N], + } + } +} + impl Storage for GenericStorage { fn max_slots(&self) -> u32 { N as u32 diff --git a/src/inventory/mod.rs b/src/inventory/mod.rs index 6ab99ca..10ca597 100644 --- a/src/inventory/mod.rs +++ b/src/inventory/mod.rs @@ -18,6 +18,9 @@ pub use item::Item; mod storage; pub use storage::{ContainerType, Storage}; +mod currency; +pub use currency::CurrencyStorage; + const MAX_NORMAL_STORAGE: usize = 35; const MAX_LARGE_STORAGE: usize = 50; @@ -37,6 +40,7 @@ pub struct Inventory { pub armoury_bracelet: GenericStorage, pub armoury_rings: GenericStorage, pub armoury_soul_crystal: GenericStorage, + pub currency: CurrencyStorage, } impl Default for Inventory { @@ -56,6 +60,7 @@ impl Default for Inventory { armoury_bracelet: GenericStorage::default(), armoury_rings: GenericStorage::default(), armoury_soul_crystal: GenericStorage::default(), + currency: CurrencyStorage::default(), } } } @@ -201,6 +206,7 @@ impl Inventory { ContainerType::Inventory2 => &mut self.pages[2], ContainerType::Inventory3 => &mut self.pages[3], ContainerType::Equipped => &mut self.equipped, + ContainerType::Currency => &mut self.currency, ContainerType::ArmoryOffWeapon => &mut self.armoury_off_hand, ContainerType::ArmoryHead => &mut self.armoury_head, ContainerType::ArmoryBody => &mut self.armoury_body, @@ -223,6 +229,7 @@ impl Inventory { ContainerType::Inventory2 => &self.pages[2], ContainerType::Inventory3 => &self.pages[3], ContainerType::Equipped => &self.equipped, + ContainerType::Currency => &self.currency, ContainerType::ArmoryOffWeapon => &self.armoury_off_hand, ContainerType::ArmoryHead => &self.armoury_head, ContainerType::ArmoryBody => &self.armoury_body, diff --git a/src/inventory/storage.rs b/src/inventory/storage.rs index 740e69f..b861fb0 100644 --- a/src/inventory/storage.rs +++ b/src/inventory/storage.rs @@ -15,6 +15,8 @@ pub enum ContainerType { Equipped = 1000, + Currency = 2000, + ArmoryOffWeapon = 3200, ArmoryHead = 3201, ArmoryBody = 3202, diff --git a/src/ipc/zone/currency_info.rs b/src/ipc/zone/currency_info.rs new file mode 100644 index 0000000..a9cd1de --- /dev/null +++ b/src/ipc/zone/currency_info.rs @@ -0,0 +1,16 @@ +use binrw::binrw; + +#[binrw] +#[brw(little)] +#[derive(Debug, Clone, Default)] +pub struct CurrencyInfo { + pub sequence: u32, + pub container: u16, + pub slot: u16, + pub quantity: u32, + pub unk1: u32, + pub catalog_id: u32, + pub unk2: u32, + pub unk3: u32, + pub unk4: u32, +} diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 4c64aa4..29a1565 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -79,6 +79,9 @@ pub use equip::Equip; mod client_trigger; pub use client_trigger::{ClientTrigger, ClientTriggerCommand}; +mod currency_info; +pub use currency_info::CurrencyInfo; + use crate::common::ObjectTypeId; use crate::common::Position; use crate::common::read_string; @@ -149,6 +152,7 @@ pub enum GameMasterCommandType { GiveItem = 0xC8, Aetheryte = 0x5E, TerritoryInfo = 0x25D, + Gil = 0xC9, } #[binrw] @@ -244,6 +248,8 @@ pub enum ServerZoneIpcData { }, /// Used to control target information ActorControlTarget(ActorControlTarget), + /// Used to update the player's currencies + CurrencyCrystalInfo(CurrencyInfo), } #[binrw] @@ -374,7 +380,8 @@ pub enum ClientZoneIpcData { event_id: u32, }, #[br(pre_assert(*magic == ClientZoneIpcType::EventHandlerReturn))] - EventHandlerReturn { // TODO: This is actually EventYieldHandler + EventHandlerReturn { + // TODO: This is actually EventYieldHandler handler_id: u32, scene: u16, error_code: u8, diff --git a/src/world/connection.rs b/src/world/connection.rs index 5f55f9a..a352d4f 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -12,15 +12,16 @@ use crate::{ OBFUSCATION_ENABLED_MODE, common::{GameData, ObjectId, ObjectTypeId, Position, timestamp_secs}, config::{WorldConfig, get_config}, - inventory::{Inventory, Item}, + inventory::{ContainerType, Inventory, Item}, ipc::{ chat::ServerChatIpcSegment, zone::{ ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory, ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, ContainerInfo, - DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, - ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, - StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange, + CurrencyInfo, DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone, ItemInfo, Move, + NpcSpawn, ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, + ServerZoneIpcSegment, StatusEffect, StatusEffectList, UpdateClassInfo, Warp, + WeatherChange, }, }, opcodes::ServerZoneIpcType, @@ -484,33 +485,66 @@ impl ZoneConnection { let mut sequence = 0; for (container_type, container) in &self.player_data.inventory.clone() { - let mut send_slot = async |slot_index: u16, item: &Item| { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::UpdateItem, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::UpdateItem(ItemInfo { - sequence, - container: container_type, - slot: slot_index, - quantity: item.quantity, - catalog_id: item.id, - condition: 30000, + // currencies + if container_type == ContainerType::Currency { + let mut send_currency = async |slot_index: u16, item: &Item| { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::CurrencyCrystalInfo, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::CurrencyCrystalInfo(CurrencyInfo { + sequence, + container: item.id as u16, + slot: slot_index, + quantity: item.quantity, + catalog_id: item.id, + ..Default::default() + }), ..Default::default() - }), - ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; }; - self.send_segment(PacketSegment { - source_actor: self.player_data.actor_id, - target_actor: self.player_data.actor_id, - segment_type: SegmentType::Ipc, - data: SegmentData::Ipc { data: ipc }, - }) - .await; - }; + for i in 0..container.max_slots() { + send_currency(i as u16, container.get_slot(i as u16)).await; + } + } else { + // items - for i in 0..container.max_slots() { - send_slot(i as u16, container.get_slot(i as u16)).await; + let mut send_slot = async |slot_index: u16, item: &Item| { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::UpdateItem, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::UpdateItem(ItemInfo { + sequence, + container: container_type, + slot: slot_index, + quantity: item.quantity, + catalog_id: item.id, + condition: 30000, + ..Default::default() + }), + ..Default::default() + }; + + self.send_segment(PacketSegment { + source_actor: self.player_data.actor_id, + target_actor: self.player_data.actor_id, + segment_type: SegmentType::Ipc, + data: SegmentData::Ipc { data: ipc }, + }) + .await; + }; + + for i in 0..container.max_slots() { + send_slot(i as u16, container.get_slot(i as u16)).await; + } } // inform the client of container state