diff --git a/Cargo.lock b/Cargo.lock index 1fbe3cf..0902815 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -575,7 +575,7 @@ dependencies = [ [[package]] name = "icarus" version = "0.0.0" -source = "git+https://github.com/redstrate/Icarus?branch=ver%2F2025.04.16.0000.0000#5d5f1dded6949b790307546c9ddc0604af636451" +source = "git+https://github.com/redstrate/Icarus?branch=ver%2F2025.04.16.0000.0000#76c7c6c18bee63927fc723d4a99e2e1fba96fcb9" dependencies = [ "physis", ] @@ -1025,7 +1025,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "physis" version = "0.4.0" -source = "git+https://github.com/redstrate/physis#309726163a0d76f26831236f43740a2c8bd1b01d" +source = "git+https://github.com/redstrate/physis#76120b2dd0843c16af648f7b18ba0f92c5d56460" dependencies = [ "binrw", "bitflags", @@ -1472,9 +1472,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.2" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" dependencies = [ "bitflags", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 5b6bee2..bf968df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,4 +104,4 @@ rkon = { version = "0.1" } tower-http = { version = "0.6", features = ["fs"] } # excel sheet data -icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.04.16.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race"], default-features = false } +icarus = { git = "https://github.com/redstrate/Icarus", branch = "ver/2025.04.16.0000.0000", features = ["Warp", "Tribe", "ClassJob", "World", "TerritoryType", "Race", "Aetheryte"], default-features = false } diff --git a/resources/scripts/Global.lua b/resources/scripts/Global.lua index b42bf70..f41a897 100644 --- a/resources/scripts/Global.lua +++ b/resources/scripts/Global.lua @@ -20,6 +20,7 @@ end -- Actions registerAction(3, "actions/Sprint.lua") +registerAction(5, "actions/Teleport.lua") registerAction(9, "actions/FastBlade.lua") -- Items diff --git a/resources/scripts/actions/Teleport.lua b/resources/scripts/actions/Teleport.lua new file mode 100644 index 0000000..d849fae --- /dev/null +++ b/resources/scripts/actions/Teleport.lua @@ -0,0 +1,11 @@ +function doAction(player) + effects = EffectsBuilder() + + -- get the aetheryte they requested + local id = player.teleport_query.aetheryte_id + + -- warp there + player:warp_aetheryte(id) + + return effects +end diff --git a/src/bin/kawari-world.rs b/src/bin/kawari-world.rs index 24f15b5..4d77e6a 100644 --- a/src/bin/kawari-world.rs +++ b/src/bin/kawari-world.rs @@ -9,9 +9,9 @@ use kawari::config::get_config; use kawari::inventory::Item; use kawari::ipc::chat::{ServerChatIpcData, ServerChatIpcSegment}; use kawari::ipc::zone::{ - ActionEffect, ActionResult, ClientZoneIpcData, CommonSpawn, EffectKind, EventStart, - GameMasterCommandType, GameMasterRank, OnlineStatus, ServerZoneIpcData, ServerZoneIpcSegment, - SocialListRequestType, + ActionEffect, ActionResult, ClientTriggerCommand, ClientZoneIpcData, CommonSpawn, EffectKind, + EventStart, GameMasterCommandType, GameMasterRank, OnlineStatus, ServerZoneIpcData, + ServerZoneIpcSegment, SocialListRequestType, }; use kawari::ipc::zone::{ ActorControlCategory, ActorControlSelf, PlayerEntry, PlayerSpawn, PlayerStatus, SocialList, @@ -371,6 +371,11 @@ async fn client_loop( connection.exit_rotation = None; } ClientZoneIpcData::ClientTrigger(trigger) => { + // store the query for scripts + if let ClientTriggerCommand::TeleportQuery { aetheryte_id } = trigger.trigger { + connection.player_data.teleport_query.aetheryte_id = aetheryte_id as u16; + } + // 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; } @@ -851,6 +856,7 @@ async fn client_loop( 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::SpawnNPC(npc) => connection.send_npc(npc).await, + FromServer::ActorControlSelf(actor_control) => connection.actor_control_self(actor_control).await, }, None => break, } diff --git a/src/common/gamedata.rs b/src/common/gamedata.rs index d58abd1..5d1e936 100644 --- a/src/common/gamedata.rs +++ b/src/common/gamedata.rs @@ -1,3 +1,4 @@ +use icarus::Aetheryte::AetheryteSheet; use icarus::ClassJob::ClassJobSheet; use icarus::World::WorldSheet; use icarus::{Tribe::TribeSheet, Warp::WarpSheet}; @@ -106,4 +107,15 @@ impl GameData { Some((*pop_range_id, *zone_id)) } + + pub fn get_aetheryte(&mut self, aetheryte_id: u32) -> Option<(u32, u16)> { + let sheet = AetheryteSheet::read_from(&mut self.game_data, Language::English)?; + let row = sheet.get_row(aetheryte_id)?; + + // TODO: just look in the level sheet? + let pop_range_id = row.Level()[0].into_u32()?; + let zone_id = row.Territory().into_u16()?; + + Some((*pop_range_id, *zone_id)) + } } diff --git a/src/ipc/zone/actor_control.rs b/src/ipc/zone/actor_control.rs index 408f8e0..6ba078d 100644 --- a/src/ipc/zone/actor_control.rs +++ b/src/ipc/zone/actor_control.rs @@ -63,6 +63,12 @@ pub enum ActorControlCategory { #[bw(map = write_bool_as::)] unlocked: bool, }, + #[brw(magic = 0xCBu16)] + TeleportStart { + #[brw(pad_before = 2)] //padding + insufficient_gil: u32, + aetheryte_id: u32, + }, } #[binrw] diff --git a/src/ipc/zone/client_trigger.rs b/src/ipc/zone/client_trigger.rs index b3a3b42..3fba1a0 100644 --- a/src/ipc/zone/client_trigger.rs +++ b/src/ipc/zone/client_trigger.rs @@ -24,6 +24,12 @@ pub enum ClientTriggerCommand { unk1: u32, pose: u32, }, + #[brw(magic = 0xCAu16)] + TeleportQuery { + #[brw(pad_before = 2)] + aetheryte_id: u32, + // TODO: fill out the rest + }, } #[binrw] diff --git a/src/world/common.rs b/src/world/common.rs index d84f881..cdc80b2 100644 --- a/src/world/common.rs +++ b/src/world/common.rs @@ -10,7 +10,9 @@ use tokio::sync::mpsc::Sender; use crate::{ common::Position, - ipc::zone::{ActorControl, ActorControlTarget, ClientTrigger, CommonSpawn, NpcSpawn}, + ipc::zone::{ + ActorControl, ActorControlSelf, ActorControlTarget, ClientTrigger, CommonSpawn, NpcSpawn, + }, }; use super::Actor; @@ -29,8 +31,10 @@ pub enum FromServer { ActorDespawn(u32), /// We need to update an actor ActorControl(u32, ActorControl), - /// We need to update an actor's target' + /// We need to update an actor's target ActorControlTarget(u32, ActorControlTarget), + /// We need to update the player actor + ActorControlSelf(ActorControlSelf), /// Spawn an NPC SpawnNPC(NpcSpawn), } diff --git a/src/world/connection.rs b/src/world/connection.rs index 4e16633..9db73ca 100644 --- a/src/world/connection.rs +++ b/src/world/connection.rs @@ -32,6 +32,11 @@ use super::{ lua::Task, }; +#[derive(Debug, Default, Clone)] +pub struct TeleportQuery { + pub aetheryte_id: u16, +} + #[derive(Debug, Default, Clone)] pub struct PlayerData { // Static data @@ -52,6 +57,8 @@ pub struct PlayerData { pub rotation: f32, pub zone_id: u16, pub inventory: Inventory, + + pub teleport_query: TeleportQuery, } /// Represents a single connection between an instance of the client and the world server @@ -363,6 +370,35 @@ impl ZoneConnection { self.change_zone(territory_type as u16).await; } + pub async fn warp_aetheryte(&mut self, aetheryte_id: u32) { + tracing::info!("Warping to aetheryte {}", aetheryte_id); + + let territory_type; + // find the pop range on the other side + { + let mut game_data = self.gamedata.lock().unwrap(); + let (pop_range_id, zone_id) = game_data + .get_aetheryte(aetheryte_id) + .expect("Failed to find the aetheryte!"); + + let new_zone = Zone::load(&mut game_data.game_data, zone_id); + + // find it on the other side + let (object, _) = new_zone.find_pop_range(pop_range_id).unwrap(); + + // set the exit position + self.exit_position = Some(Position { + x: object.transform.translation[0], + y: object.transform.translation[1], + z: object.transform.translation[2], + }); + + territory_type = zone_id; + } + + self.change_zone(territory_type as u16).await; + } + pub async fn change_weather(&mut self, new_weather_id: u16) { let ipc = ServerZoneIpcSegment { op_code: ServerZoneIpcType::WeatherId, @@ -519,6 +555,9 @@ impl ZoneConnection { self.player_data.classjob_id = *classjob_id; self.update_class_info().await; } + Task::WarpAetheryte { aetheryte_id } => { + self.warp_aetheryte(*aetheryte_id).await; + } } } player.queued_tasks.clear(); diff --git a/src/world/lua.rs b/src/world/lua.rs index b750225..48052ea 100644 --- a/src/world/lua.rs +++ b/src/world/lua.rs @@ -10,7 +10,7 @@ use crate::{ packet::{PacketSegment, SegmentData, SegmentType}, }; -use super::{PlayerData, StatusEffects, Zone}; +use super::{PlayerData, StatusEffects, Zone, connection::TeleportQuery}; pub enum Task { ChangeTerritory { zone_id: u16 }, @@ -19,6 +19,7 @@ pub enum Task { BeginLogOut, FinishEvent { handler_id: u32 }, SetClassJob { classjob_id: u8 }, + WarpAetheryte { aetheryte_id: u32 }, } #[derive(Default)] @@ -129,6 +130,10 @@ impl LuaPlayer { fn set_classjob(&mut self, classjob_id: u8) { self.queued_tasks.push(Task::SetClassJob { classjob_id }); } + + fn warp_aetheryte(&mut self, aetheryte_id: u32) { + self.queued_tasks.push(Task::WarpAetheryte { aetheryte_id }); + } } impl UserData for LuaPlayer { @@ -181,6 +186,10 @@ impl UserData for LuaPlayer { this.set_classjob(classjob_id); Ok(()) }); + methods.add_method_mut("warp_aetheryte", |_, this, aetheryte_id: u32| { + this.warp_aetheryte(aetheryte_id); + Ok(()) + }); } fn add_fields>(fields: &mut F) { @@ -190,6 +199,16 @@ impl UserData for LuaPlayer { object_type: 0, }) }); + + fields.add_field_method_get("teleport_query", |_, this| { + Ok(this.player_data.teleport_query.clone()) + }); + } +} + +impl UserData for TeleportQuery { + fn add_fields>(fields: &mut F) { + fields.add_field_method_get("aetheryte_id", |_, this| Ok(this.aetheryte_id)); } } diff --git a/src/world/server.rs b/src/world/server.rs index d7d8a04..9912899 100644 --- a/src/world/server.rs +++ b/src/world/server.rs @@ -4,7 +4,7 @@ use tokio::sync::mpsc::Receiver; use crate::{ common::{ObjectId, Position}, ipc::zone::{ - ActorControl, ActorControlCategory, ActorControlTarget, BattleNpcSubKind, + ActorControl, ActorControlCategory, ActorControlSelf, ActorControlTarget, BattleNpcSubKind, ClientTriggerCommand, CommonSpawn, NpcSpawn, ObjectKind, }, }; @@ -208,13 +208,28 @@ pub async fn server_main_loop(mut recv: Receiver) -> Result<(), std::i for (id, (handle, _)) in &mut data.clients { let id = *id; - // there's no reason to tell the actor what it just did + tracing::info!("{:#?}", trigger); + + // handle player-to-server actions if id == from_id { + match &trigger.trigger { + ClientTriggerCommand::TeleportQuery { aetheryte_id } => { + let msg = FromServer::ActorControlSelf(ActorControlSelf { + category: ActorControlCategory::TeleportStart { + insufficient_gil: 0, + aetheryte_id: *aetheryte_id, + }, + }); + + if handle.send(msg).is_err() { + to_remove.push(id); + } + } + _ => {} + } continue; } - tracing::info!("{:#?}", trigger); - match &trigger.trigger { ClientTriggerCommand::SetTarget { actor_id } => { let msg = FromServer::ActorControlTarget(