diff --git a/crates/wow-data/src/spell.rs b/crates/wow-data/src/spell.rs index 96c8a407..191396c1 100644 --- a/crates/wow-data/src/spell.rs +++ b/crates/wow-data/src/spell.rs @@ -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; diff --git a/crates/wow-world/src/handlers/character.rs b/crates/wow-world/src/handlers/character.rs index a8462756..a1a20298 100644 --- a/crates/wow-world/src/handlers/character.rs +++ b/crates/wow-world/src/handlers/character.rs @@ -66,7 +66,7 @@ use crate::session::{ CharacterPetSpellChargeRowLikeCpp, CharacterPetSpellCooldownRowLikeCpp, CharacterPetSpellRowLikeCpp, CharacterPetStableRowLikeCpp, RepresentedAlterAppearanceLikeCpp, RepresentedBankItemMoveLikeCpp, RepresentedConfirmBarbersChoiceLikeCpp, - RepresentedGameObjectUseState, + RepresentedGameObjectUseState, RepresentedHomebindLikeCpp, }; // ── Handler registration ──────────────────────────────────────────── @@ -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, +) -> 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, @@ -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() { + let homebind = RepresentedHomebindLikeCpp { + map_id: u32::from(homebind_result.try_read::(0).unwrap_or(0)), + area_id: u32::from(homebind_result.try_read::(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); + } 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::(11).unwrap_or(0)); self.load_represented_player_difficulties_like_cpp( result.try_read::(44).unwrap_or(0), @@ -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. @@ -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()); @@ -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); @@ -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(¤t, 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] { diff --git a/crates/wow-world/src/session.rs b/crates/wow-world/src/session.rs index 7c70d3f7..bdff9b9d 100644 --- a/crates/wow-world/src/session.rs +++ b/crates/wow-world/src/session.rs @@ -4516,8 +4516,8 @@ pub struct WorldSession { Option<(u32, wow_core::Position, TeleportToOptionsLikeCpp)>, /// Represented zone/area for the pending near-teleport destination. near_teleport_destination_zone_area_like_cpp: Option<(u32, u32)>, - /// C++ `Player::m_homebind` / `m_homebindAreaId` represented until - /// `character_homebind` loading/saving is wired into the player runtime. + /// C++ `Player::m_homebind` / `m_homebindAreaId` represented until full + /// player-owned load validation/fallback replaces the session field. represented_homebind_like_cpp: Option, /// C++ `Player::_resurrectionData`, represented until real Player/Spell /// resurrection request ownership exists. @@ -49340,16 +49340,24 @@ impl WorldSession { } for effect in spell_info.effects() { + let represented_home_target_data = + self.represented_home_destination_target_data_like_cpp(effect, &target_data); let represented_db_target_data = self .represented_db_caster_destination_target_data_like_cpp( &spell_info, effect, - &target_data, + represented_home_target_data + .as_ref() + .unwrap_or(&target_data), ); - let effect_target_data = represented_db_target_data.as_ref().unwrap_or(&target_data); + let effect_target_data = represented_db_target_data + .as_ref() + .or(represented_home_target_data.as_ref()) + .unwrap_or(&target_data); self.apply_effect_teleport_units_like_cpp(effect, target_guid, effect_target_data) .await; - self.apply_effect_bind_like_cpp(effect, player_guid, target_guid, effect_target_data); + self.apply_effect_bind_like_cpp(effect, player_guid, target_guid, effect_target_data) + .await; } if spell_info.effects().is_empty() { @@ -49370,7 +49378,8 @@ impl WorldSession { player_guid, target_guid, &target_data, - ); + ) + .await; } let mut force_visibility_after_gameobject_summon = false; @@ -50622,7 +50631,7 @@ impl WorldSession { self.teleport_to(target_map, destination).await; } - fn apply_effect_bind_like_cpp( + async fn apply_effect_bind_like_cpp( &mut self, effect: &wow_data::SpellEffectInfo, caster_guid: ObjectGuid, @@ -50660,30 +50669,144 @@ impl WorldSession { .and_then(|map_id| u32::try_from(map_id).ok()) .unwrap_or_else(|| u32::from(self.player_map_id_like_cpp())); - self.represented_homebind_like_cpp = Some(RepresentedHomebindLikeCpp { - map_id, - area_id, - position, - }); + self.set_homebind_like_cpp( + caster_guid, + RepresentedHomebindLikeCpp { + map_id, + area_id, + position, + }, + ) + .await; + } + + pub(crate) async fn set_homebind_to_current_location_like_cpp( + &mut self, + binder_id: ObjectGuid, + ) -> bool { + let Some(position) = self.player_position_like_cpp() else { + return false; + }; + let (_, area_id) = self.player_zone_area_like_cpp(); + let map_id = u32::from(self.player_map_id_like_cpp()); + self.set_homebind_like_cpp( + binder_id, + RepresentedHomebindLikeCpp { + map_id, + area_id, + position, + }, + ) + .await; + true + } + + pub(crate) fn player_current_map_instanceable_like_cpp(&self) -> bool { + let map_id = u32::from(self.player_map_id_like_cpp()); + self.map_store() + .and_then(|store| store.get(map_id)) + .is_some_and(|entry| entry.instance_type != wow_data::map::MAP_COMMON) + } + + pub(crate) async fn seed_represented_homebind_from_load_like_cpp( + &mut self, + homebind: RepresentedHomebindLikeCpp, + ) { + self.represented_homebind_like_cpp = Some(homebind); + self.persist_player_homebind_like_cpp(homebind).await; + } + + async fn set_homebind_like_cpp( + &mut self, + binder_id: ObjectGuid, + homebind: RepresentedHomebindLikeCpp, + ) { + self.represented_homebind_like_cpp = Some(homebind); + self.persist_player_homebind_like_cpp(homebind).await; self.send_packet(&wow_packet::packets::misc::BindPointUpdate { - x: position.x, - y: position.y, - z: position.z, - map_id: i32::try_from(map_id).unwrap_or(i32::MAX), - area_id: i32::try_from(area_id).unwrap_or(i32::MAX), + 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), }); self.send_packet(&wow_packet::packets::misc::PlayerBound { - binder_id: caster_guid, - area_id, + binder_id, + area_id: homebind.area_id, }); } + fn build_player_homebind_update_statement_like_cpp( + homebind: RepresentedHomebindLikeCpp, + guid_counter: u64, + ) -> PreparedStatement { + let mut stmt = PreparedStatement::new(CharStatements::UPD_PLAYER_HOMEBIND.sql()); + stmt.set_u16(0, u16::try_from(homebind.map_id).unwrap_or(u16::MAX)); + stmt.set_u16(1, u16::try_from(homebind.area_id).unwrap_or(u16::MAX)); + stmt.set_f32(2, homebind.position.x); + stmt.set_f32(3, homebind.position.y); + stmt.set_f32(4, homebind.position.z); + stmt.set_f32(5, homebind.position.orientation); + stmt.set_u64(6, guid_counter); + stmt + } + + fn build_player_homebind_insert_statement_like_cpp( + homebind: RepresentedHomebindLikeCpp, + guid_counter: u64, + ) -> PreparedStatement { + let mut stmt = PreparedStatement::new(CharStatements::INS_PLAYER_HOMEBIND.sql()); + stmt.set_u64(0, guid_counter); + stmt.set_u16(1, u16::try_from(homebind.map_id).unwrap_or(u16::MAX)); + stmt.set_u16(2, u16::try_from(homebind.area_id).unwrap_or(u16::MAX)); + stmt.set_f32(3, homebind.position.x); + stmt.set_f32(4, homebind.position.y); + stmt.set_f32(5, homebind.position.z); + stmt.set_f32(6, homebind.position.orientation); + stmt + } + + async fn persist_player_homebind_like_cpp(&self, homebind: RepresentedHomebindLikeCpp) { + let (Some(player_guid), Some(char_db)) = + (self.player_guid(), self.char_db().map(Arc::clone)) + else { + return; + }; + let guid_counter = player_guid.counter() as u64; + let update_stmt = + Self::build_player_homebind_update_statement_like_cpp(homebind, guid_counter); + match char_db.execute(&update_stmt).await { + Ok(0) => { + // C++ Player::_LoadHomebind creates a missing row before + // Player::SetHomebind later uses CHAR_UPD_PLAYER_HOMEBIND. + // Rust's represented login path can still lack that invariant, + // so recover it here without changing the C++ update bind order. + let insert_stmt = + Self::build_player_homebind_insert_statement_like_cpp(homebind, guid_counter); + if let Err(error) = char_db.execute(&insert_stmt).await { + warn!( + player_guid = guid_counter, + %error, + "failed to insert represented player homebind after update affected zero rows" + ); + } + } + Ok(_) => {} + Err(error) => { + warn!( + player_guid = guid_counter, + %error, + "failed to update represented player homebind" + ); + } + } + } + pub(crate) fn represented_homebind_like_cpp(&self) -> Option { self.represented_homebind_like_cpp } - #[cfg(test)] pub(crate) fn set_represented_homebind_like_cpp( &mut self, homebind: RepresentedHomebindLikeCpp, @@ -50816,6 +50939,33 @@ impl WorldSession { .map(|dynamic_object| dynamic_object.world().position()) } + fn represented_home_destination_target_data_like_cpp( + &self, + effect: &wow_data::SpellEffectInfo, + target_data: &SpellTargetData, + ) -> Option { + if target_data.dst_location.is_some() + || !matches!( + effect.implicit_target_1, + wow_data::spell::implicit_targets::TARGET_DEST_HOME + ) && !matches!( + effect.implicit_target_2, + wow_data::spell::implicit_targets::TARGET_DEST_HOME + ) + { + return None; + } + + let homebind = self.represented_homebind_like_cpp()?; + let mut target_data = target_data.clone(); + target_data.dst_location = Some(wow_packet::packets::spell::TargetLocation { + transport: ObjectGuid::EMPTY, + position: homebind.position, + }); + target_data.map_id = Some(i32::try_from(homebind.map_id).unwrap_or(i32::MAX)); + Some(target_data) + } + fn represented_db_caster_destination_target_data_like_cpp( &self, spell_info: &wow_data::SpellInfo, @@ -67756,6 +67906,110 @@ mod tests { assert_eq!(session.state, SessionState::Transfer); } + #[tokio::test] + async fn teleport_units_target_dest_home_uses_represented_homebind_like_cpp() { + let (mut session, _, _send_rx) = make_session(); + let spell_id = 8690_i32; + let player_guid = ObjectGuid::create_player(1, 7040); + let player_position = Position::new(300.0, 400.0, 60.0, 0.5); + let home_position = Position::new(40.0, 50.0, 60.0, 1.25); + let canonical = shared_canonical_map_manager(); + let hearth_effect = wow_data::SpellEffectInfo { + effect_index: 0, + effect: wow_data::spell::spell_effect_types::SPELL_EFFECT_TELEPORT_UNITS, + implicit_target_1: wow_data::spell::implicit_targets::TARGET_DEST_HOME, + ..Default::default() + }; + let spell_info = teleport_units_spell_info_like_cpp(spell_id, vec![hearth_effect]); + + session.set_canonical_map_manager(Arc::clone(&canonical)); + session.attach_player_controller_like_cpp(SessionPlayerController::new( + player_guid, + "HearthHome".to_string(), + player_position, + 571, + 1, + 1, + 80, + 0, + )); + session.set_represented_homebind_like_cpp(RepresentedHomebindLikeCpp { + map_id: 1, + area_id: 1519, + position: home_position, + }); + add_canonical_test_player_on_map(&canonical, player_guid, player_position, 571, 0); + let mut spell_store = wow_data::SpellStore::new(); + spell_store.insert(spell_id, spell_info); + session.set_spell_store(Arc::new(spell_store)); + + session + .execute_spell_with_visual_and_target_data( + spell_id, + player_guid, + ObjectGuid::EMPTY, + wow_packet::packets::spell::SpellCastVisual { + spell_visual_id: 8690, + script_visual_id: 0, + }, + SpellTargetData::default(), + ) + .await + .expect("TARGET_DEST_HOME hearthstone teleport should execute"); + + assert_eq!(session.pending_teleport, Some((1, home_position))); + assert_eq!(session.state, SessionState::Transfer); + } + + #[tokio::test] + async fn teleport_units_target_dest_home_without_homebind_is_noop_boundary_like_cpp() { + let (mut session, _, _send_rx) = make_session(); + let spell_id = 8690_i32; + let player_guid = ObjectGuid::create_player(1, 7041); + let player_position = Position::new(301.0, 401.0, 61.0, 0.5); + let canonical = shared_canonical_map_manager(); + let hearth_effect = wow_data::SpellEffectInfo { + effect_index: 0, + effect: wow_data::spell::spell_effect_types::SPELL_EFFECT_TELEPORT_UNITS, + implicit_target_1: wow_data::spell::implicit_targets::TARGET_DEST_HOME, + ..Default::default() + }; + let spell_info = teleport_units_spell_info_like_cpp(spell_id, vec![hearth_effect]); + + session.set_canonical_map_manager(Arc::clone(&canonical)); + session.attach_player_controller_like_cpp(SessionPlayerController::new( + player_guid, + "HearthNoHome".to_string(), + player_position, + 571, + 1, + 1, + 80, + 0, + )); + add_canonical_test_player_on_map(&canonical, player_guid, player_position, 571, 0); + let mut spell_store = wow_data::SpellStore::new(); + spell_store.insert(spell_id, spell_info); + session.set_spell_store(Arc::new(spell_store)); + + session + .execute_spell_with_visual_and_target_data( + spell_id, + player_guid, + ObjectGuid::EMPTY, + wow_packet::packets::spell::SpellCastVisual { + spell_visual_id: 8690, + script_visual_id: 0, + }, + SpellTargetData::default(), + ) + .await + .expect("missing represented homebind keeps current bounded no-op"); + + assert_eq!(session.pending_teleport, None); + assert_ne!(session.state, SessionState::Transfer); + } + #[tokio::test] async fn teleport_units_without_destination_is_noop_like_cpp() { let (mut session, _, _send_rx) = make_session(); @@ -99629,6 +99883,58 @@ mod tests { ); } + #[test] + fn player_homebind_update_statement_matches_cpp_bind_order() { + let guid = ObjectGuid::create_player(1, 5007); + let homebind = RepresentedHomebindLikeCpp { + map_id: 571, + area_id: 495, + position: Position::new(11.0, 22.0, 33.0, 1.5), + }; + + let stmt = WorldSession::build_player_homebind_update_statement_like_cpp( + homebind, + guid.counter() as u64, + ); + + assert_eq!(stmt.sql(), CharStatements::UPD_PLAYER_HOMEBIND.sql()); + assert!(matches!(stmt.params()[0], wow_database::SqlParam::U16(571))); + assert!(matches!(stmt.params()[1], wow_database::SqlParam::U16(495))); + assert!(matches!(stmt.params()[2], wow_database::SqlParam::F32(v) if v == 11.0)); + assert!(matches!(stmt.params()[3], wow_database::SqlParam::F32(v) if v == 22.0)); + assert!(matches!(stmt.params()[4], wow_database::SqlParam::F32(v) if v == 33.0)); + assert!(matches!(stmt.params()[5], wow_database::SqlParam::F32(v) if v == 1.5)); + assert!( + matches!(stmt.params()[6], wow_database::SqlParam::U64(v) if v == guid.counter() as u64) + ); + } + + #[test] + fn player_homebind_insert_statement_matches_cpp_load_fallback_bind_order() { + let guid = ObjectGuid::create_player(1, 5008); + let homebind = RepresentedHomebindLikeCpp { + map_id: 0, + area_id: 12, + position: Position::new(-1.0, -2.0, 3.0, 4.0), + }; + + let stmt = WorldSession::build_player_homebind_insert_statement_like_cpp( + homebind, + guid.counter() as u64, + ); + + assert_eq!(stmt.sql(), CharStatements::INS_PLAYER_HOMEBIND.sql()); + assert!( + matches!(stmt.params()[0], wow_database::SqlParam::U64(v) if v == guid.counter() as u64) + ); + assert!(matches!(stmt.params()[1], wow_database::SqlParam::U16(0))); + assert!(matches!(stmt.params()[2], wow_database::SqlParam::U16(12))); + assert!(matches!(stmt.params()[3], wow_database::SqlParam::F32(v) if v == -1.0)); + assert!(matches!(stmt.params()[4], wow_database::SqlParam::F32(v) if v == -2.0)); + assert!(matches!(stmt.params()[5], wow_database::SqlParam::F32(v) if v == 3.0)); + assert!(matches!(stmt.params()[6], wow_database::SqlParam::F32(v) if v == 4.0)); + } + #[test] fn character_powers_save_statement_matches_cpp_power_fields_like_cpp() { let guid = ObjectGuid::create_player(1, 5004);