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 }