1
Fork 0
mirror of https://github.com/redstrate/Kawari.git synced 2025-07-23 21:17:45 +00:00

Play the inn bed wakeup animation on login

Currently this is hardcoded to The Roost because I ran out of time,
but it's easy to add support for other inn rooms. I had to extend
the Lua scene API quite a bit, hence a bunch of unrelated changes
so it doesn't break every other event script.
This commit is contained in:
Joshua Goins 2025-07-20 10:22:19 -04:00
parent bfbd4ff8bc
commit 743e2b9b65
26 changed files with 185 additions and 97 deletions

View file

@ -7,12 +7,28 @@ dofile(BASE_DIR.."events/Events.lua")
dofile(BASE_DIR.."items/Items.lua")
dofile(BASE_DIR.."Global.lua")
BED_EVENT_HANDLER = 720916
BED_CUTSCENE_FLAGS = 4165480179 -- TODO: remove this hardcode
BED_SCENE_WAKEUP_ANIM = 00100
-- Lua error handlers, and other server events like player login
function onBeginLogin(player)
-- send a welcome message
player:send_message(getLoginMessage())
end
function onFinishZoning(player)
local zone_id = player.zone.id;
-- play the wakeup animation
-- the roost
-- TODO: check for other inns
if zone_id == 179 then
player:start_event(player.id, BED_EVENT_HANDLER, 15, zone_id)
player:play_scene(player.id, BED_EVENT_HANDLER, BED_SCENE_WAKEUP_ANIM, BED_CUTSCENE_FLAGS, {})
end
end
function onCommandRequiredRankInsufficientError(player)
player:send_message("You do not have permission to run this command.")
end

View file

@ -20,7 +20,7 @@ function onReturn(scene, results, player)
if scene == SCENE_SHOW_MENU then
if destination ~= AETHERNET_MENU_CANCEL then
player:finish_event(EVENT_ID) -- Need to finish the event here, because warping does not return to this callback (the game will crash or softlock otherwise)
player:finish_event(EVENT_ID, 0) -- Need to finish the event here, because warping does not return to this callback (the game will crash or softlock otherwise)
player:warp_aetheryte(destination)
return
end
@ -28,5 +28,5 @@ function onReturn(scene, results, player)
-- TODO: attunement logic
end
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -36,7 +36,7 @@ function onReturn(scene, results, player)
end ]]
elseif menu_option == AETHERNET_SUBMENU then
if decision ~= AETHERNET_SUBMENU_CANCEL then
player:finish_event(EVENT_ID) -- Need to finish the event here, because warping does not return to this callback (the game will crash or softlock otherwise)
player:finish_event(EVENT_ID, 0) -- Need to finish the event here, because warping does not return to this callback (the game will crash or softlock otherwise)
player:warp_aetheryte(decision)
return
end
@ -47,5 +47,5 @@ function onReturn(scene, results, player)
end
end
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -11,5 +11,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -5,5 +5,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -5,5 +5,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -5,5 +5,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -7,7 +7,7 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
if results[1] == 1 then
-- get warp

View file

@ -38,6 +38,6 @@ function onReturn(scene, results, player)
player:play_scene(player.id, EVENT_ID, SCENE_SHOP_END, 1 | 0x2000, {})
end
else
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end
end

View file

@ -31,7 +31,7 @@ function onReturn(scene, results, player)
if scene == SCENE_SHOW_MENU then
if decision == NOTHING or decision == CANCEL_SCENE then
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
else
if decision == LOG_OUT or decision == EXIT_GAME then
player:begin_log_out()
@ -47,7 +47,9 @@ function onReturn(scene, results, player)
end
elseif scene == SCENE_DREAMFITTING then
player:play_scene(player.id, EVENT_ID, SCENE_AWAKEN_ANIM, CUTSCENE_FLAGS, {0})
elseif scene == SCENE_AWAKEN_ANIM then
player:finish_event(EVENT_ID, player.zone.id)
else
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end
end

View file

@ -33,5 +33,5 @@ function onReturn(scene, results, player)
player:play_scene(player.id, EVENT_ID, 00003, FADE_OUT + HIDE_UI + CONDITION_CUTSCENE, {0})
return
end
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -7,5 +7,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -6,5 +6,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -3,5 +3,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -9,5 +9,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -9,5 +9,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -5,5 +5,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -8,5 +8,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -16,5 +16,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -8,5 +8,5 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end

View file

@ -21,6 +21,6 @@ function onReturn(scene, results, player)
elseif scene == SCENE_PLAY_CUTSCENE then
player:play_scene(player.id, EVENT_ID, SCENE_SHOW_MENU, 8192, {1})
else
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
end
end

View file

@ -7,7 +7,7 @@ function onTalk(target, player)
end
function onReturn(scene, results, player)
player:finish_event(EVENT_ID)
player:finish_event(EVENT_ID, 0)
if results[1] == 1 then
-- get warp

View file

@ -15,8 +15,8 @@ use kawari::ipc::zone::{
};
use kawari::ipc::zone::{
ClientTriggerCommand, ClientZoneIpcData, EventStart, GameMasterRank, OnlineStatus,
ServerZoneIpcData, ServerZoneIpcSegment, SocialListRequestType,
ClientTriggerCommand, ClientZoneIpcData, GameMasterRank, OnlineStatus, ServerZoneIpcData,
ServerZoneIpcSegment, SocialListRequestType,
};
use kawari::opcodes::{ServerChatIpcType, ServerZoneIpcType};
use kawari::packet::oodle::OodleNetwork;
@ -27,7 +27,7 @@ use kawari::world::{
ChatHandler, ExtraLuaState, LuaZone, ObsfucationData, Zone, ZoneConnection, load_init_script,
};
use kawari::world::{
ClientHandle, Event, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
ClientHandle, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
WorldDatabase, handle_custom_ipc, server_main_loop,
};
use kawari::{
@ -443,6 +443,20 @@ async fn client_loop(
})
.await;
},
ClientTriggerCommand::FinishZoning {} => {
let lua = lua.lock().unwrap();
lua.scope(|scope| {
let connection_data =
scope.create_userdata_ref_mut(&mut lua_player).unwrap();
let func: Function = lua.globals().get("onFinishZoning").unwrap();
func.call::<()>(connection_data).unwrap();
Ok(())
})
.unwrap();
}
_ => {
// inform the server of our trigger, it will handle sending it to other clients
connection.handle.send(ToServer::ClientTrigger(connection.id, connection.player_data.actor_id, trigger.clone())).await;
@ -902,15 +916,15 @@ async fn client_loop(
} else {
tracing::error!(ERR_INVENTORY_ADD_FAILED);
connection.send_message(ERR_INVENTORY_ADD_FAILED).await;
connection.event_finish(*event_id).await;
connection.event_finish(*event_id, 0).await;
}
} else {
connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await;
connection.event_finish(*event_id).await;
connection.event_finish(*event_id, 0).await;
}
} else {
connection.send_message("Unable to find shop item, this is a bug in Kawari!").await;
connection.event_finish(*event_id).await;
connection.event_finish(*event_id, 0).await;
}
} else if *buy_sell_mode == SELL {
let storage = get_container_type(*item_index).unwrap();
@ -1019,69 +1033,24 @@ async fn client_loop(
connection.event_scene(&target_id, *event_id, 10, 8193, params).await;
} else {
connection.send_message("Unable to find shop item, this is a bug in Kawari!").await;
connection.event_finish(*event_id).await;
connection.event_finish(*event_id, 0).await;
}
} else {
tracing::error!("Received unknown transaction mode {buy_sell_mode}!");
connection.event_finish(*event_id).await;
connection.event_finish(*event_id, 0).await;
}
}
ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => {
connection.player_data.target_actorid = *actor_id;
// load event
{
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::EventStart,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::EventStart(EventStart {
target_id: *actor_id,
event_id: *event_id,
event_type: 1, // talk?
..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;
}
connection.start_event(*actor_id, *event_id, 1, 0).await;
/* TODO: ServerZoneIpcType::Unk18 with data [64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]
* was observed to always be sent by the server upon interacting with shops. They open and function fine without
* it, but should we send it anyway, for the sake of accuracy? It's also still unclear if this
* happens for -every- NPC/actor. */
* was observed to always be sent by the server upon interacting with shops. They open and function fine without
* it, but should we send it anyway, for the sake of accuracy? It's also still unclear if this
* happens for -every- NPC/actor. */
let mut should_cancel = false;
{
let lua = lua.lock().unwrap();
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
if let Some(event_script) =
state.event_scripts.get(event_id)
{
connection.event = Some(Event::new(*event_id, event_script));
connection
.event
.as_mut()
.unwrap()
.talk(*actor_id, &mut lua_player);
} else {
tracing::warn!("Event {event_id} isn't scripted yet! Ignoring...");
should_cancel = true;
}
}
if should_cancel {
// give control back to the player so they aren't stuck
connection.event_finish(*event_id).await;
connection.send_message(&format!("Event {event_id} tried to start, but it doesn't have a script associated with it!")).await;
// begin talk function if it exists
if let Some(event) = connection.event.as_mut() {
event.talk(*actor_id, &mut lua_player);
}
}
ClientZoneIpcData::EventYieldHandler(handler) => {
@ -1517,6 +1486,7 @@ async fn main() {
weather_id: 0,
obsfucation_data: ObsfucationData::default(),
queued_content: None,
event_type: 0,
});
}
Some((mut socket, _)) = handle_rcon(&rcon_listener) => {

View file

@ -110,7 +110,7 @@ impl ChatHandler {
}
"!finishevent" => {
if let Some(event) = &connection.event {
connection.event_finish(event.id).await;
connection.event_finish(event.id, 0).await;
connection
.send_message("Current event forcefully finished.")
.await;

View file

@ -26,7 +26,7 @@ use crate::{
ActionEffect, ActionRequest, ActionResult, ActorControl, ActorControlCategory,
ActorControlSelf, ActorControlTarget, ClientZoneIpcSegment, CommonSpawn, Config,
ContainerInfo, CurrencyInfo, DisplayFlag, EffectEntry, EffectKind, EffectResult, Equip,
EventScene, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind,
EventScene, EventStart, GameMasterRank, InitZone, ItemInfo, Move, NpcSpawn, ObjectKind,
PlayerStats, PlayerSubKind, QuestActiveList, ServerZoneIpcData, ServerZoneIpcSegment,
StatusEffect, StatusEffectList, UpdateClassInfo, Warp, WeatherChange,
},
@ -155,6 +155,7 @@ pub struct ZoneConnection {
pub status_effects: StatusEffects,
pub event: Option<Event>,
pub event_type: u8,
pub actors: Vec<Actor>,
pub ip: SocketAddr,
@ -847,7 +848,8 @@ impl ZoneConnection {
}
player.queued_segments.clear();
for task in &player.queued_tasks {
let tasks = player.queued_tasks.clone();
for task in &tasks {
match task {
Task::ChangeTerritory { zone_id } => self.change_zone(*zone_id).await,
Task::SetRemakeMode(remake_mode) => self
@ -857,7 +859,7 @@ impl ZoneConnection {
self.warp(*warp_id).await;
}
Task::BeginLogOut => self.begin_log_out().await,
Task::FinishEvent { handler_id } => self.event_finish(*handler_id).await,
Task::FinishEvent { handler_id, arg } => self.event_finish(*handler_id, *arg).await,
Task::SetClassJob { classjob_id } => {
self.player_data.classjob_id = *classjob_id;
self.update_class_info().await;
@ -1016,6 +1018,15 @@ impl ZoneConnection {
self.set_current_exp(current_exp + amount);
self.update_class_info().await;
}
Task::StartEvent {
actor_id,
event_id,
event_type,
event_arg,
} => {
self.start_event(*actor_id, *event_id, *event_type, *event_arg)
.await;
}
}
}
player.queued_tasks.clear();
@ -1066,11 +1077,11 @@ impl ZoneConnection {
"Unable to play event {event_id}, scene {:?}, scene_flags {scene_flags}!",
scene
);
self.event_finish(event_id).await;
self.event_finish(event_id, 0).await;
}
}
pub async fn event_finish(&mut self, handler_id: u32) {
pub async fn event_finish(&mut self, handler_id: u32, arg: u32) {
self.player_data.target_actorid = ObjectTypeId::default();
// sent event finish
{
@ -1079,9 +1090,9 @@ impl ZoneConnection {
timestamp: timestamp_secs(),
data: ServerZoneIpcData::EventFinish {
handler_id,
event: 1,
event: self.event_type,
result: 1,
arg: 0,
arg,
},
..Default::default()
};
@ -1777,4 +1788,62 @@ impl ZoneConnection {
})
.await;
}
pub async fn start_event(
&mut self,
actor_id: ObjectTypeId,
event_id: u32,
event_type: u8,
event_arg: u32,
) {
self.player_data.target_actorid = actor_id;
self.event_type = event_type;
// tell the client the event has started
{
let ipc = ServerZoneIpcSegment {
op_code: ServerZoneIpcType::EventStart,
timestamp: timestamp_secs(),
data: ServerZoneIpcData::EventStart(EventStart {
target_id: actor_id,
event_id,
event_type,
event_arg,
..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;
}
// load event script if needed
let mut should_cancel = false;
{
let lua = self.lua.lock().unwrap();
let state = lua.app_data_ref::<ExtraLuaState>().unwrap();
if let Some(event_script) = state.event_scripts.get(&event_id) {
self.event = Some(Event::new(event_id, event_script));
} else {
tracing::warn!("Event {event_id} isn't scripted yet! Ignoring...");
should_cancel = true;
}
}
if should_cancel {
// give control back to the player so they aren't stuck
self.event_finish(event_id, 0).await;
self.send_message(&format!(
"Event {event_id} tried to start, but it doesn't have a script associated with it!"
))
.await;
}
}
}

View file

@ -22,6 +22,7 @@ use mlua::{FromLua, Lua, LuaSerdeExt, UserData, UserDataFields, UserDataMethods,
use super::{PlayerData, StatusEffects, Zone, connection::TeleportQuery};
#[derive(Clone)]
pub enum Task {
ChangeTerritory {
zone_id: u16,
@ -33,6 +34,7 @@ pub enum Task {
BeginLogOut,
FinishEvent {
handler_id: u32,
arg: u32,
},
SetClassJob {
classjob_id: u8,
@ -83,6 +85,12 @@ pub enum Task {
AddExp {
amount: u32,
},
StartEvent {
actor_id: ObjectTypeId,
event_id: u32,
event_type: u8,
event_arg: u32,
},
}
#[derive(Default, Clone)]
@ -199,7 +207,7 @@ impl LuaPlayer {
let error_message = "Unsupported amount of parameters in play_scene! This is likely a bug in your script! Cancelling event...".to_string();
tracing::warn!(error_message);
self.send_message(&error_message, 0);
self.finish_event(event_id);
self.finish_event(event_id, 0);
}
}
@ -273,8 +281,9 @@ impl LuaPlayer {
self.queued_tasks.push(Task::BeginLogOut);
}
fn finish_event(&mut self, handler_id: u32) {
self.queued_tasks.push(Task::FinishEvent { handler_id });
fn finish_event(&mut self, handler_id: u32, arg: u32) {
self.queued_tasks
.push(Task::FinishEvent { handler_id, arg });
}
fn set_classjob(&mut self, classjob_id: u8) {
@ -446,6 +455,21 @@ impl LuaPlayer {
fn add_exp(&mut self, amount: u32) {
self.queued_tasks.push(Task::AddExp { amount });
}
fn start_event(
&mut self,
actor_id: ObjectTypeId,
event_id: u32,
event_type: u8,
event_arg: u32,
) {
self.queued_tasks.push(Task::StartEvent {
actor_id,
event_id,
event_type,
event_arg,
});
}
}
impl UserData for LuaPlayer {
@ -533,8 +557,8 @@ impl UserData for LuaPlayer {
this.begin_log_out();
Ok(())
});
methods.add_method_mut("finish_event", |_, this, handler_id: u32| {
this.finish_event(handler_id);
methods.add_method_mut("finish_event", |_, this, (handler_id, arg): (u32, u32)| {
this.finish_event(handler_id, arg);
Ok(())
});
methods.add_method_mut("set_classjob", |_, this, classjob_id: u8| {
@ -600,6 +624,13 @@ impl UserData for LuaPlayer {
this.add_exp(amount);
Ok(())
});
methods.add_method_mut(
"start_event",
|_, this, (target, event_id, event_type, event_arg): (ObjectTypeId, u32, u8, u32)| {
this.start_event(target, event_id, event_type, event_arg);
Ok(())
},
);
}
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {