From 468ca97257d607f5276e6ffe4447bb97ceadf191 Mon Sep 17 00:00:00 2001 From: thedax Date: Tue, 22 Jul 2025 23:30:01 -0400 Subject: [PATCH] Implement walk-in trigger events in S9 (#133) Implement walk-in trigger events, and more specifically, the teleporter pads in Solution Nine. Include a buildfix in kawari-world.rs as well. --- resources/opcodes.json | 10 +++ resources/scripts/events/Events.lua | 14 +++ .../walkin_trigger/SolutionNineTeleporter.lua | 25 ++++++ src/bin/kawari-world.rs | 42 ++++++--- src/ipc/zone/actor_control.rs | 24 ++++++ src/ipc/zone/client_trigger.rs | 5 ++ src/ipc/zone/mod.rs | 41 +++++++++ src/world/chat_handler.rs | 6 +- src/world/common.rs | 1 + src/world/connection.rs | 55 +++++++----- src/world/event.rs | 24 ++++++ src/world/lua.rs | 86 +++++++++++++++++-- src/world/mod.rs | 1 + src/world/server.rs | 45 ++++++++++ 14 files changed, 334 insertions(+), 45 deletions(-) create mode 100644 resources/scripts/events/walkin_trigger/SolutionNineTeleporter.lua diff --git a/resources/opcodes.json b/resources/opcodes.json index 8b14953..2b9c05d 100644 --- a/resources/opcodes.json +++ b/resources/opcodes.json @@ -334,6 +334,11 @@ "name": "SetSearchComment", "opcode": 512, "size": 216 + }, + { + "name": "WalkInEvent", + "opcode": 316, + "size": 24 } ], "ClientZoneIpcType": [ @@ -496,6 +501,11 @@ "name": "EquipGearset", "opcode": 101, "size": 72 + }, + { + "name": "StartWalkInEvent", + "opcode": 607, + "size": 24 } ], "ServerLobbyIpcType": [ diff --git a/resources/scripts/events/Events.lua b/resources/scripts/events/Events.lua index 1e07a6e..8d33438 100644 --- a/resources/scripts/events/Events.lua +++ b/resources/scripts/events/Events.lua @@ -375,6 +375,15 @@ generic_currency_exchange = { -- 3539075, -- Dibourdier doesn't respond when interacted with right now, probably needs special handling } +solution_nine_teleporters = { + 4194305, -- Teleporter from eastern Aetheryte Plaza to Recreation Zone + 4194306, -- Teleporter from Recreation Zone to eastern Aetheryte Plaza + 4194307, -- Teleporter from northern Aetheryte Plaza to Government Sector + 4194308, -- Teleporter from Government Sector to northern Aetheryte Plaza + 4194309, -- Teleporter from Nexus Arcade ground floor to upper balcony + 4194310, -- Teleporter from upper balcony to Nexus Arcade ground floor +} + -- Not custom in the sense of non-SQEX content, just going based off the directory name custom0_events = { [720916] = "cmndefinnbed_00020.lua", @@ -397,6 +406,7 @@ TOSORT_DIR = "events/tosort/" OPENING_DIR = "events/quest/opening/" CUSTOM0_DIR = "events/custom/000/" CUSTOM1_DIR = "events/custom/001/" +TRIGGER_DIR = "events/walkin_trigger/" for _, event_id in pairs(generic_warps) do registerEvent(event_id, "events/common/GenericWarp.lua") @@ -441,3 +451,7 @@ end for event_id, script_file in pairs(quests) do registerEvent(event_id, OPENING_DIR..script_file) end + +for _, event_id in pairs(solution_nine_teleporters) do + registerEvent(event_id, TRIGGER_DIR.."SolutionNineTeleporter.lua") +end diff --git a/resources/scripts/events/walkin_trigger/SolutionNineTeleporter.lua b/resources/scripts/events/walkin_trigger/SolutionNineTeleporter.lua new file mode 100644 index 0000000..8bc06a3 --- /dev/null +++ b/resources/scripts/events/walkin_trigger/SolutionNineTeleporter.lua @@ -0,0 +1,25 @@ +-- TODO: Do any of the sheets contain any of this info so we don't have to hardcode it? +-- Yes, this table is ugly. Currently these values are complete unknowns. +TELEPORTER_INFO = { + [4194305] = { 10114730, 1965, 17743871, 2619867137, 2158200772 }, -- Teleporter from eastern Aetheryte Plaza to Recreation Zone + [4194306] = { 10114817, 1966, 17711103, 2462253057, 2136702993 }, -- Teleporter from Recreation Zone to eastern Aetheryte Plaza + [4194307] = { 10114878, 1967, 17694720, 2158166017, 1782416562 }, -- Teleporter from northern Aetheryte Plaza to Government Sector + [4194308] = { 10114891, 1968, 17727487, 2136670209, 1896185871 }, -- Teleporter from Government Sector to northern Aetheryte Plaza + [4194309] = { 10114905, 1969, 11804671, 1665204225, 1989510302 }, -- Teleporter from Nexus Arcade ground floor to upper balcony + [4194310] = { 10114944, 1970, 11837439, 1670053889, 2009628707 }, -- Teleporter from upper balcony to Nexus Arcade ground floor +} + +EVENT_ARG = { + [4194305] = 10611851, + [4194306] = 10611861, + [4194307] = 10611862, + [4194308] = 10611864, + [4194309] = 10611868, + [4194310] = 10611881, +} + +function onEnterTrigger(player) + player:do_solnine_teleporter(EVENT_ID, table.unpack(TELEPORTER_INFO[EVENT_ID])) + -- TODO: We should probably take the event arg in Event::new on the rust side, but this works for now. + player:finish_event(EVENT_ID, EVENT_ARG[EVENT_ID], 1) +end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 27a2777..948cc09 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -3,15 +3,17 @@ use std::sync::{Arc, Mutex}; use std::time::{Duration, Instant}; use kawari::common::Position; -use kawari::common::{GameData, INVALID_OBJECT_ID, ItemInfoQuery, timestamp_secs}; +use kawari::common::{ + GameData, INVALID_OBJECT_ID, ItemInfoQuery, ObjectId, ObjectTypeId, timestamp_secs, +}; use kawari::config::get_config; use kawari::inventory::{ BuyBackItem, ContainerType, CurrencyKind, Item, ItemOperationKind, get_container_type, }; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::zone::{ - ActorControlCategory, ActorControlSelf, ItemOperation, PlayerEntry, PlayerSpawn, PlayerStatus, - SocialList, + ActorControl, ActorControlCategory, ActorControlSelf, ItemOperation, PlayerEntry, PlayerSpawn, + PlayerStatus, SocialList, }; use kawari::ipc::zone::{ @@ -27,8 +29,8 @@ use kawari::world::{ ChatHandler, ExtraLuaState, LuaZone, ObsfucationData, Zone, ZoneConnection, load_init_script, }; use kawari::world::{ - ClientHandle, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer, - WorldDatabase, handle_custom_ipc, server_main_loop, + ClientHandle, EventFinishType, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, + ToServer, WorldDatabase, handle_custom_ipc, server_main_loop, }; use kawari::{ ERR_INVENTORY_ADD_FAILED, LogMessageType, RECEIVE_BUFFER_SIZE, TITLE_UNLOCK_BITMASK_SIZE, @@ -463,9 +465,6 @@ async fn client_loop( } } } - ClientZoneIpcData::Unk2 { .. } => { - // no-op - } ClientZoneIpcData::Unk3 { .. } => { // no-op } @@ -916,15 +915,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, 0).await; + connection.event_finish(*event_id, 0, EventFinishType::Normal).await; } } else { connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await; - connection.event_finish(*event_id, 0).await; + connection.event_finish(*event_id, 0, EventFinishType::Normal).await; } } else { connection.send_message("Unable to find shop item, this is a bug in Kawari!").await; - connection.event_finish(*event_id, 0).await; + connection.event_finish(*event_id, 0, EventFinishType::Normal).await; } } else if *buy_sell_mode == SELL { let storage = get_container_type(*item_index).unwrap(); @@ -1033,11 +1032,11 @@ 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, 0).await; + connection.event_finish(*event_id, 0, EventFinishType::Normal).await; } } else { tracing::error!("Received unknown transaction mode {buy_sell_mode}!"); - connection.event_finish(*event_id, 0).await; + connection.event_finish(*event_id, 0, EventFinishType::Normal).await; } } ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => { @@ -1319,6 +1318,22 @@ async fn client_loop( tracing::info!("Client tried to equip a gearset!"); connection.send_message("Gearsets are not yet implemented.").await; } + ClientZoneIpcData::StartWalkInEvent { event_arg, event_id, .. } => { + // Yes, an ActorControl is sent here, not an ActorControlSelf! + connection.actor_control(connection.player_data.actor_id, ActorControl { + category: ActorControlCategory::ToggleWeapon { + shown: false, + } + }).await; + connection.send_unk18([64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]).await; + let actor_id = ObjectTypeId { object_id: ObjectId(connection.player_data.actor_id), object_type: 0 }; + connection.start_event(actor_id, *event_id, 10, *event_arg).await; + + // begin walk-in trigger function if it exists + if let Some(event) = connection.event.as_mut() { + event.enter_trigger(&mut lua_player); + } + } ClientZoneIpcData::Unknown { .. } => { tracing::warn!("Unknown packet {:?} recieved, this should be handled!", data.op_code); } @@ -1391,6 +1406,7 @@ async fn client_loop( 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, effect_param, effect_source_actor_id) => connection.lose_effect(effect_id, effect_param, effect_source_actor_id, &mut lua_player).await, + FromServer::Unk18(unk) => connection.send_unk18(unk).await, }, None => break, } diff --git a/src/ipc/zone/actor_control.rs b/src/ipc/zone/actor_control.rs index fb3a118..68eb02c 100644 --- a/src/ipc/zone/actor_control.rs +++ b/src/ipc/zone/actor_control.rs @@ -72,6 +72,11 @@ pub enum ActorControlCategory { #[bw(map = write_bool_as::)] unlocked: bool, }, + #[brw(magic = 0x8Au16)] + EventRelatedUnk3 { + #[brw(pad_before = 2)] //padding + event_id: u32, + }, #[brw(magic = 0xCBu16)] TeleportStart { #[brw(pad_before = 2)] //padding @@ -83,6 +88,11 @@ pub enum ActorControlCategory { #[brw(pad_before = 2)] // padding speed: u16, }, + #[brw(magic = 0xECu16)] + WalkInTriggerRelatedUnk3 { + #[brw(pad_before = 2)] // padding + unk1: u32, + }, #[brw(magic = 0x386u16)] SetFestival { #[brw(pad_before = 2)] // padding @@ -177,6 +187,20 @@ pub enum ActorControlCategory { unk2: u32, unk3: u32, }, + #[brw(magic = 0x104u16)] + WalkInTriggerRelatedUnk2 { + #[brw(pad_before = 2)] // padding + unk1: u32, + unk2: u32, + unk3: u32, + /// Usually 7? + unk4: u32, + }, + #[brw(magic = 0x107u16)] + WalkInTriggerRelatedUnk1 { + #[brw(pad_before = 2)] // padding + unk1: u32, + }, Unknown { category: u16, #[brw(pad_before = 2)] // padding diff --git a/src/ipc/zone/client_trigger.rs b/src/ipc/zone/client_trigger.rs index ae61a5a..fe04814 100644 --- a/src/ipc/zone/client_trigger.rs +++ b/src/ipc/zone/client_trigger.rs @@ -50,6 +50,11 @@ pub enum ClientTriggerCommand { aetheryte_id: u32, // TODO: fill out the rest }, + #[brw(magic = 0x25eu16)] + WalkInTriggerFinished { + #[brw(pad_before = 2)] + unk1: u32, + }, #[brw(magic = 0x033Eu16)] EventRelatedUnk { // seen in haircut event diff --git a/src/ipc/zone/mod.rs b/src/ipc/zone/mod.rs index c571b67..45ee6ac 100644 --- a/src/ipc/zone/mod.rs +++ b/src/ipc/zone/mod.rs @@ -586,6 +586,18 @@ pub enum ServerZoneIpcData { // TODO: fill this out unk1: [u8; 968], }, + /// Sent by the server when walking over a trigger (e.g. the teleport pads in Solution Nine). + /// All of these fields are currently unknown in meaning. + #[br(pre_assert(*magic == ServerZoneIpcType::WalkInEvent))] + WalkInEvent { + unk1: u32, + unk2: u16, + #[brw(pad_before = 2)] + unk3: u32, + unk4: u32, + #[brw(pad_after = 4)] + unk5: u32, + }, Unknown { #[br(count = size - 32)] unk: Vec, @@ -791,6 +803,13 @@ pub enum ClientZoneIpcData { #[brw(pad_after = 2)] unk2: u16, }, + #[br(pre_assert(*magic == ClientZoneIpcType::StartWalkInEvent))] + StartWalkInEvent { + event_arg: u32, + event_id: u32, + #[brw(pad_after = 4)] + pos: Position, + }, #[br(pre_assert(*magic == ClientZoneIpcType::ContentFinderAction))] ContentFinderAction { unk1: [u8; 8] }, Unknown { @@ -1161,6 +1180,16 @@ mod tests { ServerZoneIpcType::Unk17, ServerZoneIpcData::Unk17 { unk1: [0; 968] }, ), + ( + ServerZoneIpcType::WalkInEvent, + ServerZoneIpcData::WalkInEvent { + unk1: 0, + unk2: 0, + unk3: 0, + unk4: 0, + unk5: 0, + }, + ), ]; for (opcode, data) in &ipc_types { @@ -1339,6 +1368,18 @@ mod tests { ClientZoneIpcType::UnkCall2, ClientZoneIpcData::UnkCall2 { unk1: [0; 8] }, ), + ( + ClientZoneIpcType::StartWalkInEvent, + ClientZoneIpcData::StartWalkInEvent { + event_arg: 0, + event_id: 0, + pos: Position { + x: 0.0, + y: 0.0, + z: 0.0, + }, + }, + ), ]; for (opcode, data) in &ipc_types { diff --git a/src/world/chat_handler.rs b/src/world/chat_handler.rs index 98c0eb5..d256627 100644 --- a/src/world/chat_handler.rs +++ b/src/world/chat_handler.rs @@ -3,7 +3,7 @@ use crate::{ common::ItemInfoQuery, inventory::{Item, Storage}, ipc::zone::{ChatMessage, GameMasterRank}, - world::ToServer, + world::{EventFinishType, ToServer}, }; use super::ZoneConnection; @@ -110,7 +110,9 @@ impl ChatHandler { } "!finishevent" => { if let Some(event) = &connection.event { - connection.event_finish(event.id, 0).await; + connection + .event_finish(event.id, 0, EventFinishType::Normal) + .await; connection .send_message("Current event forcefully finished.") .await; diff --git a/src/world/common.rs b/src/world/common.rs index 66ebe9c..c7ba6e5 100644 --- a/src/world/common.rs +++ b/src/world/common.rs @@ -50,6 +50,7 @@ pub enum FromServer { ReplayPacket(PacketSegment), /// The player should lose this effect. LoseEffect(u16, u16, ObjectId), + Unk18([u8; 16]), } #[derive(Debug, Clone)] diff --git a/src/world/connection.rs b/src/world/connection.rs index 0a3afd8..b56fe96 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -39,8 +39,8 @@ use crate::{ }; use super::{ - Actor, CharacterData, EffectsBuilder, Event, LuaPlayer, StatusEffects, ToServer, WorldDatabase, - Zone, + Actor, CharacterData, EffectsBuilder, Event, EventFinishType, LuaPlayer, StatusEffects, + ToServer, WorldDatabase, Zone, common::{ClientId, ServerHandle}, load_init_script, lua::Task, @@ -860,7 +860,11 @@ impl ZoneConnection { self.warp(*warp_id).await; } Task::BeginLogOut => self.begin_log_out().await, - Task::FinishEvent { handler_id, arg } => self.event_finish(*handler_id, *arg).await, + Task::FinishEvent { + handler_id, + arg, + finish_type, + } => self.event_finish(*handler_id, *arg, *finish_type).await, Task::SetClassJob { classjob_id } => { self.player_data.classjob_id = *classjob_id; self.update_class_info().await; @@ -1081,11 +1085,12 @@ impl ZoneConnection { "Unable to play event {event_id}, scene {:?}, scene_flags {scene_flags}!", scene ); - self.event_finish(event_id, 0).await; + self.event_finish(event_id, 0, EventFinishType::Normal) + .await; } } - pub async fn event_finish(&mut self, handler_id: u32, arg: u32) { + pub async fn event_finish(&mut self, handler_id: u32, arg: u32, finish_type: EventFinishType) { self.player_data.target_actorid = ObjectTypeId::default(); // sent event finish { @@ -1111,22 +1116,29 @@ impl ZoneConnection { } // give back control to the player - { - let ipc = ServerZoneIpcSegment { - op_code: ServerZoneIpcType::Unk18, - timestamp: timestamp_secs(), - data: ServerZoneIpcData::Unk18 { unk: [0; 16] }, - ..Default::default() - }; + let unk = match finish_type { + EventFinishType::Normal => [0; 16], + EventFinishType::Jumping => [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }; - 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; - } + self.send_unk18(unk).await; + } + + pub async fn send_unk18(&mut self, unk: [u8; 16]) { + let ipc = ServerZoneIpcSegment { + op_code: ServerZoneIpcType::Unk18, + timestamp: timestamp_secs(), + data: ServerZoneIpcData::Unk18 { unk }, + ..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; } pub async fn send_inventory_ack(&mut self, sequence: u32, action_type: u16) { @@ -1843,7 +1855,8 @@ impl ZoneConnection { if should_cancel { // give control back to the player so they aren't stuck - self.event_finish(event_id, 0).await; + self.event_finish(event_id, 0, EventFinishType::Normal) + .await; self.send_message(&format!( "Event {event_id} tried to start, but it doesn't have a script associated with it!" )) diff --git a/src/world/event.rs b/src/world/event.rs index bc35e53..163ce50 100644 --- a/src/world/event.rs +++ b/src/world/event.rs @@ -10,6 +10,12 @@ pub struct Event { pub id: u32, } +#[derive(Copy, Clone)] +pub enum EventFinishType { + Normal, + Jumping, +} + impl Event { pub fn new(id: u32, path: &str) -> Self { let lua = Lua::new(); @@ -52,6 +58,24 @@ impl Event { } } + pub fn enter_trigger(&mut self, player: &mut LuaPlayer) { + let mut run_script = || { + self.lua.scope(|scope| { + let player = scope.create_userdata_ref_mut(player)?; + + let func: Function = self.lua.globals().get("onEnterTrigger")?; + + func.call::<()>(player)?; + + Ok(()) + }) + }; + + if let Err(err) = run_script() { + tracing::warn!("Syntax error in {}: {:?}", self.file_name, err); + } + } + pub fn scene_finished(&mut self, player: &mut LuaPlayer, scene: u16) { let mut run_script = || { self.lua.scope(|scope| { diff --git a/src/world/lua.rs b/src/world/lua.rs index b87abde..11a2403 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -16,7 +16,7 @@ use crate::{ }, opcodes::ServerZoneIpcType, packet::{PacketSegment, SegmentData, SegmentType}, - world::ExtraLuaState, + world::{EventFinishType, ExtraLuaState}, }; use mlua::{FromLua, Lua, LuaSerdeExt, UserData, UserDataFields, UserDataMethods, Value}; @@ -35,6 +35,7 @@ pub enum Task { FinishEvent { handler_id: u32, arg: u32, + finish_type: EventFinishType, }, SetClassJob { classjob_id: u8, @@ -210,7 +211,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, 0); + self.finish_event(event_id, 0, EventFinishType::Normal); } } @@ -284,9 +285,12 @@ impl LuaPlayer { self.queued_tasks.push(Task::BeginLogOut); } - fn finish_event(&mut self, handler_id: u32, arg: u32) { - self.queued_tasks - .push(Task::FinishEvent { handler_id, arg }); + fn finish_event(&mut self, handler_id: u32, arg: u32, finish_type: EventFinishType) { + self.queued_tasks.push(Task::FinishEvent { + handler_id, + arg, + finish_type, + }); } fn set_classjob(&mut self, classjob_id: u8) { @@ -455,6 +459,53 @@ impl LuaPlayer { } } + fn do_solnine_teleporter( + &mut self, + event_id: u32, + unk1: u32, + unk2: u16, + unk3: u32, + unk4: u32, + unk5: u32, + ) { + let packets_to_send = [ + ( + ServerZoneIpcType::ActorControlSelf, + ServerZoneIpcData::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::EventRelatedUnk3 { event_id }, + }), + ), + ( + ServerZoneIpcType::WalkInEvent, + ServerZoneIpcData::WalkInEvent { + unk1, + unk2, + unk3, + unk4, + unk5, + }, + ), + ( + ServerZoneIpcType::ActorControlSelf, + ServerZoneIpcData::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::WalkInTriggerRelatedUnk3 { + unk1: 1, // Sometimes the server sends 2 for this, but it's still completely unknown what it means. + }, + }), + ), + ( + ServerZoneIpcType::ActorControlSelf, + ServerZoneIpcData::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::WalkInTriggerRelatedUnk1 { unk1: 1 }, + }), + ), + ]; + + for (op_code, data) in packets_to_send { + self.create_segment_self(op_code, data); + } + } + fn add_exp(&mut self, amount: u32) { self.queued_tasks.push(Task::AddExp { amount }); } @@ -564,10 +615,20 @@ impl UserData for LuaPlayer { this.begin_log_out(); Ok(()) }); - methods.add_method_mut("finish_event", |_, this, (handler_id, arg): (u32, u32)| { - this.finish_event(handler_id, arg); - Ok(()) - }); + methods.add_method_mut( + "finish_event", + |lua, this, (handler_id, arg, finish_type): (u32, u32, Value)| { + // It's desirable for finish_type to be optional since we do normal finishes 99% of the time. + let finish_type: u32 = lua.from_value(finish_type).unwrap_or(0); + let finish_type = match finish_type { + 0 => EventFinishType::Normal, + 1 => EventFinishType::Jumping, + _ => EventFinishType::Normal, + }; + this.finish_event(handler_id, arg, finish_type); + Ok(()) + }, + ); methods.add_method_mut("set_classjob", |_, this, classjob_id: u8| { this.set_classjob(classjob_id); Ok(()) @@ -642,6 +703,13 @@ impl UserData for LuaPlayer { this.set_inn_wakeup(watched); Ok(()) }); + methods.add_method_mut( + "do_solnine_teleporter", + |_, this, (event_id, unk1, unk2, unk3, unk4, unk5): (u32, u32, u16, u32, u32, u32)| { + this.do_solnine_teleporter(event_id, unk1, unk2, unk3, unk4, unk5); + Ok(()) + }, + ); } fn add_fields>(fields: &mut F) { diff --git a/src/world/mod.rs b/src/world/mod.rs index f8cb825..c4e89ff 100644 --- a/src/world/mod.rs +++ b/src/world/mod.rs @@ -15,6 +15,7 @@ pub use lua::{EffectsBuilder, LuaPlayer, LuaZone, load_init_script}; mod event; pub use event::Event; +pub use event::EventFinishType; mod actor; pub use actor::Actor; diff --git a/src/world/server.rs b/src/world/server.rs index 065e661..789e9ce 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -568,6 +568,51 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i to_remove.push(id); } } + + if let ClientTriggerCommand::WalkInTriggerFinished { .. } = &trigger.trigger + { + // This is where we finally release the client after the walk-in trigger. + let msg = FromServer::Unk18([0; 16]); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::WalkInTriggerRelatedUnk1 { + unk1: 0, + }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + + // Yes, this is actually sent every time the trigger event finishes... + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::CompanionUnlock { + unk1: 0, + unk2: 1, + }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::WalkInTriggerRelatedUnk2 { + unk1: 0, + unk2: 0, + unk3: 0, + unk4: 7, + }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + } continue; }