1
Fork 0
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:
Joshua Goins 2025-07-14 19:52:50 -04:00
parent 404681f395
commit 3d81d1ed01
11 changed files with 221 additions and 49 deletions

View file

@ -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

View file

@ -1,2 +1,2 @@
registerEffect(50, "Sprint.lua")
registerEffect(4209, "Jog.lua")
registerEffect(50, "effects/Sprint.lua")
registerEffect(4209, "effects/Jog.lua")

View file

@ -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

View file

@ -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,
}

View file

@ -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)]

View file

@ -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,

View file

@ -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;

View file

@ -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)]

View file

@ -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;
}
}

View file

@ -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(())
});
}
}

View file

@ -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();