1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-06-30 11:47:45 +00:00

Send action requests to global server state

This is to lay the groundwork for keeping track of cast spell timings,
and eventually networking actions to other players.

See #36
This commit is contained in:
Joshua Goins 2025-06-21 10:36:44 -04:00
parent 01508fd506
commit 39beefbef3
7 changed files with 159 additions and 129 deletions

View file

@ -1,4 +1,3 @@
use std::collections::HashMap;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -10,19 +9,18 @@ use kawari::config::get_config;
use kawari::inventory::Item; use kawari::inventory::Item;
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
use kawari::ipc::zone::{ use kawari::ipc::zone::{
ActionEffect, ActionResult, ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EffectKind, ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList,
EventStart, GameMasterCommandType, GameMasterRank, OnlineStatus, ServerZoneIpcData,
ServerZoneIpcSegment, SocialListRequestType,
}; };
use kawari::ipc::zone::{ use kawari::ipc::zone::{
ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList, ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EventStart, GameMasterCommandType,
GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
}; };
use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType}; use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType};
use kawari::packet::oodle::OodleNetwork; use kawari::packet::oodle::OodleNetwork;
use kawari::packet::{ use kawari::packet::{
ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive, ConnectionType, PacketSegment, PacketState, SegmentData, SegmentType, send_keep_alive,
}; };
use kawari::world::{ChatHandler, Zone, ZoneConnection}; use kawari::world::{ChatHandler, ExtraLuaState, Zone, ZoneConnection};
use kawari::world::{ use kawari::world::{
ClientHandle, EffectsBuilder, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, ClientHandle, EffectsBuilder, Event, FromServer, LuaPlayer, PlayerData, ServerHandle,
StatusEffects, ToServer, WorldDatabase, handle_custom_ipc, server_main_loop, StatusEffects, ToServer, WorldDatabase, handle_custom_ipc, server_main_loop,
@ -36,13 +34,6 @@ use tokio::sync::mpsc::{Receiver, UnboundedReceiver, UnboundedSender, channel, u
use tokio::sync::oneshot; use tokio::sync::oneshot;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
#[derive(Default)]
struct ExtraLuaState {
action_scripts: HashMap<u32, String>,
event_scripts: HashMap<u32, String>,
command_scripts: HashMap<String, String>,
}
fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) { fn spawn_main_loop() -> (ServerHandle, JoinHandle<()>) {
let (send, recv) = channel(64); let (send, recv) = channel(64);
@ -683,116 +674,14 @@ async fn client_loop(
connection.change_zone(new_territory).await; connection.change_zone(new_territory).await;
} }
ClientZoneIpcData::ActionRequest(request) => { ClientZoneIpcData::ActionRequest(request) => {
let mut effects_builder = None; connection
.handle
// run action script .send(ToServer::ActionRequest(
{ connection.id,
let lua = lua.lock().unwrap(); connection.player_data.actor_id,
let state = lua.app_data_ref::<ExtraLuaState>().unwrap(); request.clone(),
))
let key = request.action_key; .await;
if let Some(action_script) =
state.action_scripts.get(&key)
{
lua.scope(|scope| {
let connection_data = scope
.create_userdata_ref_mut(&mut lua_player)
.unwrap();
let config = get_config();
let file_name = format!(
"{}/{}",
&config.world.scripts_location, action_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("doAction").unwrap();
effects_builder = Some(
func.call::<EffectsBuilder>(connection_data)
.unwrap(),
);
Ok(())
})
.unwrap();
} else {
tracing::warn!("Action {key} isn't scripted yet! Ignoring...");
}
}
// 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) =
connection.get_actor_mut(request.target.object_id)
{
for effect in &effects_builder.effects {
match effect.kind {
EffectKind::Damage { amount, .. } => {
actor.hp = actor.hp.saturating_sub(amount as u32);
}
_ => todo!()
}
}
let actor = *actor;
connection.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: connection.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()
};
connection
.send_segment(PacketSegment {
source_actor: connection.player_data.actor_id,
target_actor: connection.player_data.actor_id,
segment_type: SegmentType::Ipc,
data: SegmentData::Ipc { data: ipc },
})
.await;
if let Some(actor) =
connection.get_actor(request.target.object_id)
{
if actor.hp == 0 {
tracing::info!("Despawning {} because they died!", actor.id.0);
// if the actor died, despawn them
/*connection.handle
* .send(ToServer::ActorDespawned(connection.id, actor.id.0))
* .await;*/
}
}
}
} }
ClientZoneIpcData::Unk16 { .. } => { ClientZoneIpcData::Unk16 { .. } => {
// no-op // no-op
@ -938,6 +827,8 @@ async fn client_loop(
FromServer::ActorControl(actor_id, actor_control) => connection.actor_control(actor_id, actor_control).await, FromServer::ActorControl(actor_id, actor_control) => connection.actor_control(actor_id, actor_control).await,
FromServer::ActorControlTarget(actor_id, actor_control) => connection.actor_control_target(actor_id, actor_control).await, FromServer::ActorControlTarget(actor_id, actor_control) => connection.actor_control_target(actor_id, actor_control).await,
FromServer::ActorControlSelf(actor_control) => connection.actor_control_self(actor_control).await, FromServer::ActorControlSelf(actor_control) => connection.actor_control_self(actor_control).await,
FromServer::ActionComplete(request) => connection.execute_action(request, &mut lua_player).await,
FromServer::ActionCancelled() => connection.cancel_action().await,
}, },
None => break, None => break,
} }

View file

@ -82,6 +82,8 @@ pub enum ActorControlCategory {
festival3: u32, festival3: u32,
festival4: u32, festival4: u32,
}, },
#[brw(magic = 0xFu16)]
CancelCast {},
} }
#[binrw] #[binrw]

View file

@ -11,7 +11,8 @@ use tokio::sync::mpsc::Sender;
use crate::{ use crate::{
common::Position, common::Position,
ipc::zone::{ ipc::zone::{
ActorControl, ActorControlSelf, ActorControlTarget, ClientTrigger, CommonSpawn, NpcSpawn, ActionRequest, ActorControl, ActorControlSelf, ActorControlTarget, ClientTrigger,
CommonSpawn, NpcSpawn,
}, },
}; };
@ -35,6 +36,10 @@ pub enum FromServer {
ActorControlTarget(u32, ActorControlTarget), ActorControlTarget(u32, ActorControlTarget),
/// We need to update the player actor /// We need to update the player actor
ActorControlSelf(ActorControlSelf), ActorControlSelf(ActorControlSelf),
/// Action has completed and needs to be executed
ActionComplete(ActionRequest),
/// Action has been cancelled
ActionCancelled(),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -92,6 +97,8 @@ pub enum ToServer {
DebugNewEnemy(ClientId, u32), DebugNewEnemy(ClientId, u32),
/// Spawn a debug clone. /// Spawn a debug clone.
DebugSpawnClone(ClientId, u32), DebugSpawnClone(ClientId, u32),
/// Request to perform an action
ActionRequest(ClientId, u32, ActionRequest),
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]

View file

@ -1,9 +1,11 @@
use std::{ use std::{
collections::HashMap,
net::SocketAddr, net::SocketAddr,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
use mlua::Function;
use tokio::net::TcpStream; use tokio::net::TcpStream;
use crate::{ use crate::{
@ -14,8 +16,9 @@ use crate::{
ipc::{ ipc::{
chat::ServerChatIpcSegment, chat::ServerChatIpcSegment,
zone::{ zone::{
ActorControl, ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
ContainerInfo, DisplayFlag, Equip, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, ContainerInfo,
DisplayFlag, EffectKind, Equip, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn,
ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment, ObjectKind, PlayerStats, PlayerSubKind, ServerZoneIpcData, ServerZoneIpcSegment,
StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange, StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange,
}, },
@ -28,11 +31,19 @@ use crate::{
}; };
use super::{ use super::{
Actor, CharacterData, Event, LuaPlayer, StatusEffects, ToServer, WorldDatabase, Zone, Actor, CharacterData, EffectsBuilder, Event, LuaPlayer, StatusEffects, ToServer, WorldDatabase,
Zone,
common::{ClientId, ServerHandle}, common::{ClientId, ServerHandle},
lua::Task, lua::Task,
}; };
#[derive(Default)]
pub struct ExtraLuaState {
pub action_scripts: HashMap<u32, String>,
pub event_scripts: HashMap<u32, String>,
pub command_scripts: HashMap<String, String>,
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct TeleportQuery { pub struct TeleportQuery {
pub aetheryte_id: u16, pub aetheryte_id: u16,
@ -834,4 +845,106 @@ impl ZoneConnection {
}) })
.await; .await;
} }
pub async fn execute_action(&mut self, request: ActionRequest, lua_player: &mut LuaPlayer) {
let mut effects_builder = None;
// run action script
{
let lua = self.lua.lock().unwrap();
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
let key = request.action_key;
if let Some(action_script) = state.action_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, action_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("doAction").unwrap();
effects_builder = Some(func.call::<EffectsBuilder>(connection_data).unwrap());
Ok(())
})
.unwrap();
} else {
tracing::warn!("Action {key} isn't scripted yet! Ignoring...");
}
}
// 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 {
EffectKind::Damage { amount, .. } => {
actor.hp = actor.hp.saturating_sub(amount as u32);
}
_ => todo!(),
}
}
let actor = *actor;
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()
};
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 {
tracing::info!("Despawning {} because they died!", actor.id.0);
// if the actor died, despawn them
/*connection.handle
* .send(ToServer::ActorDespawned(connection.id, actor.id.0))
* .await;*/
}
}
}
}
pub async fn cancel_action(&mut self) {
self.actor_control_self(ActorControlSelf {
category: ActorControlCategory::CancelCast {},
})
.await;
}
} }

View file

@ -5,7 +5,7 @@ mod chat_handler;
pub use chat_handler::ChatHandler; pub use chat_handler::ChatHandler;
mod connection; mod connection;
pub use connection::{PlayerData, ZoneConnection}; pub use connection::{ExtraLuaState, PlayerData, ZoneConnection};
mod database; mod database;
pub use database::{CharacterData, WorldDatabase}; pub use database::{CharacterData, WorldDatabase};

View file

@ -502,6 +502,21 @@ pub async fn server_main_loop(mut recv: Receiver<ToServer>) -> Result<(), std::i
spawn, spawn,
); );
} }
ToServer::ActionRequest(from_id, _from_actor_id, request) => {
// immediately send back to the client, for now
for (id, (handle, _)) in &mut data.clients {
let id = *id;
if id == from_id {
let msg = FromServer::ActionComplete(request);
if handle.send(msg).is_err() {
data.to_remove.push(id);
}
break;
}
}
}
ToServer::Disconnected(from_id) => { ToServer::Disconnected(from_id) => {
data.to_remove.push(from_id); data.to_remove.push(from_id);
} }

View file

@ -43,7 +43,9 @@ impl Zone {
tracing::info!("Loading {path}"); tracing::info!("Loading {path}");
let lgb = LayerGroup::from_existing(&lgb_file); let lgb = LayerGroup::from_existing(&lgb_file);
if lgb.is_none() { if lgb.is_none() {
tracing::warn!("Failed to parse {path}, this is most likely a bug in Physis and should be reported somewhere!") tracing::warn!(
"Failed to parse {path}, this is most likely a bug in Physis and should be reported somewhere!"
)
} }
lgb lgb
}; };