From 5accb992a9308192ee3cf0e036a34875ad05b72f Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Mon, 12 May 2025 22:32:03 -0400 Subject: [PATCH] Add !equip command to quickly change your equipped item See USAGE, this is actually extremely useful! No more hunting for item IDs! --- Cargo.toml | 2 +- USAGE.md | 1 + src/common/gamedata.rs | 101 ++++++++++++++++++++++++++++++++++++++ src/world/chat_handler.rs | 27 ++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index bf968df..116b505 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,4 +104,4 @@ rkon = { version = "0.1" } tower-http = { version = "0.6", features = ["fs"] } # excel sheet data -icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.04.16.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte"], default-features = false } +icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.04.16.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte", "EquipSlotCategory"], default-features = false } diff --git a/USAGE.md b/USAGE.md index bb3ddc9..087f056 100644 --- a/USAGE.md +++ b/USAGE.md @@ -88,6 +88,7 @@ These special debug commands start with `!` and are custom to Kawari. * `!spawnclone`: Spawn a clone of yourself * `!classjob `: Changes to another class/job * `!unlockaction `: Unlock an action, for example: `1` for Return and `4` for Teleport. +* `!equip `: Forcefully equip an item, useful for bypassing class/job and other client restrictions. This will *overwrite* any item in that slot! ### GM commands diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index 5d1e936..5db2a4b 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -1,5 +1,6 @@ use icarus::Aetheryte::AetheryteSheet; use icarus::ClassJob::ClassJobSheet; +use icarus::EquipSlotCategory::EquipSlotCategorySheet; use icarus::World::WorldSheet; use icarus::{Tribe::TribeSheet, Warp::WarpSheet}; use physis::common::{Language, Platform}; @@ -118,4 +119,104 @@ impl GameData { Some((*pop_range_id, *zone_id)) } + + /// 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 { + 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.FingerL().into_i8()?; + if *soul_crystal == 1 { + return Some(13); + } + + None + } } diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index f453473..b51d26b 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -1,5 +1,6 @@ use crate::{ common::{CustomizeData, ObjectId, ObjectTypeId, timestamp_secs}, + inventory::Storage, ipc::zone::{ ActorControl, ActorControlCategory, ActorControlSelf, BattleNpcSubKind, ChatMessage, CommonSpawn, EventStart, NpcSpawn, ObjectKind, OnlineStatus, ServerZoneIpcData, @@ -214,6 +215,32 @@ impl ChatHandler { }) .await; } + "!equip" => { + let (_, name) = chat_message.message.split_once(' ').unwrap(); + + { + let mut gamedata = connection.gamedata.lock().unwrap(); + + if let Some((equip_category, id)) = gamedata.get_item_by_name(name) { + let slot = gamedata.get_equipslot_category(equip_category).unwrap(); + + connection + .player_data + .inventory + .equipped + .get_slot_mut(slot as u16) + .id = id; + connection + .player_data + .inventory + .equipped + .get_slot_mut(slot as u16) + .quantity = 1; + } + } + + connection.send_inventory(true).await; + } _ => {} } }