From 3b6fcb7ba19738368362bba15f1b0c75f73661de Mon Sep 17 00:00:00 2001 From: The Dax Date: Tue, 1 Jul 2025 07:52:50 -0400 Subject: [PATCH] Extend the Lua API: -Implement GM command collect (subtracts gil from player), but the ingame command's params are unknown, Lua only -Implement GM command inspect, but the IPC opcode for it is different than GMCommand and is thus far unimplemented, Lua only -The Lua API can now access all of the player's inventory (read-only) --- resources/opcodes.json | 6 +- resources/scripts/commands/Commands.lua | 8 ++ resources/scripts/commands/gm/Collect.lua | 12 +++ .../scripts/commands/gm/InspectPlayer.lua | 79 +++++++++++++++++ src/world/connection.rs | 4 + src/world/lua.rs | 87 +++++++++++++++++++ 6 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 resources/scripts/commands/gm/Collect.lua create mode 100644 resources/scripts/commands/gm/InspectPlayer.lua diff --git a/resources/opcodes.json b/resources/opcodes.json index 094cdd8..127c684 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -368,9 +368,9 @@ "size": 16 }, { - "name": "GilShopTransaction", - "opcode": 108, - "size": 24 + "name": "GilShopTransaction", + "opcode": 108, + "size": 24 } ], "ServerLobbyIpcType": [ diff --git a/resources/scripts/commands/Commands.lua b/resources/scripts/commands/Commands.lua index a5f3b9b..bd635c1 100644 --- a/resources/scripts/commands/Commands.lua +++ b/resources/scripts/commands/Commands.lua @@ -12,6 +12,8 @@ GM_EXP = 104 GM_ORCHESTRION = 116 GM_GIVE_ITEM = 200 GM_GIL = 201 +GM_COLLECT = 202 +GM_INSPECT = 422 GM_WIREFRAME = 550 GM_TERRITORY = 600 GM_TERRITORY_INFO = 605 @@ -20,11 +22,15 @@ registerGMCommand(GM_SET_LEVEL, GM_DIR.."SetLevel.lua") registerGMCommand(GM_CHANGE_WEATHER, GM_DIR.."ChangeWeather.lua") registerGMCommand(GM_SPEED, GM_DIR.."SetSpeed.lua") registerGMCommand(GM_INVISIBILITY, GM_DIR.."ToggleInvisibility.lua") +-- TODO: Implement the GMInspect IPC opcode, it's completely different than the normal GMCommand opcode +--registerGMCommand(GM_INSPECT, GM_DIR.."InspectPlayer.lua") registerGMCommand(GM_AETHERYTE, GM_DIR.."UnlockAetheryte.lua") registerGMCommand(GM_EXP, GM_DIR.."Exp.lua") registerGMCommand(GM_ORCHESTRION, GM_DIR.."Orchestrion.lua") registerGMCommand(GM_GIVE_ITEM, GM_DIR.."GiveItem.lua") registerGMCommand(GM_GIL, GM_DIR.."Gil.lua") +-- TODO: Figure out how the //gm collect command works ingame, its parameters are not the same as //gm gil +--registerGMCommand(GM_COLLECT, GM_DIR.."Collect.lua") registerGMCommand(GM_WIREFRAME, GM_DIR.."ToggleWireframe.lua") registerGMCommand(GM_TERRITORY, GM_DIR.."ChangeTerritory.lua") registerGMCommand(GM_TERRITORY_INFO, GM_DIR.."TerritoryInfo.lua") @@ -33,7 +39,9 @@ registerGMCommand(GM_TERRITORY_INFO, GM_DIR.."TerritoryInfo.lua") -- Please keep these in alphabetical order! registerCommand("classjob", DBG_DIR.."ClassJob.lua") +registerCommand("collect", GM_DIR.."Collect.lua") -- TODO: remove this once we figure out the //gm collect command's parameters (see comments above) registerCommand("festival", DBG_DIR.."Festival.lua") +registerCommand("inspect", GM_DIR.."InspectPlayer.lua") -- TODO: remove this once we figure out the GMInspect IPC opcode registerCommand("nudge", DBG_DIR.."Nudge.lua") registerCommand("ost", DBG_DIR.."OnScreenTest.lua") registerCommand("permtest", DBG_DIR.."PermissionTest.lua") diff --git a/resources/scripts/commands/gm/Collect.lua b/resources/scripts/commands/gm/Collect.lua new file mode 100644 index 0000000..81b2169 --- /dev/null +++ b/resources/scripts/commands/gm/Collect.lua @@ -0,0 +1,12 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[collect] " + +function onCommand(args, player) + local amount = tonumber(args[1]) + if player.gil >= amount then + player:remove_gil(amount) + printf(player, "Collected %s gil.", amount) + else + printf(player, "Player does not have that much gil to take! They only possess %s.", player.gil) + end +end diff --git a/resources/scripts/commands/gm/InspectPlayer.lua b/resources/scripts/commands/gm/InspectPlayer.lua new file mode 100644 index 0000000..da4ad60 --- /dev/null +++ b/resources/scripts/commands/gm/InspectPlayer.lua @@ -0,0 +1,79 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[inspect] " + +function getItemCondition(condition) + return (condition / 30000) * 100 +end + +function onCommand(args, player) + info = "\z + --- Info for player ---\n\z + Current region: %s\n\z + Current zone: %s (%s, %s)\n\z + Position: %.3f %.3f %.3f\n\z + Rotation: %.3f\n\z + --- Currency ---\n\z + Gil: %s\n\z + --- Equipped items ---\n\z + Main hand: (id: %s, condition: %s%%)\n\z + Off hand: (id: %s, condition: %s%%)\n\z + Head: (id: %s, condition: %s%%)\n\z + Body: (id: %s, condition: %s%%)\n\z + Hands: (id: %s, condition: %s%%)\n\z + Legs: (id: %s, condition: %s%%)\n\z + Feet: (id: %s, condition: %s%%)\n\z + Ears: (id: %s, condition: %s%%)\n\z + Neck: (id: %s, condition: %s%%)\n\z + Wrists: (id: %s, condition: %s%%)\n\z + Right Ring: (id: %s, condition: %s%%)\n\z + Left Ring: (id: %s, condition: %s%%)\n\z + Soul Crystal: (id: %s, condition: %s%%)\z + " + -- Skipping belts because they don't exist anymore. + main_hand = player.inventory.equipped.main_hand + off_hand = player.inventory.equipped.off_hand + head = player.inventory.equipped.head + body = player.inventory.equipped.body + hands = player.inventory.equipped.hands + legs = player.inventory.equipped.legs + feet = player.inventory.equipped.feet + ears = player.inventory.equipped.ears + neck = player.inventory.equipped.neck + wrists = player.inventory.equipped.wrists + rring = player.inventory.equipped.right_ring + lring = player.inventory.equipped.left_ring + scrystal = player.inventory.equipped.soul_crystal + + printf(player, info, + player.zone.region_name, player.zone.place_name, player.zone.internal_name, player.zone.id, + player.position.x, player.position.y, player.position.z, + player.rotation, player.gil, + main_hand.id, getItemCondition(main_hand.condition), + off_hand.id, getItemCondition(off_hand.condition), + head.id, getItemCondition(head.condition), + body.id, getItemCondition(body.condition), + hands.id, getItemCondition(hands.condition), + legs.id, getItemCondition(legs.condition), + feet.id, getItemCondition(feet.condition), + ears.id, getItemCondition(ears.condition), + neck.id, getItemCondition(neck.condition), + wrists.id, getItemCondition(wrists.condition), + lring.id, getItemCondition(lring.condition), + rring.id, getItemCondition(rring.condition), + scrystal.id, getItemCondition(scrystal.condition) + ) + + local NO_ITEM = 0 + + command_sender = "" -- hush further sender printfs, it looks ugly here + printf(player, "--- Player's inventory ---") + + for page_num, page in pairs(player.inventory.pages) do + printf(player, "--- Page %s ---", page_num) + for slot_num, slot in pairs(page.slots) do + if slot.id ~= NO_ITEM then + printf(player, "slot %s: (id: %s, condition: %s%%)", slot_num, slot.id, getItemCondition(slot.condition)) + end + end + end +end diff --git a/src/world/connection.rs b/src/world/connection.rs index 630fd80..fce42d0 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -745,6 +745,10 @@ impl ZoneConnection { self.player_data.inventory.currency.get_slot_mut(0).quantity += *amount; self.send_inventory(false).await; } + Task::RemoveGil { amount } => { + self.player_data.inventory.currency.get_slot_mut(0).quantity -= *amount; + self.send_inventory(false).await; + } Task::UnlockOrchestrion { id, on } => { // id == 0 means "all" if *id == 0 { diff --git a/src/world/lua.rs b/src/world/lua.rs index 4f47bd7..b9c4be9 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -6,6 +6,7 @@ use crate::{ write_quantized_rotation, }, config::get_config, + inventory::{CurrencyStorage, EquippedStorage, GenericStorage, Inventory, Item}, ipc::zone::{ ActionEffect, ActorControlCategory, ActorControlSelf, DamageElement, DamageKind, DamageType, EffectKind, EventScene, ServerZoneIpcData, ServerZoneIpcSegment, Warp, @@ -32,6 +33,7 @@ pub enum Task { SetLevel { level: i32 }, ChangeWeather { id: u16 }, AddGil { amount: u32 }, + RemoveGil { amount: u32 }, UnlockOrchestrion { id: u16, on: bool }, AddItem { id: u32 }, } @@ -353,6 +355,10 @@ impl LuaPlayer { self.queued_tasks.push(Task::AddGil { amount }); } + fn remove_gil(&mut self, amount: u32) { + self.queued_tasks.push(Task::RemoveGil { amount }); + } + fn unlock_orchestrion(&mut self, unlocked: u32, id: u16) { self.queued_tasks.push(Task::UnlockOrchestrion { id, @@ -478,6 +484,10 @@ impl UserData for LuaPlayer { this.add_gil(amount); Ok(()) }); + methods.add_method_mut("remove_gil", |_, this, amount: u32| { + this.remove_gil(amount); + Ok(()) + }); methods.add_method_mut("unlock_orchestrion", |_, this, (unlock, id): (u32, u16)| { this.unlock_orchestrion(unlock, id); Ok(()) @@ -501,7 +511,14 @@ impl UserData for LuaPlayer { }); fields.add_field_method_get("rotation", |_, this| Ok(this.player_data.rotation)); fields.add_field_method_get("position", |_, this| Ok(this.player_data.position)); + fields.add_field_method_get("inventory", |_, this| { + Ok(this.player_data.inventory.clone()) + }); fields.add_field_method_get("zone", |_, this| Ok(this.zone_data.clone())); + // Helper method to reduce the amount of typing for gil + fields.add_field_method_get("gil", |_, this| { + Ok(this.player_data.inventory.currency.gil.quantity) + }); } } @@ -519,6 +536,76 @@ impl UserData for Position { } } +impl UserData for Inventory { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("equipped", |_, this| Ok(this.equipped)); + fields.add_field_method_get("pages", |_, this| Ok(this.pages.clone())); + fields.add_field_method_get("armoury_main_hand", |_, this| { + Ok(this.armoury_main_hand.clone()) + }); + fields.add_field_method_get("armoury_head", |_, this| Ok(this.armoury_head.clone())); + fields.add_field_method_get("armoury_body", |_, this| Ok(this.armoury_body.clone())); + fields.add_field_method_get("armoury_hands", |_, this| Ok(this.armoury_hands.clone())); + fields.add_field_method_get("armoury_legs", |_, this| Ok(this.armoury_legs.clone())); + fields.add_field_method_get("armoury_feet", |_, this| Ok(this.armoury_feet.clone())); + fields.add_field_method_get("armoury_off_hand", |_, this| { + Ok(this.armoury_off_hand.clone()) + }); + fields.add_field_method_get("armoury_earring", |_, this| { + Ok(this.armoury_earring.clone()) + }); + fields.add_field_method_get("armoury_necklace", |_, this| { + Ok(this.armoury_necklace.clone()) + }); + fields.add_field_method_get("armoury_bracelet", |_, this| Ok(this.armoury_body.clone())); + fields.add_field_method_get("armoury_rings", |_, this| Ok(this.armoury_rings.clone())); + fields.add_field_method_get("armoury_soul_crystal", |_, this| { + Ok(this.armoury_soul_crystal.clone()) + }); + fields.add_field_method_get("currency", |_, this| Ok(this.currency)); + } +} + +impl UserData for Item { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("quantity", |_, this| Ok(this.quantity)); + fields.add_field_method_get("id", |_, this| Ok(this.id)); + fields.add_field_method_get("condition", |_, this| Ok(this.condition)); + fields.add_field_method_get("glamour_catalog_id", |_, this| Ok(this.condition)); + } +} + +impl UserData for CurrencyStorage { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("gil", |_, this| Ok(this.gil)); + } +} + +impl UserData for GenericStorage { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("slots", |_, this| Ok(this.slots.clone())); + } +} + +impl UserData for EquippedStorage { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("main_hand", |_, this| Ok(this.main_hand)); + fields.add_field_method_get("off_hand", |_, this| Ok(this.off_hand)); + fields.add_field_method_get("head", |_, this| Ok(this.head)); + fields.add_field_method_get("body", |_, this| Ok(this.body)); + fields.add_field_method_get("hands", |_, this| Ok(this.hands)); + fields.add_field_method_get("belt", |_, this| Ok(this.belt)); + fields.add_field_method_get("legs", |_, this| Ok(this.legs)); + fields.add_field_method_get("feet", |_, this| Ok(this.feet)); + fields.add_field_method_get("ears", |_, this| Ok(this.ears)); + fields.add_field_method_get("neck", |_, this| Ok(this.neck)); + fields.add_field_method_get("wrists", |_, this| Ok(this.wrists)); + fields.add_field_method_get("right_ring", |_, this| Ok(this.right_ring)); + fields.add_field_method_get("left_ring", |_, this| Ok(this.left_ring)); + fields.add_field_method_get("soul_crystal", |_, this| Ok(this.soul_crystal)); + } +} + impl UserData for ObjectTypeId {} impl FromLua for ObjectTypeId {