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 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 gil <amount>`: Adds the specified amount of gil to the player

View file

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

View file

@ -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 {

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};
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GenericStorage<const N: usize> {
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> {
fn max_slots(&self) -> u32 {
N as u32

View file

@ -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<MAX_NORMAL_STORAGE>,
pub armoury_rings: GenericStorage<MAX_LARGE_STORAGE>,
pub armoury_soul_crystal: GenericStorage<MAX_NORMAL_STORAGE>,
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,

View file

@ -15,6 +15,8 @@ pub enum ContainerType {
Equipped = 1000,
Currency = 2000,
ArmoryOffWeapon = 3200,
ArmoryHead = 3201,
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;
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,

View file

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