mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-17 10:47:44 +00:00
Implement enough status effect handling to make Sprint work
This includes making Sprint degrade into Jog, and now status effects start to be scripted.
This commit is contained in:
parent
404681f395
commit
3d81d1ed01
11 changed files with 221 additions and 49 deletions
|
@ -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
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
registerEffect(50, "Sprint.lua")
|
||||
registerEffect(4209, "Jog.lua")
|
||||
registerEffect(50, "effects/Sprint.lua")
|
||||
registerEffect(4209, "effects/Jog.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
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<ServerZoneIpcSegment>),
|
||||
/// 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)]
|
||||
|
|
|
@ -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::<ExtraLuaState>().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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -807,6 +807,43 @@ pub async fn server_main_loop(mut recv: Receiver<ToServer>) -> Result<(), std::i
|
|||
}
|
||||
});
|
||||
}
|
||||
ToServer::GainEffect(from_id, _from_actor_id, effect_id, effect_duration) => {
|
||||
let send_lost_effect =
|
||||
|from_id: ClientId, data: Arc<Mutex<WorldServer>>, 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();
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue