mirror of
https://github.com/redstrate/Kawari.git
synced 2025-07-24 21:47:45 +00:00
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.
This commit is contained in:
parent
2acbfe6df8
commit
468ca97257
14 changed files with 334 additions and 45 deletions
|
@ -334,6 +334,11 @@
|
||||||
"name": "SetSearchComment",
|
"name": "SetSearchComment",
|
||||||
"opcode": 512,
|
"opcode": 512,
|
||||||
"size": 216
|
"size": 216
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "WalkInEvent",
|
||||||
|
"opcode": 316,
|
||||||
|
"size": 24
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ClientZoneIpcType": [
|
"ClientZoneIpcType": [
|
||||||
|
@ -496,6 +501,11 @@
|
||||||
"name": "EquipGearset",
|
"name": "EquipGearset",
|
||||||
"opcode": 101,
|
"opcode": 101,
|
||||||
"size": 72
|
"size": 72
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "StartWalkInEvent",
|
||||||
|
"opcode": 607,
|
||||||
|
"size": 24
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ServerLobbyIpcType": [
|
"ServerLobbyIpcType": [
|
||||||
|
|
|
@ -375,6 +375,15 @@ generic_currency_exchange = {
|
||||||
-- 3539075, -- Dibourdier <Mahjong Vendor> doesn't respond when interacted with right now, probably needs special handling
|
-- 3539075, -- Dibourdier <Mahjong Vendor> 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
|
-- Not custom in the sense of non-SQEX content, just going based off the directory name
|
||||||
custom0_events = {
|
custom0_events = {
|
||||||
[720916] = "cmndefinnbed_00020.lua",
|
[720916] = "cmndefinnbed_00020.lua",
|
||||||
|
@ -397,6 +406,7 @@ TOSORT_DIR = "events/tosort/"
|
||||||
OPENING_DIR = "events/quest/opening/"
|
OPENING_DIR = "events/quest/opening/"
|
||||||
CUSTOM0_DIR = "events/custom/000/"
|
CUSTOM0_DIR = "events/custom/000/"
|
||||||
CUSTOM1_DIR = "events/custom/001/"
|
CUSTOM1_DIR = "events/custom/001/"
|
||||||
|
TRIGGER_DIR = "events/walkin_trigger/"
|
||||||
|
|
||||||
for _, event_id in pairs(generic_warps) do
|
for _, event_id in pairs(generic_warps) do
|
||||||
registerEvent(event_id, "events/common/GenericWarp.lua")
|
registerEvent(event_id, "events/common/GenericWarp.lua")
|
||||||
|
@ -441,3 +451,7 @@ end
|
||||||
for event_id, script_file in pairs(quests) do
|
for event_id, script_file in pairs(quests) do
|
||||||
registerEvent(event_id, OPENING_DIR..script_file)
|
registerEvent(event_id, OPENING_DIR..script_file)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
for _, event_id in pairs(solution_nine_teleporters) do
|
||||||
|
registerEvent(event_id, TRIGGER_DIR.."SolutionNineTeleporter.lua")
|
||||||
|
end
|
||||||
|
|
|
@ -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
|
|
@ -3,15 +3,17 @@ use std::sync::{Arc, Mutex};
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
use kawari::common::Position;
|
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::config::get_config;
|
||||||
use kawari::inventory::{
|
use kawari::inventory::{
|
||||||
BuyBackItem, ContainerType, CurrencyKind, Item, ItemOperationKind, get_container_type,
|
BuyBackItem, ContainerType, CurrencyKind, Item, ItemOperationKind, get_container_type,
|
||||||
};
|
};
|
||||||
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
|
use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment};
|
||||||
use kawari::ipc::zone::{
|
use kawari::ipc::zone::{
|
||||||
ActorControlCategory, ActorControlSelf, ItemOperation, PlayerEntry, PlayerSpawn, PlayerStatus,
|
ActorControl, ActorControlCategory, ActorControlSelf, ItemOperation, PlayerEntry, PlayerSpawn,
|
||||||
SocialList,
|
PlayerStatus, SocialList,
|
||||||
};
|
};
|
||||||
|
|
||||||
use kawari::ipc::zone::{
|
use kawari::ipc::zone::{
|
||||||
|
@ -27,8 +29,8 @@ use kawari::world::{
|
||||||
ChatHandler, ExtraLuaState, LuaZone, ObsfucationData, Zone, ZoneConnection, load_init_script,
|
ChatHandler, ExtraLuaState, LuaZone, ObsfucationData, Zone, ZoneConnection, load_init_script,
|
||||||
};
|
};
|
||||||
use kawari::world::{
|
use kawari::world::{
|
||||||
ClientHandle, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects, ToServer,
|
ClientHandle, EventFinishType, FromServer, LuaPlayer, PlayerData, ServerHandle, StatusEffects,
|
||||||
WorldDatabase, handle_custom_ipc, server_main_loop,
|
ToServer, WorldDatabase, handle_custom_ipc, server_main_loop,
|
||||||
};
|
};
|
||||||
use kawari::{
|
use kawari::{
|
||||||
ERR_INVENTORY_ADD_FAILED, LogMessageType, RECEIVE_BUFFER_SIZE, TITLE_UNLOCK_BITMASK_SIZE,
|
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 { .. } => {
|
ClientZoneIpcData::Unk3 { .. } => {
|
||||||
// no-op
|
// no-op
|
||||||
}
|
}
|
||||||
|
@ -916,15 +915,15 @@ async fn client_loop(
|
||||||
} else {
|
} else {
|
||||||
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
tracing::error!(ERR_INVENTORY_ADD_FAILED);
|
||||||
connection.send_message(ERR_INVENTORY_ADD_FAILED).await;
|
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 {
|
} else {
|
||||||
connection.send_message("Insufficient gil to buy item. Nice try bypassing the client-side check!").await;
|
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 {
|
} else {
|
||||||
connection.send_message("Unable to find shop item, this is a bug in Kawari!").await;
|
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 {
|
} else if *buy_sell_mode == SELL {
|
||||||
let storage = get_container_type(*item_index).unwrap();
|
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;
|
connection.event_scene(&target_id, *event_id, 10, 8193, params).await;
|
||||||
} else {
|
} else {
|
||||||
connection.send_message("Unable to find shop item, this is a bug in Kawari!").await;
|
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 {
|
} else {
|
||||||
tracing::error!("Received unknown transaction mode {buy_sell_mode}!");
|
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 } => {
|
ClientZoneIpcData::StartTalkEvent { actor_id, event_id } => {
|
||||||
|
@ -1319,6 +1318,22 @@ async fn client_loop(
|
||||||
tracing::info!("Client tried to equip a gearset!");
|
tracing::info!("Client tried to equip a gearset!");
|
||||||
connection.send_message("Gearsets are not yet implemented.").await;
|
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 { .. } => {
|
ClientZoneIpcData::Unknown { .. } => {
|
||||||
tracing::warn!("Unknown packet {:?} recieved, this should be handled!", data.op_code);
|
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::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, effect_param, effect_source_actor_id) => connection.lose_effect(effect_id, effect_param, effect_source_actor_id, &mut lua_player).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,
|
None => break,
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,11 @@ pub enum ActorControlCategory {
|
||||||
#[bw(map = write_bool_as::<u32>)]
|
#[bw(map = write_bool_as::<u32>)]
|
||||||
unlocked: bool,
|
unlocked: bool,
|
||||||
},
|
},
|
||||||
|
#[brw(magic = 0x8Au16)]
|
||||||
|
EventRelatedUnk3 {
|
||||||
|
#[brw(pad_before = 2)] //padding
|
||||||
|
event_id: u32,
|
||||||
|
},
|
||||||
#[brw(magic = 0xCBu16)]
|
#[brw(magic = 0xCBu16)]
|
||||||
TeleportStart {
|
TeleportStart {
|
||||||
#[brw(pad_before = 2)] //padding
|
#[brw(pad_before = 2)] //padding
|
||||||
|
@ -83,6 +88,11 @@ pub enum ActorControlCategory {
|
||||||
#[brw(pad_before = 2)] // padding
|
#[brw(pad_before = 2)] // padding
|
||||||
speed: u16,
|
speed: u16,
|
||||||
},
|
},
|
||||||
|
#[brw(magic = 0xECu16)]
|
||||||
|
WalkInTriggerRelatedUnk3 {
|
||||||
|
#[brw(pad_before = 2)] // padding
|
||||||
|
unk1: u32,
|
||||||
|
},
|
||||||
#[brw(magic = 0x386u16)]
|
#[brw(magic = 0x386u16)]
|
||||||
SetFestival {
|
SetFestival {
|
||||||
#[brw(pad_before = 2)] // padding
|
#[brw(pad_before = 2)] // padding
|
||||||
|
@ -177,6 +187,20 @@ pub enum ActorControlCategory {
|
||||||
unk2: u32,
|
unk2: u32,
|
||||||
unk3: 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 {
|
Unknown {
|
||||||
category: u16,
|
category: u16,
|
||||||
#[brw(pad_before = 2)] // padding
|
#[brw(pad_before = 2)] // padding
|
||||||
|
|
|
@ -50,6 +50,11 @@ pub enum ClientTriggerCommand {
|
||||||
aetheryte_id: u32,
|
aetheryte_id: u32,
|
||||||
// TODO: fill out the rest
|
// TODO: fill out the rest
|
||||||
},
|
},
|
||||||
|
#[brw(magic = 0x25eu16)]
|
||||||
|
WalkInTriggerFinished {
|
||||||
|
#[brw(pad_before = 2)]
|
||||||
|
unk1: u32,
|
||||||
|
},
|
||||||
#[brw(magic = 0x033Eu16)]
|
#[brw(magic = 0x033Eu16)]
|
||||||
EventRelatedUnk {
|
EventRelatedUnk {
|
||||||
// seen in haircut event
|
// seen in haircut event
|
||||||
|
|
|
@ -586,6 +586,18 @@ pub enum ServerZoneIpcData {
|
||||||
// TODO: fill this out
|
// TODO: fill this out
|
||||||
unk1: [u8; 968],
|
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 {
|
Unknown {
|
||||||
#[br(count = size - 32)]
|
#[br(count = size - 32)]
|
||||||
unk: Vec<u8>,
|
unk: Vec<u8>,
|
||||||
|
@ -791,6 +803,13 @@ pub enum ClientZoneIpcData {
|
||||||
#[brw(pad_after = 2)]
|
#[brw(pad_after = 2)]
|
||||||
unk2: u16,
|
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))]
|
#[br(pre_assert(*magic == ClientZoneIpcType::ContentFinderAction))]
|
||||||
ContentFinderAction { unk1: [u8; 8] },
|
ContentFinderAction { unk1: [u8; 8] },
|
||||||
Unknown {
|
Unknown {
|
||||||
|
@ -1161,6 +1180,16 @@ mod tests {
|
||||||
ServerZoneIpcType::Unk17,
|
ServerZoneIpcType::Unk17,
|
||||||
ServerZoneIpcData::Unk17 { unk1: [0; 968] },
|
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 {
|
for (opcode, data) in &ipc_types {
|
||||||
|
@ -1339,6 +1368,18 @@ mod tests {
|
||||||
ClientZoneIpcType::UnkCall2,
|
ClientZoneIpcType::UnkCall2,
|
||||||
ClientZoneIpcData::UnkCall2 { unk1: [0; 8] },
|
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 {
|
for (opcode, data) in &ipc_types {
|
||||||
|
|
|
@ -3,7 +3,7 @@ use crate::{
|
||||||
common::ItemInfoQuery,
|
common::ItemInfoQuery,
|
||||||
inventory::{Item, Storage},
|
inventory::{Item, Storage},
|
||||||
ipc::zone::{ChatMessage, GameMasterRank},
|
ipc::zone::{ChatMessage, GameMasterRank},
|
||||||
world::ToServer,
|
world::{EventFinishType, ToServer},
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ZoneConnection;
|
use super::ZoneConnection;
|
||||||
|
@ -110,7 +110,9 @@ impl ChatHandler {
|
||||||
}
|
}
|
||||||
"!finishevent" => {
|
"!finishevent" => {
|
||||||
if let Some(event) = &connection.event {
|
if let Some(event) = &connection.event {
|
||||||
connection.event_finish(event.id, 0).await;
|
connection
|
||||||
|
.event_finish(event.id, 0, EventFinishType::Normal)
|
||||||
|
.await;
|
||||||
connection
|
connection
|
||||||
.send_message("Current event forcefully finished.")
|
.send_message("Current event forcefully finished.")
|
||||||
.await;
|
.await;
|
||||||
|
|
|
@ -50,6 +50,7 @@ pub enum FromServer {
|
||||||
ReplayPacket(PacketSegment<ServerZoneIpcSegment>),
|
ReplayPacket(PacketSegment<ServerZoneIpcSegment>),
|
||||||
/// The player should lose this effect.
|
/// The player should lose this effect.
|
||||||
LoseEffect(u16, u16, ObjectId),
|
LoseEffect(u16, u16, ObjectId),
|
||||||
|
Unk18([u8; 16]),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -39,8 +39,8 @@ use crate::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
Actor, CharacterData, EffectsBuilder, Event, LuaPlayer, StatusEffects, ToServer, WorldDatabase,
|
Actor, CharacterData, EffectsBuilder, Event, EventFinishType, LuaPlayer, StatusEffects,
|
||||||
Zone,
|
ToServer, WorldDatabase, Zone,
|
||||||
common::{ClientId, ServerHandle},
|
common::{ClientId, ServerHandle},
|
||||||
load_init_script,
|
load_init_script,
|
||||||
lua::Task,
|
lua::Task,
|
||||||
|
@ -860,7 +860,11 @@ impl ZoneConnection {
|
||||||
self.warp(*warp_id).await;
|
self.warp(*warp_id).await;
|
||||||
}
|
}
|
||||||
Task::BeginLogOut => self.begin_log_out().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 } => {
|
Task::SetClassJob { classjob_id } => {
|
||||||
self.player_data.classjob_id = *classjob_id;
|
self.player_data.classjob_id = *classjob_id;
|
||||||
self.update_class_info().await;
|
self.update_class_info().await;
|
||||||
|
@ -1081,11 +1085,12 @@ impl ZoneConnection {
|
||||||
"Unable to play event {event_id}, scene {:?}, scene_flags {scene_flags}!",
|
"Unable to play event {event_id}, scene {:?}, scene_flags {scene_flags}!",
|
||||||
scene
|
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();
|
self.player_data.target_actorid = ObjectTypeId::default();
|
||||||
// sent event finish
|
// sent event finish
|
||||||
{
|
{
|
||||||
|
@ -1111,22 +1116,29 @@ impl ZoneConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// give back control to the player
|
// give back control to the player
|
||||||
{
|
let unk = match finish_type {
|
||||||
let ipc = ServerZoneIpcSegment {
|
EventFinishType::Normal => [0; 16],
|
||||||
op_code: ServerZoneIpcType::Unk18,
|
EventFinishType::Jumping => [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||||
timestamp: timestamp_secs(),
|
};
|
||||||
data: ServerZoneIpcData::Unk18 { unk: [0; 16] },
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
self.send_segment(PacketSegment {
|
self.send_unk18(unk).await;
|
||||||
source_actor: self.player_data.actor_id,
|
}
|
||||||
target_actor: self.player_data.actor_id,
|
|
||||||
segment_type: SegmentType::Ipc,
|
pub async fn send_unk18(&mut self, unk: [u8; 16]) {
|
||||||
data: SegmentData::Ipc { data: ipc },
|
let ipc = ServerZoneIpcSegment {
|
||||||
})
|
op_code: ServerZoneIpcType::Unk18,
|
||||||
.await;
|
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) {
|
pub async fn send_inventory_ack(&mut self, sequence: u32, action_type: u16) {
|
||||||
|
@ -1843,7 +1855,8 @@ impl ZoneConnection {
|
||||||
|
|
||||||
if should_cancel {
|
if should_cancel {
|
||||||
// give control back to the player so they aren't stuck
|
// 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!(
|
self.send_message(&format!(
|
||||||
"Event {event_id} tried to start, but it doesn't have a script associated with it!"
|
"Event {event_id} tried to start, but it doesn't have a script associated with it!"
|
||||||
))
|
))
|
||||||
|
|
|
@ -10,6 +10,12 @@ pub struct Event {
|
||||||
pub id: u32,
|
pub id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone)]
|
||||||
|
pub enum EventFinishType {
|
||||||
|
Normal,
|
||||||
|
Jumping,
|
||||||
|
}
|
||||||
|
|
||||||
impl Event {
|
impl Event {
|
||||||
pub fn new(id: u32, path: &str) -> Self {
|
pub fn new(id: u32, path: &str) -> Self {
|
||||||
let lua = Lua::new();
|
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) {
|
pub fn scene_finished(&mut self, player: &mut LuaPlayer, scene: u16) {
|
||||||
let mut run_script = || {
|
let mut run_script = || {
|
||||||
self.lua.scope(|scope| {
|
self.lua.scope(|scope| {
|
||||||
|
|
|
@ -16,7 +16,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
opcodes::ServerZoneIpcType,
|
opcodes::ServerZoneIpcType,
|
||||||
packet::{PacketSegment, SegmentData, SegmentType},
|
packet::{PacketSegment, SegmentData, SegmentType},
|
||||||
world::ExtraLuaState,
|
world::{EventFinishType, ExtraLuaState},
|
||||||
};
|
};
|
||||||
use mlua::{FromLua, Lua, LuaSerdeExt, UserData, UserDataFields, UserDataMethods, Value};
|
use mlua::{FromLua, Lua, LuaSerdeExt, UserData, UserDataFields, UserDataMethods, Value};
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ pub enum Task {
|
||||||
FinishEvent {
|
FinishEvent {
|
||||||
handler_id: u32,
|
handler_id: u32,
|
||||||
arg: u32,
|
arg: u32,
|
||||||
|
finish_type: EventFinishType,
|
||||||
},
|
},
|
||||||
SetClassJob {
|
SetClassJob {
|
||||||
classjob_id: u8,
|
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();
|
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);
|
tracing::warn!(error_message);
|
||||||
self.send_message(&error_message, 0);
|
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);
|
self.queued_tasks.push(Task::BeginLogOut);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn finish_event(&mut self, handler_id: u32, arg: u32) {
|
fn finish_event(&mut self, handler_id: u32, arg: u32, finish_type: EventFinishType) {
|
||||||
self.queued_tasks
|
self.queued_tasks.push(Task::FinishEvent {
|
||||||
.push(Task::FinishEvent { handler_id, arg });
|
handler_id,
|
||||||
|
arg,
|
||||||
|
finish_type,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_classjob(&mut self, classjob_id: u8) {
|
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) {
|
fn add_exp(&mut self, amount: u32) {
|
||||||
self.queued_tasks.push(Task::AddExp { amount });
|
self.queued_tasks.push(Task::AddExp { amount });
|
||||||
}
|
}
|
||||||
|
@ -564,10 +615,20 @@ impl UserData for LuaPlayer {
|
||||||
this.begin_log_out();
|
this.begin_log_out();
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
methods.add_method_mut("finish_event", |_, this, (handler_id, arg): (u32, u32)| {
|
methods.add_method_mut(
|
||||||
this.finish_event(handler_id, arg);
|
"finish_event",
|
||||||
Ok(())
|
|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| {
|
methods.add_method_mut("set_classjob", |_, this, classjob_id: u8| {
|
||||||
this.set_classjob(classjob_id);
|
this.set_classjob(classjob_id);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -642,6 +703,13 @@ impl UserData for LuaPlayer {
|
||||||
this.set_inn_wakeup(watched);
|
this.set_inn_wakeup(watched);
|
||||||
Ok(())
|
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<F: UserDataFields<Self>>(fields: &mut F) {
|
fn add_fields<F: UserDataFields<Self>>(fields: &mut F) {
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub use lua::{EffectsBuilder, LuaPlayer, LuaZone, load_init_script};
|
||||||
|
|
||||||
mod event;
|
mod event;
|
||||||
pub use event::Event;
|
pub use event::Event;
|
||||||
|
pub use event::EventFinishType;
|
||||||
|
|
||||||
mod actor;
|
mod actor;
|
||||||
pub use actor::Actor;
|
pub use actor::Actor;
|
||||||
|
|
|
@ -568,6 +568,51 @@ pub async fn server_main_loop(mut recv: Receiver<ToServer>) -> Result<(), std::i
|
||||||
to_remove.push(id);
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue