1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-30 11:47:45 +00:00

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.
This commit is contained in:
Joshua Goins 2025-06-24 19:15:25 -04:00
parent 03609fb8c1
commit 01697c8f62
10 changed files with 156 additions and 31 deletions

View file

@ -133,3 +133,4 @@ These GM commands are implemented in the FFXIV protocol, but only some of them a
* `//gm orchestrion <on/off> <id>`: Unlock an Orchestrion song. * `//gm orchestrion <on/off> <id>`: Unlock an Orchestrion song.
* `//gm exp <amount>`: Adds the specified amount of EXP to the current class/job. * `//gm exp <amount>`: 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 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 <amount>`: Adds the specified amount of gil to the player

View file

@ -169,6 +169,11 @@
"name": "ActorControlTarget", "name": "ActorControlTarget",
"opcode": 593, "opcode": 593,
"size": 28 "size": 28
},
{
"name": "CurrencyCrystalInfo",
"opcode": 548,
"size": 32
} }
], ],
"ClientZoneIpcType": [ "ClientZoneIpcType": [

View file

@ -6,7 +6,7 @@ use kawari::RECEIVE_BUFFER_SIZE;
use kawari::common::Position; use kawari::common::Position;
use kawari::common::{GameData, TerritoryNameKind, timestamp_secs}; use kawari::common::{GameData, TerritoryNameKind, timestamp_secs};
use kawari::config::get_config; use kawari::config::get_config;
use kawari::inventory::Item; use kawari::inventory::{Item, Storage};
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
use kawari::ipc::zone::{ use kawari::ipc::zone::{
ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList, ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList,
@ -716,7 +716,12 @@ async fn client_loop(
"Internal name: {}\n", "Internal name: {}\n",
"Region name: {}\n", "Region name: {}\n",
"Place name: {}"), id, weather_id, internal_name, region_name, place_name).as_str()).await; "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 { ClientZoneIpcData::ZoneJump {

40
src/inventory/currency.rs Normal file
View file

@ -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),
}
}
}

View file

@ -2,11 +2,19 @@ use serde::{Deserialize, Serialize};
use super::{Item, Storage}; use super::{Item, Storage};
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GenericStorage<const N: usize> { pub struct GenericStorage<const N: usize> {
pub slots: Vec<Item>, pub slots: Vec<Item>,
} }
impl<const N: usize> GenericStorage<N> {
pub fn default() -> Self {
Self {
slots: vec![Item::default(); N],
}
}
}
impl<const N: usize> Storage for GenericStorage<N> { impl<const N: usize> Storage for GenericStorage<N> {
fn max_slots(&self) -> u32 { fn max_slots(&self) -> u32 {
N as u32 N as u32

View file

@ -18,6 +18,9 @@ pub use item::Item;
mod storage; mod storage;
pub use storage::{ContainerType, Storage}; pub use storage::{ContainerType, Storage};
mod currency;
pub use currency::CurrencyStorage;
const MAX_NORMAL_STORAGE: usize = 35; const MAX_NORMAL_STORAGE: usize = 35;
const MAX_LARGE_STORAGE: usize = 50; const MAX_LARGE_STORAGE: usize = 50;
@ -37,6 +40,7 @@ pub struct Inventory {
pub armoury_bracelet: GenericStorage<MAX_NORMAL_STORAGE>, pub armoury_bracelet: GenericStorage<MAX_NORMAL_STORAGE>,
pub armoury_rings: GenericStorage<MAX_LARGE_STORAGE>, pub armoury_rings: GenericStorage<MAX_LARGE_STORAGE>,
pub armoury_soul_crystal: GenericStorage<MAX_NORMAL_STORAGE>, pub armoury_soul_crystal: GenericStorage<MAX_NORMAL_STORAGE>,
pub currency: CurrencyStorage,
} }
impl Default for Inventory { impl Default for Inventory {
@ -56,6 +60,7 @@ impl Default for Inventory {
armoury_bracelet: GenericStorage::default(), armoury_bracelet: GenericStorage::default(),
armoury_rings: GenericStorage::default(), armoury_rings: GenericStorage::default(),
armoury_soul_crystal: GenericStorage::default(), armoury_soul_crystal: GenericStorage::default(),
currency: CurrencyStorage::default(),
} }
} }
} }
@ -201,6 +206,7 @@ impl Inventory {
ContainerType::Inventory2 => &mut self.pages[2], ContainerType::Inventory2 => &mut self.pages[2],
ContainerType::Inventory3 => &mut self.pages[3], ContainerType::Inventory3 => &mut self.pages[3],
ContainerType::Equipped => &mut self.equipped, ContainerType::Equipped => &mut self.equipped,
ContainerType::Currency => &mut self.currency,
ContainerType::ArmoryOffWeapon => &mut self.armoury_off_hand, ContainerType::ArmoryOffWeapon => &mut self.armoury_off_hand,
ContainerType::ArmoryHead => &mut self.armoury_head, ContainerType::ArmoryHead => &mut self.armoury_head,
ContainerType::ArmoryBody => &mut self.armoury_body, ContainerType::ArmoryBody => &mut self.armoury_body,
@ -223,6 +229,7 @@ impl Inventory {
ContainerType::Inventory2 => &self.pages[2], ContainerType::Inventory2 => &self.pages[2],
ContainerType::Inventory3 => &self.pages[3], ContainerType::Inventory3 => &self.pages[3],
ContainerType::Equipped => &self.equipped, ContainerType::Equipped => &self.equipped,
ContainerType::Currency => &self.currency,
ContainerType::ArmoryOffWeapon => &self.armoury_off_hand, ContainerType::ArmoryOffWeapon => &self.armoury_off_hand,
ContainerType::ArmoryHead => &self.armoury_head, ContainerType::ArmoryHead => &self.armoury_head,
ContainerType::ArmoryBody => &self.armoury_body, ContainerType::ArmoryBody => &self.armoury_body,

View file

@ -15,6 +15,8 @@ pub enum ContainerType {
Equipped = 1000, Equipped = 1000,
Currency = 2000,
ArmoryOffWeapon = 3200, ArmoryOffWeapon = 3200,
ArmoryHead = 3201, ArmoryHead = 3201,
ArmoryBody = 3202, ArmoryBody = 3202,

View file

@ -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,
}

View file

@ -79,6 +79,9 @@ pub use equip::Equip;
mod client_trigger; mod client_trigger;
pub use client_trigger::{ClientTrigger, ClientTriggerCommand}; pub use client_trigger::{ClientTrigger, ClientTriggerCommand};
mod currency_info;
pub use currency_info::CurrencyInfo;
use crate::common::ObjectTypeId; use crate::common::ObjectTypeId;
use crate::common::Position; use crate::common::Position;
use crate::common::read_string; use crate::common::read_string;
@ -149,6 +152,7 @@ pub enum GameMasterCommandType {
GiveItem = 0xC8, GiveItem = 0xC8,
Aetheryte = 0x5E, Aetheryte = 0x5E,
TerritoryInfo = 0x25D, TerritoryInfo = 0x25D,
Gil = 0xC9,
} }
#[binrw] #[binrw]
@ -244,6 +248,8 @@ pub enum ServerZoneIpcData {
}, },
/// Used to control target information /// Used to control target information
ActorControlTarget(ActorControlTarget), ActorControlTarget(ActorControlTarget),
/// Used to update the player's currencies
CurrencyCrystalInfo(CurrencyInfo),
} }
#[binrw] #[binrw]
@ -374,7 +380,8 @@ pub enum ClientZoneIpcData {
event_id: u32, event_id: u32,
}, },
#[br(pre_assert(*magic == ClientZoneIpcType::EventHandlerReturn))] #[br(pre_assert(*magic == ClientZoneIpcType::EventHandlerReturn))]
EventHandlerReturn { // TODO: This is actually EventYieldHandler EventHandlerReturn {
// TODO: This is actually EventYieldHandler
handler_id: u32, handler_id: u32,
scene: u16, scene: u16,
error_code: u8, error_code: u8,

View file

@ -12,15 +12,16 @@ use crate::{
OBFUSCATION_ENABLED_MODE, OBFUSCATION_ENABLED_MODE,
common::{GameData, ObjectId, ObjectTypeId, Position, timestamp_secs}, common::{GameData, ObjectId, ObjectTypeId, Position, timestamp_secs},
config::{WorldConfig, get_config}, config::{WorldConfig, get_config},
inventory::{Inventory, Item}, inventory::{ContainerType, Inventory, Item},
ipc::{ ipc::{
chat::ServerChatIpcSegment, chat::ServerChatIpcSegment,
zone::{ zone::{
ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory, ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, ContainerInfo, ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, ContainerInfo,
DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, CurrencyInfo, DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone, ItemInfo, Move,
ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, NpcSpawn, ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData,
StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange, ServerZoneIpcSegment, StatusEffect, StatusEffectList, UpdateClassInfo, Warp,
WeatherChange,
}, },
}, },
opcodes::ServerZoneIpcType, opcodes::ServerZoneIpcType,
@ -484,33 +485,66 @@ impl ZoneConnection {
let mut sequence = 0; let mut sequence = 0;
for (container_type, container) in &self.player_data.inventory.clone() { for (container_type, container) in &self.player_data.inventory.clone() {
let mut send_slot = async |slot_index: u16, item: &Item| { // currencies
let ipc = ServerZoneIpcSegment { if container_type == ContainerType::Currency {
op_code: ServerZoneIpcType::UpdateItem, let mut send_currency = async |slot_index: u16, item: &Item| {
timestamp: timestamp_secs(), let ipc = ServerZoneIpcSegment {
data: ServerZoneIpcData::UpdateItem(ItemInfo { op_code: ServerZoneIpcType::CurrencyCrystalInfo,
sequence, timestamp: timestamp_secs(),
container: container_type, data: ServerZoneIpcData::CurrencyCrystalInfo(CurrencyInfo {
slot: slot_index, sequence,
quantity: item.quantity, container: item.id as u16,
catalog_id: item.id, slot: slot_index,
condition: 30000, quantity: item.quantity,
catalog_id: item.id,
..Default::default()
}),
..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 { for i in 0..container.max_slots() {
source_actor: self.player_data.actor_id, send_currency(i as u16, container.get_slot(i as u16)).await;
target_actor: self.player_data.actor_id, }
segment_type: SegmentType::Ipc, } else {
data: SegmentData::Ipc { data: ipc }, // items
})
.await;
};
for i in 0..container.max_slots() { let mut send_slot = async |slot_index: u16, item: &Item| {
send_slot(i as u16, container.get_slot(i as u16)).await; 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 // inform the client of container state