Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/wow-data/src/spell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,7 @@ pub mod aura_types {

/// Selected `Targets` ids from C++ `SpellImplicitTargetInfo::_data`.
pub mod implicit_targets {
pub const TARGET_DEST_HOME: u32 = 9;
pub const TARGET_DEST_DB: u32 = 17;
pub const TARGET_DEST_NEARBY_ENTRY: u32 = 46;
pub const TARGET_DEST_NEARBY_ENTRY_2: u32 = 107;
Expand Down
204 changes: 191 additions & 13 deletions crates/wow-world/src/handlers/character.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ use crate::session::{
CharacterPetSpellChargeRowLikeCpp, CharacterPetSpellCooldownRowLikeCpp,
CharacterPetSpellRowLikeCpp, CharacterPetStableRowLikeCpp, RepresentedAlterAppearanceLikeCpp,
RepresentedBankItemMoveLikeCpp, RepresentedConfirmBarbersChoiceLikeCpp,
RepresentedGameObjectUseState,
RepresentedGameObjectUseState, RepresentedHomebindLikeCpp,
};

// ── Handler registration ────────────────────────────────────────────
Expand Down Expand Up @@ -349,6 +349,31 @@ fn movement_time_ms_for_transport_segment_like_cpp(
}
}

fn login_bind_point_update_like_cpp(
position: &Position,
map_id: i32,
zone_id: i32,
homebind: Option<RepresentedHomebindLikeCpp>,
) -> BindPointUpdate {
if let Some(homebind) = homebind {
return BindPointUpdate {
x: homebind.position.x,
y: homebind.position.y,
z: homebind.position.z,
map_id: i32::try_from(homebind.map_id).unwrap_or(i32::MAX),
area_id: i32::try_from(homebind.area_id).unwrap_or(i32::MAX),
};
}

BindPointUpdate {
x: position.x,
y: position.y,
z: position.z,
map_id,
area_id: zone_id,
}
}

fn transport_position_for_login_like_cpp(
nodes: &[TaxiPathNodeEntry],
move_speed: u32,
Expand Down Expand Up @@ -4195,6 +4220,42 @@ impl WorldSession {
// (`Player::SendInitialPacketsAfterAddToMap`). Seed from DB until
// that post-add terrain pass runs.
self.set_player_zone_area_like_cpp(zone as u32, zone as u32);
{
let mut homebind_stmt = char_db.prepare(CharStatements::SEL_CHARACTER_HOMEBIND);
homebind_stmt.set_u64(0, guid.counter() as u64);
match char_db.query(&homebind_stmt).await {
Ok(homebind_result) => {
if !homebind_result.is_empty() {
Comment thread
alseif0x marked this conversation as resolved.
let homebind = RepresentedHomebindLikeCpp {
map_id: u32::from(homebind_result.try_read::<u16>(0).unwrap_or(0)),
area_id: u32::from(homebind_result.try_read::<u16>(1).unwrap_or(0)),
position: Position::new(
homebind_result.try_read(2).unwrap_or(0.0),
homebind_result.try_read(3).unwrap_or(0.0),
homebind_result.try_read(4).unwrap_or(0.0),
homebind_result.try_read(5).unwrap_or(0.0),
),
};
self.set_represented_homebind_like_cpp(homebind);
Comment thread
alseif0x marked this conversation as resolved.
} else {
let homebind = RepresentedHomebindLikeCpp {
map_id: u32::try_from(map_id).unwrap_or(0),
area_id: u32::try_from(zone).unwrap_or(0),
position,
};
self.seed_represented_homebind_from_load_like_cpp(homebind)
.await;
}
}
Err(error) => {
warn!(
player_guid = guid.counter(),
%error,
"failed to load represented character_homebind row"
);
}
}
}
self.set_represented_guild_id_like_cpp(result.try_read::<u64>(11).unwrap_or(0));
self.load_represented_player_difficulties_like_cpp(
result.try_read::<u32>(44).unwrap_or(0),
Expand Down Expand Up @@ -9046,16 +9107,47 @@ impl WorldSession {
}

/// CMSG_BINDER_ACTIVATE — player sets hearthstone at innkeeper.
/// C++ ref: `HandleBinderActivateOpcode`
/// (`Handlers/NPCHandler.cpp:373-381`).
/// C++ refs: `WorldSession::HandleBinderActivateOpcode` /
/// `WorldSession::SendBindPoint` (`Handlers/NPCHandler.cpp:373-402`).
pub async fn handle_binder_activate(&mut self, hello: Hello) {
use wow_packet::packets::misc::NpcInteractionOpenResult;
info!(
"BinderActivate {:?} account {}",
hello.unit, self.account_id
);
// TODO: actually set hearthstone bind point in DB.
self.send_packet(&NpcInteractionOpenResult::new(hello.unit, 20)); // Binder
if !self.player_is_alive_like_cpp() {
return;
}
let Some(_innkeeper) = self.represented_npc_can_interact_with_like_cpp(
hello.unit,
NPCFlags1::INNKEEPER.bits(),
0,
) else {
debug!(
innkeeper_guid = ?hello.unit,
account = self.account_id,
"BinderActivate rejected: NPC missing, out of range, dead, or lacks INNKEEPER flag"
);
return;
};
if self.player_current_map_instanceable_like_cpp() {
debug!(
innkeeper_guid = ?hello.unit,
map_id = self.player_map_id_like_cpp(),
"BinderActivate rejected: current map is instanceable like C++ SendBindPoint"
);
return;
}

// C++ SendBindPoint has the creature cast spell 3286 on the player.
// The represented spell hook stores the same current-location bind.
if self
.set_homebind_to_current_location_like_cpp(hello.unit)
.await
{
self.send_packet(&GossipComplete {
suppress_sound: false,
});
}
}

/// CMSG_TABARD_VENDOR_ACTIVATE — player talks to a tabard designer.
Expand Down Expand Up @@ -13386,14 +13478,13 @@ impl WorldSession {
// 7. ContactList — C++ `GetSocial()->SendSocialList(this, SOCIAL_FLAG_ALL)`.
self.send_contact_list_like_cpp(7).await;

// 8. BindPointUpdate (hearthstone location = start position)
self.send_packet(&BindPointUpdate {
x: position.x,
y: position.y,
z: position.z,
// 8. BindPointUpdate (hearthstone location = loaded homebind)
self.send_packet(&login_bind_point_update_like_cpp(
position,
map_id,
area_id: zone_id,
});
zone_id,
self.represented_homebind_like_cpp(),
));

// 9. UpdateTalentData — C++ `Player::SendTalentsInfoData`.
self.send_packet(&self.represented_update_talent_data_packet_like_cpp());
Expand Down Expand Up @@ -16477,6 +16568,75 @@ mod tests {
assert!(send_rx.try_recv().is_err());
}

#[tokio::test]
async fn binder_activate_sets_current_homebind_and_sends_bind_packets_like_cpp() {
let (mut session, send_rx, canonical) = make_bank_slot_session(4);
let innkeeper = ObjectGuid::create_world_object(HighGuid::Creature, 0, 1, 571, 0, 2456, 30);
insert_banker_creature(&canonical, innkeeper, NPCFlags1::INNKEEPER.bits());
session.set_player_zone_area_like_cpp(12, 34);

session
.handle_binder_activate(Hello { unit: innkeeper })
.await;

assert_eq!(
session.represented_homebind_like_cpp(),
Some(RepresentedHomebindLikeCpp {
map_id: 571,
area_id: 34,
position: Position::new(0.0, 0.0, 0.0, 0.0),
})
);
assert_eq!(
drain_server_opcodes(&send_rx),
vec![
ServerOpcodes::BindPointUpdate,
ServerOpcodes::PlayerBound,
ServerOpcodes::GossipComplete,
]
);
}

#[tokio::test]
async fn binder_activate_rejects_instanceable_map_like_cpp() {
let (mut session, send_rx, canonical) = make_bank_slot_session(1);
let innkeeper = ObjectGuid::create_world_object(HighGuid::Creature, 0, 1, 571, 0, 2456, 31);
insert_banker_creature(&canonical, innkeeper, NPCFlags1::INNKEEPER.bits());
session.set_map_store(Arc::new(wow_data::MapStore::from_entries([
wow_data::MapEntry {
id: 571,
instance_type: wow_data::map::MAP_INSTANCE,
expansion_id: 2,
parent_map_id: -1,
cosmetic_parent_map_id: -1,
flags1: 0,
flags2: 0,
},
])));

session
.handle_binder_activate(Hello { unit: innkeeper })
.await;

assert!(session.represented_homebind_like_cpp().is_none());
assert!(send_rx.try_recv().is_err());
}

#[tokio::test]
async fn binder_activate_rejects_non_innkeeper_like_cpp() {
let (mut session, send_rx, canonical) = make_bank_slot_session(1);
let creature = ObjectGuid::create_world_object(HighGuid::Creature, 0, 1, 571, 0, 2456, 31);
insert_banker_creature(&canonical, creature, NPCFlags1::BANKER.bits());
session.set_player_zone_area_like_cpp(12, 34);

session
.handle_binder_activate(Hello { unit: creature })
.await;

assert!(session.represented_homebind_like_cpp().is_none());
assert!(send_rx.try_recv().is_err());
}

#[tokio::test]
async fn autobank_item_records_move_after_banker_activation_like_cpp() {
let (mut session, send_rx, canonical) = make_bank_slot_session(4);
Expand Down Expand Up @@ -16923,6 +17083,24 @@ mod tests {
}
}

#[test]
fn login_bind_point_update_uses_loaded_homebind_like_cpp() {
let current = Position::new(1.0, 2.0, 3.0, 0.5);
let homebind = RepresentedHomebindLikeCpp {
map_id: 571,
area_id: 77,
position: Position::new(10.0, 20.0, 30.0, 1.5),
};

let packet = login_bind_point_update_like_cpp(&current, 1, 14, Some(homebind));

assert_eq!(packet.x, 10.0);
assert_eq!(packet.y, 20.0);
assert_eq!(packet.z, 30.0);
assert_eq!(packet.map_id, 571);
assert_eq!(packet.area_id, 77);
}

#[test]
fn display_ids_are_valid() {
for race in [1, 2, 3, 4, 5, 6, 7, 8, 10, 11] {
Expand Down
Loading
Loading