diff --git a/resources/scripts/actions/Sprint.lua b/resources/scripts/actions/Sprint.lua index fce4490..9d42ae0 100644 --- a/resources/scripts/actions/Sprint.lua +++ b/resources/scripts/actions/Sprint.lua @@ -1,9 +1,9 @@ +EFFECT_SPRINT = 50 + function doAction(player) effects = EffectsBuilder() - -- TODO: go through effectsbuilder - -- give sprint - player:give_status_effect(50, 5.0) + effects:gain_effect(EFFECT_SPRINT) return effects end diff --git a/resources/scripts/effects/Effects.lua b/resources/scripts/effects/Effects.lua index 586d0cc..e2d02da 100644 --- a/resources/scripts/effects/Effects.lua +++ b/resources/scripts/effects/Effects.lua @@ -1,2 +1,2 @@ -registerEffect(50, "Sprint.lua") -registerEffect(4209, "Jog.lua") +registerEffect(50, "effects/Sprint.lua") +registerEffect(4209, "effects/Jog.lua") diff --git a/resources/scripts/effects/Sprint.lua b/resources/scripts/effects/Sprint.lua index 376e134..38882ea 100644 --- a/resources/scripts/effects/Sprint.lua +++ b/resources/scripts/effects/Sprint.lua @@ -1,9 +1,9 @@ -EFFECT_JOG = 4029 +EFFECT_JOG = 4209 function onGain(player) -- it does nothing end function onLose(player) - player:gain_effect(EFFECT_JOG) + player:gain_effect(EFFECT_JOG, 20.0) end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index ff539fc..80c6dc7 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -1191,6 +1191,7 @@ async fn client_loop( FromServer::UpdateConfig(actor_id, config) => connection.update_config(actor_id, config).await, FromServer::ActorEquip(actor_id, main_weapon_id, model_ids) => connection.update_equip(actor_id, main_weapon_id, model_ids).await, FromServer::ReplayPacket(segment) => connection.send_segment(segment).await, + FromServer::LoseEffect(effect_id) => connection.lose_effect(effect_id, &mut lua_player).await, }, None => break, } diff --git a/src/ipc/zone/action_result.rs b/src/ipc/zone/action_result.rs index d848e5b..73e2210 100644 --- a/src/ipc/zone/action_result.rs +++ b/src/ipc/zone/action_result.rs @@ -40,7 +40,7 @@ pub enum EffectKind { #[brw(magic = 27u8)] BeginCombo, #[brw(magic = 14u8)] - Unk1 { unk1: u8, unk2: u32, effect_id: u8 }, // seen during sprint + Unk1 { unk1: u8, unk2: u32, effect_id: u16 }, // seen during sprint } #[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Deserialize, Serialize)] diff --git a/src/ipc/zone/effect_result.rs b/src/ipc/zone/effect_result.rs index 4f44117..4661789 100644 --- a/src/ipc/zone/effect_result.rs +++ b/src/ipc/zone/effect_result.rs @@ -4,7 +4,7 @@ use crate::common::ObjectId; #[binrw] #[brw(little)] -#[derive(Clone, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub struct EffectEntry { pub index: u8, pub unk1: u8, diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index 9a9d711..1f9e364 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -95,7 +95,7 @@ mod quest_active_list; pub use quest_active_list::QuestActiveList; mod effect_result; -pub use effect_result::EffectResult; +pub use effect_result::{EffectEntry, EffectResult}; use crate::COMPLETED_LEVEQUEST_BITMASK_SIZE; use crate::COMPLETED_QUEST_BITMASK_SIZE; diff --git a/src/world/common.rs b/src/world/common.rs index d64b2a7..0b49346 100644 --- a/src/world/common.rs +++ b/src/world/common.rs @@ -45,7 +45,10 @@ pub enum FromServer { UpdateConfig(u32, Config), /// Update an actor's model IDs. ActorEquip(u32, u64, [u32; 10]), + /// Informs the connection to replay packet data to the client. ReplayPacket(PacketSegment), + /// The player should lose this effect. + LoseEffect(u16), } #[derive(Debug, Clone)] @@ -110,6 +113,8 @@ pub enum ToServer { Equip(ClientId, u32, u64, [u32; 10]), /// Begins a packet replay. BeginReplay(ClientId, String), + /// The player gains an effect. + GainEffect(ClientId, u32, u16, f32), } #[derive(Clone, Debug)] diff --git a/src/world/connection.rs b/src/world/connection.rs index faba077..cd5653d 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -12,8 +12,8 @@ use crate::{ CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE, ERR_INVENTORY_ADD_FAILED, common::{ - GameData, ItemInfoQuery, ObjectId, ObjectTypeId, Position, timestamp_secs, - value_to_flag_byte_index_value, + GameData, INVALID_OBJECT_ID, ItemInfoQuery, ObjectId, ObjectTypeId, Position, + timestamp_secs, value_to_flag_byte_index_value, }, config::{WorldConfig, get_config}, inventory::{ContainerType, Inventory, Item, Storage}, @@ -22,10 +22,10 @@ use crate::{ zone::{ ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory, ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, Config, - ContainerInfo, CurrencyInfo, DisplayFlag, EffectKind, Equip, EventScene, - GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats, - PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect, - StatusEffectList, UpdateClassInfo, Warp, WeatherChange, + ContainerInfo, CurrencyInfo, DisplayFlag, EffectEntry, EffectKind, EffectResult, Equip, + EventScene, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind, + PlayerStats, PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment, + StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange, }, }, opcodes::ServerZoneIpcType, @@ -1345,9 +1345,6 @@ impl ZoneConnection { // tell them the action results if let Some(effects_builder) = effects_builder { - let mut effects = [ActionEffect::default(); 8]; - effects[..effects_builder.effects.len()].copy_from_slice(&effects_builder.effects); - if let Some(actor) = self.get_actor_mut(request.target.object_id) { for effect in &effects_builder.effects { match effect.kind { @@ -1362,34 +1359,102 @@ impl ZoneConnection { self.update_hp_mp(actor.id, actor.hp, 10000).await; } - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::ActionResult, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::ActionResult(ActionResult { - main_target: request.target, - target_id_again: request.target, - action_id: request.action_key, - animation_lock_time: 0.6, - rotation: self.player_data.rotation, - action_animation_id: request.action_key as u16, // assuming action id == animation id - flag: 1, - effect_count: effects_builder.effects.len() as u8, - effects, - unk1: 2662353, - unk2: 3758096384, - hidden_animation: 1, - ..Default::default() - }), - ..Default::default() - }; + // TODO: send Cooldown ActorControlSelf - 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; + // ActionResult + { + let mut effects = [ActionEffect::default(); 8]; + effects[..effects_builder.effects.len()].copy_from_slice(&effects_builder.effects); + + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::ActionResult, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::ActionResult(ActionResult { + main_target: request.target, + target_id_again: request.target, + action_id: request.action_key, + animation_lock_time: 0.6, + rotation: self.player_data.rotation, + action_animation_id: request.action_key as u16, // assuming action id == animation id + flag: 1, + effect_count: effects_builder.effects.len() as u8, + effects, + unk1: 2662353, + unk2: 3758096384, + hidden_animation: 1, + ..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; + } + + // EffectResult + // TODO: is this always sent? needs investigation + { + let mut num_entries = 0u8; + let mut entries = [EffectEntry::default(); 4]; + + for effect in &effects_builder.effects { + if let EffectKind::Unk1 { effect_id, .. } = effect.kind { + entries[num_entries as usize] = EffectEntry { + index: num_entries, + unk1: 0, + id: effect_id, + param: 30, + unk2: 0, + duration: 20.0, + source_actor_id: INVALID_OBJECT_ID, + }; + num_entries += 1; + + // also inform the server of our new status effect + self.handle + .send(ToServer::GainEffect( + self.id, + self.player_data.actor_id, + effect_id, + 20.0, // TODO: fill out + )) + .await; + } + } + + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::EffectResult, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::EffectResult(EffectResult { + unk1: 1, + unk2: 776386, + target_id: request.target.object_id, + current_hp: 0, + max_hp: 0, + current_mp: 0, + unk3: 0, + class_id: 0, + shield: 0, + entry_count: num_entries, + unk4: 0, + statuses: entries, + }), + ..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; + } if let Some(actor) = self.get_actor(request.target.object_id) { if actor.hp == 0 { @@ -1511,4 +1576,48 @@ impl ZoneConnection { .send(ToServer::BeginReplay(self.id, path.to_string())) .await; } + + pub async fn lose_effect(&mut self, effect_id: u16, lua_player: &mut LuaPlayer) { + // first, inform the effect script + { + let lua = self.lua.lock().unwrap(); + let state = lua.app_data_ref::().unwrap(); + + let key = effect_id as u32; + if let Some(effect_script) = state.effect_scripts.get(&key) { + lua.scope(|scope| { + let connection_data = scope.create_userdata_ref_mut(lua_player).unwrap(); + + let config = get_config(); + + let file_name = format!("{}/{}", &config.world.scripts_location, effect_script); + lua.load( + std::fs::read(&file_name).expect("Failed to locate scripts directory!"), + ) + .set_name("@".to_string() + &file_name) + .exec() + .unwrap(); + + let func: Function = lua.globals().get("onLose").unwrap(); + + func.call::<()>(connection_data).unwrap(); + + Ok(()) + }) + .unwrap(); + } else { + tracing::warn!("Effect {effect_id} isn't scripted yet! Ignoring..."); + } + } + + // then send the actor control to lose the effect + self.actor_control_self(ActorControlSelf { + category: ActorControlCategory::LoseEffect { + effect_id: effect_id as u32, + unk2: 0, + source_actor_id: INVALID_OBJECT_ID, // TODO: fill + }, + }) + .await; + } } diff --git a/src/world/lua.rs b/src/world/lua.rs index 0a8cd7a..af2fb0b 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -2,8 +2,8 @@ use mlua::{FromLua, Lua, LuaSerdeExt, UserData, UserDataFields, UserDataMethods, use crate::{ common::{ - ObjectId, ObjectTypeId, Position, timestamp_secs, workdefinitions::RemakeMode, - write_quantized_rotation, + INVALID_OBJECT_ID, ObjectId, ObjectTypeId, Position, timestamp_secs, + workdefinitions::RemakeMode, write_quantized_rotation, }, config::get_config, inventory::{CurrencyStorage, EquippedStorage, GenericStorage, Inventory, Item}, @@ -117,6 +117,16 @@ impl LuaPlayer { } fn give_status_effect(&mut self, effect_id: u16, duration: f32) { + let op_code = ServerZoneIpcType::ActorControlSelf; + let data = ServerZoneIpcData::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::GainEffect { + effect_id: effect_id as u32, + unk2: 0, + source_actor_id: INVALID_OBJECT_ID, // TODO: fill + }, + }); + self.create_segment_self(op_code, data); + self.status_effects.add(effect_id, duration); } @@ -287,7 +297,7 @@ impl UserData for LuaPlayer { }, ); methods.add_method_mut( - "give_status_effect", + "gain_effect", |_, this, (effect_id, duration): (u16, f32)| { this.give_status_effect(effect_id, duration); Ok(()) @@ -576,6 +586,16 @@ impl UserData for EffectsBuilder { }); Ok(()) }); + methods.add_method_mut("gain_effect", |_, this, effect_id: u16| { + this.effects.push(ActionEffect { + kind: EffectKind::Unk1 { + unk1: 0, + unk2: 7728, + effect_id, + }, + }); + Ok(()) + }); } } diff --git a/src/world/server.rs b/src/world/server.rs index 6ea1b31..f7f4422 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -807,6 +807,43 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i } }); } + ToServer::GainEffect(from_id, _from_actor_id, effect_id, effect_duration) => { + let send_lost_effect = + |from_id: ClientId, data: Arc>, effect_id: u16| { + let mut data = data.lock().unwrap(); + + tracing::info!("Now losing effect {}!", effect_id); + + for (id, (handle, _)) in &mut data.clients { + let id = *id; + + if id == from_id { + let msg = FromServer::LoseEffect(effect_id); + + if handle.send(msg).is_err() { + data.to_remove.push(id); + } + break; + } + } + }; + + // Eventually tell the player they lost this effect + // NOTE: I know this won't scale, but it's a fine hack for now + + tracing::info!("Effect {effect_id} lasts for {effect_duration} seconds"); + + // we have to shadow these variables to tell rust not to move them into the async closure + let data = data.clone(); + tokio::task::spawn(async move { + let mut interval = tokio::time::interval(Duration::from_millis( + (effect_duration * 1000.0) as u64, + )); + interval.tick().await; + interval.tick().await; + send_lost_effect(from_id, data, effect_id); + }); + } ToServer::Disconnected(from_id) => { let mut data = data.lock().unwrap();