mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-19 11:17:46 +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)
|
function doAction(player)
|
||||||
effects = EffectsBuilder()
|
effects = EffectsBuilder()
|
||||||
|
|
||||||
-- TODO: go through effectsbuilder
|
effects:gain_effect(EFFECT_SPRINT)
|
||||||
-- give sprint
|
|
||||||
player:give_status_effect(50, 5.0)
|
|
||||||
|
|
||||||
return effects
|
return effects
|
||||||
end
|
end
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
registerEffect(50, "Sprint.lua")
|
registerEffect(50, "effects/Sprint.lua")
|
||||||
registerEffect(4209, "Jog.lua")
|
registerEffect(4209, "effects/Jog.lua")
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
EFFECT_JOG = 4029
|
EFFECT_JOG = 4209
|
||||||
|
|
||||||
function onGain(player)
|
function onGain(player)
|
||||||
-- it does nothing
|
-- it does nothing
|
||||||
end
|
end
|
||||||
|
|
||||||
function onLose(player)
|
function onLose(player)
|
||||||
player:gain_effect(EFFECT_JOG)
|
player:gain_effect(EFFECT_JOG, 20.0)
|
||||||
end
|
end
|
||||||
|
|
|
@ -1191,6 +1191,7 @@ async fn client_loop(
|
||||||
FromServer::UpdateConfig(actor_id, config) => connection.update_config(actor_id, config).await,
|
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::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::ReplayPacket(segment) => connection.send_segment(segment).await,
|
||||||
|
FromServer::LoseEffect(effect_id) => connection.lose_effect(effect_id, &mut lua_player).await,
|
||||||
},
|
},
|
||||||
None => break,
|
None => break,
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,7 +40,7 @@ pub enum EffectKind {
|
||||||
#[brw(magic = 27u8)]
|
#[brw(magic = 27u8)]
|
||||||
BeginCombo,
|
BeginCombo,
|
||||||
#[brw(magic = 14u8)]
|
#[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)]
|
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default, Deserialize, Serialize)]
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::common::ObjectId;
|
||||||
|
|
||||||
#[binrw]
|
#[binrw]
|
||||||
#[brw(little)]
|
#[brw(little)]
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
pub struct EffectEntry {
|
pub struct EffectEntry {
|
||||||
pub index: u8,
|
pub index: u8,
|
||||||
pub unk1: u8,
|
pub unk1: u8,
|
||||||
|
|
|
@ -95,7 +95,7 @@ mod quest_active_list;
|
||||||
pub use quest_active_list::QuestActiveList;
|
pub use quest_active_list::QuestActiveList;
|
||||||
|
|
||||||
mod effect_result;
|
mod effect_result;
|
||||||
pub use effect_result::EffectResult;
|
pub use effect_result::{EffectEntry, EffectResult};
|
||||||
|
|
||||||
use crate::COMPLETED_LEVEQUEST_BITMASK_SIZE;
|
use crate::COMPLETED_LEVEQUEST_BITMASK_SIZE;
|
||||||
use crate::COMPLETED_QUEST_BITMASK_SIZE;
|
use crate::COMPLETED_QUEST_BITMASK_SIZE;
|
||||||
|
|
|
@ -45,7 +45,10 @@ pub enum FromServer {
|
||||||
UpdateConfig(u32, Config),
|
UpdateConfig(u32, Config),
|
||||||
/// Update an actor's model IDs.
|
/// Update an actor's model IDs.
|
||||||
ActorEquip(u32, u64, [u32; 10]),
|
ActorEquip(u32, u64, [u32; 10]),
|
||||||
|
/// Informs the connection to replay packet data to the client.
|
||||||
ReplayPacket(PacketSegment<ServerZoneIpcSegment>),
|
ReplayPacket(PacketSegment<ServerZoneIpcSegment>),
|
||||||
|
/// The player should lose this effect.
|
||||||
|
LoseEffect(u16),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -110,6 +113,8 @@ pub enum ToServer {
|
||||||
Equip(ClientId, u32, u64, [u32; 10]),
|
Equip(ClientId, u32, u64, [u32; 10]),
|
||||||
/// Begins a packet replay.
|
/// Begins a packet replay.
|
||||||
BeginReplay(ClientId, String),
|
BeginReplay(ClientId, String),
|
||||||
|
/// The player gains an effect.
|
||||||
|
GainEffect(ClientId, u32, u16, f32),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
|
|
@ -12,8 +12,8 @@ use crate::{
|
||||||
CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE,
|
CLASSJOB_ARRAY_SIZE, COMPLETED_LEVEQUEST_BITMASK_SIZE, COMPLETED_QUEST_BITMASK_SIZE,
|
||||||
ERR_INVENTORY_ADD_FAILED,
|
ERR_INVENTORY_ADD_FAILED,
|
||||||
common::{
|
common::{
|
||||||
GameData, ItemInfoQuery, ObjectId, ObjectTypeId, Position, timestamp_secs,
|
GameData, INVALID_OBJECT_ID, ItemInfoQuery, ObjectId, ObjectTypeId, Position,
|
||||||
value_to_flag_byte_index_value,
|
timestamp_secs, value_to_flag_byte_index_value,
|
||||||
},
|
},
|
||||||
config::{WorldConfig, get_config},
|
config::{WorldConfig, get_config},
|
||||||
inventory::{ContainerType, Inventory, Item, Storage},
|
inventory::{ContainerType, Inventory, Item, Storage},
|
||||||
|
@ -22,10 +22,10 @@ use crate::{
|
||||||
zone::{
|
zone::{
|
||||||
ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
|
ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
|
||||||
ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, Config,
|
ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, Config,
|
||||||
ContainerInfo, CurrencyInfo, DisplayFlag, EffectKind, Equip, EventScene,
|
ContainerInfo, CurrencyInfo, DisplayFlag, EffectEntry, EffectKind, EffectResult, Equip,
|
||||||
GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind, PlayerStats,
|
EventScene, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind,
|
||||||
PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment, StatusEffect,
|
PlayerStats, PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment,
|
||||||
StatusEffectList, UpdateClassInfo, Warp, WeatherChange,
|
StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
opcodes::ServerZoneIpcType,
|
opcodes::ServerZoneIpcType,
|
||||||
|
@ -1345,9 +1345,6 @@ impl ZoneConnection {
|
||||||
|
|
||||||
// tell them the action results
|
// tell them the action results
|
||||||
if let Some(effects_builder) = effects_builder {
|
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) {
|
if let Some(actor) = self.get_actor_mut(request.target.object_id) {
|
||||||
for effect in &effects_builder.effects {
|
for effect in &effects_builder.effects {
|
||||||
match effect.kind {
|
match effect.kind {
|
||||||
|
@ -1362,34 +1359,102 @@ impl ZoneConnection {
|
||||||
self.update_hp_mp(actor.id, actor.hp, 10000).await;
|
self.update_hp_mp(actor.id, actor.hp, 10000).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
let ipc = ServerZoneIpcSegment {
|
// TODO: send Cooldown ActorControlSelf
|
||||||
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 {
|
// ActionResult
|
||||||
source_actor: self.player_data.actor_id,
|
{
|
||||||
target_actor: self.player_data.actor_id,
|
let mut effects = [ActionEffect::default(); 8];
|
||||||
segment_type: SegmentType::Ipc,
|
effects[..effects_builder.effects.len()].copy_from_slice(&effects_builder.effects);
|
||||||
data: SegmentData::Ipc { data: ipc },
|
|
||||||
})
|
let ipc = ServerZoneIpcSegment {
|
||||||
.await;
|
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 let Some(actor) = self.get_actor(request.target.object_id) {
|
||||||
if actor.hp == 0 {
|
if actor.hp == 0 {
|
||||||
|
@ -1511,4 +1576,48 @@ impl ZoneConnection {
|
||||||
.send(ToServer::BeginReplay(self.id, path.to_string()))
|
.send(ToServer::BeginReplay(self.id, path.to_string()))
|
||||||
.await;
|
.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::{
|
use crate::{
|
||||||
common::{
|
common::{
|
||||||
ObjectId, ObjectTypeId, Position, timestamp_secs, workdefinitions::RemakeMode,
|
INVALID_OBJECT_ID, ObjectId, ObjectTypeId, Position, timestamp_secs,
|
||||||
write_quantized_rotation,
|
workdefinitions::RemakeMode, write_quantized_rotation,
|
||||||
},
|
},
|
||||||
config::get_config,
|
config::get_config,
|
||||||
inventory::{CurrencyStorage, EquippedStorage, GenericStorage, Inventory, Item},
|
inventory::{CurrencyStorage, EquippedStorage, GenericStorage, Inventory, Item},
|
||||||
|
@ -117,6 +117,16 @@ impl LuaPlayer {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn give_status_effect(&mut self, effect_id: u16, duration: f32) {
|
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);
|
self.status_effects.add(effect_id, duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,7 +297,7 @@ impl UserData for LuaPlayer {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
methods.add_method_mut(
|
methods.add_method_mut(
|
||||||
"give_status_effect",
|
"gain_effect",
|
||||||
|_, this, (effect_id, duration): (u16, f32)| {
|
|_, this, (effect_id, duration): (u16, f32)| {
|
||||||
this.give_status_effect(effect_id, duration);
|
this.give_status_effect(effect_id, duration);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -576,6 +586,16 @@ impl UserData for EffectsBuilder {
|
||||||
});
|
});
|
||||||
Ok(())
|
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) => {
|
ToServer::Disconnected(from_id) => {
|
||||||
let mut data = data.lock().unwrap();
|
let mut data = data.lock().unwrap();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue