From 6951f9448dab017480dd8a54cb25bd2d7cf7c3b0 Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 28 Jun 2025 09:56:04 -0400 Subject: [PATCH] Port GM commands to Lua This removes a ton of implementation overlap between the two command systems. For example, we had two different implementations of unlocking aetherytes which is just unnecessary. On the flipside, this makes implementing new GM commands just as easy as writing debug ones. I moved the existing debug Lua implementations into their GM counterparts and updated the USAGE accordingly. --- USAGE.md | 4 - resources/scripts/commands/Commands.lua | 35 ++- .../commands/debug/ChangeTerritory.lua | 23 -- .../commands/debug/UnlockAetheryte.lua | 39 ---- .../scripts/commands/gm/ChangeTerritory.lua | 9 + .../scripts/commands/gm/ChangeWeather.lua | 9 + resources/scripts/commands/gm/Exp.lua | 0 resources/scripts/commands/gm/Gil.lua | 9 + resources/scripts/commands/gm/Orchestrion.lua | 10 + resources/scripts/commands/gm/SetLevel.lua | 5 + .../commands/{debug => gm}/SetSpeed.lua | 12 +- .../scripts/commands/gm/TerritoryInfo.lua | 11 + .../{debug => gm}/ToggleInvisibility.lua | 2 - .../{debug => gm}/ToggleWireframe.lua | 2 - .../scripts/commands/gm/UnlockAetheryte.lua | 10 + src/bin/kawari-world.rs | 204 +++++++----------- src/ipc/zone/mod.rs | 21 +- src/world/connection.rs | 52 ++++- src/world/lua.rs | 72 +++++++ src/world/mod.rs | 2 +- src/world/zone.rs | 23 +- 21 files changed, 307 insertions(+), 247 deletions(-) delete mode 100644 resources/scripts/commands/debug/ChangeTerritory.lua delete mode 100644 resources/scripts/commands/debug/UnlockAetheryte.lua create mode 100644 resources/scripts/commands/gm/ChangeTerritory.lua create mode 100644 resources/scripts/commands/gm/ChangeWeather.lua create mode 100644 resources/scripts/commands/gm/Exp.lua create mode 100644 resources/scripts/commands/gm/Gil.lua create mode 100644 resources/scripts/commands/gm/Orchestrion.lua create mode 100644 resources/scripts/commands/gm/SetLevel.lua rename resources/scripts/commands/{debug => gm}/SetSpeed.lua (50%) create mode 100644 resources/scripts/commands/gm/TerritoryInfo.lua rename resources/scripts/commands/{debug => gm}/ToggleInvisibility.lua (67%) rename resources/scripts/commands/{debug => gm}/ToggleWireframe.lua (66%) create mode 100644 resources/scripts/commands/gm/UnlockAetheryte.lua diff --git a/USAGE.md b/USAGE.md index a70f74e..f81898a 100644 --- a/USAGE.md +++ b/USAGE.md @@ -113,10 +113,6 @@ These special debug commands start with `!` and are custom to Kawari. * `!nudge `: Teleport forward, back, up or down `distance` yalms. Specifying up or down will move the player up or down instead of forward or back. Examples: `!nudge 5 up` to move up 5 yalms, `!nudge 5` to move forward 5 yalms, `!nudge -5` to move backward 5 yalms. * `!festival `: Sets the festival in the current zone. Multiple festivals can be set together to create interesting effects. * `!reload`: Reloads `Global.lua` that is normally only loaded once at start-up. -* `!wireframe: Toggles the hidden GM wireframe rendering mode on or off.` -* `!invis: Toggles the hidden GM invisibility mode on or off for your character.` -* `!unlockaetheryte `: Unlock an aetheryte. For the first parameter, the literal words are required. -* `!teri `: Changes to the specified territory. * `!finishevent`: Forcefully finishes the current event, useful if the script has an error and you're stuck talking to something. ### GM commands diff --git a/resources/scripts/commands/Commands.lua b/resources/scripts/commands/Commands.lua index 2ed54de..a5f3b9b 100644 --- a/resources/scripts/commands/Commands.lua +++ b/resources/scripts/commands/Commands.lua @@ -1,16 +1,41 @@ DBG_DIR = "commands/debug/" +GM_DIR = "commands/gm/" +-- GM commands + +GM_SET_LEVEL = 1 +GM_CHANGE_WEATHER = 6 +GM_SPEED = 9 +GM_INVISIBILITY = 13 +GM_AETHERYTE = 94 +GM_EXP = 104 +GM_ORCHESTRION = 116 +GM_GIVE_ITEM = 200 +GM_GIL = 201 +GM_WIREFRAME = 550 +GM_TERRITORY = 600 +GM_TERRITORY_INFO = 605 + +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") +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") +registerGMCommand(GM_WIREFRAME, GM_DIR.."ToggleWireframe.lua") +registerGMCommand(GM_TERRITORY, GM_DIR.."ChangeTerritory.lua") +registerGMCommand(GM_TERRITORY_INFO, GM_DIR.."TerritoryInfo.lua") + +-- Debug commands -- Please keep these in alphabetical order! registerCommand("classjob", DBG_DIR.."ClassJob.lua") registerCommand("festival", DBG_DIR.."Festival.lua") -registerCommand("invis", DBG_DIR.."ToggleInvisibility.lua") registerCommand("nudge", DBG_DIR.."Nudge.lua") registerCommand("ost", DBG_DIR.."OnScreenTest.lua") registerCommand("permtest", DBG_DIR.."PermissionTest.lua") registerCommand("setpos", DBG_DIR.."SetPos.lua") -registerCommand("setspeed", DBG_DIR.."SetSpeed.lua") -registerCommand("teri", DBG_DIR.."ChangeTerritory.lua") registerCommand("unlock", DBG_DIR.."Unlock.lua") -registerCommand("unlockaetheryte", DBG_DIR.."UnlockAetheryte.lua") -registerCommand("wireframe", DBG_DIR.."ToggleWireframe.lua") diff --git a/resources/scripts/commands/debug/ChangeTerritory.lua b/resources/scripts/commands/debug/ChangeTerritory.lua deleted file mode 100644 index e1e6202..0000000 --- a/resources/scripts/commands/debug/ChangeTerritory.lua +++ /dev/null @@ -1,23 +0,0 @@ -required_rank = GM_RANK_DEBUG -command_sender = "[teri] " - -function onCommand(args, player) - local parts = split(args) - local argc = #parts - local usage = "\nThis command moves the user to a new zone/territory.\nUsage: !teri " - - if argc == 0 then - printf(player, "A territory id is required to use this command."..usage) - return - end - - local id = tonumber(parts[1]) - - if not id or id < 0 or id > 65535 then -- Must be in range of unsigned 16-bit value - printf(player, "Error parsing territory id! Make sure your input is an integer between 0 and 65535."..usage) - return - end - - player:change_territory(id) - printf(player, "Changing territory to %s.", id) -end diff --git a/resources/scripts/commands/debug/UnlockAetheryte.lua b/resources/scripts/commands/debug/UnlockAetheryte.lua deleted file mode 100644 index 5a72280..0000000 --- a/resources/scripts/commands/debug/UnlockAetheryte.lua +++ /dev/null @@ -1,39 +0,0 @@ -required_rank = GM_RANK_DEBUG -command_sender = "[unlockaetheryte] " - -function onCommand(args, player) - local parts = split(args) - local argc = #parts - local usage = "\nThis command unlocks an aetheryte for the user.\nUsage: !unlockaetheryte " - - if argc < 2 then - printf(player, "This command requires two parameters."..usage) - return - end - - local on = parts[1] - - if on == "on" then - on = 1 - elseif on == "off" then - on = 0 - else - printf(player, "Error parsing first parameter. Must be either of the words: 'on' or 'off'."..usage) - return - end - - local id = tonumber(parts[2]) - - if not id then - id = parts[2] - if id == "all" then - id = 0 - else - printf(player, "Error parsing id parameter. Must be an aetheryte id or the word 'all'."..usage) - return - end - end - - player:unlock_aetheryte(on, id) - printf(player, "Aetheryte(s) %s had their unlocked status changed!", id) -end diff --git a/resources/scripts/commands/gm/ChangeTerritory.lua b/resources/scripts/commands/gm/ChangeTerritory.lua new file mode 100644 index 0000000..49c480a --- /dev/null +++ b/resources/scripts/commands/gm/ChangeTerritory.lua @@ -0,0 +1,9 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[teri] " + +function onCommand(args, player) + local id = args[1] + + player:change_territory(id) + printf(player, "Changing territory to %s.", id) +end diff --git a/resources/scripts/commands/gm/ChangeWeather.lua b/resources/scripts/commands/gm/ChangeWeather.lua new file mode 100644 index 0000000..fc1ef61 --- /dev/null +++ b/resources/scripts/commands/gm/ChangeWeather.lua @@ -0,0 +1,9 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[weather] " + +function onCommand(args, player) + local id = args[1] + + player:change_weather(id) + printf(player, "Changing weather to %s.", id) +end diff --git a/resources/scripts/commands/gm/Exp.lua b/resources/scripts/commands/gm/Exp.lua new file mode 100644 index 0000000..e69de29 diff --git a/resources/scripts/commands/gm/Gil.lua b/resources/scripts/commands/gm/Gil.lua new file mode 100644 index 0000000..53a3a21 --- /dev/null +++ b/resources/scripts/commands/gm/Gil.lua @@ -0,0 +1,9 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[gil] " + +function onCommand(args, player) + local amount = args[1] + + player:add_gil(amount) + printf(player, "Added %s gil.", amount) +end diff --git a/resources/scripts/commands/gm/Orchestrion.lua b/resources/scripts/commands/gm/Orchestrion.lua new file mode 100644 index 0000000..824b6d7 --- /dev/null +++ b/resources/scripts/commands/gm/Orchestrion.lua @@ -0,0 +1,10 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[orchestrion] " + +function onCommand(args, player) + local on = args[1] -- TODO: reverse + local id = args[2] + + player:unlock_aetheryte(on, id) + printf(player, "Orchestrion(s) %s had their unlocked status changed!", id) +end diff --git a/resources/scripts/commands/gm/SetLevel.lua b/resources/scripts/commands/gm/SetLevel.lua new file mode 100644 index 0000000..ceaea97 --- /dev/null +++ b/resources/scripts/commands/gm/SetLevel.lua @@ -0,0 +1,5 @@ +required_rank = GM_RANK_DEBUG + +function onCommand(args, player) + player:set_level(args[1]) +end diff --git a/resources/scripts/commands/debug/SetSpeed.lua b/resources/scripts/commands/gm/SetSpeed.lua similarity index 50% rename from resources/scripts/commands/debug/SetSpeed.lua rename to resources/scripts/commands/gm/SetSpeed.lua index b3666c7..8861bc1 100644 --- a/resources/scripts/commands/debug/SetSpeed.lua +++ b/resources/scripts/commands/gm/SetSpeed.lua @@ -2,18 +2,8 @@ required_rank = GM_RANK_DEBUG command_sender = "[setspeed] " function onCommand(args, player) - local parts = split(args) - local argc = #parts - local usage = "\nThis command sets the user's speed to a desired multiplier.\nUsage: !setspeed " local SPEED_MAX = 10 -- Arbitrary, but it's more or less unplayable even at this amount - local speed_multiplier = tonumber(parts[1]) - - if argc == 1 and not speed_multiplier then - printf(player, "Error parsing speed multiplier! Make sure the multiplier is an integer."..usage) - return - elseif argc == 0 then - speed_multiplier = 1 - end + local speed_multiplier = args[1] if speed_multiplier <= 0 then speed_multiplier = 1 diff --git a/resources/scripts/commands/gm/TerritoryInfo.lua b/resources/scripts/commands/gm/TerritoryInfo.lua new file mode 100644 index 0000000..f5376ee --- /dev/null +++ b/resources/scripts/commands/gm/TerritoryInfo.lua @@ -0,0 +1,11 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[teri_info] " + +function onCommand(args, player) + local teri_info = "Territory Info for zone "..player.zone.id..":" + local current_weather = "Current weather: "..player.zone.weather_id + local internal_name = "Internal name: "..player.zone.internal_name + local region_name = "Region name: "..player.zone.region_name + local place_name = "Place name: "..player.zone.place_name + printf(player, teri_info.."\n"..current_weather.."\n"..internal_name.."\n"..region_name.."\n"..place_name) +end diff --git a/resources/scripts/commands/debug/ToggleInvisibility.lua b/resources/scripts/commands/gm/ToggleInvisibility.lua similarity index 67% rename from resources/scripts/commands/debug/ToggleInvisibility.lua rename to resources/scripts/commands/gm/ToggleInvisibility.lua index b611fda..1b41258 100644 --- a/resources/scripts/commands/debug/ToggleInvisibility.lua +++ b/resources/scripts/commands/gm/ToggleInvisibility.lua @@ -2,8 +2,6 @@ required_rank = GM_RANK_DEBUG command_sender = "[invis] " function onCommand(args, player) - local usage = "\nThis command makes the user invisible to all other actors." - player:toggle_invisibility() printf(player, "Invisibility toggled.") end diff --git a/resources/scripts/commands/debug/ToggleWireframe.lua b/resources/scripts/commands/gm/ToggleWireframe.lua similarity index 66% rename from resources/scripts/commands/debug/ToggleWireframe.lua rename to resources/scripts/commands/gm/ToggleWireframe.lua index 630f0fc..0631e8e 100644 --- a/resources/scripts/commands/debug/ToggleWireframe.lua +++ b/resources/scripts/commands/gm/ToggleWireframe.lua @@ -2,8 +2,6 @@ required_rank = GM_RANK_DEBUG command_sender = "[wireframe] " function onCommand(args, player) - local usage = "\nThis command allows the user to view the world in wireframe mode." - player:toggle_wireframe() printf(player, "Wireframe mode toggled.") end diff --git a/resources/scripts/commands/gm/UnlockAetheryte.lua b/resources/scripts/commands/gm/UnlockAetheryte.lua new file mode 100644 index 0000000..1243f33 --- /dev/null +++ b/resources/scripts/commands/gm/UnlockAetheryte.lua @@ -0,0 +1,10 @@ +required_rank = GM_RANK_DEBUG +command_sender = "[unlockaetheryte] " + +function onCommand(args, player) + local on = args[1] -- TODO: reverse + local id = args[2] + + player:unlock_aetheryte(on, id) + printf(player, "Aetheryte(s) %s had their unlocked status changed!", id) +end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index c4156f1..aa08f72 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -3,24 +3,23 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use kawari::RECEIVE_BUFFER_SIZE; -use kawari::common::{GameData, TerritoryNameKind, timestamp_secs}; -use kawari::common::{Position, value_to_flag_byte_index_value}; +use kawari::common::Position; +use kawari::common::{GameData, timestamp_secs}; use kawari::config::get_config; -use kawari::inventory::{Item, Storage}; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::zone::{ ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList, }; use kawari::ipc::zone::{ - ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterCommandType, - GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType, + ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterRank, OnlineStatus, + ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType, }; use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType}; use kawari::packet::oodle::OodleNetwork; use kawari::packet::{ ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive, }; -use kawari::world::{ChatHandler, ExtraLuaState, Zone, ZoneConnection, load_init_script}; +use kawari::world::{ChatHandler, ExtraLuaState, LuaZone, Zone, ZoneConnection, load_init_script}; use kawari::world::{ ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer, WorldDatabase, handle_custom_ipc, server_main_loop, @@ -617,136 +616,70 @@ async fn client_loop( } } ClientZoneIpcData::GMCommand { command, arg0, arg1, .. } => { - tracing::info!("Got a game master command!"); + let lua = lua.lock().unwrap(); + let state = lua.app_data_ref::().unwrap(); - match &command { - GameMasterCommandType::SetLevel => { - connection.player_data.set_current_level(*arg0 as i32); - connection.update_class_info().await; - } - GameMasterCommandType::ChangeWeather => { - connection.change_weather(*arg0 as u16).await - } - GameMasterCommandType::Speed => { - // TODO: Maybe allow setting the speed of a targeted player too? - connection - .actor_control_self(ActorControlSelf { - category: - ActorControlCategory::Flee { - speed: *arg0 as u16, - }, - }) - .await - } - GameMasterCommandType::ChangeTerritory => { - connection.change_zone(*arg0 as u16).await - } - GameMasterCommandType::ToggleInvisibility => { - connection.player_data.gm_invisible = !connection.player_data.gm_invisible; - connection - .actor_control_self(ActorControlSelf { - category: - ActorControlCategory::ToggleInvisibility { - invisible: connection.player_data.gm_invisible, - }, - }) - .await - } - GameMasterCommandType::ToggleWireframe => connection - .actor_control_self(ActorControlSelf { - category: - ActorControlCategory::ToggleWireframeRendering(), - }) - .await, - GameMasterCommandType::GiveItem => { - connection.player_data.inventory.add_in_next_free_slot(Item { id: *arg0, quantity: 1 }); - connection.send_inventory(false).await; - } - GameMasterCommandType::Orchestrion => { - let on = *arg0 == 1; // This command uses 1 for on, 2 for off. - let id = *arg1 as u16; + if let Some(command_script) = + state.gm_command_scripts.get(command) + { + let file_name = format!( + "{}/{}", + &config.world.scripts_location, command_script + ); - // id == 0 means "all" - if id == 0 { - /* Currently 792 songs ingame. - * Commented out because this learns literally zero songs - * for some unknown reason. */ - /*for i in 1..793 { - let idd = i as u16; - connection.send_message("test!").await; - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } }).await; - }*/ - } else { - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } - }).await; - } - } - GameMasterCommandType::Aetheryte => { - let on = *arg0 == 0; - let id = *arg1; + let mut run_script = || -> mlua::Result<()> { + lua.scope(|scope| { + let connection_data = scope + .create_userdata_ref_mut(&mut lua_player)?; - // id == 0 means "all" - if id == 0 { - for i in 1..239 { - let (value, index) = value_to_flag_byte_index_value(i); - if on { - connection.player_data.aetherytes[index as usize] |= value; - } else { - connection.player_data.aetherytes[index as usize] ^= value; + lua.load( + std::fs::read(&file_name) + .expect(format!("Failed to load script file {}!", &file_name).as_str()), + ) + .set_name("@".to_string() + &file_name) + .exec()?; + + let required_rank = lua.globals().get("required_rank"); + if let Err(error) = required_rank { + tracing::info!("Script is missing required_rank! Unable to run command, sending error to user. Additional information: {}", error); + let func: Function = + lua.globals().get("onCommandRequiredRankMissingError")?; + func.call::<()>((error.to_string(), connection_data))?; + return Ok(()); + } + + /* Reset state for future commands. Without this it'll stay set to the last value + * and allow other commands that omit required_rank to run, which is undesirable. */ + lua.globals().set("required_rank", mlua::Value::Nil)?; + + if connection.player_data.gm_rank as u8 >= required_rank? { + let func: Function = + lua.globals().get("onCommand")?; + func.call::<()>(([*arg0, *arg1], connection_data))?; + + /* `command_sender` is an optional variable scripts can define to identify themselves in print messages. + * It's okay if this global isn't set. We also don't care what its value is, just that it exists. + * This is reset -after- running the command intentionally. Resetting beforehand will never display the command's identifier. + */ + let command_sender: Result = lua.globals().get("command_sender"); + if let Ok(_) = command_sender { + lua.globals().set("command_sender", mlua::Value::Nil)?; } - - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::LearnTeleport { id: i, unlocked: on } }).await; - } - } else { - let (value, index) = value_to_flag_byte_index_value(id); - if on { - connection.player_data.aetherytes[index as usize] |= value; + Ok(()) } else { - connection.player_data.aetherytes[index as usize] ^= value; + tracing::info!("User with account_id {} tried to invoke GM command {} with insufficient privileges!", + connection.player_data.account_id, command); + let func: Function = + lua.globals().get("onCommandRequiredRankInsufficientError")?; + func.call::<()>(connection_data)?; + Ok(()) } + }) + }; - connection.actor_control_self(ActorControlSelf { - category: ActorControlCategory::LearnTeleport { id, unlocked: on } }).await; - } + if let Err(err) = run_script() { + tracing::warn!("Lua error in {file_name}: {:?}", err); } - GameMasterCommandType::EXP => { - let amount = *arg0; - connection.player_data.set_current_exp(connection.player_data.current_exp() + amount); - connection.update_class_info().await; - } - - GameMasterCommandType::TerritoryInfo => { - let id: u32 = connection.zone.as_ref().unwrap().id.into(); - let weather_id; - let internal_name; - let place_name; - let region_name; - { - let mut game_data = connection.gamedata.lock().unwrap(); - // TODO: Maybe the current weather should be cached somewhere like the zone id? - weather_id = game_data - .get_weather(id) - .unwrap_or(1) as u16; - let fallback = ""; - internal_name = game_data.get_territory_name(id, TerritoryNameKind::Internal).unwrap_or(fallback.to_string()); - region_name = game_data.get_territory_name(id, TerritoryNameKind::Region).unwrap_or(fallback.to_string()); - place_name = game_data.get_territory_name(id, TerritoryNameKind::Place).unwrap_or(fallback.to_string()); - } - - connection.send_message(format!(concat!("Territory Info for zone {}:\n", - "Current weather: {}\n", - "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 { @@ -770,7 +703,7 @@ async fn client_loop( // find the pop range on the other side let mut game_data = game_data.lock().unwrap(); - let new_zone = Zone::load(&mut game_data.game_data, exit_box.territory_type); + let new_zone = Zone::load(&mut game_data, exit_box.territory_type); let (destination_object, _) = new_zone .find_pop_range(exit_box.destination_instance_id) .unwrap(); @@ -977,6 +910,16 @@ async fn client_loop( // update lua player lua_player.player_data = connection.player_data.clone(); lua_player.status_effects = connection.status_effects.clone(); + + if let Some(zone) = &connection.zone { + lua_player.zone_data = LuaZone { + zone_id: zone.id, + weather_id: connection.weather_id, + internal_name: zone.internal_name.clone(), + region_name: zone.region_name.clone(), + place_name: zone.place_name.clone(), + }; + } } }, Err(_) => { @@ -1092,7 +1035,8 @@ async fn main() { exit_position: None, exit_rotation: None, last_keep_alive: Instant::now(), - gracefully_logged_out: false + gracefully_logged_out: false, + weather_id: 0, }); } Some((mut socket, _)) = handle_rcon(&rcon_listener) => { diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 88d505e..35fd96b 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -148,24 +148,6 @@ impl Default for ServerZoneIpcSegment { } } -#[binrw] -#[brw(repr = u8)] -#[derive(Clone, PartialEq, Debug)] -pub enum GameMasterCommandType { - SetLevel = 0x1, - ChangeWeather = 0x6, - Speed = 0x9, - ToggleInvisibility = 0xD, - ToggleWireframe = 0x26, - ChangeTerritory = 0x58, - EXP = 0x68, - Orchestrion = 0x74, - GiveItem = 0xC8, - Aetheryte = 0x5E, - TerritoryInfo = 0x25D, - Gil = 0xC9, -} - #[binrw] #[br(import(magic: &ServerZoneIpcType, _size: &u32))] #[derive(Debug, Clone)] @@ -405,8 +387,7 @@ pub enum ClientZoneIpcData { /// Sent by the client when they send a GM command. This can only be sent by the client if they are sent a GM rank. #[br(pre_assert(*magic == ClientZoneIpcType::GMCommand))] GMCommand { - #[brw(pad_after = 3)] // padding - command: GameMasterCommandType, + command: u32, arg0: u32, arg1: u32, arg2: u32, diff --git a/src/world/connection.rs b/src/world/connection.rs index 931bb89..26dd0c4 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -14,7 +14,7 @@ use crate::{ GameData, ObjectId, ObjectTypeId, Position, timestamp_secs, value_to_flag_byte_index_value, }, config::{WorldConfig, get_config}, - inventory::{ContainerType, Inventory, Item}, + inventory::{ContainerType, Inventory, Item, Storage}, ipc::{ chat::ServerChatIpcSegment, zone::{ @@ -46,6 +46,7 @@ pub struct ExtraLuaState { pub action_scripts: HashMap, pub event_scripts: HashMap, pub command_scripts: HashMap, + pub gm_command_scripts: HashMap, } #[derive(Debug, Default, Clone)] @@ -132,6 +133,9 @@ pub struct ZoneConnection { /// Whether the player was gracefully logged out pub gracefully_logged_out: bool, + + // TODO: really needs to be moved somewhere else + pub weather_id: u16, } impl ZoneConnection { @@ -350,7 +354,7 @@ impl ZoneConnection { // load the new zone now { let mut game_data = self.gamedata.lock().unwrap(); - self.zone = Some(Zone::load(&mut game_data.game_data, new_zone_id)); + self.zone = Some(Zone::load(&mut game_data, new_zone_id)); } self.player_data.zone_id = new_zone_id; @@ -362,10 +366,9 @@ impl ZoneConnection { { let config = get_config(); - let weather_id; { let mut game_data = self.gamedata.lock().unwrap(); - weather_id = game_data + self.weather_id = game_data .get_weather(self.zone.as_ref().unwrap().id.into()) .unwrap_or(1) as u16; } @@ -375,7 +378,7 @@ impl ZoneConnection { timestamp: timestamp_secs(), data: ServerZoneIpcData::InitZone(InitZone { territory_type: self.zone.as_ref().unwrap().id, - weather_id, + weather_id: self.weather_id, obsfucation_mode: if config.world.enable_packet_obsfucation { OBFUSCATION_ENABLED_MODE } else { @@ -405,7 +408,7 @@ impl ZoneConnection { .get_warp(warp_id) .expect("Failed to find the warp!"); - let new_zone = Zone::load(&mut game_data.game_data, zone_id); + let new_zone = Zone::load(&mut game_data, zone_id); // find it on the other side if let Some((object, _)) = new_zone.find_pop_range(pop_range_id) { @@ -438,7 +441,7 @@ impl ZoneConnection { .get_aetheryte(aetheryte_id) .expect("Failed to find the aetheryte!"); - let new_zone = Zone::load(&mut game_data.game_data, zone_id); + let new_zone = Zone::load(&mut game_data, zone_id); // find it on the other side if let Some((object, _)) = new_zone.find_pop_range(pop_range_id) { @@ -462,6 +465,8 @@ impl ZoneConnection { } pub async fn change_weather(&mut self, new_weather_id: u16) { + self.weather_id = new_weather_id; + let ipc = ServerZoneIpcSegment { op_code: ServerZoneIpcType::WeatherId, timestamp: timestamp_secs(), @@ -729,6 +734,39 @@ impl ZoneConnection { .await; } } + Task::SetLevel { level } => { + self.player_data.set_current_level(*level); + self.update_class_info().await; + } + Task::ChangeWeather { id } => { + self.change_weather(*id).await; + } + Task::AddGil { amount } => { + self.player_data.inventory.currency.get_slot_mut(0).quantity += *amount as u32; + self.send_inventory(false).await; + } + Task::UnlockOrchestrion { id, on } => { + // id == 0 means "all" + if *id == 0 { + /* Currently 792 songs ingame. + * Commented out because this learns literally zero songs + * for some unknown reason. */ + /*for i in 1..793 { + let idd = i as u16; + connection.send_message("test!").await; + connection.actor_control_self(ActorControlSelf { + category: ActorControlCategory::ToggleOrchestrionUnlock { song_id: id, unlocked: on } }).await; + }*/ + } else { + self.actor_control_self(ActorControlSelf { + category: ActorControlCategory::ToggleOrchestrionUnlock { + song_id: *id, + unlocked: *on, + }, + }) + .await; + } + } } } player.queued_tasks.clear(); diff --git a/src/world/lua.rs b/src/world/lua.rs index e5b3bca..ab1d0e2 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -29,6 +29,29 @@ pub enum Task { ToggleInvisibility { invisible: bool }, Unlock { id: u32 }, UnlockAetheryte { id: u32, on: bool }, + SetLevel { level: i32 }, + ChangeWeather { id: u16 }, + AddGil { amount: u32 }, + UnlockOrchestrion { id: u16, on: bool }, +} + +#[derive(Default, Clone)] +pub struct LuaZone { + pub zone_id: u16, + pub weather_id: u16, + pub internal_name: String, + pub region_name: String, + pub place_name: String, +} + +impl UserData for LuaZone { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("id", |_, this| Ok(this.zone_id)); + fields.add_field_method_get("weather_id", |_, this| Ok(this.weather_id)); + fields.add_field_method_get("internal_name", |_, this| Ok(this.internal_name.clone())); + fields.add_field_method_get("region_name", |_, this| Ok(this.region_name.clone())); + fields.add_field_method_get("place_name", |_, this| Ok(this.place_name.clone())); + } } #[derive(Default)] @@ -37,6 +60,7 @@ pub struct LuaPlayer { pub status_effects: StatusEffects, pub queued_segments: Vec>, pub queued_tasks: Vec, + pub zone_data: LuaZone, } impl LuaPlayer { @@ -197,11 +221,31 @@ impl LuaPlayer { fn reload_scripts(&mut self) { self.queued_tasks.push(Task::ReloadScripts); } + fn toggle_invisiblity(&mut self) { self.queued_tasks.push(Task::ToggleInvisibility { invisible: !self.player_data.gm_invisible, }); } + + fn set_level(&mut self, level: i32) { + self.queued_tasks.push(Task::SetLevel { level }); + } + + fn change_weather(&mut self, id: u16) { + self.queued_tasks.push(Task::ChangeWeather { id }); + } + + fn add_gil(&mut self, amount: u32) { + self.queued_tasks.push(Task::AddGil { amount }); + } + + fn unlock_orchestrion(&mut self, unlocked: u32, id: u16) { + self.queued_tasks.push(Task::UnlockOrchestrion { + id, + on: unlocked == 1, + }); + } } impl UserData for LuaPlayer { @@ -317,6 +361,22 @@ impl UserData for LuaPlayer { this.reload_scripts(); Ok(()) }); + methods.add_method_mut("set_level", |_, this, level: i32| { + this.set_level(level); + Ok(()) + }); + methods.add_method_mut("change_weather", |_, this, id: u16| { + this.change_weather(id); + Ok(()) + }); + methods.add_method_mut("add_gil", |_, this, amount: u32| { + this.add_gil(amount); + Ok(()) + }); + methods.add_method_mut("unlock_orchestrion", |_, this, (unlock, id): (u32, u16)| { + this.unlock_orchestrion(unlock, id); + Ok(()) + }); } fn add_fields>(fields: &mut F) { @@ -332,6 +392,7 @@ 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("zone", |_, this| Ok(this.zone_data.clone())); } } @@ -441,11 +502,22 @@ pub fn load_init_script(lua: &mut Lua) -> mlua::Result<()> { Ok(()) })?; + let register_gm_command_func = + lua.create_function(|lua, (command_type, command_script): (u32, String)| { + let mut state = lua.app_data_mut::().unwrap(); + let _ = state + .gm_command_scripts + .insert(command_type, command_script); + Ok(()) + })?; + lua.set_app_data(ExtraLuaState::default()); lua.globals().set("registerAction", register_action_func)?; lua.globals().set("registerEvent", register_event_func)?; lua.globals() .set("registerCommand", register_command_func)?; + lua.globals() + .set("registerGMCommand", register_gm_command_func)?; let effectsbuilder_constructor = lua.create_function(|_, ()| Ok(EffectsBuilder::default()))?; lua.globals() diff --git a/src/world/mod.rs b/src/world/mod.rs index 8658cf1..be805d2 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -11,7 +11,7 @@ mod database; pub use database::{CharacterData, WorldDatabase}; mod lua; -pub use lua::{EffectsBuilder, LuaPlayer, load_init_script}; +pub use lua::{EffectsBuilder, LuaPlayer, LuaZone, load_init_script}; mod event; pub use event::Event; diff --git a/src/world/zone.rs b/src/world/zone.rs index fd86785..300b03f 100644 --- a/src/world/zone.rs +++ b/src/world/zone.rs @@ -1,16 +1,20 @@ use icarus::TerritoryType::TerritoryTypeSheet; use physis::{ common::Language, - gamedata::GameData, layer::{ ExitRangeInstanceObject, InstanceObject, LayerEntryData, LayerGroup, PopRangeInstanceObject, }, }; +use crate::common::{GameData, TerritoryNameKind}; + /// Represents a loaded zone #[derive(Default, Debug)] pub struct Zone { pub id: u16, + pub internal_name: String, + pub region_name: String, + pub place_name: String, planevent: Option, vfx: Option, planmap: Option, @@ -27,7 +31,8 @@ impl Zone { ..Default::default() }; - let sheet = TerritoryTypeSheet::read_from(game_data, Language::None).unwrap(); + let sheet = + TerritoryTypeSheet::read_from(&mut game_data.game_data, Language::None).unwrap(); let Some(row) = sheet.get_row(id as u32) else { tracing::warn!("Invalid zone id {id}, allowing anyway..."); return zone; @@ -42,7 +47,7 @@ impl Zone { let mut load_lgb = |name: &str| -> Option { let path = format!("bg/{}/level/{}.lgb", &bg_path[..level_index], name); - let lgb_file = game_data.extract(&path)?; + let lgb_file = game_data.game_data.extract(&path)?; tracing::info!("Loading {path}"); let lgb = LayerGroup::from_existing(&lgb_file); if lgb.is_none() { @@ -61,6 +66,18 @@ impl Zone { zone.sound = load_lgb("sound"); zone.planlive = load_lgb("planlive"); + // load names + let fallback = ""; + zone.internal_name = game_data + .get_territory_name(id as u32, TerritoryNameKind::Internal) + .unwrap_or(fallback.to_string()); + zone.region_name = game_data + .get_territory_name(id as u32, TerritoryNameKind::Region) + .unwrap_or(fallback.to_string()); + zone.place_name = game_data + .get_territory_name(id as u32, TerritoryNameKind::Place) + .unwrap_or(fallback.to_string()); + zone }