diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index f0df59482..7f921ac4d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,22 +18,17 @@ permissions: jobs: fmt: runs-on: ubuntu-latest - # Non-blocking: 61 files on main already fail this. Reformatting them is a separate change. steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Check formatting - continue-on-error: true run: cargo fmt --all -- --check clippy: runs-on: ubuntu-latest - # Non-blocking: lyracore-module, -gateway and -importer are red on main today. lyracore-shared - # and -package-delta are clean, so this flips to blocking once the other three are fixed. steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - name: Run clippy - continue-on-error: true run: cargo clippy --workspace --all-targets --all-features -- -D warnings test: diff --git a/crates/lyracore-shared/src/spatial.rs b/crates/lyracore-shared/src/spatial.rs index e40cca967..2b9673857 100644 --- a/crates/lyracore-shared/src/spatial.rs +++ b/crates/lyracore-shared/src/spatial.rs @@ -402,7 +402,9 @@ mod tests { let side = (2 * BOX_HALF_SPAN + 1) as usize; assert_eq!(got.len(), side * side); assert_eq!( - got.iter().copied().collect::>() + got.iter() + .copied() + .collect::>() .len(), got.len(), "a duplicate id inside one box would double-deliver a cell" diff --git a/gateway/src/codec/breath.rs b/gateway/src/codec/breath.rs index e85bddec9..c6bff7a23 100644 --- a/gateway/src/codec/breath.rs +++ b/gateway/src/codec/breath.rs @@ -1,10 +1,10 @@ //! Breath mirror-timer and drowning combat-log packets. +use wow_world_messages::vanilla::opcodes::ServerOpcodeMessage; use wow_world_messages::vanilla::{ - EnvironmentalDamageType, SMSG_ENVIRONMENTAL_DAMAGE_LOG, SMSG_START_MIRROR_TIMER, - SMSG_STOP_MIRROR_TIMER, TimerType, + EnvironmentalDamageType, TimerType, SMSG_ENVIRONMENTAL_DAMAGE_LOG, SMSG_START_MIRROR_TIMER, + SMSG_STOP_MIRROR_TIMER, }; -use wow_world_messages::vanilla::opcodes::ServerOpcodeMessage; /// The only timer this relay currently mirrors: underwater breath. const BREATH_TIMER: TimerType = TimerType::Breath; @@ -25,7 +25,9 @@ pub fn build_breath_timer_start(time_remaining_ms: u32, duration_ms: u32) -> Ser /// Client-side countdown stop on the surface edge. pub fn build_breath_timer_stop() -> ServerOpcodeMessage { - ServerOpcodeMessage::SMSG_STOP_MIRROR_TIMER(SMSG_STOP_MIRROR_TIMER { timer: BREATH_TIMER }) + ServerOpcodeMessage::SMSG_STOP_MIRROR_TIMER(SMSG_STOP_MIRROR_TIMER { + timer: BREATH_TIMER, + }) } /// Combat-log line for exactly the health amount the server's drowning resolver removed. diff --git a/gateway/src/codec/gameobject.rs b/gateway/src/codec/gameobject.rs index ca8186119..c745041e3 100644 --- a/gateway/src/codec/gameobject.rs +++ b/gateway/src/codec/gameobject.rs @@ -107,13 +107,16 @@ pub fn build_gameobject_create_object(go: &GameObjectView) -> SMSG_UPDATE_OBJECT /// matching vanilla's Z-axis-only yaw convention — rot0/rot1 stay 0, i.e. no terrain pitch/roll, which /// is the best a bare `orientation` float can express) instead. pub fn build_gameobject_rotation_values(go: &GameObjectView) -> (u16, Vec) { - let (rot0, rot1, rot2, rot3) = - if go.rotation_0 == 0.0 && go.rotation_1 == 0.0 && go.rotation_2 == 0.0 && go.rotation_3 == 0.0 { - let half = go.orientation * 0.5; - (0.0, 0.0, half.sin(), half.cos()) - } else { - (go.rotation_0, go.rotation_1, go.rotation_2, go.rotation_3) - }; + let (rot0, rot1, rot2, rot3) = if go.rotation_0 == 0.0 + && go.rotation_1 == 0.0 + && go.rotation_2 == 0.0 + && go.rotation_3 == 0.0 + { + let half = go.orientation * 0.5; + (0.0, 0.0, half.sin(), half.cos()) + } else { + (go.rotation_0, go.rotation_1, go.rotation_2, go.rotation_3) + }; let mut mask = update_mask::UpdateMaskValues::new(); mask.set_f32(update_mask::idx::GAMEOBJECT_ROTATION, rot0); mask.set_f32(update_mask::idx::GAMEOBJECT_ROTATION + 1, rot1); @@ -196,6 +199,7 @@ pub fn build_dynamicobject_create_object( #[cfg(test)] mod tests { use super::*; + use std::f32::consts::FRAC_1_SQRT_2; use wow_world_messages::vanilla::opcodes::ServerOpcodeMessage; #[test] @@ -282,7 +286,10 @@ mod tests { let block_count = body[mask_start] as usize; let values_start = mask_start + 1 + block_count * 4; let mut out = [0.0f32; 4]; - for (i, chunk) in body[values_start..values_start + 16].chunks_exact(4).enumerate() { + for (i, chunk) in body[values_start..values_start + 16] + .chunks_exact(4) + .enumerate() + { out[i] = f32::from_le_bytes(chunk.try_into().unwrap()); } out @@ -305,14 +312,14 @@ mod tests { display_id: 259, rotation_0: 0.1, rotation_1: 0.2, - rotation_2: 0.70710678, - rotation_3: 0.70710678, + rotation_2: FRAC_1_SQRT_2, + rotation_3: FRAC_1_SQRT_2, size: 0.0, }; let (opcode, body) = build_gameobject_rotation_values(&go); assert_eq!(opcode, 0x00A9, "SMSG_UPDATE_OBJECT opcode"); let floats = decode_rotation_floats(go.guid, &body); - assert_eq!(floats, [0.1, 0.2, 0.70710678, 0.70710678]); + assert_eq!(floats, [0.1, 0.2, FRAC_1_SQRT_2, FRAC_1_SQRT_2]); } #[test] diff --git a/gateway/src/codec/mod.rs b/gateway/src/codec/mod.rs index 1d3af4210..ee04ab2b4 100644 --- a/gateway/src/codec/mod.rs +++ b/gateway/src/codec/mod.rs @@ -20,9 +20,9 @@ pub mod social; pub mod update_mask; // Packet-family submodules. -mod char; mod auction; mod breath; +mod char; mod combat; mod corpse; mod duel; @@ -36,8 +36,8 @@ mod movement; mod npc; mod pet; mod quest; -mod trainer; mod taxi; +mod trainer; mod values; mod weather; @@ -50,9 +50,9 @@ pub use social::{ }; // Re-export packet-family surfaces through `crate::codec::`. -pub use char::*; pub use auction::*; pub use breath::*; +pub use char::*; pub use combat::*; pub use corpse::*; pub use duel::*; @@ -66,8 +66,8 @@ pub use movement::*; pub use npc::*; pub use pet::*; pub use quest::*; -pub use trainer::*; pub use taxi::*; +pub use trainer::*; pub use values::*; pub use weather::*; @@ -80,9 +80,9 @@ use lyracore_shared::opcodes::movement as movement_opcodes; use lyracore_shared::packing::unpack4; use wow_world_messages::vanilla::opcodes::{ClientOpcodeMessage, ServerOpcodeMessage}; use wow_world_messages::vanilla::{ - Area, AllowedClass, AllowedRace, + Area, BagFamily, // Item binding: "Binds when picked up/equipped" tooltip line. Bonding, diff --git a/gateway/src/codec/movement.rs b/gateway/src/codec/movement.rs index 51a8f9ada..1c74fbe71 100644 --- a/gateway/src/codec/movement.rs +++ b/gateway/src/codec/movement.rs @@ -306,9 +306,12 @@ pub fn build_taxi_move_raw( duration_ms: u32, spline_id: u32, ) -> Option<(u16, Vec)> { - if points.is_empty() || points.iter().chain([&start]).any(|p| { - !p.x.is_finite() || !p.y.is_finite() || !p.z.is_finite() - }) { + if points.is_empty() + || points + .iter() + .chain([&start]) + .any(|p| !p.x.is_finite() || !p.y.is_finite() || !p.z.is_finite()) + { return None; } let mut body = Vec::with_capacity(34 + points.len() * 12); diff --git a/gateway/src/codec/npc.rs b/gateway/src/codec/npc.rs index 9276fd77f..6fcdd14c7 100644 --- a/gateway/src/codec/npc.rs +++ b/gateway/src/codec/npc.rs @@ -77,7 +77,7 @@ pub fn option_condition_holds(cond_type: u32, quest_taken: bool, quest_rewarded: gossip_condition::QUEST_TAKEN => quest_taken, gossip_condition::QUEST_REWARDED => quest_rewarded, gossip_condition::NEVER => false, // fail-closed placeholder (seasonal events don't exist here) - _ => true, // unrecognized → fail open, never silently hide an option + _ => true, // unrecognized → fail open, never silently hide an option } } diff --git a/gateway/src/codec/tests.rs b/gateway/src/codec/tests.rs index 545571c91..9bccabda0 100644 --- a/gateway/src/codec/tests.rs +++ b/gateway/src/codec/tests.rs @@ -7,7 +7,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; // Only the loot byte-match test needs gtker's typed loot response (the runtime path is raw). use wow_world_messages::vanilla::ServerMessage; use wow_world_messages::vanilla::{ - EnvironmentalDamageType, SMSG_LOOT_RESPONSE_LootMethod, SMSG_LOOT_RESPONSE, TimerType, + EnvironmentalDamageType, SMSG_LOOT_RESPONSE_LootMethod, TimerType, SMSG_LOOT_RESPONSE, }; // Group loot methods: the vote-kind byte constants + wire RollVote enum. use lyracore_shared::loot_roll::vote_kind; @@ -793,7 +793,9 @@ fn breath_mirror_timer_edges_roundtrip_on_the_vanilla_wire() { let mut stop_bytes = Vec::new(); stop.write_unencrypted_server(&mut stop_bytes).unwrap(); match ServerOpcodeMessage::read_unencrypted(&mut stop_bytes.as_slice()).unwrap() { - ServerOpcodeMessage::SMSG_STOP_MIRROR_TIMER(msg) => assert_eq!(msg.timer, TimerType::Breath), + ServerOpcodeMessage::SMSG_STOP_MIRROR_TIMER(msg) => { + assert_eq!(msg.timer, TimerType::Breath) + } other => panic!("expected SMSG_STOP_MIRROR_TIMER, got {other}"), } } @@ -2064,7 +2066,11 @@ fn gossip_message_renders_imported_options_verbatim_with_a_trailing_farewell() { let opts = vec![ opt(0, "Care for a chat?", gossip_option::GOSSIP), opt(1, "Let me see your goods.", gossip_option::VENDOR), - opt(0, "I'd like to stay here a while.", gossip_option::INNKEEPER), + opt( + 0, + "I'd like to stay here a while.", + gossip_option::INNKEEPER, + ), ]; let m = build_gossip_message( 1, @@ -3078,8 +3084,8 @@ fn active_taxi_mount_is_present_in_the_player_create_mask() { other => panic!("expected a Player CreateObject2, got {other:?}"), } - let unmounted = build_create_object(&warrior_entity(), CreateKind::SelfPlayer, &[], &[]) - .unwrap(); + let unmounted = + build_create_object(&warrior_entity(), CreateKind::SelfPlayer, &[], &[]).unwrap(); match &unmounted.objects[0] { Object::CreateObject2 { mask2: UpdateMask::Player(player), @@ -3443,11 +3449,27 @@ fn monster_move_facing_carries_the_angle_and_a_degenerate_single_point_spline() #[test] fn taxi_move_writes_absolute_catmull_rom_points_byte_for_byte() { - let start = Vector3d { x: -9465.0, y: -1300.0, z: 50.0 }; + let start = Vector3d { + x: -9465.0, + y: -1300.0, + z: 50.0, + }; let points = vec![ - Vector3d { x: -9460.0, y: -1298.0, z: 52.0 }, - Vector3d { x: -9450.0, y: -1290.0, z: 54.0 }, - Vector3d { x: -9440.0, y: -1280.0, z: 56.0 }, + Vector3d { + x: -9460.0, + y: -1298.0, + z: 52.0, + }, + Vector3d { + x: -9450.0, + y: -1290.0, + z: 54.0, + }, + Vector3d { + x: -9440.0, + y: -1280.0, + z: 56.0, + }, ]; let (opcode, bytes) = build_taxi_move_raw(0xF00D, start, points.clone(), 12_345, 77).unwrap(); assert_eq!(opcode, 0x00DD); diff --git a/gateway/src/config.rs b/gateway/src/config.rs index be5432f28..1f441f33b 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -733,7 +733,6 @@ pub fn synthetic_owner_identity(account_id: u64) -> [u8; 32] { id } - /// A crash-diagnosis probe for the SMSG_COMPRESSED_MOVES corruption at crowd scale: /// `LYRACORE_WRITER_TRACE=1` turns on a per-session black-box ring inside `spawn_writer` — the /// last 32 (opcode, declared size, rolling checksum) triples for the exact bytes handed to the diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 2cd1dec08..8725d0799 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -15,11 +15,11 @@ mod accept; mod codec; mod config; -mod fd_limit; #[cfg(test)] #[allow(dead_code)] // Shared commands are also used by Module integration tests. #[path = "../../module/tests/support/mod.rs"] mod durable_test_support; +mod fd_limit; mod load_sample; mod logon; mod movement_batch_metrics; @@ -151,7 +151,7 @@ async fn run() -> Result<()> { tokio::time::sleep(std::time::Duration::from_secs(secs)).await; logon.abort(); world.abort(); - return Ok(()); + Ok(()) } #[cfg(not(feature = "dhat-heap"))] { diff --git a/gateway/src/stdb/account_sessions.rs b/gateway/src/stdb/account_sessions.rs index 6d35e65e5..83f34aefe 100644 --- a/gateway/src/stdb/account_sessions.rs +++ b/gateway/src/stdb/account_sessions.rs @@ -53,7 +53,6 @@ impl SessionEpochs { } } - #[cfg(test)] mod session_epoch_tests { use super::SessionEpochs; diff --git a/gateway/src/stdb/aoi.rs b/gateway/src/stdb/aoi.rs index 0d0452401..8d0ce1474 100644 --- a/gateway/src/stdb/aoi.rs +++ b/gateway/src/stdb/aoi.rs @@ -93,7 +93,11 @@ mod frozen_sql_oracle { /// ONE point-probe query per (table, cell) — the shape the missing-AOI-index fix landed. Kept alongside the range /// form because a future change has to satisfy BOTH or it has narrowed visibility on one of /// them. - fn cell_queries(map_id: u32, instance_id: u64, cells: impl Iterator) -> Vec { + fn cell_queries( + map_id: u32, + instance_id: u64, + cells: impl Iterator, + ) -> Vec { let cells: Vec = cells.collect(); let mut out = Vec::with_capacity(BOX_SCOPED_TABLES.len() * cells.len()); for table in BOX_SCOPED_TABLES { diff --git a/gateway/src/stdb/mod.rs b/gateway/src/stdb/mod.rs index 35ebee065..9880af40c 100644 --- a/gateway/src/stdb/mod.rs +++ b/gateway/src/stdb/mod.rs @@ -13,6 +13,9 @@ //! - `subscriptions`: `PlayerSubscriptions`, viewer setup, and shared packet builders. //! - `views`: row→view converters + the thin `RealmRow`/`AccountRow` mirrors. +// The SpacetimeDB codegen writes one function per table subscription; several run past any +// reasonable length ceiling and no edit here survives a regeneration. +#[allow(clippy::too_many_lines)] pub mod bindings; mod account_sessions; // per-account session-epoch + live-socket registry, split out of `connection` @@ -24,8 +27,8 @@ mod reads; mod reducers; pub(crate) mod subscriptions; mod views; -mod world_store; // impl WorldStore for Coordinator (replaces the former WorldCoordinatorStore newtype) pub(crate) mod world_index; +mod world_store; // impl WorldStore for Coordinator (replaces the former WorldCoordinatorStore newtype) pub(crate) mod world_view; // shared per-shard spatial, broadcast, private, and owner dispatch pub use connection::Coordinator; diff --git a/gateway/src/stdb/reads/items.rs b/gateway/src/stdb/reads/items.rs index b0e867f23..d86bc50f7 100644 --- a/gateway/src/stdb/reads/items.rs +++ b/gateway/src/stdb/reads/items.rs @@ -355,5 +355,4 @@ mod tests { ); } } - } diff --git a/gateway/src/stdb/reads/talent_reputation.rs b/gateway/src/stdb/reads/talent_reputation.rs index 9a819a148..b6bebef68 100644 --- a/gateway/src/stdb/reads/talent_reputation.rs +++ b/gateway/src/stdb/reads/talent_reputation.rs @@ -263,5 +263,4 @@ mod tests { let sworn = ft(3, 14, 8, 0, 1, 0); assert!(super::faction_template_hostile(&sworn, &player)); } - } diff --git a/gateway/src/stdb/reducers.rs b/gateway/src/stdb/reducers.rs index c5df6892e..1067939a0 100644 --- a/gateway/src/stdb/reducers.rs +++ b/gateway/src/stdb/reducers.rs @@ -1229,6 +1229,8 @@ impl Coordinator { /// Cast a GROUND-TARGETED spell at a clicked world point (`CMSG_CAST_SPELL` with a DEST_LOCATION — /// Flamestrike/Blizzard/Rain of Fire). Same per-account attribution as `cast_spell`; the `(x,y,z)` is /// the ground click so the module anchors the AoE/patch there. + // The ground point rides flat because it mirrors the `gw_cast_spell_at` reducer signature. + #[allow(clippy::too_many_arguments)] pub fn cast_spell_at( &self, _account_id: u64, diff --git a/gateway/src/stdb/subscriptions.rs b/gateway/src/stdb/subscriptions.rs index 210602884..89c70df5c 100644 --- a/gateway/src/stdb/subscriptions.rs +++ b/gateway/src/stdb/subscriptions.rs @@ -25,11 +25,11 @@ use wow_world_messages::vanilla::opcodes::ServerOpcodeMessage; use wow_world_messages::vanilla::{Vector3d, WeatherChangeType}; use super::aoi::ViewerGates; -use super::world_index::{CellKey, EntityLayer}; -use super::world_view::{self, Viewer, WorldView}; use super::bindings::*; use super::connection::Coordinator; use super::views::{corpse_view, entity_view, go_view, hunter_pet_protocol_view}; +use super::world_index::{CellKey, EntityLayer}; +use super::world_view::{self, Viewer, WorldView}; /// RAII guard for one world session's shared-view registration, held by the world connection. /// Dropping the guard makes the session unreachable from every shard-level dispatcher. @@ -342,7 +342,10 @@ fn viewer_relative_dynamic_flags( let Some(tap) = rows.tap.filter(|tap| tap.creature_guid == entity.guid) else { return entity.dynamic_flags; }; - let entitled = match rows.tag_group.filter(|group| group.creature_guid == entity.guid) { + let entitled = match rows + .tag_group + .filter(|group| group.creature_guid == entity.guid) + { Some(group) => { rows.tag_members.iter().any(|member| { member.creature_guid == entity.guid && member.character_guid == viewer_guid @@ -359,12 +362,11 @@ fn viewer_relative_dynamic_flags( } } -fn projected_dynamic_flags( - db: &RemoteTables, - viewer_guid: u64, - entity: &WorldEntity, -) -> u32 { - let tap = db.game_creature_quest_tap().creature_guid().find(&entity.guid); +fn projected_dynamic_flags(db: &RemoteTables, viewer_guid: u64, entity: &WorldEntity) -> u32 { + let tap = db + .game_creature_quest_tap() + .creature_guid() + .find(&entity.guid); let tag_group = db .game_creature_loot_tag_group() .creature_guid() @@ -839,12 +841,7 @@ pub(crate) fn offer_peer_create_for( { // CREATE is already first in `out`. A resident flight must follow it in the same writer // work item so AOI entry can never observe MONSTER_MOVE for an unknown passenger. - append_resident_taxi_after_create( - &mut out, - &viewer.created, - viewer.self_guid, - &spline, - ); + append_resident_taxi_after_create(&mut out, &viewer.created, viewer.self_guid, &spline); } out } @@ -904,10 +901,11 @@ pub(crate) fn relay_entity_update( let guard = coord.0.coord(); projected_dynamic_flags(&guard.conn.db, viewer.self_guid, new) }; - let mut out: Vec = entity_update_to_outbound_with_dynamic_flags(old, new, dynamic_flags) - .into_iter() - .map(Outbound::One) - .collect(); + let mut out: Vec = + entity_update_to_outbound_with_dynamic_flags(old, new, dynamic_flags) + .into_iter() + .map(Outbound::One) + .collect(); if new.guid == viewer.self_guid { if let Some(is_ghost) = ghost_transition(old.player_flags, new.player_flags, GHOST_PLAYER_FLAG) @@ -1242,9 +1240,9 @@ pub(crate) fn cast_event_outbound(self_guid: u64, row: &SpellCastEvent) -> Vec> 48, 0xF130 | 0xF140); if is_creature || row.caster_guid == self_guid { let m = codec::build_spell_failure(row.caster_guid, row.spell_id); - out.push(Outbound::One( - ServerOpcodeMessage::SMSG_SPELL_FAILURE(Box::new(m)), - )); + out.push(Outbound::One(ServerOpcodeMessage::SMSG_SPELL_FAILURE( + Box::new(m), + ))); } // A row that names a concrete reason (an on-next-swing strike that could not pay its cost at // the swing) follows the teardown with the failed cast result, in vanilla's order @@ -1299,13 +1297,8 @@ pub(crate) fn cast_event_outbound(self_guid: u64, row: &SpellCastEvent) -> Vec 0 { // Cast-START (a timed spell): SMSG_SPELL_START with the cast-bar duration so observers // see the bar FILL. The GO/COOLDOWN follow on the cast-GO COMPLETION event. - let start = codec::build_spell_start( - row.caster_guid, - row.spell_id, - row.cast_time_ms, - 0, - None, - ); + let start = + codec::build_spell_start(row.caster_guid, row.spell_id, row.cast_time_ms, 0, None); out.push(Outbound::One(ServerOpcodeMessage::SMSG_SPELL_START( Box::new(start), ))); @@ -1334,8 +1327,7 @@ pub(crate) fn cast_event_outbound(self_guid: u64, row: &SpellCastEvent) -> Vec Vec Vec Vec 0 { - let cd = - codec::build_spell_cooldown(row.caster_guid, row.spell_id, row.cooldown_ms); + let cd = codec::build_spell_cooldown(row.caster_guid, row.spell_id, row.cooldown_ms); out.push(Outbound::One(ServerOpcodeMessage::SMSG_SPELL_COOLDOWN( Box::new(cd), ))); @@ -1458,7 +1448,11 @@ pub(crate) fn aura_sync( // both land the right value, exactly like `run_speed_packet`). Returns `None` unless `changed` is a // self armor aura, so ordinary buffs/debuffs don't spam the opcode. Self-scoped: the sheet shows // only your own armor, so no peer relay is needed. -pub(crate) fn armor_packet(coord: &Coordinator, changed: &Aura, self_guid: u64) -> Option { +pub(crate) fn armor_packet( + coord: &Coordinator, + changed: &Aura, + self_guid: u64, +) -> Option { const A_MOD_RESISTANCE: u8 = 0xA1; // taxonomy A_MOD_RESISTANCE const RESIST_ARMOR_MASK: u32 = 0x01; // taxonomy RESIST_ARMOR bit (eff_p0 is a school MASK) if changed.target_guid != self_guid @@ -1492,7 +1486,11 @@ pub(crate) fn armor_packet(coord: &Coordinator, changed: &Aura, self_guid: u64) // `recompute_sheet` actually reacts to (`aura_moves_sheet`'s mirror), so an unrelated buff/debuff // (a DoT, a slow) doesn't spam the opcode — purely a re-push filter, not aura interpretation. Self- // scoped: the sheet shows only your own numbers, so no peer relay is needed. -pub(crate) fn sheet_packet(coord: &Coordinator, changed: &Aura, self_guid: u64) -> Option { +pub(crate) fn sheet_packet( + coord: &Coordinator, + changed: &Aura, + self_guid: u64, +) -> Option { const A_MOD_STAT: u8 = 0xA0; // taxonomy A_MOD_STAT const A_MOD_COMBAT: u8 = 0xA3; // taxonomy A_MOD_COMBAT const COMBAT_ATTACK_POWER: i32 = 0; // taxonomy COMBAT_ATTACK_POWER @@ -1606,7 +1604,8 @@ pub(crate) fn aura_insert_outbound( let mut out = Vec::new(); // A stealth-hidden peer (not in `created`) must get NO per-peer relay: a partial VALUES on // a DESTROYed object is a client crash/desync vector. - let visible = row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); + let visible = + row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); let current = visible.then(|| view.auras.on_target(shard, row.target_guid)); if let Some(current) = ¤t { out.push(aura_sync(current.iter().cloned(), row.target_guid)); @@ -1650,7 +1649,8 @@ pub(crate) fn aura_update_outbound( expires_changed: bool, ) -> Vec { let mut out = Vec::new(); - let visible = row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); + let visible = + row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); let current = visible.then(|| view.auras.on_target(shard, row.target_guid)); if let Some(current) = ¤t { out.push(aura_sync(current.iter().cloned(), row.target_guid)); @@ -1689,7 +1689,8 @@ pub(crate) fn aura_delete_outbound( stealth_count: usize, ) -> Vec { let mut out = Vec::new(); - let visible = row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); + let visible = + row.target_guid == self_guid || created.lock().unwrap().contains(&row.target_guid); if visible { let current = view.auras.on_target(shard, row.target_guid); out.push(aura_sync(current.iter().cloned(), row.target_guid)); @@ -1940,13 +1941,14 @@ pub(crate) fn duel_event_outbound( raw_values(row.initiator_guid, None, Some(1)), raw_values(row.challenged_guid, None, Some(2)), ], - event_kind::OUT_OF_BOUNDS => vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_OUTOFBOUNDS)], + event_kind::OUT_OF_BOUNDS => { + vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_OUTOFBOUNDS)] + } event_kind::IN_BOUNDS => vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_INBOUNDS)], event_kind::COMPLETE => { let mut outbound = vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_COMPLETE( SMSG_DUEL_COMPLETE { - ended_without_interruption: row.completion_kind - != completion_kind::INTERRUPTED, + ended_without_interruption: row.completion_kind != completion_kind::INTERRUPTED, }, ))]; outbound.extend(duel_winner_outbound(row)); @@ -1987,13 +1989,13 @@ pub(crate) fn duel_winner_outbound(row: &DuelEvent) -> Vec { }; // `opponent_name` is the first CString on the wire and carries the WINNER: mangos sends // this packet from the loser, announcing its opponent as victor. - vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_WINNER(Box::new( - SMSG_DUEL_WINNER { + vec![Outbound::One(ServerOpcodeMessage::SMSG_DUEL_WINNER( + Box::new(SMSG_DUEL_WINNER { reason, opponent_name: row.winner_name.clone(), initiator_name: row.loser_name.clone(), - }, - )))] + }), + ))] } /// Decode an `OFFER_*` payload into the fixed-444-byte `SMSG_TRADE_STATUS_EXTENDED` (#121): @@ -2086,14 +2088,22 @@ pub(crate) fn group_event_outbound( group_kind::LIST => match lyracore_shared::group::decode_roster(&row.payload) { Some((leader, loot_method, loot_threshold, master_looter_guid, members)) => { let members = name_the_roster(coord, members); - Some(ServerOpcodeMessage::SMSG_GROUP_LIST(Box::new(codec::build_group_list( - self_guid, leader, loot_method, loot_threshold, master_looter_guid, &members, - )))) + Some(ServerOpcodeMessage::SMSG_GROUP_LIST(Box::new( + codec::build_group_list( + self_guid, + leader, + loot_method, + loot_threshold, + master_looter_guid, + &members, + ), + ))) } None => { log::warn!( "group LIST relay: unparseable roster payload {:?} (event {})", - row.payload, row.id + row.payload, + row.id ); None } @@ -2112,63 +2122,104 @@ pub(crate) fn group_event_outbound( codec::build_party_chat(row.other_guid, message), ))), None => { - log::warn!("party PARTY_CHAT relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "party PARTY_CHAT relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, roll_kind::ROLL_START => match lyracore_shared::loot_roll::decode_start(&row.payload) { Some((corpse_guid, slot, item_entry, countdown_ms)) => { - Some(ServerOpcodeMessage::SMSG_LOOT_START_ROLL(Box::new(codec::build_loot_start_roll( - corpse_guid, slot, item_entry, countdown_ms, - )))) + Some(ServerOpcodeMessage::SMSG_LOOT_START_ROLL(Box::new( + codec::build_loot_start_roll(corpse_guid, slot, item_entry, countdown_ms), + ))) } None => { - log::warn!("loot ROLL_START relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "loot ROLL_START relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, roll_kind::ROLL_VOTE => match lyracore_shared::loot_roll::decode_vote(&row.payload) { - Some((corpse_guid, slot, item_entry, roll_number, vote, auto_pass)) => { - Some(ServerOpcodeMessage::SMSG_LOOT_ROLL(Box::new(codec::build_loot_roll( - corpse_guid, slot, row.other_guid, item_entry, roll_number, vote, auto_pass, - )))) - } + Some((corpse_guid, slot, item_entry, roll_number, vote, auto_pass)) => Some( + ServerOpcodeMessage::SMSG_LOOT_ROLL(Box::new(codec::build_loot_roll( + corpse_guid, + slot, + row.other_guid, + item_entry, + roll_number, + vote, + auto_pass, + ))), + ), None => { - log::warn!("loot ROLL_VOTE relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "loot ROLL_VOTE relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, roll_kind::ROLL_WON => match lyracore_shared::loot_roll::decode_won(&row.payload) { - Some((corpse_guid, slot, item_entry, winning_roll, winning_vote)) => { - Some(ServerOpcodeMessage::SMSG_LOOT_ROLL_WON(Box::new(codec::build_loot_roll_won( - corpse_guid, slot, item_entry, row.other_guid, winning_roll, winning_vote, - )))) - } + Some((corpse_guid, slot, item_entry, winning_roll, winning_vote)) => Some( + ServerOpcodeMessage::SMSG_LOOT_ROLL_WON(Box::new(codec::build_loot_roll_won( + corpse_guid, + slot, + item_entry, + row.other_guid, + winning_roll, + winning_vote, + ))), + ), None => { - log::warn!("loot ROLL_WON relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "loot ROLL_WON relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, - roll_kind::MASTER_LIST => match lyracore_shared::loot_roll::decode_master_list(&row.payload) { - Some((_corpse_guid, eligible)) => Some(ServerOpcodeMessage::SMSG_LOOT_MASTER_LIST(Box::new( - codec::build_loot_master_list(&eligible), - ))), - None => { - log::warn!("loot MASTER_LIST relay: unparseable payload {:?} (event {})", row.payload, row.id); - None + roll_kind::MASTER_LIST => { + match lyracore_shared::loot_roll::decode_master_list(&row.payload) { + Some((_corpse_guid, eligible)) => Some(ServerOpcodeMessage::SMSG_LOOT_MASTER_LIST( + Box::new(codec::build_loot_master_list(&eligible)), + )), + None => { + log::warn!( + "loot MASTER_LIST relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); + None + } } - }, + } // A grouped money-loot split's per-recipient share → the SAME // `SMSG_LOOT_MONEY_NOTIFY` the (now-removed) unconditional gateway send used to build, // just per-recipient and carrying the SHARE instead of the total (`amount` here IS the // wire field, matching `codec::build_loot_money_notify`'s single `amount: u32`). - roll_kind::MONEY_SHARE => match lyracore_shared::loot_roll::decode_money_share(&row.payload) { - Some(share) => Some(ServerOpcodeMessage::SMSG_LOOT_MONEY_NOTIFY(codec::build_loot_money_notify(share))), - None => { - log::warn!("loot MONEY_SHARE relay: unparseable payload {:?} (event {})", row.payload, row.id); - None + roll_kind::MONEY_SHARE => { + match lyracore_shared::loot_roll::decode_money_share(&row.payload) { + Some(share) => Some(ServerOpcodeMessage::SMSG_LOOT_MONEY_NOTIFY( + codec::build_loot_money_notify(share), + )), + None => { + log::warn!( + "loot MONEY_SHARE relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); + None + } } - }, + } // Quest sharing: an eligible party member receives the shared quest — // `row.other_guid` is the SHARER (resolved/pushed by `group::push_event`), `row.payload` // is the quest entry. Opens the DETAILS screen with the SHARER as "giver" (the @@ -2181,16 +2232,26 @@ pub(crate) fn group_event_outbound( return vec![Outbound::Raw { opcode, body }]; } Ok(None) => { - log::warn!("quest QUEST_SHARE relay: quest {quest_id} not loaded (event {})", row.id); + log::warn!( + "quest QUEST_SHARE relay: quest {quest_id} not loaded (event {})", + row.id + ); None } Err(e) => { - log::warn!("quest QUEST_SHARE relay: quest_detail lookup failed (event {}): {e}", row.id); + log::warn!( + "quest QUEST_SHARE relay: quest_detail lookup failed (event {}): {e}", + row.id + ); None } }, Err(_) => { - log::warn!("quest QUEST_SHARE relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "quest QUEST_SHARE relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, @@ -2202,7 +2263,11 @@ pub(crate) fn group_event_outbound( codec::build_quest_push_result(row.other_guid, code), ))), Err(_) => { - log::warn!("quest QUEST_PUSH_RESULT relay: unparseable payload {:?} (event {})", row.payload, row.id); + log::warn!( + "quest QUEST_PUSH_RESULT relay: unparseable payload {:?} (event {})", + row.payload, + row.id + ); None } }, @@ -2269,7 +2334,9 @@ pub(crate) fn impact_event_outbound(row: &SpellImpactEvent) -> Vec { row.resisted, row.absorbed, ); - vec![Outbound::One(ServerOpcodeMessage::SMSG_SPELLNONMELEEDAMAGELOG(Box::new(log)))] + vec![Outbound::One( + ServerOpcodeMessage::SMSG_SPELLNONMELEEDAMAGELOG(Box::new(log)), + )] } /// Nearby chat: the one body both legs run. A player speaker always hears @@ -2412,12 +2479,9 @@ pub(crate) fn emote_event_outbound(coord: &Coordinator, row: &EmoteEvent) -> Vec None }; let mut out = Vec::new(); - if let Some(m) = codec::build_text_emote( - row.sender_guid, - row.text_emote, - row.emote_anim, - target_name, - ) { + if let Some(m) = + codec::build_text_emote(row.sender_guid, row.text_emote, row.emote_anim, target_name) + { out.push(Outbound::One(ServerOpcodeMessage::SMSG_TEXT_EMOTE( Box::new(m), ))); @@ -2526,9 +2590,14 @@ pub(crate) fn creature_leg_outbound( // `FacingAngle` variant — the only 1.12 `SMSG_MONSTER_MOVE` shape that turns a STATIONARY // creature without a spline the client would otherwise have nothing to interpolate from. if row.facing { - return vec![Outbound::One(ServerOpcodeMessage::SMSG_MONSTER_MOVE(Box::new( - codec::build_monster_move_facing(row.guid, start, row.facing_angle, row.spline_id), - )))]; + return vec![Outbound::One(ServerOpcodeMessage::SMSG_MONSTER_MOVE( + Box::new(codec::build_monster_move_facing( + row.guid, + start, + row.facing_angle, + row.spline_id, + )), + ))]; } let dest = Vector3d { x: row.dx, @@ -2586,7 +2655,9 @@ pub(crate) fn taxi_spline_outbound( row.duration_ms, row.spline_id, ) - .map_or_else(Vec::new, |(opcode, body)| vec![Outbound::Raw { opcode, body }]) + .map_or_else(Vec::new, |(opcode, body)| { + vec![Outbound::Raw { opcode, body }] + }) } fn append_resident_taxi_after_create( @@ -2924,15 +2995,15 @@ pub(crate) fn quest_update_feedback( row: &CharacterQuest, objectives: &[(u8, u8, u32, u32)], ) -> Vec { - let mut outbound = codec::kill_progress_add_kills( - row.quest_entry, - &old.counts, - &row.counts, - objectives, - ) - .into_iter() - .map(|packet| Outbound::One(ServerOpcodeMessage::SMSG_QUESTUPDATE_ADD_KILL(Box::new(packet)))) - .collect::>(); + let mut outbound = + codec::kill_progress_add_kills(row.quest_entry, &old.counts, &row.counts, objectives) + .into_iter() + .map(|packet| { + Outbound::One(ServerOpcodeMessage::SMSG_QUESTUPDATE_ADD_KILL(Box::new( + packet, + ))) + }) + .collect::>(); if !old.failed && row.failed { outbound.push(Outbound::One( ServerOpcodeMessage::SMSG_QUESTUPDATE_FAILEDTIMER( @@ -3142,7 +3213,8 @@ pub(crate) fn item_instance_update_outbound( } let moved_into_equipment = old.slot != row.slot && row.slot <= 18; let emptied_equipment = old.slot != row.slot && old.slot <= 18 && old_slot_is_empty; - if moved_into_equipment || emptied_equipment + if moved_into_equipment + || emptied_equipment || row.slot <= 18 && (old.durability == 0) != (row.durability == 0) { append_item_armor_and_sheet(db, self_guid, &mut out); @@ -3415,8 +3487,6 @@ impl Coordinator { view: Some(view), }) } - - } impl Coordinator { @@ -3646,8 +3716,7 @@ mod tests { ..swing_event() }; let out = combat_event_outbound(&created, &row); - let [Outbound::One(ServerOpcodeMessage::SMSG_SPELL_GO(go))] = out.as_slice() - else { + let [Outbound::One(ServerOpcodeMessage::SMSG_SPELL_GO(go))] = out.as_slice() else { panic!("a ranged launch must emit exactly one SPELL_GO"); }; assert_eq!((go.caster.guid(), go.spell), (7, 75)); @@ -3690,7 +3759,12 @@ mod tests { panic!("an impact must emit exactly one damage log"); }; assert_eq!( - (log.attacker.guid(), log.target.guid(), log.spell, log.damage), + ( + log.attacker.guid(), + log.target.guid(), + log.spell, + log.damage + ), (7, 11, 75, 23) ); } @@ -3702,7 +3776,9 @@ mod tests { let out = combat_event_outbound(&created, &row); assert!(matches!( out.as_slice(), - [Outbound::One(ServerOpcodeMessage::SMSG_ATTACKERSTATEUPDATE(_))] + [Outbound::One( + ServerOpcodeMessage::SMSG_ATTACKERSTATEUPDATE(_) + )] )); row.killing_blow = true; @@ -3786,12 +3862,8 @@ mod tests { (equipment.clone(), true, (0, 0, 0)), ] { let out = encounter_equip_outbound(&viewer, &row, cleared); - let (expected_opcode, expected_body) = codec::build_virtual_item_values( - creature_guid, - expected.0, - expected.1, - expected.2, - ); + let (expected_opcode, expected_body) = + codec::build_virtual_item_values(creature_guid, expected.0, expected.1, expected.2); assert!(matches!( out.as_slice(), [Outbound::Raw { opcode, body }] @@ -3937,8 +4009,14 @@ mod tests { }) .collect(); assert_eq!(requests.len(), 1); - assert_eq!(requests[0].initiator, wow_world_messages::Guid::new(row.flag_guid)); - assert_eq!(requests[0].target, wow_world_messages::Guid::new(row.initiator_guid)); + assert_eq!( + requests[0].initiator, + wow_world_messages::Guid::new(row.flag_guid) + ); + assert_eq!( + requests[0].target, + wow_world_messages::Guid::new(row.initiator_guid) + ); } #[test] @@ -4037,7 +4115,7 @@ mod tests { let out = trade_event_outbound(&event(k)); assert_eq!(out.len(), 1, "kind {k} must decode to exactly one packet"); match &out[0] { - Outbound::One(ServerOpcodeMessage::SMSG_TRADE_STATUS(s)) => (**s).clone(), + Outbound::One(ServerOpcodeMessage::SMSG_TRADE_STATUS(s)) => **s, Outbound::One(other) => panic!("kind {k}: expected SMSG_TRADE_STATUS, got {other}"), _ => panic!("kind {k}: expected a single SMSG_TRADE_STATUS packet"), } @@ -4051,38 +4129,87 @@ mod tests { "BeginTrade must carry the initiator guid off the row" ); assert_eq!(status_of(kind::OPEN_WINDOW), SMSG_TRADE_STATUS::OpenWindow); - assert_eq!(status_of(kind::TRADE_CANCELED), SMSG_TRADE_STATUS::TradeCanceled); + assert_eq!( + status_of(kind::TRADE_CANCELED), + SMSG_TRADE_STATUS::TradeCanceled + ); assert_eq!(status_of(kind::BUSY), SMSG_TRADE_STATUS::Busy); assert_eq!(status_of(kind::NO_TARGET), SMSG_TRADE_STATUS::NoTarget); - assert_eq!(status_of(kind::TARGET_TO_FAR), SMSG_TRADE_STATUS::TargetToFar); - assert_eq!(status_of(kind::WRONG_FACTION), SMSG_TRADE_STATUS::WrongFaction); + assert_eq!( + status_of(kind::TARGET_TO_FAR), + SMSG_TRADE_STATUS::TargetToFar + ); + assert_eq!( + status_of(kind::WRONG_FACTION), + SMSG_TRADE_STATUS::WrongFaction + ); assert_eq!(status_of(kind::YOU_DEAD), SMSG_TRADE_STATUS::YouDead); assert_eq!(status_of(kind::TARGET_DEAD), SMSG_TRADE_STATUS::TargetDead); assert_eq!(status_of(kind::IGNORE_YOU), SMSG_TRADE_STATUS::IgnoreYou); - assert_eq!(status_of(kind::TRADE_ACCEPT), SMSG_TRADE_STATUS::TradeAccept); - assert_eq!(status_of(kind::BACK_TO_TRADE), SMSG_TRADE_STATUS::BackToTrade); - assert_eq!(status_of(kind::TRADE_COMPLETE), SMSG_TRADE_STATUS::TradeComplete); + assert_eq!( + status_of(kind::TRADE_ACCEPT), + SMSG_TRADE_STATUS::TradeAccept + ); + assert_eq!( + status_of(kind::BACK_TO_TRADE), + SMSG_TRADE_STATUS::BackToTrade + ); + assert_eq!( + status_of(kind::TRADE_COMPLETE), + SMSG_TRADE_STATUS::TradeComplete + ); // The bag-space refusal closes the window naming WHOSE bags overflowed. let full_self = status_of(kind::INV_FULL_SELF); let full_partner = status_of(kind::INV_FULL_PARTNER); match (&full_self, &full_partner) { ( - SMSG_TRADE_STATUS::CloseWindow { target_error: false, inventory_result: a, .. }, - SMSG_TRADE_STATUS::CloseWindow { target_error: true, inventory_result: b, .. }, + SMSG_TRADE_STATUS::CloseWindow { + target_error: false, + inventory_result: a, + .. + }, + SMSG_TRADE_STATUS::CloseWindow { + target_error: true, + inventory_result: b, + .. + }, ) => { - assert_eq!(*a, wow_world_messages::vanilla::InventoryResult::InventoryFull); - assert_eq!(*b, wow_world_messages::vanilla::InventoryResult::InventoryFull); + assert_eq!( + *a, + wow_world_messages::vanilla::InventoryResult::InventoryFull + ); + assert_eq!( + *b, + wow_world_messages::vanilla::InventoryResult::InventoryFull + ); } other => panic!("expected CloseWindow pair with flipped target_error, got {other:?}"), } // The purse-failure pair mirrors it with NotEnoughMoney. - match (&status_of(kind::GOLD_FAIL_SELF), &status_of(kind::GOLD_FAIL_PARTNER)) { + match ( + &status_of(kind::GOLD_FAIL_SELF), + &status_of(kind::GOLD_FAIL_PARTNER), + ) { ( - SMSG_TRADE_STATUS::CloseWindow { target_error: false, inventory_result: a, .. }, - SMSG_TRADE_STATUS::CloseWindow { target_error: true, inventory_result: b, .. }, + SMSG_TRADE_STATUS::CloseWindow { + target_error: false, + inventory_result: a, + .. + }, + SMSG_TRADE_STATUS::CloseWindow { + target_error: true, + inventory_result: b, + .. + }, ) => { - assert_eq!(*a, wow_world_messages::vanilla::InventoryResult::NotEnoughMoney); - assert_eq!(*b, wow_world_messages::vanilla::InventoryResult::NotEnoughMoney); + assert_eq!( + *a, + wow_world_messages::vanilla::InventoryResult::NotEnoughMoney + ); + assert_eq!( + *b, + wow_world_messages::vanilla::InventoryResult::NotEnoughMoney + ); } other => panic!("expected NotEnoughMoney CloseWindow pair, got {other:?}"), } @@ -4126,9 +4253,7 @@ mod tests { let extended = |k: u8| -> SMSG_TRADE_STATUS_EXTENDED { let out = trade_event_outbound(&event(k, &payload)); match out.first() { - Some(Outbound::One(ServerOpcodeMessage::SMSG_TRADE_STATUS_EXTENDED(m))) => { - (**m).clone() - } + Some(Outbound::One(ServerOpcodeMessage::SMSG_TRADE_STATUS_EXTENDED(m))) => **m, _ => panic!("kind {k}: expected one SMSG_TRADE_STATUS_EXTENDED"), } }; @@ -4136,9 +4261,15 @@ mod tests { // Wire polarity (mangoszero): the `self_player` BYTE is "1 means traders data, 0 means // own", so your OWN echo carries 0 and the partner's side carries 1. let own = extended(kind::OFFER_SELF); - assert!(!own.self_player, "OFFER_SELF describes your own window: wire byte 0"); + assert!( + !own.self_player, + "OFFER_SELF describes your own window: wire byte 0" + ); let theirs = extended(kind::OFFER_PARTNER); - assert!(theirs.self_player, "OFFER_PARTNER describes the partner's side: wire byte 1"); + assert!( + theirs.self_player, + "OFFER_PARTNER describes the partner's side: wire byte 1" + ); assert_eq!(own.money_in_trade.as_int(), 1_2345); assert_eq!((own.trade_slot_count1, own.trade_slot_count2), (7, 7)); @@ -4406,7 +4537,15 @@ mod tests { let members = [tag_member(7), tag_member(8)]; let current = [current_member(8, 42)]; assert_eq!( - project_flags(8, &entity, Some(&tap), Some(&group), &members, ¤t, &[]), + project_flags( + 8, + &entity, + Some(&tap), + Some(&group), + &members, + ¤t, + &[] + ), entity.dynamic_flags | lyracore_shared::constants::unit_dynamic_flags::TAPPED_BY_PLAYER ); } @@ -4432,7 +4571,15 @@ mod tests { let members = [tag_member(7)]; let current = [current_member(8, 42)]; assert_eq!( - project_flags(8, &entity, Some(&tap), Some(&group), &members, ¤t, &[]), + project_flags( + 8, + &entity, + Some(&tap), + Some(&group), + &members, + ¤t, + &[] + ), entity.dynamic_flags ); } @@ -4448,7 +4595,15 @@ mod tests { let members = [tag_member(7), tag_member(8)]; let current = [current_member(8, 99)]; assert_eq!( - project_flags(8, &entity, Some(&tap), Some(&group), &members, ¤t, &[]), + project_flags( + 8, + &entity, + Some(&tap), + Some(&group), + &members, + ¤t, + &[] + ), entity.dynamic_flags ); } @@ -4479,7 +4634,10 @@ mod tests { corpse_guid: entity.guid, eligible_guid: 7, }]; - assert_eq!(project_flags(8, &entity, None, None, &[], &[], &eligible), 0); + assert_eq!( + project_flags(8, &entity, None, None, &[], &[], &eligible), + 0 + ); } #[test] @@ -4572,8 +4730,8 @@ mod tests { old.dynamic_flags = 0; let mut new = old.clone(); new.dynamic_flags = lyracore_shared::constants::unit_dynamic_flags::TAPPED; - let projected = new.dynamic_flags - | lyracore_shared::constants::unit_dynamic_flags::TAPPED_BY_PLAYER; + let projected = + new.dynamic_flags | lyracore_shared::constants::unit_dynamic_flags::TAPPED_BY_PLAYER; assert_eq!( entity_update_to_outbound_with_dynamic_flags(&old, &new, projected), vec![ServerOpcodeMessage::SMSG_UPDATE_OBJECT(Box::new( @@ -5554,8 +5712,72 @@ mod tests { assert!(!instance_relay_gate(7, None)); } - /// The non-test half of this file as the source scans below may read it: `//` comments STRIPPED - /// and every whitespace run collapsed to one space. + /// Remove line and nested block comments while preserving ordinary string literals. + /// + /// This is deliberately limited to the Rust source spans scanned in this module. The scans need + /// quoted labels, so comment markers inside a quoted label remain part of the source. + fn without_comments(text: &str) -> String { + let mut output = String::with_capacity(text.len()); + let mut chars = text.char_indices().peekable(); + let (mut in_string, mut in_line_comment, mut block_depth) = (false, false, 0usize); + + while let Some((index, character)) = chars.next() { + if in_line_comment { + if character == '\n' { + in_line_comment = false; + output.push(character); + } + continue; + } + + if block_depth > 0 { + if character == '/' && text[index..].starts_with("/*") { + block_depth += 1; + chars.next(); + } else if character == '*' && text[index..].starts_with("*/") { + block_depth -= 1; + chars.next(); + } else if character == '\n' { + output.push(character); + } + continue; + } + + if in_string { + output.push(character); + if character == '\\' { + if let Some((_, escaped)) = chars.next() { + output.push(escaped); + } + } else if character == '"' { + in_string = false; + } + continue; + } + + match character { + '"' => { + in_string = true; + output.push(character); + } + '/' if text[index..].starts_with("//") => { + in_line_comment = true; + output.push(' '); + } + '/' if text[index..].starts_with("/*") => { + block_depth = 1; + output.push(' '); + chars.next(); + } + _ => output.push(character), + } + } + + output + } + + /// The non-test half of this file as the source scans below may read it: comments stripped and + /// every whitespace run collapsed to one space. /// /// Both steps are mutation findings, not tidiness. Counting matches in the raw text made every /// scan below satisfiable by a COMMENT quoting the pattern while the real call was deleted — @@ -5570,11 +5792,7 @@ mod tests { .split("mod tests {") .next() .expect("the non-test half of this file"); - let decommented: String = body - .lines() - .map(|l| l.split("//").next().unwrap_or("")) - .collect::>() - .join("\n"); + let decommented = without_comments(body); decommented.split_whitespace().collect::>().join(" ") } @@ -5611,16 +5829,46 @@ mod tests { } /// Comment-stripped, whitespace-collapsed form of an arbitrary source span — the same two-step - /// mutation-hardening [`scanned_source`] applies to the whole file (strip a `//` line comment + /// mutation-hardening [`scanned_source`] applies to the whole file (strip comments /// FIRST, so a scan can't be satisfied by a comment quoting the pattern; collapse whitespace /// SECOND, so a rustfmt line-split can't fail a scan that pins a whole statement). + /// + /// The last step drops what a broken-out call leaves behind — the space after `(`, the trailing + /// comma before `)` — so a needle reads as one written call whichever way rustfmt wrapped it. fn decommented(text: &str) -> String { - let stripped: String = text - .lines() - .map(|l| l.split("//").next().unwrap_or("")) - .collect::>() - .join("\n"); - stripped.split_whitespace().collect::>().join(" ") + let stripped = without_comments(text); + let one_line = stripped.split_whitespace().collect::>().join(" "); + one_line + .replace("( ", "(") + .replace(", )", ")") + .replace(" )", ")") + } + + #[test] + fn source_scans_ignore_block_commented_code() { + let source = r#" + wire_insert_live( + db.live_table(), + "live.label", + &view, + |v, row| relay(v, row), + ); + /* + wire_insert_live(db.decoy_table(), "decoy.label", &view, relay); + /* wire_insert_live(db.nested_decoy(), "nested.label", &view, relay); */ + */ + let label = "/* quoted label */"; + "#; + + let scanned = decommented(source); + let compact: String = scanned.chars().filter(|c| !c.is_whitespace()).collect(); + + assert!(compact.contains( + "wire_insert_live(db.live_table(),\"live.label\",&view,|v,row|relay(v,row));" + )); + assert!(!scanned.contains("decoy_table")); + assert!(!scanned.contains("nested_decoy")); + assert!(scanned.contains("\"/* quoted label */\"")); } /// The call-site tripwire for the shared-view `Viewer`'s construction. @@ -5650,7 +5898,6 @@ mod tests { ); } - /// Tripwire for the realm-core PRIVATE tier (#22 → #483): the cross-shard whisper/group /// twins are armed ONCE per realm-core connection (`arm_realm_private`), gated on realm-core /// being a DISTINCT database. Without the gate a single-database gateway registers a SECOND @@ -5660,7 +5907,10 @@ mod tests { /// coordinator connection), so the wiring is pinned in source, comment-stripped. #[test] fn the_realm_private_tier_is_armed_once_gated_on_a_distinct_database() { - let body = decommented(top_level_fn_body_of("connection.rs", "arm_shared_world_view")); + let body = decommented(top_level_fn_body_of( + "connection.rs", + "arm_shared_world_view", + )); assert!( body.contains("if let Ok(realm) = self.realm_core() {"), "arming no longer resolves the realm-core handle — pointed anywhere else it hears \ @@ -5674,7 +5924,9 @@ mod tests { whisper line twice." ); assert!( - body.contains("super::world_view::arm_realm_private(view.clone(), realm.clone(), world.clone());"), + body.contains( + "super::world_view::arm_realm_private(view.clone(), realm.clone(), world.clone());" + ), "the realm-core private tier is never armed — a cross-shard whisper is written on \ realm-core and delivered to nobody" ); @@ -5697,11 +5949,11 @@ mod tests { let body = decommented(top_level_fn_body_of("world_view.rs", "arm_realm_private")); let compact: String = body.chars().filter(|c| !c.is_whitespace()).collect(); assert!( - compact.contains("wire_insert_live(db.game_whisper_event(),\"realm.game_whisper_event.insert\",&view,|v,row|whisper_appeared(v,row),);"), + compact.contains("wire_insert_live(db.game_whisper_event(),\"realm.game_whisper_event.insert\",&view,|v,row|whisper_appeared(v,row));"), "arm_realm_private no longer relays realm-core whispers through `whisper_appeared`" ); assert!( - compact.contains("wire_insert_live(db.game_group_event(),\"realm.game_group_event.insert\",&view,move|v,row|group_event_appeared(v,&coord,row),);"), + compact.contains("wire_insert_live(db.game_group_event(),\"realm.game_group_event.insert\",&view,move|v,row|group_event_appeared(v,&coord,row));"), "arm_realm_private no longer relays realm-core group events through \ `group_event_appeared` (which also carries the QUEST_SHARE detail JOIN through a \ WORLD handle — realm-core's cache has no quest catalogue)" @@ -5711,7 +5963,8 @@ mod tests { let whisper = decommented(top_level_fn_body_of("world_view.rs", "whisper_appeared")); assert!( whisper.contains("session_of_owner(row.recipient_guid)") - && whisper.contains("private_recipient_audience(row.recipient_guid, viewer.self_guid)"), + && whisper + .contains("private_recipient_audience(row.recipient_guid, viewer.self_guid)"), "whisper_appeared is no longer recipient-keyed — on an owner-token read that is a \ privacy leak: every session would receive every player's private whispers" ); @@ -5759,7 +6012,11 @@ mod tests { } } - fn relay_creature_leg(tx: &SessionTx, created: &Arc>>, row: &CreatureSpline) { + fn relay_creature_leg( + tx: &SessionTx, + created: &Arc>>, + row: &CreatureSpline, + ) { if shed_motion_at_depth(tx.depth()) { MOTION_DROPPED.fetch_add(1, std::sync::atomic::Ordering::Relaxed); return; @@ -6077,8 +6334,14 @@ mod tests { fn the_motionstat_line_reports_delivery_dropped_and_fanout_287() { // 1000 callbacks, 900 packets queued, 7 discarded, from 10 submitted movements. let line = motionstat_line(5000, 4500, 21, 1000, 900, 7, 10, 400); - assert!(line.contains("calls=5000"), "cumulative calls missing: {line}"); - assert!(line.contains("sent=4500"), "cumulative sent missing: {line}"); + assert!( + line.contains("calls=5000"), + "cumulative calls missing: {line}" + ); + assert!( + line.contains("sent=4500"), + "cumulative sent missing: {line}" + ); assert!( line.contains("dropped=21"), "cumulative dropped count missing: {line}" @@ -6095,9 +6358,18 @@ mod tests { line.contains("fanout=100.0/move"), "per-movement fan-out missing/wrong: {line}" ); - assert!(line.contains("submitted=400"), "submitted count missing: {line}"); - assert!(!line.contains("completed="), "retired completed count remains: {line}"); - assert!(!line.contains("outstanding="), "retired outstanding count remains: {line}"); + assert!( + line.contains("submitted=400"), + "submitted count missing: {line}" + ); + assert!( + !line.contains("completed="), + "retired completed count remains: {line}" + ); + assert!( + !line.contains("outstanding="), + "retired outstanding count remains: {line}" + ); // An idle window must not divide by zero or print a fake 0 %. let idle = motionstat_line(5000, 4500, 21, 0, 0, 0, 0, 400); @@ -6439,13 +6711,34 @@ mod tests { // `arm_shard`'s own body — what's left to scan for is the (helper, table, label) triple // that proves the callback is still wired, not skipped. for (helper, table, label, what) in [ - ("wire_insert", "game_entity_motion", "game_entity_motion.insert", "a mover's FIRST heartbeat"), - ("wire_update", "game_entity_motion", "game_entity_motion.update", "every heartbeat after the first — i.e. all peer movement"), - ("wire_insert", "game_creature_spline", "game_creature_spline.insert", "a creature's first leg"), - ("wire_update", "game_creature_spline", "game_creature_spline.update", "every leg after the first"), + ( + "wire_insert", + "game_entity_motion", + "game_entity_motion.insert", + "a mover's FIRST heartbeat", + ), + ( + "wire_update", + "game_entity_motion", + "game_entity_motion.update", + "every heartbeat after the first — i.e. all peer movement", + ), + ( + "wire_insert", + "game_creature_spline", + "game_creature_spline.insert", + "a creature's first leg", + ), + ( + "wire_update", + "game_creature_spline", + "game_creature_spline.update", + "every leg after the first", + ), ] { assert!( - arm.contains(&format!("{helper}(db.{table}()")) && arm.contains(&format!("\"{label}\"")), + arm.contains(&format!("{helper}(db.{table}()")) + && arm.contains(&format!("\"{label}\"")), "the {table} {label} half is no longer registered on the shared coordinator \ dispatch — {what} is silently dropped for every session" ); @@ -6459,17 +6752,13 @@ mod tests { ); } - /// And the instrument's wiring: the counters are only ever read by the 10-second task in /// `world::run`. Without this scan, reverting that task to the old hand-formatted line — no /// delivery ratio, no dropped count — leaves the whole suite green, which is precisely how the /// 63 % under-delivery went unlogged in the first place. #[test] fn the_10s_task_prints_the_delivery_instrument_287() { - let body = crate::test_scan::code_of( - include_str!("../world/mod.rs"), - "pub async fn run(", - ); + let body = crate::test_scan::code_of(include_str!("../world/mod.rs"), "pub async fn run("); assert!( body.contains("crate::stdb::subscriptions::MOTION_DROPPED.load(Relaxed)") && body.contains("crate::stdb::subscriptions::motionstat_line("), @@ -6488,10 +6777,7 @@ mod tests { /// "reported the call, not the effect" defect class). #[test] fn the_10s_task_warns_on_a_fanout_collapse_b1() { - let body = crate::test_scan::code_of( - include_str!("../world/mod.rs"), - "pub async fn run(", - ); + let body = crate::test_scan::code_of(include_str!("../world/mod.rs"), "pub async fn run("); assert!( body.contains("crate::stdb::subscriptions::fanout_health_step(fan, fanout, submitted_delta)"), "the 10s task no longer runs the fan-out collapse check — peer movement can degrade by \ @@ -6504,5 +6790,4 @@ mod tests { something out loud. Body was:\n{body}" ); } - } diff --git a/gateway/src/stdb/world_index.rs b/gateway/src/stdb/world_index.rs index 619434be8..aedddc423 100644 --- a/gateway/src/stdb/world_index.rs +++ b/gateway/src/stdb/world_index.rs @@ -208,7 +208,9 @@ impl WorldIndex { // Matches the file-wide `.lock().unwrap()` discipline in `connection.rs`: a poisoned index // must degrade to a possibly-stale read, never take every future relay down with it. self.inner.lock().unwrap_or_else(|p| { - log::error!("world index lock poisoned (a prior panic in a critical section) — recovering"); + log::error!( + "world index lock poisoned (a prior panic in a critical section) — recovering" + ); p.into_inner() }) } @@ -503,11 +505,7 @@ impl WorldIndex { /// Every entity a viewer can see, with the shard whose cache holds each row — the login / /// world-entry sweep, and the inverse the differential test checks. O(25 + E) with cell scoping /// on; O(world) with `LYRACORE_AOI=0`. - pub fn visible_entities( - &self, - layer: EntityLayer, - session: SessionId, - ) -> Vec<(u64, ShardId)> { + pub fn visible_entities(&self, layer: EntityLayer, session: SessionId) -> Vec<(u64, ShardId)> { let inner = self.lock(); let Some(anchor) = inner.viewer_cell.get(&session).copied() else { return Vec::new(); @@ -601,7 +599,9 @@ mod tests { /// AOI builders emit — `col = n`, `col >= n`, `col <= n`, joined by ` AND ` — and PANICS on /// anything else, so a future query shape cannot silently pass by being unparsed. fn sql_selects(query: &str, row: &Row) -> bool { - let (_, where_clause) = query.split_once(" WHERE ").expect("an AOI query has a WHERE"); + let (_, where_clause) = query + .split_once(" WHERE ") + .expect("an AOI query has a WHERE"); where_clause.split(" AND ").all(|term| { let mut parts = term.split_whitespace(); let col = parts.next().expect("a column"); @@ -611,14 +611,19 @@ mod tests { .expect("a literal") .parse() .expect("an integer literal"); - assert!(parts.next().is_none(), "unexpected trailing token in `{term}`"); + assert!( + parts.next().is_none(), + "unexpected trailing token in `{term}`" + ); let actual: i64 = match col { "map_id" => row.map_id as i64, "instance_id" => row.instance_id as i64, "grid_x" => row.gx as i64, "grid_y" => row.gy as i64, "cell" => grid_cell_id(row.gx, row.gy), - other => panic!("the oracle does not know column `{other}` — teach it or fix the query"), + other => { + panic!("the oracle does not know column `{other}` — teach it or fix the query") + } }; match op { "=" => actual == literal, @@ -823,7 +828,10 @@ mod tests { let delta = index.move_viewer_delta(1, to); let after = sql_visible(EntityLayer::WorldEntity, to, &rows); match delta { - None => assert_eq!(from, to, "a delta of None must mean the anchor did not move"), + None => assert_eq!( + from, to, + "a delta of None must mean the anchor did not move" + ), Some(d) => { for (layer, layer_delta) in [ (EntityLayer::WorldEntity, d.world_entities), @@ -831,8 +839,7 @@ mod tests { ] { let entered: HashSet = layer_delta.entered.iter().map(|(g, _)| *g).collect(); - let left: HashSet = - layer_delta.left.iter().map(|(g, _)| *g).collect(); + let left: HashSet = layer_delta.left.iter().map(|(g, _)| *g).collect(); assert_eq!( entered, after.difference(&before).copied().collect::>(), @@ -862,7 +869,9 @@ mod tests { .neighbourhood() .map(|k| lyracore_shared::spatial::grid_cell_of_id(k.cell)) .collect(); - let want: HashSet<(i32, i32)> = (x0..=x1).flat_map(|x| (y0..=y1).map(move |y| (x, y))).collect(); + let want: HashSet<(i32, i32)> = (x0..=x1) + .flat_map(|x| (y0..=y1).map(move |y| (x, y))) + .collect(); assert_eq!(got, want); assert_eq!(got.len(), ((2 * BOX_HALF_SPAN + 1) as usize).pow(2)); // Every neighbour keeps the partition — a leak here is a dungeon copy visible from the @@ -972,8 +981,13 @@ mod tests { let index = WorldIndex::new(true); let a = CellKey::at(0, 0, 0, 0); index.add_viewer(1, a); - assert!(index.move_viewer_delta(1, a).is_none(), "same cell is not a recenter"); - assert!(index.move_viewer_delta(1, CellKey::at(0, 0, 1, 0)).is_some()); + assert!( + index.move_viewer_delta(1, a).is_none(), + "same cell is not a recenter" + ); + assert!(index + .move_viewer_delta(1, CellKey::at(0, 0, 1, 0)) + .is_some()); assert_eq!(index.viewer_cell(1), Some(CellKey::at(0, 0, 1, 0))); // Only one anchor is ever held: walking away must not leave the session visible from the // cell it started in. @@ -1007,12 +1021,17 @@ mod tests { index.upsert_entity(EntityLayer::WorldEntity, 3, CellKey::at(0, 77, 0, 0), 0, 0); // another instance index.upsert_entity(EntityLayer::WorldEntity, 4, CellKey::at(1, 0, 0, 0), 0, 0); // another map index.add_viewer(1, CellKey::at(0, 0, 0, 0)); - assert_eq!(index_visible(&index, EntityLayer::WorldEntity, 1), HashSet::from([1, 2])); + assert_eq!( + index_visible(&index, EntityLayer::WorldEntity, 1), + HashSet::from([1, 2]) + ); assert_eq!( index.viewers_of(EntityLayer::WorldEntity, CellKey::at(0, 0, 900, 900)), vec![1] ); - assert!(index.viewers_of(EntityLayer::WorldEntity, CellKey::at(0, 77, 0, 0)).is_empty()); + assert!(index + .viewers_of(EntityLayer::WorldEntity, CellKey::at(0, 77, 0, 0)) + .is_empty()); } /// `can_see` is a point query and `visible_entities` is the set form of the same question — @@ -1068,5 +1087,4 @@ mod tests { index.add_viewer(1, CellKey::at(0, 0, 0, 0)); assert_eq!(index.stats(EntityLayer::WorldEntity), (3, 1, 2)); } - } diff --git a/gateway/src/stdb/world_view.rs b/gateway/src/stdb/world_view.rs index 5547be001..2b6435d90 100644 --- a/gateway/src/stdb/world_view.rs +++ b/gateway/src/stdb/world_view.rs @@ -515,6 +515,7 @@ pub(crate) fn arm_shard(view: Arc, coord: Coordinator, shard: ShardId }) } +#[allow(clippy::too_many_lines)] // One callback registration per relayed shard table. fn register_shard_callbacks( view: Arc, coord: Coordinator, @@ -2340,12 +2341,16 @@ fn duel_winner_audience(viewer_guid: u64, initiator_guid: u64, challenged_guid: viewer_guid != 0 && viewer_guid != initiator_guid && viewer_guid != challenged_guid } +// The relay-order tests below scan this file's own source, so the production items that follow +// them must stay where they are. +#[allow(clippy::items_after_test_module)] #[cfg(test)] mod cell_audience_tests; #[cfg(test)] mod relay_bench; +#[allow(clippy::items_after_test_module)] #[cfg(test)] mod family_audience_tests { use super::{ @@ -2640,9 +2645,18 @@ mod family_audience_tests { #[test] fn private_rows_reach_their_addressee_and_nobody_else() { assert!(private_recipient_audience(42, 42)); - assert!(!private_recipient_audience(42, 43), "a third party must never receive a private row"); - assert!(!private_recipient_audience(0, 42), "recipient 0 (unaddressed) matches no viewer"); - assert!(!private_recipient_audience(42, 0), "an uninitialized viewer guid matches no row"); + assert!( + !private_recipient_audience(42, 43), + "a third party must never receive a private row" + ); + assert!( + !private_recipient_audience(0, 42), + "recipient 0 (unaddressed) matches no viewer" + ); + assert!( + !private_recipient_audience(42, 0), + "an uninitialized viewer guid matches no row" + ); assert!( !private_recipient_audience(0, 0), "the all-zero case is a leak, not a match — both sides unset must still deny" @@ -2868,7 +2882,9 @@ mod family_audience_tests { assert_eq!(view.viewers_on_shard(4).len(), 1); view.remove_viewer(replacement.session); - assert!(view.viewer_of_owner(OwnerGuid(replacement.self_guid)).is_none()); + assert!(view + .viewer_of_owner(OwnerGuid(replacement.self_guid)) + .is_none()); assert!(view .viewer_of_identity(BoundIdentity(replacement.bound_identity)) .is_none()); @@ -2888,19 +2904,28 @@ mod family_audience_tests { teleport_appeared(&view, 0, &teleport(owner.self_guid)); - assert!(other_rx.try_recv().is_err(), "an unrelated viewer receives nothing"); + assert!( + other_rx.try_recv().is_err(), + "an unrelated viewer receives nothing" + ); let out = queued_job(&owner_rx); assert_eq!(out.len(), 1, "one teleport row creates one writer result"); match &out[0] { Outbound::Batch(messages) => { assert!(matches!( messages.as_slice(), - [ServerOpcodeMessage::SMSG_TRANSFER_PENDING(_), ServerOpcodeMessage::SMSG_NEW_WORLD(_)] + [ + ServerOpcodeMessage::SMSG_TRANSFER_PENDING(_), + ServerOpcodeMessage::SMSG_NEW_WORLD(_) + ] )); } _ => panic!("cross-map teleport must remain one ordered writer batch"), } - assert!(owner_rx.try_recv().is_err(), "the owner receives exactly one job"); + assert!( + owner_rx.try_recv().is_err(), + "the owner receives exactly one job" + ); } #[test] @@ -2917,27 +2942,48 @@ mod family_audience_tests { view.add_viewer_on_shard(other, anchor, 0); item_owner_job(&view, 0, source.self_guid, |_| { - vec![Outbound::Raw { opcode: 1, body: vec![1] }] + vec![Outbound::Raw { + opcode: 1, + body: vec![1], + }] }); - assert!(matches!(queued_job(&source_rx).as_slice(), [Outbound::Raw { .. }])); - assert!(other_rx.try_recv().is_err(), "an unrelated item owner receives nothing"); + assert!(matches!( + queued_job(&source_rx).as_slice(), + [Outbound::Raw { .. }] + )); + assert!( + other_rx.try_recv().is_err(), + "an unrelated item owner receives nothing" + ); view.remove_viewer(source.session); item_owner_job(&view, 0, source.self_guid, |_| { - vec![Outbound::Raw { opcode: 2, body: vec![2] }] + vec![Outbound::Raw { + opcode: 2, + body: vec![2], + }] }); - assert!(source_rx.try_recv().is_err(), "a removed source viewer receives no cascade delete"); + assert!( + source_rx.try_recv().is_err(), + "a removed source viewer receives no cascade delete" + ); view.add_viewer_on_shard(destination, anchor, 1); item_owner_job(&view, 0, source.self_guid, |_| { - vec![Outbound::Raw { opcode: 2, body: vec![2] }] + vec![Outbound::Raw { + opcode: 2, + body: vec![2], + }] }); assert!( destination_rx.try_recv().is_err(), "a source-shard delta applied after destination registration must be dropped" ); item_owner_job(&view, 1, source.self_guid, |_| { - vec![Outbound::Raw { opcode: 3, body: vec![3] }] + vec![Outbound::Raw { + opcode: 3, + body: vec![3], + }] }); assert!(matches!( queued_job(&destination_rx).as_slice(), @@ -3055,12 +3101,18 @@ mod family_audience_tests { let (discovery_opcode, discovery_body) = crate::codec::build_exploration_experience_raw(88, 121); match fresh_out.as_slice() { - [ - Outbound::Raw { opcode: fog_opcode, body: fog_body }, - Outbound::Raw { opcode: toast_opcode, body: toast_body }, - ] => { + [Outbound::Raw { + opcode: fog_opcode, + body: fog_body, + }, Outbound::Raw { + opcode: toast_opcode, + body: toast_body, + }] => { assert_eq!((*fog_opcode, fog_body), (new_fog_opcode, &new_fog_body)); - assert_eq!((*toast_opcode, toast_body), (discovery_opcode, &discovery_body)); + assert_eq!( + (*toast_opcode, toast_body), + (discovery_opcode, &discovery_body) + ); } _ => panic!("fog VALUES must precede discovery feedback for a new area"), } @@ -3080,18 +3132,27 @@ mod family_audience_tests { reputation_appeared(&view, 0, &reputation(9001, 19, 3175)); reputation_appeared(&view, 0, &reputation(9001, 19, 3200)); - assert!(other_rx.try_recv().is_err(), "an unrelated owner receives nothing"); + assert!( + other_rx.try_recv().is_err(), + "an unrelated owner receives nothing" + ); for standing in [3175, 3200] { let (opcode, body) = crate::codec::build_set_faction_standing_raw(19, standing) .expect("a valid reputation index has a packet"); match queued_job(&owner_rx).as_slice() { - [Outbound::Raw { opcode: actual_opcode, body: actual_body }] => { + [Outbound::Raw { + opcode: actual_opcode, + body: actual_body, + }] => { assert_eq!((*actual_opcode, actual_body), (opcode, &body)); } _ => panic!("the reputation row must use its reputation_index, not faction_id"), } } - assert!(owner_rx.try_recv().is_err(), "each reputation row queues exactly one job"); + assert!( + owner_rx.try_recv().is_err(), + "each reputation row queues exactly one job" + ); } #[test] diff --git a/gateway/src/world/handlers/auction.rs b/gateway/src/world/handlers/auction.rs index 772b457fc..149c779f8 100644 --- a/gateway/src/world/handlers/auction.rs +++ b/gateway/src/world/handlers/auction.rs @@ -462,8 +462,9 @@ pub(crate) fn dispatch_auction_action( other => return Ok(AuctionActionOutcome::PassThrough(other)), }; let auctioneer_guid = auctioneer.guid(); - let Some((player_guid, interaction)) = auction_actor_interaction(store, player, auctioneer_guid)? - .filter(|(_, interaction)| !interaction.refuses_interaction) + let Some((player_guid, interaction)) = + auction_actor_interaction(store, player, auctioneer_guid)? + .filter(|(_, interaction)| !interaction.refuses_interaction) else { return Ok(AuctionActionOutcome::Handled { outbound: Vec::new(), diff --git a/gateway/src/world/handlers/cast/mod.rs b/gateway/src/world/handlers/cast/mod.rs index 583bc1c10..ffb1d1771 100644 --- a/gateway/src/world/handlers/cast/mod.rs +++ b/gateway/src/world/handlers/cast/mod.rs @@ -709,10 +709,12 @@ pub(super) mod tests { if let Some(e) = &self.ranged_error { return Err(anyhow!("{e}")); } - self.ranged_attacks - .lock() - .unwrap() - .push((account_id, self_guid, target_guid, spell_id)); + self.ranged_attacks.lock().unwrap().push(( + account_id, + self_guid, + target_guid, + spell_id, + )); Ok(()) } @@ -728,7 +730,12 @@ pub(super) mod tests { /// The ranged teardown the cast module shares with the melee seam. `start_attack` is never /// reached from a cast route; it exists because the two share one durable engagement row. impl MeleeActionStore for InMemoryCasts { - fn start_attack(&self, _account_id: u64, _actor_guid: u64, _target_guid: u64) -> Result<()> { + fn start_attack( + &self, + _account_id: u64, + _actor_guid: u64, + _target_guid: u64, + ) -> Result<()> { unreachable!("no cast route arms a melee engagement") } diff --git a/gateway/src/world/handlers/cast/ranged.rs b/gateway/src/world/handlers/cast/ranged.rs index 27ceede52..be5b25e24 100644 --- a/gateway/src/world/handlers/cast/ranged.rs +++ b/gateway/src/world/handlers/cast/ranged.rs @@ -75,7 +75,10 @@ fn refuse( ); let outbound = vec![Outbound::Raw { opcode: OP_CAST_RESULT, - body: codec::build_cast_result_failed(spell, codec::cast_failure_reason_for(&e.to_string())), + body: codec::build_cast_result_failed( + spell, + codec::cast_failure_reason_for(&e.to_string()), + ), }]; // A refused RETARGET drops the client's toggle on that failure result, so the still-firing OLD // loop has to go too — otherwise the server keeps shooting a target the client believes it @@ -261,7 +264,10 @@ mod tests { "no CAST_RESULT(OK) and no GO: the activation is not a completed shot" ); let start = spell_start(&outbound); - assert_eq!(start.timer, 0, "the wind-up is an attack timer, not a cast bar"); + assert_eq!( + start.timer, 0, + "the wind-up is an attack timer, not a cast bar" + ); assert_eq!( start .targets @@ -465,7 +471,10 @@ mod tests { #[test] fn the_lowest_slot_live_stack_wins() { - let store = armed_with(launcher(100, 2), vec![(9, 300, 20), (4, 201, 60), (2, 200, 0)]); + let store = armed_with( + launcher(100, 2), + vec![(9, 300, 20), (4, 201, 60), (2, 200, 0)], + ); assert_eq!( activate_ammo(&store), diff --git a/gateway/src/world/handlers/combat.rs b/gateway/src/world/handlers/combat.rs index c4742cd5c..9396cfde0 100644 --- a/gateway/src/world/handlers/combat.rs +++ b/gateway/src/world/handlers/combat.rs @@ -81,7 +81,8 @@ pub(crate) fn handle_combat( } } ClientOpcodeMessage::CMSG_LEAVE_CHANNEL(c) => { - if let Err(e) = store.leave_channel(conn.account_id, self_guid, c.channel_name.clone()) { + if let Err(e) = store.leave_channel(conn.account_id, self_guid, c.channel_name.clone()) + { log::debug!( "world: leave_channel failed (account {}): {e}", conn.account_id diff --git a/gateway/src/world/handlers/mail.rs b/gateway/src/world/handlers/mail.rs index 9f952553f..77f7cbc4a 100644 --- a/gateway/src/world/handlers/mail.rs +++ b/gateway/src/world/handlers/mail.rs @@ -8,6 +8,7 @@ use super::super::*; use wow_world_messages::vanilla::SMSG_SEND_MAIL_RESULT_MailResultTwo; +#[allow(clippy::too_many_lines)] // One arm per mail opcode. pub(crate) fn handle_mail( tx: &SessionTx, store: &St, diff --git a/gateway/src/world/handlers/mod.rs b/gateway/src/world/handlers/mod.rs index b64ff8955..21d9ba31e 100644 --- a/gateway/src/world/handlers/mod.rs +++ b/gateway/src/world/handlers/mod.rs @@ -10,8 +10,8 @@ use super::*; // a player context, decides refusal-versus-fatal itself, and returns the outbound batch for the // world session to send, so the family can be tested without a socket. -mod bank; mod auction; +mod bank; mod cast; mod char; mod combat; @@ -28,16 +28,15 @@ mod trainer; mod vendor; mod weather; -pub(crate) use bank::handle_bank; pub(crate) use auction::{ decode_auction_browse, dispatch_auction_action, dispatch_auction_browse_action, AuctionActionOutcome, AuctionActionPlayer, AuctionActionStore, AuctionBrowseRequest, AuctionPage, AuctionQuery, CreateAuctionOutcome, CreateAuctionRequest, PlaceBidOutcome, - PlaceBidRequest, - CMSG_AUCTION_LIST_ITEMS_OPCODE, + PlaceBidRequest, CMSG_AUCTION_LIST_ITEMS_OPCODE, }; #[cfg(test)] pub(crate) use auction::{AuctionHousePolicy, AuctionInteraction}; +pub(crate) use bank::handle_bank; pub(crate) use cast::{dispatch_cast, CastOutcome, CastPlayer, CastStore, CastTransition}; pub(crate) use char::handle_char; pub(crate) use combat::handle_combat; @@ -55,10 +54,12 @@ pub(crate) use melee::{ }; pub(crate) use query::handle_query; pub(crate) use quest::{ - dispatch_quest_action, quest_giver_menu, QuestActionOutcome, QuestActionPlayer, QuestActionStore, + dispatch_quest_action, quest_giver_menu, QuestActionOutcome, QuestActionPlayer, + QuestActionStore, }; pub(crate) use taxi::{ - dispatch_taxi_action, queue_reply_then_arm, TaxiActionOutcome, TaxiActionPlayer, TaxiActionStore, + dispatch_taxi_action, queue_reply_then_arm, TaxiActionOutcome, TaxiActionPlayer, + TaxiActionStore, }; pub(crate) use trade::handle_trade; pub(crate) use trainer::{handle_trainer, TrainerBuyOutcome}; diff --git a/gateway/src/world/handlers/query.rs b/gateway/src/world/handlers/query.rs index 6717774d6..66dac0ce5 100644 --- a/gateway/src/world/handlers/query.rs +++ b/gateway/src/world/handlers/query.rs @@ -51,6 +51,7 @@ fn filtered_gossip_options( /// Query / social family: name / creature / item lookups + the gossip / npc-text round-trips, plus /// the social tier (say / yell / whisper chat + text emotes) — grouped as the stateless /// request→reply / broadcast opcodes. +#[allow(clippy::too_many_lines)] // One arm per query and social opcode. pub(crate) fn handle_query( tx: &SessionTx, store: &St, diff --git a/gateway/src/world/handlers/quest.rs b/gateway/src/world/handlers/quest.rs index 412d424ee..0a9eb527e 100644 --- a/gateway/src/world/handlers/quest.rs +++ b/gateway/src/world/handlers/quest.rs @@ -329,7 +329,8 @@ pub(crate) fn dispatch_quest_action( // The gate fails open — missing standing data must not lock a player out of a // giver — but a dead reducer transport is not missing data and ends the session. Err(e) - if classify_quest_action_error(&e) == QuestActionErrorClass::GameplayRefusal => + if classify_quest_action_error(&e) + == QuestActionErrorClass::GameplayRefusal => { log::debug!( "world: questgiver {giver} interaction gate unavailable (account {}): {e}", @@ -371,7 +372,8 @@ pub(crate) fn dispatch_quest_action( match store.accept_quest(player.account_id, self_guid, a.guid.guid(), a.quest_id) { Ok(()) => {} Err(e) - if classify_quest_action_error(&e) == QuestActionErrorClass::GameplayRefusal => + if classify_quest_action_error(&e) + == QuestActionErrorClass::GameplayRefusal => { log::debug!( "world: accept_quest ignored (account {}): {e}", @@ -436,7 +438,8 @@ pub(crate) fn dispatch_quest_action( }, }), Err(e) - if classify_quest_action_error(&e) == QuestActionErrorClass::GameplayRefusal => + if classify_quest_action_error(&e) + == QuestActionErrorClass::GameplayRefusal => { log::debug!( "world: turn_in_quest refused (quest {}, reward index {}): {e}", @@ -493,7 +496,8 @@ pub(crate) fn dispatch_quest_action( match store.push_quest(player.account_id, self_guid, p.quest_id) { Ok(()) => {} Err(e) - if classify_quest_action_error(&e) == QuestActionErrorClass::GameplayRefusal => + if classify_quest_action_error(&e) + == QuestActionErrorClass::GameplayRefusal => { log::debug!( "world: push_quest ignored (account {}): {e}", @@ -796,7 +800,9 @@ mod tests { fn outbound(outcome: QuestActionOutcome) -> Vec { match outcome { QuestActionOutcome::Handled { outbound } => outbound, - QuestActionOutcome::PassThrough(_) => panic!("expected the quest module to handle this"), + QuestActionOutcome::PassThrough(_) => { + panic!("expected the quest module to handle this") + } } } @@ -828,7 +834,8 @@ mod tests { ..Default::default() }; - let batch = outbound(dispatch_quest_action(&actions, player(), status_query(GIVER)).unwrap()); + let batch = + outbound(dispatch_quest_action(&actions, player(), status_query(GIVER)).unwrap()); assert!(matches!( batch.as_slice(), @@ -850,7 +857,9 @@ mod tests { ..one_quest(codec::ROLE_START, false, false) }; - assert!(outbound(dispatch_quest_action(&actions, player(), hello(GIVER)).unwrap()).is_empty()); + assert!( + outbound(dispatch_quest_action(&actions, player(), hello(GIVER)).unwrap()).is_empty() + ); assert_eq!( actions.gate_requests.lock().unwrap().as_slice(), &[(GIVER, SELF_GUID)] diff --git a/gateway/src/world/handlers/vendor.rs b/gateway/src/world/handlers/vendor.rs index bac4b5de1..57c552668 100644 --- a/gateway/src/world/handlers/vendor.rs +++ b/gateway/src/world/handlers/vendor.rs @@ -30,13 +30,23 @@ pub(crate) trait VendorActionStore: Send + Sync { fn vendor_repair(&self, account_id: u64, self_guid: u64, npc_guid: u64, slot: u8) -> Result<()>; - fn vendor_sell(&self, account_id: u64, self_guid: u64, vendor_guid: u64, slot: u8) - -> Result<()>; + fn vendor_sell( + &self, + account_id: u64, + self_guid: u64, + vendor_guid: u64, + slot: u8, + ) -> Result<()>; /// Re-purchase the ring entry at 0-based `slot` from `vendor_guid`. The gateway maps the wire /// `BuybackSlot` enum via [`BUYBACK_WIRE_SLOT_BASE`] before calling. - fn vendor_buyback(&self, account_id: u64, self_guid: u64, vendor_guid: u64, slot: u8) - -> Result<()>; + fn vendor_buyback( + &self, + account_id: u64, + self_guid: u64, + vendor_guid: u64, + slot: u8, + ) -> Result<()>; } impl VendorActionStore for crate::stdb::Coordinator { @@ -57,7 +67,12 @@ impl VendorActionStore for crate::stdb::Coordinator { count: u32, ) -> Result<()> { crate::stdb::Coordinator::buy_item( - self, account_id, self_guid, vendor_guid, item_entry, count, + self, + account_id, + self_guid, + vendor_guid, + item_entry, + count, ) } @@ -214,13 +229,20 @@ pub(crate) fn dispatch_vendor_action( c.amount as u32, ) { Ok(()) => Vec::new(), - Err(e) if classify_vendor_action_error(&e) == VendorActionErrorClass::GameplayRefusal => { + Err(e) + if classify_vendor_action_error(&e) + == VendorActionErrorClass::GameplayRefusal => + { log::debug!( "world: vendor_buy failed (account {}): {e}", player.account_id ); vec![Outbound::One(ServerOpcodeMessage::SMSG_BUY_FAILED( - Box::new(codec::build_buy_failed(vendor_guid, item_entry, &e.to_string())), + Box::new(codec::build_buy_failed( + vendor_guid, + item_entry, + &e.to_string(), + )), ))] } Err(e) => return Err(e), @@ -286,7 +308,10 @@ pub(crate) fn dispatch_vendor_action( Ok(()) => Ok(VendorActionOutcome::Handled { outbound: build_buyback_view(store, self_guid), }), - Err(e) if classify_vendor_action_error(&e) == VendorActionErrorClass::GameplayRefusal => { + Err(e) + if classify_vendor_action_error(&e) + == VendorActionErrorClass::GameplayRefusal => + { log::debug!( "world: sell_item ignored (account {}): {e}", player.account_id @@ -319,7 +344,10 @@ pub(crate) fn dispatch_vendor_action( None => Vec::new(), }, }), - Err(e) if classify_vendor_action_error(&e) == VendorActionErrorClass::GameplayRefusal => { + Err(e) + if classify_vendor_action_error(&e) + == VendorActionErrorClass::GameplayRefusal => + { log::debug!( "world: buyback_item ignored (account {}): {e}", player.account_id @@ -408,11 +436,20 @@ mod tests { CMSG_REPAIR_ITEM, CMSG_SELL_ITEM, }; + #[derive(Debug, Eq, PartialEq)] + struct BuyRequest { + account_id: u64, + actor_guid: u64, + vendor_guid: u64, + item_entry: u32, + count: u32, + } + #[derive(Default)] struct InMemoryVendorActions { stock_requests: Mutex>, gate_requests: Mutex>, - buy_requests: Mutex>, + buy_requests: Mutex>, repair_requests: Mutex>, sell_requests: Mutex>, stock: Vec, @@ -456,10 +493,13 @@ mod tests { item_entry: u32, count: u32, ) -> Result<()> { - self.buy_requests - .lock() - .unwrap() - .push((account_id, self_guid, vendor_guid, item_entry, count)); + self.buy_requests.lock().unwrap().push(BuyRequest { + account_id, + actor_guid: self_guid, + vendor_guid, + item_entry, + count, + }); match &self.buy_error { Some(error) => Err(anyhow::anyhow!("{error}")), None => Ok(()), @@ -726,7 +766,13 @@ mod tests { ); assert_eq!( actions.buy_requests.lock().unwrap().as_slice(), - &[(7, 42, VENDOR, 2589, 3)] + &[BuyRequest { + account_id: 7, + actor_guid: 42, + vendor_guid: VENDOR, + item_entry: 2589, + count: 3, + }] ); } @@ -761,7 +807,13 @@ mod tests { assert_eq!( actions.buy_requests.lock().unwrap().as_slice(), - &[(7, 0, VENDOR, 2589, 1)] + &[BuyRequest { + account_id: 7, + actor_guid: 0, + vendor_guid: VENDOR, + item_entry: 2589, + count: 1, + }] ); } diff --git a/gateway/src/world/mail_tests.rs b/gateway/src/world/mail_tests.rs index eb712ebdb..effbaef2d 100644 --- a/gateway/src/world/mail_tests.rs +++ b/gateway/src/world/mail_tests.rs @@ -1,4 +1,3 @@ - use super::party_tests::{character, DORMANT, GINGER, TRIN, VIM}; use super::*; const MAILBOX: u64 = 0xF110_0000_0000_0042; diff --git a/gateway/src/world/mod.rs b/gateway/src/world/mod.rs index 4e8b88caf..cf55a8bf3 100644 --- a/gateway/src/world/mod.rs +++ b/gateway/src/world/mod.rs @@ -50,25 +50,22 @@ use coalesce::CoalesceState; use handlers::{ decode_auction_browse, dispatch_auction_action, dispatch_auction_browse_action, dispatch_cast, dispatch_duel_action, dispatch_item_action, dispatch_loot_window, dispatch_melee_action, - dispatch_quest_action, - dispatch_taxi_action, dispatch_vendor_action, handle_bank, handle_char, handle_combat, - handle_loot, handle_mail, handle_query, handle_trade, handle_trainer, queue_reply_then_arm, - AuctionActionOutcome, AuctionActionPlayer, CastOutcome, CastPlayer, CastTransition, - DuelActionOutcome, DuelActionPlayer, ItemActionOutcome, ItemActionPlayer, LootWindowOutcome, - LootWindowPlayer, MeleeActionOutcome, - MeleeActionPlayer, OpenLootState, QuestActionOutcome, QuestActionPlayer, TaxiActionOutcome, - TaxiActionPlayer, VendorActionOutcome, VendorActionPlayer, - CMSG_AUCTION_LIST_ITEMS_OPCODE, quest_giver_menu, + dispatch_quest_action, dispatch_taxi_action, dispatch_vendor_action, handle_bank, handle_char, + handle_combat, handle_loot, handle_mail, handle_query, handle_trade, handle_trainer, + quest_giver_menu, queue_reply_then_arm, AuctionActionOutcome, AuctionActionPlayer, CastOutcome, + CastPlayer, CastTransition, DuelActionOutcome, DuelActionPlayer, ItemActionOutcome, + ItemActionPlayer, LootWindowOutcome, LootWindowPlayer, MeleeActionOutcome, MeleeActionPlayer, + OpenLootState, QuestActionOutcome, QuestActionPlayer, TaxiActionOutcome, TaxiActionPlayer, + VendorActionOutcome, VendorActionPlayer, CMSG_AUCTION_LIST_ITEMS_OPCODE, }; pub(crate) use handlers::{ zone_weather_message, AuctionBrowseRequest, AuctionPage, AuctionQuery, CreateAuctionOutcome, CreateAuctionRequest, ItemActionResult, LootActionStatus, LootWindowRefusal, - LootWindowRequestStatus, - PlaceBidOutcome, PlaceBidRequest, TrainerBuyOutcome, WeatherStore, + LootWindowRequestStatus, PlaceBidOutcome, PlaceBidRequest, TrainerBuyOutcome, WeatherStore, }; use login_queue::{Admission, LoginQueue}; -pub(crate) use social::ContactOutcome; use social::handle_social; +pub(crate) use social::ContactOutcome; pub use store::WorldStore; use transfer::{EscrowedTransfer, TransferPlan}; @@ -1172,6 +1169,7 @@ fn apply_cast_transition(conn: &mut WorldConn, transition: CastTransition) { /// Route one decrypted client message through the per-family handlers. Each stage either consumes /// its opcode or passes it onward, so the disjoint-family chain ends in the movement-relay catch-all. +#[allow(clippy::too_many_lines)] // One stage per opcode family. fn dispatch( tx: &SessionTx, store: &St, diff --git a/gateway/src/world/social.rs b/gateway/src/world/social.rs index 05f6bafcc..d929be2b1 100644 --- a/gateway/src/world/social.rs +++ b/gateway/src/world/social.rs @@ -312,9 +312,7 @@ fn friend_result_for(refusal: ContactRefusal, is_ignore: bool) -> FriendResult { // row that is not there, both read as the one "no such entry" line vanilla has. (ContactRefusal::NotOnList, false) | (ContactRefusal::NoSuchPlayer, _) - | (ContactRefusal::ActorUnavailable, _) => { - FriendResult::NotFound - } + | (ContactRefusal::ActorUnavailable, _) => FriendResult::NotFound, } } diff --git a/gateway/src/world/tests.rs b/gateway/src/world/tests.rs index 155c8b13a..e325b0d90 100644 --- a/gateway/src/world/tests.rs +++ b/gateway/src/world/tests.rs @@ -1,6 +1,6 @@ use super::handlers::{ - AuctionActionStore, AuctionInteraction, CastStore, DuelActionStore, - ItemActionStore, LootWindowRefusal, LootWindowRequestStatus, LootWindowStore, MeleeActionStore, + AuctionActionStore, AuctionInteraction, CastStore, DuelActionStore, ItemActionStore, + LootWindowRefusal, LootWindowRequestStatus, LootWindowStore, MeleeActionStore, QuestActionStore, TaxiActionStore, VendorActionStore, WeatherStore, }; use super::party::PartyOutcome; @@ -128,9 +128,6 @@ mod framing_tests; #[path = "alpha_test_tools_tests.rs"] mod alpha_test_tools_tests; -/// The per-account connection release regressions. A sibling of the modules above for the -/// same reason — it reaches `InMemoryStore` without widening anything. - /// Multi-shard routing — reducer calls and subscriptions never target a shard other than the /// player's home shard. A sibling of the modules above for the same reason. `ShardCallLog` is /// `pub(super)` here and re-exported below because this file's own pre-shard-routing tests (and @@ -167,6 +164,7 @@ use wow_world_messages::vanilla::{ ItemQuality, Language, Level, + MSG_AUCTION_HELLO_Client, Map, Object, Race, @@ -179,13 +177,14 @@ use wow_world_messages::vanilla::{ TrainingFailureReason, WeatherType, WorldResult, + CMSG_ACTIVATETAXI, CMSG_ADD_FRIEND, CMSG_ADD_IGNORE, + CMSG_ATTACKSTOP, + CMSG_ATTACKSWING, CMSG_AUCTION_LIST_BIDDER_ITEMS, CMSG_AUCTION_LIST_ITEMS, CMSG_AUCTION_LIST_OWNER_ITEMS, - CMSG_ATTACKSTOP, - CMSG_ATTACKSWING, CMSG_AUTH_SESSION, CMSG_AUTOBANK_ITEM, CMSG_AUTOEQUIP_ITEM, @@ -233,13 +232,11 @@ use wow_world_messages::vanilla::{ CMSG_SETSHEATHED, CMSG_SET_SELECTION, CMSG_SPIRIT_HEALER_ACTIVATE, - CMSG_ACTIVATETAXI, CMSG_TAXINODE_STATUS_QUERY, CMSG_TAXIQUERYAVAILABLENODES, CMSG_TRAINER_BUY_SPELL, CMSG_TRAINER_LIST, CMSG_WHO, - MSG_AUCTION_HELLO_Client, // Cross-map teleport: the client's world-port-finished ack. MSG_MOVE_WORLDPORT_ACK, SMSG_WEATHER, @@ -2906,11 +2903,7 @@ impl TaxiActionStore for InMemoryStore { Ok(self.taxi_status) } - fn open_taxi( - &self, - character_guid: u64, - npc_guid: u64, - ) -> Result> { + fn open_taxi(&self, character_guid: u64, npc_guid: u64) -> Result> { self.taxi_calls .lock() .unwrap() @@ -5488,7 +5481,10 @@ fn item_query_preserves_imported_eligibility_through_the_encrypted_world_session assert_eq!(found.allowed_class.as_int(), template.allowed_class); assert_eq!(found.allowed_race.as_int(), template.allowed_race); - assert_eq!(u32::from(found.required_skill.as_int()), template.required_skill); + assert_eq!( + u32::from(found.required_skill.as_int()), + template.required_skill + ); assert_eq!(found.required_skill_rank, template.required_skill_rank); assert_eq!( u32::from(found.required_faction.as_int()), @@ -7993,7 +7989,10 @@ fn trainer_buy_rank_upgrade_supersedes_the_previous_rank_spell() { m.new_spell_id, 1233, "first wire slot carries the OLD rank (cmangos order)" ); - assert_eq!(m.old_spell_id, 1234, "second wire slot carries the NEW rank"); + assert_eq!( + m.old_spell_id, 1234, + "second wire slot carries the NEW rank" + ); } other => panic!("expected SMSG_SUPERCEDED_SPELL, got {other}"), } @@ -8332,10 +8331,7 @@ fn taxi_status_query_returns_the_persisted_bit_without_opening() { } other => panic!("expected SMSG_TAXINODE_STATUS, got {other}"), } - assert_eq!( - *store.taxi_calls.lock().unwrap(), - vec![("status", 1, 90)] - ); + assert_eq!(*store.taxi_calls.lock().unwrap(), vec![("status", 1, 90)]); drop(client); server.join().unwrap(); } diff --git a/gateway/src/world/trade_tests.rs b/gateway/src/world/trade_tests.rs index 02ae067ab..c5367a9af 100644 --- a/gateway/src/world/trade_tests.rs +++ b/gateway/src/world/trade_tests.rs @@ -8,9 +8,9 @@ use super::*; use wow_world_messages::vanilla::{ - CMSG_ACCEPT_TRADE, CMSG_BEGIN_TRADE, CMSG_BUSY_TRADE, CMSG_CANCEL_TRADE, - CMSG_CLEAR_TRADE_ITEM, CMSG_IGNORE_TRADE, CMSG_INITIATE_TRADE, CMSG_SET_TRADE_GOLD, - CMSG_SET_TRADE_ITEM, CMSG_UNACCEPT_TRADE, + CMSG_ACCEPT_TRADE, CMSG_BEGIN_TRADE, CMSG_BUSY_TRADE, CMSG_CANCEL_TRADE, CMSG_CLEAR_TRADE_ITEM, + CMSG_IGNORE_TRADE, CMSG_INITIATE_TRADE, CMSG_SET_TRADE_GOLD, CMSG_SET_TRADE_ITEM, + CMSG_UNACCEPT_TRADE, }; /// **AC: initiating a trade with a targeted player reaches the store with the wire's target.** @@ -18,11 +18,9 @@ use wow_world_messages::vanilla::{ fn initiate_trade_dispatches_with_the_wire_target_guid() { let store = std::sync::Arc::new(quest_store()); let (mut client, mut c_enc, _c_dec, server) = enter_world(store.clone(), 1); - CMSG_INITIATE_TRADE { - guid: Guid::new(2), - } - .write_encrypted_client(&mut client, &mut c_enc) - .unwrap(); + CMSG_INITIATE_TRADE { guid: Guid::new(2) } + .write_encrypted_client(&mut client, &mut c_enc) + .unwrap(); drop(client); // every status rides the game_trade_event relay, no direct SMSG here server.join().unwrap(); assert_eq!(store.initiated_trades.lock().unwrap().as_slice(), &[(1, 2)]); @@ -34,11 +32,9 @@ fn initiate_trade_dispatches_with_the_wire_target_guid() { fn initiate_trade_dispatches_from_the_other_side_too() { let store = std::sync::Arc::new(quest_store()); let (mut client, mut c_enc, _c_dec, server) = enter_world(store.clone(), 2); - CMSG_INITIATE_TRADE { - guid: Guid::new(1), - } - .write_encrypted_client(&mut client, &mut c_enc) - .unwrap(); + CMSG_INITIATE_TRADE { guid: Guid::new(1) } + .write_encrypted_client(&mut client, &mut c_enc) + .unwrap(); drop(client); server.join().unwrap(); assert_eq!(store.initiated_trades.lock().unwrap().as_slice(), &[(2, 1)]); @@ -52,11 +48,9 @@ fn the_handshake_flow_dispatches_initiate_then_begin_then_cancel() { let store = std::sync::Arc::new(quest_store()); let (mut a, mut a_enc, _a_dec, a_server) = enter_world(store.clone(), 1); - CMSG_INITIATE_TRADE { - guid: Guid::new(2), - } - .write_encrypted_client(&mut a, &mut a_enc) - .unwrap(); + CMSG_INITIATE_TRADE { guid: Guid::new(2) } + .write_encrypted_client(&mut a, &mut a_enc) + .unwrap(); drop(a); a_server.join().unwrap(); @@ -108,9 +102,18 @@ fn offer_mutations_dispatch_with_wire_arguments() { .unwrap(); drop(client); server.join().unwrap(); - assert_eq!(store.set_trade_items.lock().unwrap().as_slice(), &[(1, 2, 23)]); - assert_eq!(store.cleared_trade_items.lock().unwrap().as_slice(), &[(1, 2)]); - assert_eq!(store.set_trade_golds.lock().unwrap().as_slice(), &[(1, 1_2345)]); + assert_eq!( + store.set_trade_items.lock().unwrap().as_slice(), + &[(1, 2, 23)] + ); + assert_eq!( + store.cleared_trade_items.lock().unwrap().as_slice(), + &[(1, 2)] + ); + assert_eq!( + store.set_trade_golds.lock().unwrap().as_slice(), + &[(1, 1_2345)] + ); } /// **AC (#121): sub-bag items are out of scope, not mis-addressed** — a `CMSG_SET_TRADE_ITEM` @@ -145,9 +148,13 @@ fn the_full_loop_dispatches_offer_and_dual_accept_in_order() { CMSG_INITIATE_TRADE { guid: Guid::new(2) } .write_encrypted_client(&mut a, &mut a_enc) .unwrap(); - CMSG_SET_TRADE_ITEM { trade_slot: 0, bag: 255, slot: 23 } - .write_encrypted_client(&mut a, &mut a_enc) - .unwrap(); + CMSG_SET_TRADE_ITEM { + trade_slot: 0, + bag: 255, + slot: 23, + } + .write_encrypted_client(&mut a, &mut a_enc) + .unwrap(); CMSG_ACCEPT_TRADE { unknown1: 0 } .write_encrypted_client(&mut a, &mut a_enc) .unwrap(); @@ -158,9 +165,11 @@ fn the_full_loop_dispatches_offer_and_dual_accept_in_order() { CMSG_BEGIN_TRADE {} .write_encrypted_client(&mut b, &mut b_enc) .unwrap(); - CMSG_SET_TRADE_GOLD { gold: wow_world_messages::vanilla::Gold::new(500) } - .write_encrypted_client(&mut b, &mut b_enc) - .unwrap(); + CMSG_SET_TRADE_GOLD { + gold: wow_world_messages::vanilla::Gold::new(500), + } + .write_encrypted_client(&mut b, &mut b_enc) + .unwrap(); CMSG_ACCEPT_TRADE { unknown1: 0 } .write_encrypted_client(&mut b, &mut b_enc) .unwrap(); @@ -168,8 +177,14 @@ fn the_full_loop_dispatches_offer_and_dual_accept_in_order() { b_server.join().unwrap(); assert_eq!(store.initiated_trades.lock().unwrap().as_slice(), &[(1, 2)]); - assert_eq!(store.set_trade_items.lock().unwrap().as_slice(), &[(1, 0, 23)]); - assert_eq!(store.set_trade_golds.lock().unwrap().as_slice(), &[(2, 500)]); + assert_eq!( + store.set_trade_items.lock().unwrap().as_slice(), + &[(1, 0, 23)] + ); + assert_eq!( + store.set_trade_golds.lock().unwrap().as_slice(), + &[(2, 500)] + ); assert_eq!(store.accepted_trades.lock().unwrap().as_slice(), &[1, 2]); } @@ -183,9 +198,11 @@ fn unaccept_and_post_accept_mutations_dispatch_for_the_acting_seat() { CMSG_ACCEPT_TRADE { unknown1: 0 } .write_encrypted_client(&mut client, &mut c_enc) .unwrap(); - CMSG_SET_TRADE_GOLD { gold: wow_world_messages::vanilla::Gold::new(9) } - .write_encrypted_client(&mut client, &mut c_enc) - .unwrap(); + CMSG_SET_TRADE_GOLD { + gold: wow_world_messages::vanilla::Gold::new(9), + } + .write_encrypted_client(&mut client, &mut c_enc) + .unwrap(); CMSG_UNACCEPT_TRADE {} .write_encrypted_client(&mut client, &mut c_enc) .unwrap(); diff --git a/gateway/src/world/whisper.rs b/gateway/src/world/whisper.rs index 8a58d2cb7..4a8b05217 100644 --- a/gateway/src/world/whisper.rs +++ b/gateway/src/world/whisper.rs @@ -85,7 +85,12 @@ pub(crate) fn run( message: String, ) -> Result<()> { let Some(realm) = store.realm_store() else { - return store.send_whisper(account_id, self_guid.unwrap_or(0), target_name.to_string(), message); + return store.send_whisper( + account_id, + self_guid.unwrap_or(0), + target_name.to_string(), + message, + ); }; let Some(sender_guid) = self_guid.filter(|&g| party::live_anywhere(store, g)) else { anyhow::bail!(lyracore_shared::whisper::NOT_IN_WORLD); diff --git a/gateway/tests/schema_parity.rs b/gateway/tests/schema_parity.rs index 1c8bef02b..373e89374 100644 --- a/gateway/tests/schema_parity.rs +++ b/gateway/tests/schema_parity.rs @@ -84,6 +84,9 @@ #[path = "../src/stdb/bindings/mod.rs"] #[allow(dead_code)] +// The SpacetimeDB codegen writes one function per table subscription; several run past any +// reasonable length ceiling and no edit there survives a regeneration. +#[allow(clippy::too_many_lines)] mod bindings; use spacetimedb_lib::db::raw_def::v9::RawModuleDefV9Builder; diff --git a/importer/src/dbc.rs b/importer/src/dbc.rs index 9097b0533..df74131e1 100644 --- a/importer/src/dbc.rs +++ b/importer/src/dbc.rs @@ -868,6 +868,7 @@ struct TaxiCatalogueSql { /// costs/indices must be non-negative, and the operator's data may not enter the reserved fixture /// namespace. A point for an absent path or a path without geometry is omitted with a stable warning. /// Any other malformed client extract fails before `run_sql_statements` can execute the first DELETE. +#[allow(clippy::too_many_lines)] // One validated statement per taxi table. fn taxi_catalogue_sql( nodes: &DbcTaxiNodes, paths: &DbcTaxiPath, diff --git a/importer/src/eventai.rs b/importer/src/eventai.rs index aab610b86..9c5c76bd0 100644 --- a/importer/src/eventai.rs +++ b/importer/src/eventai.rs @@ -1251,6 +1251,7 @@ struct RawRelayStep { } impl RelaySource { + #[allow(clippy::too_many_lines)] // One parse pass per dumped table. fn parse(dump: &str) -> Self { let mut source = Self::default(); source.broadcast_ids.extend( @@ -2164,6 +2165,7 @@ impl RawRelayStep { Ok(()) } + #[allow(clippy::too_many_lines)] // One arm per action kind that can name a dependency. fn resolved_dependencies( &self, source: &RelaySource, @@ -2625,6 +2627,7 @@ impl RawRelayStep { self.encode_instruction() } + #[allow(clippy::too_many_lines)] // One arm per action kind. fn encode_instruction(&self) -> Result { let bool_value = |value: u32| u8::from(value != 0); let forced = |value: i32| match value { @@ -3253,6 +3256,7 @@ fn source_overlap_census(dump: &str, rules: &[RawRule]) -> (u64, u64, u64) { } impl EventAiSource { + #[allow(clippy::too_many_lines)] // One assembly pass per EventAI rule family. pub(crate) fn assemble( &self, imported_entries: &HashSet, @@ -3394,12 +3398,14 @@ impl EventAiSource { rule.id, rule.subject, slot, - broadcasts, - summon_locations, - &self.quest_entries, - importable_templates, - &self.source_maps, - &self.relays, + ActionLookups { + broadcasts, + summon_locations, + quest_entries: &self.quest_entries, + importable_templates, + source_maps: &self.source_maps, + relays: &self.relays, + }, ) { Ok(action) => actions.push(action), Err(mut action_failures) => failures.append(&mut action_failures), @@ -4540,19 +4546,33 @@ fn repeats_on_each_event(event: u32) -> bool { ) } +/// The import-wide lookup tables one action mapping consults. +#[derive(Clone, Copy)] +struct ActionLookups<'a> { + broadcasts: &'a BTreeMap, + summon_locations: &'a BTreeMap, + quest_entries: &'a HashSet, + importable_templates: &'a HashSet, + source_maps: &'a HashMap>, + relays: &'a RelaySource, +} + fn map_action( action: [u32; 4], raw_subject: i32, rule_id: u64, subject: i32, slot: usize, - broadcasts: &BTreeMap, - summon_locations: &BTreeMap, - quest_entries: &HashSet, - importable_templates: &HashSet, - source_maps: &HashMap>, - relays: &RelaySource, + lookups: ActionLookups, ) -> Result> { + let ActionLookups { + broadcasts, + summon_locations, + quest_entries, + importable_templates, + source_maps, + relays, + } = lookups; let kind = action[0]; if let Some(mapped) = eventai_presentation::map_action(action, rule_id, subject, slot, importable_templates) @@ -6386,18 +6406,21 @@ mod tests { let templates = HashSet::new(); let maps = HashMap::new(); let relays = RelaySource::default(); + let lookups = ActionLookups { + broadcasts: &broadcasts, + summon_locations: &summons, + quest_entries: &quest_entries, + importable_templates: &templates, + source_maps: &maps, + relays: &relays, + }; let fail = map_action( [ACTION_SET_INSTANCE_DATA, 1, 2, 0], 4_832, 483_201, 4_832, 0, - &broadcasts, - &summons, - &quest_entries, - &templates, - &maps, - &relays, + lookups, ) .unwrap(); let complete = map_action( @@ -6406,12 +6429,7 @@ mod tests { 483_202, 4_832, 0, - &broadcasts, - &summons, - &quest_entries, - &templates, - &maps, - &relays, + lookups, ) .unwrap(); assert_eq!( @@ -6462,12 +6480,14 @@ mod tests { 10, 4_832, 1, - &BTreeMap::new(), - &BTreeMap::new(), - &HashSet::new(), - &HashSet::new(), - &maps, - &RelaySource::default(), + ActionLookups { + broadcasts: &BTreeMap::new(), + summon_locations: &BTreeMap::new(), + quest_entries: &HashSet::new(), + importable_templates: &HashSet::new(), + source_maps: &maps, + relays: &RelaySource::default(), + }, ); let Err(failures) = result else { panic!("cross-map encounter binding was accepted"); @@ -6970,12 +6990,14 @@ mod tests { 10, 100, slot, - &BTreeMap::new(), - &BTreeMap::new(), - &HashSet::new(), - &templates, - &HashMap::new(), - &relays, + ActionLookups { + broadcasts: &BTreeMap::new(), + summon_locations: &BTreeMap::new(), + quest_entries: &HashSet::new(), + importable_templates: &templates, + source_maps: &HashMap::new(), + relays: &relays, + }, ) .unwrap(); assert_eq!(mapped.encoded, expected); @@ -8180,6 +8202,7 @@ mod tests { #[test] #[ignore = "requires LYRACORE_CLASSIC_DB_SQL pointing at the pinned decompressed SQL or gzip"] + #[allow(clippy::too_many_lines)] // One assertion per censused source family. fn pinned_full_dump_source_census_matches_the_profile() { use std::io::Read; @@ -8398,12 +8421,14 @@ mod tests { rule.id, rule.subject, slot, - &source.broadcasts, - &source.summon_locations, - &source.quest_entries, - &HashSet::new(), - &source.source_maps, - &source.relays, + ActionLookups { + broadcasts: &source.broadcasts, + summon_locations: &source.summon_locations, + quest_entries: &source.quest_entries, + importable_templates: &HashSet::new(), + source_maps: &source.source_maps, + relays: &source.relays, + }, ) .unwrap() .encoded, diff --git a/importer/src/go_model.rs b/importer/src/go_model.rs index 622ffd136..47717e521 100644 --- a/importer/src/go_model.rs +++ b/importer/src/go_model.rs @@ -183,7 +183,10 @@ pub(crate) fn run(args: &crate::Args) -> Result<()> { ); if !args.apply { - println!("-- DRY RUN: would import {} game_go_model row(s)", rows.len()); + println!( + "-- DRY RUN: would import {} game_go_model row(s)", + rows.len() + ); return Ok(()); } if rows.is_empty() { @@ -214,7 +217,11 @@ pub(crate) fn run(args: &crate::Args) -> Result<()> { }; crate::call_reducer(args, reducer, batch)?; } - println!("go-models: applied {} row(s) across {} batch(es).", rows.len(), batches.len()); + println!( + "go-models: applied {} row(s) across {} batch(es).", + rows.len(), + batches.len() + ); Ok(()) } @@ -243,7 +250,11 @@ mod tests { (4,10,40,'Goober',0,0,0,1.0,0,0);"; let templates = door_templates(dump); let entries: Vec = templates.iter().map(|t| t.entry).collect(); - assert_eq!(entries, vec![1, 2], "only the DOOR (type 0) and BUTTON (type 1) rows survive"); + assert_eq!( + entries, + vec![1, 2], + "only the DOOR (type 0) and BUTTON (type 1) rows survive" + ); assert_eq!(templates[0].display_id, 10); assert_eq!(templates[1].display_id, 20); } @@ -252,7 +263,8 @@ mod tests { fn door_templates_drops_a_row_with_no_classification() { // Real cmangos type 25 (FISHINGHOLE) collides with the synthetic GATHER marker and is // always dropped by `classify_go_type` — must not appear as a DOOR/BUTTON either. - let dump = "INSERT INTO `gameobject_template` VALUES (5,25,50,'Fishing Hole',0,0,0,1.0,0,0);"; + let dump = + "INSERT INTO `gameobject_template` VALUES (5,25,50,'Fishing Hole',0,0,0,1.0,0,0);"; assert!(door_templates(dump).is_empty()); } @@ -292,7 +304,10 @@ mod tests { + (v[1] - center[1]).powi(2) + (v[2] - center[2]).powi(2)) .sqrt(); - assert!(d <= radius + 1e-4, "vertex {v:?} lies outside radius {radius}"); + assert!( + d <= radius + 1e-4, + "vertex {v:?} lies outside radius {radius}" + ); } } assert!(radius > 0.0); diff --git a/importer/src/main.rs b/importer/src/main.rs index 95da7b4a5..e5b25f569 100644 --- a/importer/src/main.rs +++ b/importer/src/main.rs @@ -1047,6 +1047,7 @@ fn parse_args() -> Result { parse_args_from(std::env::args().skip(1)) } +#[allow(clippy::too_many_lines)] // One arm per CLI flag. fn parse_args_from(args: I) -> Result where I: IntoIterator, @@ -2754,6 +2755,7 @@ fn vendor_stock_item_set( /// have templates. Auto-inc ids are counter-assigned over deterministic (sorted) orders so clear+reload /// is idempotent. Only KILL objectives (positive `ReqCreatureOrGOId`) + guaranteed reward items /// (`RewItemId`) are mapped; gameobject objectives, choice rewards, item/source objectives are later slices. +#[allow(clippy::too_many_lines)] // Column-by-column mapping of one imported dump family. fn build_quests( dump: &str, local_creatures: &std::collections::HashSet, @@ -3089,6 +3091,7 @@ fn build_quests( /// chance) are skipped until a quest system exists. Loot ids are counter-assigned over a sorted /// creature order so clear+reload stays idempotent. `extra_item_entries` (e.g. quest reward items) are /// force-included in the item-template load so every referenced item gets a template. +#[allow(clippy::too_many_lines)] // Column-by-column mapping of one imported dump family. fn build_items_and_loot( dump: &str, creature_loot_ids: &std::collections::HashMap, diff --git a/importer/src/vmap.rs b/importer/src/vmap.rs index 5530fec6b..f3a359c53 100644 --- a/importer/src/vmap.rs +++ b/importer/src/vmap.rs @@ -823,9 +823,7 @@ fn generation_info(args: &crate::Args, generation_id: u64) -> Result = all.iter().copied().collect(); - assert!(pending_cells(&all, &all_covered).is_empty(), "a fully resumed run submits nothing"); + assert!( + pending_cells(&all, &all_covered).is_empty(), + "a fully resumed run submits nothing" + ); } /// A fixture too large for one `BATCH_BYTES` call must split into several, and the split must @@ -1250,7 +1251,9 @@ mod tests { /// coverage identity (the reducer keys each cell independently). #[test] fn coverage_batches_split_a_fixture_larger_than_one_batch_and_reassemble_exactly() { - let cells: Vec<(u16, u16)> = (0..2000u32).map(|i| (i as u16, (i * 7 % 997) as u16)).collect(); + let cells: Vec<(u16, u16)> = (0..2000u32) + .map(|i| (i as u16, (i * 7 % 997) as u16)) + .collect(); let batches = coverage_batches(&cells); assert!(batches.len() > 1, "fixture must not fit in one batch"); for b in &batches { diff --git a/module/build.rs b/module/build.rs index aa722f7df..d05c83a12 100644 --- a/module/build.rs +++ b/module/build.rs @@ -282,6 +282,7 @@ const PACKAGE_API_ROOT_ITEMS: &[&str] = &[ /// carry a reason, so an exemption is always readable where it is used and greppable across a tree. const PACKAGE_API_EXEMPT: &str = "// package-api: exempt"; +#[allow(clippy::too_many_lines)] // One scan-and-emit step per generated registry. fn main() { let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR is set by cargo"); diff --git a/module/src/action_bar.rs b/module/src/action_bar.rs index 2f6b323f4..e95549db9 100644 --- a/module/src/action_bar.rs +++ b/module/src/action_bar.rs @@ -11,9 +11,7 @@ //! login, byte-identical to today. Import lands → creation copies the real rows → the gateway prefers //! them. [entity] -use spacetimedb::{ - table, Identity, ReducerContext, Table, -}; +use spacetimedb::{table, Identity, ReducerContext, Table}; use crate::spell::spellbook::createinfo_row_matches; // shared (race,class) wildcard-match helper diff --git a/module/src/auction.rs b/module/src/auction.rs index 5af0c1928..143603989 100644 --- a/module/src/auction.rs +++ b/module/src/auction.rs @@ -1850,34 +1850,6 @@ impl ExpirySink for CtxExpiry<'_> { } } -fn listing_request( - operation_id: u64, - seller_guid: u64, - item_guid: u64, - house: u32, - deposit_rate: u32, - consignment_rate: u32, - start_bid: u32, - buyout: u32, - duration_minutes: u32, -) -> ListingRequest { - ListingRequest { - operation_id, - seller_guid, - item_guid, - house: AuctionHousePolicy { - id: house, - deposit_rate, - consignment_rate, - }, - terms: ListingTerms { - start_bid, - buyout, - duration_minutes, - }, - } -} - /// Single-database listing: item, deposit, Auction, receipt, and expiry are one transaction. #[reducer] #[allow(clippy::too_many_arguments)] @@ -1920,17 +1892,17 @@ pub fn gw_auction_list_local( } create_local_listing( &mut CtxSource { ctx }, - listing_request( + ListingRequest { operation_id, seller_guid, item_guid, - house.id, - house.deposit_rate, - house.consignment_rate, - start_bid, - buyout, - duration_minutes, - ), + house, + terms: ListingTerms { + start_bid, + buyout, + duration_minutes, + }, + }, ) .map(|_| ()) .map_err(|refusal| refused(refusal, "listing rejected")) @@ -1986,17 +1958,17 @@ pub fn gw_auction_hold_listing( } fence_listing( &mut CtxSource { ctx }, - listing_request( + ListingRequest { operation_id, seller_guid, item_guid, - house.id, - house.deposit_rate, - house.consignment_rate, - start_bid, - buyout, - duration_minutes, - ), + house, + terms: ListingTerms { + start_bid, + buyout, + duration_minutes, + }, + }, ) .map_err(|refusal| refused(refusal, "listing Hold rejected")) } @@ -2026,17 +1998,21 @@ pub fn realm_auction_commit_listing( ) -> Result<(), String> { crate::helpers::require_operator(ctx)?; let listing = PreparedListing { - request: listing_request( + request: ListingRequest { operation_id, seller_guid, item_guid, - house, - deposit_rate, - consignment_rate, - start_bid, - buyout, - duration_minutes, - ), + house: AuctionHousePolicy { + id: house, + deposit_rate, + consignment_rate, + }, + terms: ListingTerms { + start_bid, + buyout, + duration_minutes, + }, + }, snapshot: crate::items::ItemSnapshot { entry: item_entry, stack_count: item_stack_count, @@ -3130,7 +3106,7 @@ mod tests { } fn commit_refund(&mut self, refund: ListingRefund) { - self.mails.push(refund.mail.clone()); + self.mails.push(refund.mail); self.refund = Some(refund); } } @@ -3473,7 +3449,10 @@ mod tests { "source cleanup may not treat a sentinel as receipt evidence" ); assert!(source.hold.is_some()); - assert_eq!(source.receipt.as_ref().map(|receipt| receipt.auction_id), Some(0)); + assert_eq!( + source.receipt.as_ref().map(|receipt| receipt.auction_id), + Some(0) + ); } #[test] diff --git a/module/src/breath.rs b/module/src/breath.rs index 97bd435f8..306b0475b 100644 --- a/module/src/breath.rs +++ b/module/src/breath.rs @@ -6,8 +6,8 @@ use spacetimedb::{reducer, table, ReducerContext, ScheduleAt, Table, Timestamp}; use crate::{game_world_entity, world::WorldEntity}; use lyracore_shared::env::{advance_breath, drowning_damage, FULL_AIR_MICROS}; -/// Vanilla's `Player::HandleDrowning` gives a player about one minute of air. Surfacing recovers -/// the same bar over ten seconds; the slower recovery prevents brief surface bobs from resetting it. +// Vanilla's `Player::HandleDrowning` gives a player about one minute of air. Surfacing recovers +// the same bar over ten seconds; the slower recovery prevents brief surface bobs from resetting it. /// One active breath timer. This is deliberately non-spatial: the scheduled tick must iterate all /// rows, and spatial iteration would silently omit players outside the caller's subscribed slice. diff --git a/module/src/breath_relay.rs b/module/src/breath_relay.rs index 5f6be3294..40b9ceb49 100644 --- a/module/src/breath_relay.rs +++ b/module/src/breath_relay.rs @@ -46,7 +46,14 @@ crate::character_owned!(delete, fn sweep_delete_game_breath_relay_event(ctx, cha // its next movement edge instead. crate::character_owned!(not_transported, fn sweep_transfer_game_breath_relay_event()); -fn insert(ctx: &ReducerContext, character_guid: u64, kind: u8, time_remaining_ms: u32, duration_ms: u32, damage: u32) { +fn insert( + ctx: &ReducerContext, + character_guid: u64, + kind: u8, + time_remaining_ms: u32, + duration_ms: u32, + damage: u32, +) { ctx.db.game_breath_relay_event().insert(BreathRelayEvent { id: 0, character_guid, @@ -59,8 +66,20 @@ fn insert(ctx: &ReducerContext, character_guid: u64, kind: u8, time_remaining_ms } /// Write the single packet-worthy edge when a player submerges. -pub(crate) fn start(ctx: &ReducerContext, character_guid: u64, time_remaining_ms: u32, duration_ms: u32) { - insert(ctx, character_guid, KIND_START, time_remaining_ms, duration_ms, 0); +pub(crate) fn start( + ctx: &ReducerContext, + character_guid: u64, + time_remaining_ms: u32, + duration_ms: u32, +) { + insert( + ctx, + character_guid, + KIND_START, + time_remaining_ms, + duration_ms, + 0, + ); } /// Write the single packet-worthy edge when a player surfaces. diff --git a/module/src/bridge.rs b/module/src/bridge.rs index a94bac24e..335e9b0c4 100644 --- a/module/src/bridge.rs +++ b/module/src/bridge.rs @@ -13,9 +13,7 @@ //! fn. The `game_client_command!` build.rs marker (the 143-substrate ideal from the 184 plan) can //! replace the match when a second package needs registration without touching this file. -use spacetimedb::{ - table, Identity, ReducerContext, Table, Timestamp, -}; +use spacetimedb::{table, Identity, ReducerContext, Table, Timestamp}; use crate::game_character; diff --git a/module/src/combat/folds.rs b/module/src/combat/folds.rs index abc84d859..4dcbc5278 100644 --- a/module/src/combat/folds.rs +++ b/module/src/combat/folds.rs @@ -390,7 +390,10 @@ pub(crate) fn equipped_offhand_weapon_damage( /// ranged slot (17) — a bow/gun/crossbow (Auto Shot) or a wand (Shoot) — or `None` when nothing usable /// is equipped there. The ranged swing tick uses `delay_ms` as its swing interval and rolls damage from /// `[damage_min, damage_max]`. [entity] -pub(crate) fn equipped_ranged_weapon(ctx: &ReducerContext, player_guid: u64) -> Option<(u32, u32, u32, u8)> { +pub(crate) fn equipped_ranged_weapon( + ctx: &ReducerContext, + player_guid: u64, +) -> Option<(u32, u32, u32, u8)> { weapon_profile_in_slot(ctx, player_guid, crate::items::equip_slot::RANGED) } @@ -718,7 +721,6 @@ pub(crate) fn roll_money(ctx: &ReducerContext, min: u32, max: u32) -> u32 { min + ctx.random::() % (max - min + 1) } - #[cfg(test)] mod seal_tests { use super::seal_swing_holy; diff --git a/module/src/creatures/cycle/harness.rs b/module/src/creatures/cycle/harness.rs index cbf41bc44..d2a80e2e2 100644 --- a/module/src/creatures/cycle/harness.rs +++ b/module/src/creatures/cycle/harness.rs @@ -5794,6 +5794,7 @@ fn discovery_stays_on_the_narrow_candidate_universes() { /// PLACE, which is what carries the swing clock across a threat switch, and only this assertion /// says so. Re-bless a deliberate change here with the same care. #[test] +#[allow(clippy::too_many_lines)] // One assertion per method the production adapter passes through. fn the_production_adapter_is_the_pass_through_the_harness_assumes() { let src = include_str!("ctx.rs"); for (signature, want) in [ diff --git a/module/src/creatures/cycle/harness/eventai_combat.rs b/module/src/creatures/cycle/harness/eventai_combat.rs index 654627ce5..87eab4fee 100644 --- a/module/src/creatures/cycle/harness/eventai_combat.rs +++ b/module/src/creatures/cycle/harness/eventai_combat.rs @@ -974,12 +974,12 @@ fn nearest_area_picks_the_closest_threat_holder_not_the_top_threat() { assert_eq!(scenario.casts(), vec![(CREATURE, 232, 0)]); assert_eq!( scenario.eventai_spell_starts.borrow().as_slice(), - &[(( + &[( SpellStartMode::Direct, SpellCastTarget::CasterArea, false, SpellCasterAdmission::Living, - ))] + )] ); } @@ -1007,12 +1007,12 @@ fn no_explicit_target_checks_the_caster_without_inventing_a_unit_target() { assert_eq!(ready.casts(), vec![(CREATURE, 521, 0)]); assert_eq!( ready.eventai_spell_starts.borrow().as_slice(), - &[(( + &[( SpellStartMode::Direct, SpellCastTarget::None, false, SpellCasterAdmission::Living, - ))] + )] ); } @@ -1050,12 +1050,12 @@ fn caster_area_requires_an_eligible_candidate_but_keeps_the_area_target() { assert_eq!(ready.casts(), vec![(CREATURE, 522, 0)]); assert_eq!( ready.eventai_spell_starts.borrow().as_slice(), - &[(( + &[( SpellStartMode::Direct, SpellCastTarget::CasterArea, false, SpellCasterAdmission::Living, - ))] + )] ); } @@ -1301,12 +1301,12 @@ fn triggered_cast_starts_during_a_pending_direct_cast() { assert_eq!(scenario.casts(), vec![(CREATURE, 300, TARGET)]); assert_eq!( scenario.eventai_spell_starts.borrow().as_slice(), - &[(( + &[( SpellStartMode::Triggered, SpellCastTarget::Unit(TARGET), false, SpellCasterAdmission::Living, - ))] + )] ); } diff --git a/module/src/creatures/eventai/combat.rs b/module/src/creatures/eventai/combat.rs index 41c51d9cb..538e0acd6 100644 --- a/module/src/creatures/eventai/combat.rs +++ b/module/src/creatures/eventai/combat.rs @@ -63,6 +63,7 @@ fn contexts_for_actors(world: &W, actors: Vec) -> V } /// Checks the rule's event condition and may name the actor selected by that condition. +#[allow(clippy::too_many_lines)] // One arm per EventAI event kind. pub(crate) fn condition( world: &W, context: &EventContext, @@ -520,10 +521,11 @@ pub(super) fn cast( return ActionResult::Refused; } } - if world.eventai_is_casting(caster.guid) { - if !cast.interrupt_previous && cast.start_mode == SpellStartMode::Direct { - return ActionResult::Refused; - } + if world.eventai_is_casting(caster.guid) + && !cast.interrupt_previous + && cast.start_mode == SpellStartMode::Direct + { + return ActionResult::Refused; } if world.eventai_start_spell( &caster, diff --git a/module/src/creatures/eventai/loader.rs b/module/src/creatures/eventai/loader.rs index 25d813505..fb46542d8 100644 --- a/module/src/creatures/eventai/loader.rs +++ b/module/src/creatures/eventai/loader.rs @@ -480,6 +480,7 @@ fn parse_recurrence(encoded: &str) -> Result { } } +#[allow(clippy::too_many_lines)] // One arm per instruction kind. fn parse_instruction(encoded: &str) -> Result { let fields: Vec<&str> = encoded.split(':').collect(); match fields.as_slice() { diff --git a/module/src/creatures/eventai/relay.rs b/module/src/creatures/eventai/relay.rs index af09db83c..095f674cc 100644 --- a/module/src/creatures/eventai/relay.rs +++ b/module/src/creatures/eventai/relay.rs @@ -1382,6 +1382,7 @@ fn apply_leaf( Ok(()) } +#[allow(clippy::too_many_lines)] // One arm per relay leaf kind. fn apply_leaf_for_pair( ctx: &ReducerContext, run: &mut RelayRun, diff --git a/module/src/creatures/hunter_pet.rs b/module/src/creatures/hunter_pet.rs index 85885c346..278b2f3cd 100644 --- a/module/src/creatures/hunter_pet.rs +++ b/module/src/creatures/hunter_pet.rs @@ -384,9 +384,12 @@ mod tests { assert_eq!(eligible().validate(), Ok(())); } + /// One gate broken in an otherwise valid tame. + type BreakGate = Box; + #[test] fn every_tame_gate_rejects_independently() { - let cases: Vec> = vec![ + let cases: Vec = vec![ Box::new(|v| v.caster_class = 9), Box::new(|v| v.caster_is_player = false), Box::new(|v| v.caster_owned = false), diff --git a/module/src/creatures/pet_care.rs b/module/src/creatures/pet_care.rs index 0881f0a5e..ef79d22f6 100644 --- a/module/src/creatures/pet_care.rs +++ b/module/src/creatures/pet_care.rs @@ -349,9 +349,12 @@ mod tests { } } + /// One gate broken in an otherwise valid feed. + type BreakGate = Box; + #[test] fn every_feed_gate_refuses_before_consumption() { - let cases: Vec> = vec![ + let cases: Vec = vec![ Box::new(|v| v.actor_is_player = false), Box::new(|v| v.item_owned = false), Box::new(|v| v.pet_exists = false), diff --git a/module/src/creatures/tick/mod.rs b/module/src/creatures/tick/mod.rs index 40f08c218..824396be0 100644 --- a/module/src/creatures/tick/mod.rs +++ b/module/src/creatures/tick/mod.rs @@ -343,9 +343,7 @@ pub(crate) fn active_cell_creatures( if e.owner_guid != 0 { pets.push(e.guid); } - if global - && e.unit_flags & lyracore_shared::constants::unit_flags::IN_COMBAT != 0 - { + if global && e.unit_flags & lyracore_shared::constants::unit_flags::IN_COMBAT != 0 { in_combat.push(e.guid); } } diff --git a/module/src/debug/instance.rs b/module/src/debug/instance.rs index c9ff5b5e5..390d15cc5 100644 --- a/module/src/debug/instance.rs +++ b/module/src/debug/instance.rs @@ -251,6 +251,7 @@ pub fn debug_set_nav_coverage_enabled(ctx: &ReducerContext, enabled: bool) -> Re /// hit/miss + the first-hit point for each flavor so a doodad-vs-wall distinction is directly /// observable: a doodad blocks `collision` but leaves `los` clear. #[reducer] +#[allow(clippy::too_many_arguments)] // A reducer's arguments are its call signature. pub fn debug_vmap_ray( ctx: &ReducerContext, map: u32, @@ -950,6 +951,7 @@ pub fn debug_assert_unreachable_goal_stops_at_wall( /// this reducer does no timing itself — it only produces the query volume and logs a checksum /// (`hits`) so two runs can be diffed for correctness, not just speed. #[reducer] +#[allow(clippy::too_many_arguments)] // A reducer's arguments are its call signature. pub fn debug_bench_los( ctx: &ReducerContext, map: u32, @@ -1017,6 +1019,7 @@ pub fn debug_bench_los( /// probes the ray actually truncated (checksum, not just speed, so two runs can be diffed for /// correctness too). #[reducer] +#[allow(clippy::too_many_arguments)] // A reducer's arguments are its call signature. pub fn debug_bench_collision_gate( ctx: &ReducerContext, map: u32, diff --git a/module/src/debug/repair.rs b/module/src/debug/repair.rs index 7907d7512..e08d1f054 100644 --- a/module/src/debug/repair.rs +++ b/module/src/debug/repair.rs @@ -38,6 +38,7 @@ use crate::{ /// provenance) with the total row count across every seeded/rearmed family, rather than the 13 /// separate per-family stamps the old reducers wrote. #[reducer] +#[allow(clippy::too_many_lines)] // One idempotent repair pass per row family. pub fn debug_repair_after_publish(ctx: &ReducerContext) -> Result<(), String> { crate::helpers::require_operator(ctx)?; diff --git a/module/src/encounter.rs b/module/src/encounter.rs index 08cfb2f70..6c042d20f 100644 --- a/module/src/encounter.rs +++ b/module/src/encounter.rs @@ -956,12 +956,14 @@ mod tests { assert!(matches!(package_handler_for(binding, &[]), Ok(None))); let claimed = [(binding, handler as EncounterPackageHandler)]; - assert!(matches!(package_handler_for(binding, &claimed), Ok(Some(_)))); + assert!(matches!( + package_handler_for(binding, &claimed), + Ok(Some(_)) + )); let duplicate = [(binding, handler as EncounterPackageHandler); 2]; - let duplicate = package_handler_for(binding, &duplicate) - .err() - .expect("duplicate authority must refuse"); + let duplicate = + package_handler_for(binding, &duplicate).expect_err("duplicate authority must refuse"); assert!(duplicate.contains("more than one installed package authority")); } diff --git a/module/src/gc.rs b/module/src/gc.rs index a413918ef..a80ee7a92 100644 --- a/module/src/gc.rs +++ b/module/src/gc.rs @@ -3,6 +3,7 @@ use spacetimedb::{reducer, table, ReducerContext, ScheduleAt, Table}; +use crate::breath_relay::game_breath_relay_event; use crate::{ game_addon_message, game_bot_invite_intent, game_channel_event, game_chat_event, game_combat_event, game_duel_event, game_emote_event, game_group_event, game_group_invite, @@ -10,7 +11,6 @@ use crate::{ game_spell_impact_event, game_system_message_event, game_teleport_event, game_trade_event, game_trade_session, game_whisper_event, game_xp_event, EVENT_TTL_MICROS, INVITE_TTL_MICROS, }; -use crate::breath_relay::game_breath_relay_event; // `rest` isn't re-exported at crate scope (`mod rest;`, no `pub use rest::*;` in lib.rs) — every // other event table's accessor trait rides that glob, so this is the one accessor here needing its // own import. @@ -125,7 +125,9 @@ pub fn reap_movement_events(ctx: &ReducerContext, _schedule: EventReaperSchedule let now = ctx.timestamp.to_micros_since_unix_epoch(); let stale: Vec = sessions .iter() - .filter(|s| crate::trade::session_is_stale(s.created_at.to_micros_since_unix_epoch(), now)) + .filter(|s| { + crate::trade::session_is_stale(s.created_at.to_micros_since_unix_epoch(), now) + }) .map(|s| s.initiator_guid) .collect(); for guid in stale { diff --git a/module/src/go_model.rs b/module/src/go_model.rs index e97895e60..df38b19a4 100644 --- a/module/src/go_model.rs +++ b/module/src/go_model.rs @@ -48,9 +48,15 @@ fn parse_row(row: &str) -> Result<(u32, f32, f32, Vec), String> { if f.len() != 4 { return Err(format!("go_model row needs 4 fields, got {}", f.len())); } - let entry = f[0].parse::().map_err(|_| format!("bad entry: {}", f[0]))?; - let scale = f[1].parse::().map_err(|_| format!("bad scale: {}", f[1]))?; - let radius = f[2].parse::().map_err(|_| format!("bad radius: {}", f[2]))?; + let entry = f[0] + .parse::() + .map_err(|_| format!("bad entry: {}", f[0]))?; + let scale = f[1] + .parse::() + .map_err(|_| format!("bad scale: {}", f[1]))?; + let radius = f[2] + .parse::() + .map_err(|_| format!("bad radius: {}", f[2]))?; let blob = hex_decode(f[3])?; lyracore_shared::vmap::decode(&blob) .map_err(|err| format!("invalid go_model blob: {err:?}"))?; @@ -171,7 +177,11 @@ mod tests { h.import(&batch).unwrap(); let after_second: Vec<_> = h.rows.keys().copied().collect(); assert_eq!(after_first, after_second); - assert_eq!(h.rows.len(), 2, "re-importing the same batch must not duplicate rows"); + assert_eq!( + h.rows.len(), + 2, + "re-importing the same batch must not duplicate rows" + ); } #[test] @@ -180,7 +190,10 @@ mod tests { h.import(&packed_row(1, 1.0, 2.0)).unwrap(); h.append(&packed_row(1, 1.0, 5.0)).unwrap(); // corrected re-extract, same entry assert_eq!(h.rows.len(), 1); - assert_eq!(h.rows[&1].1, 5.0, "the newer radius must win, not error or duplicate"); + assert_eq!( + h.rows[&1].1, 5.0, + "the newer radius must win, not error or duplicate" + ); } #[test] @@ -188,7 +201,11 @@ mod tests { let mut h = GoModelHarness::default(); h.import(&packed_row(1, 1.0, 2.0)).unwrap(); h.import(&packed_row(2, 1.0, 3.0)).unwrap(); - assert_eq!(h.rows.len(), 1, "import (not append) must clear the prior batch"); + assert_eq!( + h.rows.len(), + 1, + "import (not append) must clear the prior batch" + ); assert!(h.rows.contains_key(&2)); assert!(!h.rows.contains_key(&1)); } diff --git a/module/src/group.rs b/module/src/group.rs index 675765dd8..4e3a01a10 100644 --- a/module/src/group.rs +++ b/module/src/group.rs @@ -489,10 +489,7 @@ fn checked_group_membership( /// all; [`GroupRefusal::NotLeader`] if it does but isn't the leader. `invite_core_on` treats /// `NotInGroup` as an ALLOWED case rather than propagating it — starting a brand-new group (where /// the inviter becomes leader) is fine; see its call site. -fn led_group_of( - ctx: &ReducerContext, - guid: u64, -) -> Result<(GroupMember, Group), GroupOpError> { +fn led_group_of(ctx: &ReducerContext, guid: u64) -> Result<(GroupMember, Group), GroupOpError> { let (m, group) = checked_group_membership(ctx, guid)?.ok_or(GroupRefusal::NotInGroup)?; if group.leader_guid != guid { return Err(GroupRefusal::NotLeader.into()); @@ -546,13 +543,12 @@ pub(crate) fn invite_core( inviter_guid: u64, target_guid: u64, ) -> Result<(), String> { - invite_core_on(ctx, Plane::Shard, inviter_guid, target_guid) - .map_err(|error| { - group_op_error( - error, - &format!("{inviter_guid} could not invite {target_guid}"), - ) - }) + invite_core_on(ctx, Plane::Shard, inviter_guid, target_guid).map_err(|error| { + group_op_error( + error, + &format!("{inviter_guid} could not invite {target_guid}"), + ) + }) } fn invite_core_on( @@ -646,13 +642,12 @@ fn invite_core_on( /// The identity-free accept core: shared by `gw::gw_accept_group_invite` and any server-driven /// acceptor (a playerbot's auto-accept hook calls this with the bot's guid). pub(crate) fn accept_invite_for(ctx: &ReducerContext, acceptor_guid: u64) -> Result<(), String> { - accept_invite_on(ctx, Plane::Shard, acceptor_guid) - .map_err(|error| { - group_op_error( - error, - &format!("{acceptor_guid} could not accept its invite"), - ) - }) + accept_invite_on(ctx, Plane::Shard, acceptor_guid).map_err(|error| { + group_op_error( + error, + &format!("{acceptor_guid} could not accept its invite"), + ) + }) } fn accept_invite_on( @@ -775,12 +770,8 @@ fn decline_invite_on(ctx: &ReducerContext, decliner_guid: u64) -> Result<(), Gro /// The identity-free leave core — the body `group_leave` used to inline. pub(crate) fn leave_group_for(ctx: &ReducerContext, leaver_guid: u64) -> Result<(), String> { - leave_group_on(ctx, leaver_guid).map_err(|error| { - group_op_error( - error, - &format!("{leaver_guid} could not leave its party"), - ) - }) + leave_group_on(ctx, leaver_guid) + .map_err(|error| group_op_error(error, &format!("{leaver_guid} could not leave its party"))) } fn leave_group_on(ctx: &ReducerContext, leaver_guid: u64) -> Result<(), GroupOpError> { @@ -811,8 +802,8 @@ fn uninvite_on( target_guid: u64, ) -> Result<(), GroupOpError> { let (m, _group) = led_group_of(ctx, leader_guid)?; - let (target, _target_group) = checked_group_membership(ctx, target_guid)? - .ok_or(GroupRefusal::TargetNotInGroup)?; + let (target, _target_group) = + checked_group_membership(ctx, target_guid)?.ok_or(GroupRefusal::TargetNotInGroup)?; if target.group_id != m.group_id { return Err(GroupRefusal::TargetNotInGroup.into()); } @@ -853,8 +844,8 @@ fn set_loot_method_on( return Err(GroupRefusal::InvalidLootRules.into()); } let resolved_master = if loot_setting == loot_method::MASTER { - let (target, _target_group) = checked_group_membership(ctx, master_guid)? - .ok_or(GroupRefusal::TargetNotInGroup)?; + let (target, _target_group) = + checked_group_membership(ctx, master_guid)?.ok_or(GroupRefusal::TargetNotInGroup)?; if target.group_id != m.group_id { return Err(GroupRefusal::TargetNotInGroup.into()); } @@ -1037,9 +1028,7 @@ pub fn realm_group_op( realm_op::LOOT_METHOD => set_loot_method_on(ctx, actor_guid, arg_a, target_guid, arg_b), other => return Err(format!("unknown realm group op {other}")), }; - ran.map_err(|error| { - group_op_error(error, &format!("realm group op {op} for {actor_guid}")) - }) + ran.map_err(|error| group_op_error(error, &format!("realm group op {op} for {actor_guid}"))) } /// Replace this database's MIRROR of one party with realm-core's authoritative roster. diff --git a/module/src/gw.rs b/module/src/gw.rs index 7af1414de..8bb79da0a 100644 --- a/module/src/gw.rs +++ b/module/src/gw.rs @@ -169,8 +169,8 @@ pub fn reap_gateway_leases( /// `is_desync_error` on the gateway classifies on "not in world", and the two entries must be /// indistinguishable to that classifier. fn actor(ctx: &ReducerContext, actor_guid: u64) -> Result { - let actor = acting_entity_by_guid(ctx, actor_guid) - .ok_or_else(|| "mover not in world".to_string())?; + let actor = + acting_entity_by_guid(ctx, actor_guid).ok_or_else(|| "mover not in world".to_string())?; crate::taxi::reject_action_while_in_flight(ctx, actor_guid)?; Ok(actor) } @@ -421,8 +421,8 @@ pub fn gw_turn_in_quest( /// Taxi flight is an expected item Refusal. Missing actors still carry the generic actor error, /// which the Gateway treats as a failure with an unknown result. fn item_actor(ctx: &ReducerContext, actor_guid: u64) -> Result { - let actor = acting_entity_by_guid(ctx, actor_guid) - .ok_or_else(|| "mover not in world".to_string())?; + let actor = + acting_entity_by_guid(ctx, actor_guid).ok_or_else(|| "mover not in world".to_string())?; if crate::taxi::is_in_flight(ctx, actor_guid) { return Err(crate::items::refused(crate::items::refuse( lyracore_shared::item::ItemRefusal::NotRightNow, @@ -456,7 +456,11 @@ pub fn gw_cast_item_target( /// [`crate::actor::loot_money`] behind the gateway gate. #[reducer] -pub fn gw_loot_money(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> Result<(), String> { +pub fn gw_loot_money( + ctx: &ReducerContext, + actor_guid: u64, + target_guid: u64, +) -> Result<(), String> { require_operator(ctx).map_err(loot_operator_error)?; loot_actor(ctx, actor_guid)?; crate::actor::loot_money(ctx, actor_guid, target_guid) @@ -512,8 +516,8 @@ pub fn gw_use_gameobject( /// Taxi flight is an expected trainer Refusal. Missing actors still carry the generic actor error, /// which the Gateway treats as a failure with an unknown result. fn trainer_actor(ctx: &ReducerContext, actor_guid: u64) -> Result { - let actor = acting_entity_by_guid(ctx, actor_guid) - .ok_or_else(|| "mover not in world".to_string())?; + let actor = + acting_entity_by_guid(ctx, actor_guid).ok_or_else(|| "mover not in world".to_string())?; if crate::taxi::is_in_flight(ctx, actor_guid) { return Err(crate::trainer::refused( lyracore_shared::trainer::TrainerRefusal::Unavailable, @@ -676,7 +680,10 @@ pub fn gw_leave_world(ctx: &ReducerContext, actor_guid: u64) -> Result<(), Strin if let Some(entity) = ctx.db.game_world_entity().guid().find(actor_guid) { crate::world::remove_from_world(ctx, entity.owner_identity); } - ctx.db.game_gateway_session().entity_guid().delete(actor_guid); + ctx.db + .game_gateway_session() + .entity_guid() + .delete(actor_guid); Ok(()) } @@ -758,8 +765,8 @@ pub fn gw_send_channel_message( /// Taxi flight is an expected contact Refusal. Missing actors still carry the generic actor error, /// which the Gateway treats as a failure with an unknown result. fn contact_actor(ctx: &ReducerContext, actor_guid: u64) -> Result { - let actor = acting_entity_by_guid(ctx, actor_guid) - .ok_or_else(|| "mover not in world".to_string())?; + let actor = + acting_entity_by_guid(ctx, actor_guid).ok_or_else(|| "mover not in world".to_string())?; if crate::taxi::is_in_flight(ctx, actor_guid) { return Err(crate::chat::refused_contact( lyracore_shared::social::ContactRefusal::ActorUnavailable, @@ -771,7 +778,11 @@ fn contact_actor(ctx: &ReducerContext, actor_guid: u64) -> Result Result<(), String> { +pub fn gw_add_friend( + ctx: &ReducerContext, + actor_guid: u64, + target_guid: u64, +) -> Result<(), String> { require_operator(ctx)?; let sender = contact_actor(ctx, actor_guid)?; crate::chat::add_contact(ctx, sender, target_guid, false) @@ -779,7 +790,11 @@ pub fn gw_add_friend(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> /// [`crate::chat::remove_contact`] (friend arm) with the owner named by guid. #[reducer] -pub fn gw_del_friend(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> Result<(), String> { +pub fn gw_del_friend( + ctx: &ReducerContext, + actor_guid: u64, + target_guid: u64, +) -> Result<(), String> { require_operator(ctx)?; let sender = contact_actor(ctx, actor_guid)?; crate::chat::remove_contact(ctx, sender, target_guid, false) @@ -787,7 +802,11 @@ pub fn gw_del_friend(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> /// [`crate::chat::add_contact`] (ignore arm) with the owner named by guid. #[reducer] -pub fn gw_add_ignore(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> Result<(), String> { +pub fn gw_add_ignore( + ctx: &ReducerContext, + actor_guid: u64, + target_guid: u64, +) -> Result<(), String> { require_operator(ctx)?; let sender = contact_actor(ctx, actor_guid)?; crate::chat::add_contact(ctx, sender, target_guid, true) @@ -795,7 +814,11 @@ pub fn gw_add_ignore(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> /// [`crate::chat::remove_contact`] (ignore arm) with the owner named by guid. #[reducer] -pub fn gw_del_ignore(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> Result<(), String> { +pub fn gw_del_ignore( + ctx: &ReducerContext, + actor_guid: u64, + target_guid: u64, +) -> Result<(), String> { require_operator(ctx)?; let sender = contact_actor(ctx, actor_guid)?; crate::chat::remove_contact(ctx, sender, target_guid, true) @@ -808,8 +831,8 @@ pub fn gw_del_ignore(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> /// Taxi flight is an expected group Refusal. Missing actors still carry the generic actor error, /// which the Gateway treats as a failure with an unknown result. fn group_actor(ctx: &ReducerContext, actor_guid: u64) -> Result { - let actor = acting_entity_by_guid(ctx, actor_guid) - .ok_or_else(|| "mover not in world".to_string())?; + let actor = + acting_entity_by_guid(ctx, actor_guid).ok_or_else(|| "mover not in world".to_string())?; if crate::taxi::is_in_flight(ctx, actor_guid) { return Err(crate::group::refused( lyracore_shared::group::GroupRefusal::ActorUnavailable, @@ -985,13 +1008,7 @@ pub fn gw_group_loot_method( ) -> Result<(), String> { require_operator(ctx)?; group_actor(ctx, actor_guid)?; - crate::group::set_loot_method_for( - ctx, - actor_guid, - loot_setting, - master_guid, - loot_threshold, - ) + crate::group::set_loot_method_for(ctx, actor_guid, loot_setting, master_guid, loot_threshold) } /// [`crate::quest::apply_push_quest_to_party`] with the sharer named by guid. @@ -1229,7 +1246,11 @@ pub fn gw_inspect(ctx: &ReducerContext, actor_guid: u64, target_guid: u64) -> Re /// [`crate::talent::do_learn_talent`] with the learner named by guid. The owner identity stamped on /// the learned rows is the ACTOR's binding, never `ctx.sender()` (the shared connection's operator). #[reducer] -pub fn gw_learn_talent(ctx: &ReducerContext, actor_guid: u64, talent_id: u32) -> Result<(), String> { +pub fn gw_learn_talent( + ctx: &ReducerContext, + actor_guid: u64, + talent_id: u32, +) -> Result<(), String> { require_operator(ctx)?; let learner = actor(ctx, actor_guid)?; crate::talent::do_learn_talent(ctx, actor_guid, learner.owner_identity, talent_id).map(|_| ()) @@ -1335,10 +1356,6 @@ pub fn gw_pet_command( crate::creatures::apply_pet_command(ctx, actor_guid, data, target_guid) } - - - - /// [`crate::bridge::apply_client_command`] with the sender named by guid (operator call: /// the GM/bridge surface rides the gateway path like every other verb; authorization was never /// the connection identity). @@ -1432,7 +1449,7 @@ mod tests { assert!(!lease_expired(1_000_000, 1_000_000, LEASE_TTL_MICROS)); assert!(!lease_expired(LEASE_TTL_MICROS, 0, LEASE_TTL_MICROS)); assert!(lease_expired(LEASE_TTL_MICROS + 1, 0, LEASE_TTL_MICROS)); - assert!( + const _: () = assert!( LEASE_TTL_MICROS >= 3 * LEASE_REAP_MICROS, "TTL must tolerate at least two missed heartbeats plus a reap interval" ); diff --git a/module/src/helpers.rs b/module/src/helpers.rs index 480a407cc..c0d57c80e 100644 --- a/module/src/helpers.rs +++ b/module/src/helpers.rs @@ -56,7 +56,9 @@ pub fn entity_by_owner(ctx: &ReducerContext, owner: Identity) -> Option Option { - gate_by_guid(ctx, ctx.db.game_world_entity().guid().find(guid), |e| e.guid) + gate_by_guid(ctx, ctx.db.game_world_entity().guid().find(guid), |e| { + e.guid + }) } /// THE by-guid chokepoint — `entity_by_owner`'s twin for every reducer that reaches a diff --git a/module/src/items/economy.rs b/module/src/items/economy.rs index 18736dec9..669493b1f 100644 --- a/module/src/items/economy.rs +++ b/module/src/items/economy.rs @@ -77,7 +77,13 @@ fn npc_interaction_gate( /// must be alive with a live BANKER npc in reach. pub(crate) fn bank_access_gate(ctx: &ReducerContext, player_guid: u64) -> Result<(), String> { use lyracore_shared::constants::npc_flags; - flagged_npc_in_reach_gate(ctx, player_guid, npc_flags::BANKER, "banker", "use the bank") + flagged_npc_in_reach_gate( + ctx, + player_guid, + npc_flags::BANKER, + "banker", + "use the bank", + ) } /// The hearth-bind trust gate: binding a home needs a live INNKEEPER npc in reach. diff --git a/module/src/lib.rs b/module/src/lib.rs index 3e7550bda..8fcbcbbac 100644 --- a/module/src/lib.rs +++ b/module/src/lib.rs @@ -249,9 +249,9 @@ mod action_bar; mod actor; mod auction; mod auth; +mod breath; mod breath_relay; mod bridge; -mod breath; mod character; mod chat; mod combat; @@ -269,8 +269,8 @@ mod exploration; mod debug; // Encounter kernel (work-item 228): instance-scoped encounter state, HP-threshold/death/GO-use // hooks, and the shared choreography primitives Packages consume (`packages/dungeons/` owns it). -mod encounter; mod duel; +mod encounter; mod faction; mod gameobject; mod gc; @@ -382,15 +382,13 @@ pub use debug::*; pub use duel::*; pub use encounter::*; pub use exploration::CharacterExplored; // re-exported for the gateway schema-parity test (282) -pub use rest::RestStateEvent; // re-exported for the gateway schema-parity test (4c) -pub use spell::stacking::SpellGroupRule; // Keeps this generated table in schema-parity coverage. pub use faction::*; pub use gameobject::*; -pub use gw::*; pub use gc::*; pub use gm::*; pub use graveyard::*; pub use group::*; +pub use gw::*; pub use instance::*; pub use items::*; pub use load::*; @@ -402,8 +400,10 @@ pub use quest::*; pub use realm_core::*; pub use region::*; pub use reputation::*; +pub use rest::RestStateEvent; // re-exported for the gateway schema-parity test (4c) pub use skill::*; pub use skilldata::*; +pub use spell::stacking::SpellGroupRule; // Keeps this generated table in schema-parity coverage. pub use spell::*; pub use stats::*; pub use talent::*; diff --git a/module/src/load.rs b/module/src/load.rs index 6f53a1059..e72d933f4 100644 --- a/module/src/load.rs +++ b/module/src/load.rs @@ -133,7 +133,10 @@ pub(crate) fn realm_wide_sessions(samples: impl Iterator }) .or_insert((id, sessions)); } - latest_per_gateway.values().map(|(_, sessions)| *sessions).sum() + latest_per_gateway + .values() + .map(|(_, sessions)| *sessions) + .sum() } /// The occupancy sanity gate, decided purely so both reducer and test share the exact rule. See diff --git a/module/src/loot/tag.rs b/module/src/loot/tag.rs index d5090cd97..fda5f7a58 100644 --- a/module/src/loot/tag.rs +++ b/module/src/loot/tag.rs @@ -340,6 +340,7 @@ const LOOT_TAG_FIXTURE_CHARACTER_F: u64 = 5_093_856; /// Module rows in one isolated standalone transaction. #[cfg(feature = "debug_reducers")] #[spacetimedb::reducer] +#[allow(clippy::too_many_lines)] // One durable step per staged fixture row. pub fn debug_verify_loot_tag_fixture(ctx: &ReducerContext) -> Result<(), String> { let origin = ctx .db @@ -373,7 +374,14 @@ pub fn debug_verify_loot_tag_fixture(ctx: &ReducerContext) -> Result<(), String> crate::combat::disengage(ctx, aggression); let pet = fixture_creature_guid(3); - insert_fixture_entity(ctx, &origin, pet, base_x, false, LOOT_TAG_FIXTURE_CHARACTER_B); + insert_fixture_entity( + ctx, + &origin, + pet, + base_x, + false, + LOOT_TAG_FIXTURE_CHARACTER_B, + ); let pet_target = fixture_creature_guid(4); insert_fixture_entity(ctx, &origin, pet_target, base_x, false, 0); crate::threat::add_threat(ctx, pet_target, pet, 10); @@ -496,7 +504,11 @@ pub fn debug_verify_loot_tag_fixture(ctx: &ReducerContext) -> Result<(), String> leash_x: 0.0, leash_y: 0.0, }); - crate::combat::kill_player(ctx, LOOT_TAG_FIXTURE_CHARACTER_F, LOOT_TAG_FIXTURE_CHARACTER_B); + crate::combat::kill_player( + ctx, + LOOT_TAG_FIXTURE_CHARACTER_F, + LOOT_TAG_FIXTURE_CHARACTER_B, + ); expect_tagger(ctx, retained, LOOT_TAG_FIXTURE_CHARACTER_F)?; crate::combat::disengage(ctx, retained); if tagger(ctx, retained).is_some() diff --git a/module/src/mail_escrow/harness.rs b/module/src/mail_escrow/harness.rs index 03a7c35f6..de0fb2c3d 100644 --- a/module/src/mail_escrow/harness.rs +++ b/module/src/mail_escrow/harness.rs @@ -141,7 +141,7 @@ impl FakeShard { .borrow() .values() .filter(|(o, _)| *o == owner) - .map(|(_, i)| i.clone()) + .map(|(_, i)| *i) .collect(); held.sort_by_key(|i| i.entry); held @@ -243,9 +243,7 @@ impl PayoutSink for FakeShard { } let guid = self.next_item_guid.get() + 1; self.next_item_guid.set(guid); - self.items - .borrow_mut() - .insert(guid, (payee_guid, item.clone())); + self.items.borrow_mut().insert(guid, (payee_guid, *item)); Ok(()) } fn file_receipt(&mut self, row: MailDelivery) { @@ -363,7 +361,7 @@ impl TakeFenceSink for FakeMailPlane { .borrow() .iter() .find(|m| m.id == mail_id) - .map(|m| (m.recipient_guid, m.item.clone())) + .map(|m| (m.recipient_guid, m.item)) } fn clear_mail_item(&mut self, mail_id: u64) { if let Some(m) = self.mails.borrow_mut().iter_mut().find(|m| m.id == mail_id) { @@ -395,7 +393,7 @@ impl DeliverySink for FakeMailPlane { body: letter.body.clone(), money: letter.money, cod: letter.cod, - item: item.clone(), + item: *item, }); id } diff --git a/module/src/motion.rs b/module/src/motion.rs index cdb74a78c..10d99ee0f 100644 --- a/module/src/motion.rs +++ b/module/src/motion.rs @@ -675,7 +675,10 @@ mod tests { body.contains("ctx.sender() != ctx.database_identity()"), "the tick must stay scheduler-only. Body was:\n{body}" ); - let drain = crate::test_scan::code_of(include_str!("motion.rs"), "pub(crate) fn publish_staged(ctx: &ReducerContext) {"); + let drain = crate::test_scan::code_of( + include_str!("motion.rs"), + "pub(crate) fn publish_staged(ctx: &ReducerContext) {", + ); assert!( drain.contains("tick_action(entities.guid().find(row.guid).is_some())"), "every queued row must go through `tick_action` with a REAL liveness probe — a \ diff --git a/module/src/nav.rs b/module/src/nav.rs index c07d0bfa4..1b1791178 100644 --- a/module/src/nav.rs +++ b/module/src/nav.rs @@ -281,8 +281,12 @@ pub fn nav_step( let stepped = if !nav_enabled(ctx) { crate::creatures::chase_step(cur.0, cur.1, dest.0, dest.1, max_step, stop_dist) } else { - match lyracore_shared::nav::find_leg(&mut fetcher(ctx, map_id), cur, dest, LEG_MAX_EXPANSIONS) - { + match lyracore_shared::nav::find_leg( + &mut fetcher(ctx, map_id), + cur, + dest, + LEG_MAX_EXPANSIONS, + ) { // Real detour: head for the first corner, no stop-short (that's for the final approach). Some(path) if path.len() > 1 || path[0] != dest => { let wp = path[0]; @@ -293,9 +297,7 @@ pub fn nav_step( crate::creatures::chase_step(cur.0, cur.1, dest.0, dest.1, max_step, stop_dist) } // Keep aiming at an unreachable goal; the commit gate truncates the move at geometry. - None => { - crate::creatures::chase_step(cur.0, cur.1, dest.0, dest.1, max_step, stop_dist) - } + None => crate::creatures::chase_step(cur.0, cur.1, dest.0, dest.1, max_step, stop_dist), } }; step_gate(ctx, map_id, cur, stepped, z) @@ -338,7 +340,10 @@ fn step_gate( if land_dist <= 0.0 { cur // hit inside the clearance margin — hold in place } else { - (cur.0 + dx / hit_dist * land_dist, cur.1 + dy / hit_dist * land_dist) + ( + cur.0 + dx / hit_dist * land_dist, + cur.1 + dy / hit_dist * land_dist, + ) } } None => stepped, // clear (or no data under this segment) — the plain step stands diff --git a/module/src/package_config.rs b/module/src/package_config.rs index 85ce800ad..678e9e897 100644 --- a/module/src/package_config.rs +++ b/module/src/package_config.rs @@ -143,6 +143,7 @@ pub(crate) fn should_seed_default(row_exists: bool) -> bool { /// when no row exists yet — see [`should_seed_default`] for the decision this carries out. `pub(crate)` /// — a Package's own defaults are seeded from inside the module build, not through a reducer an /// external caller could hit. +#[cfg_attr(not(has_packages), allow(dead_code))] // package-only caller — see `build.rs` pub(crate) fn ensure_package_config_default( ctx: &ReducerContext, package: &str, diff --git a/module/src/professions.rs b/module/src/professions.rs index d3ca1a9ea..734b37c20 100644 --- a/module/src/professions.rs +++ b/module/src/professions.rs @@ -12,7 +12,7 @@ //! `apply_enchant_item`/`entry_is_beast` call sites (debug.rs's twins) keep compiling unchanged — //! `loot.rs` re-exports this module's public surface. -use spacetimedb::{ReducerContext}; +use spacetimedb::ReducerContext; use crate::game_creature_template; use crate::game_fishing_loot; diff --git a/module/src/quest.rs b/module/src/quest.rs index cb9342403..adb652559 100644 --- a/module/src/quest.rs +++ b/module/src/quest.rs @@ -1118,15 +1118,15 @@ const CHOICE_FIXTURE_GIVER_SOURCE: u64 = (0xF130_u64 << 48) | ((620_u64) << 24) #[cfg(feature = "debug_reducers")] const CHOICE_FIXTURE_QUEST: u32 = 509_033; #[cfg(feature = "debug_reducers")] -const CHOICE_FIXTURE_OBJECTIVE_ITEM: u32 = 509_033_0; +const CHOICE_FIXTURE_OBJECTIVE_ITEM: u32 = 5_090_330; #[cfg(feature = "debug_reducers")] -const CHOICE_FIXTURE_CHOICE_0: u32 = 509_033_1; +const CHOICE_FIXTURE_CHOICE_0: u32 = 5_090_331; #[cfg(feature = "debug_reducers")] -const CHOICE_FIXTURE_CHOICE_1: u32 = 509_033_2; +const CHOICE_FIXTURE_CHOICE_1: u32 = 5_090_332; #[cfg(feature = "debug_reducers")] -const CHOICE_FIXTURE_GUARANTEED: u32 = 509_033_3; +const CHOICE_FIXTURE_GUARANTEED: u32 = 5_090_333; #[cfg(feature = "debug_reducers")] -const CHOICE_FIXTURE_FILLER: u32 = 509_033_4; +const CHOICE_FIXTURE_FILLER: u32 = 5_090_334; /// Stage a quest-33-shaped durable turn-in fixture for the standalone reducer test. #[cfg(feature = "debug_reducers")] @@ -1212,9 +1212,9 @@ pub fn debug_stage_choice_reward_fixture( }); let objectives = ctx.db.game_quest_objective(); - objectives.id().delete(509_033_0); + objectives.id().delete(5_090_330); objectives.insert(QuestObjective { - id: 509_033_0, + id: 5_090_330, quest_entry: CHOICE_FIXTURE_QUEST, obj_index: 0, kind: objective_kind::COLLECT_ITEM, @@ -1222,34 +1222,34 @@ pub fn debug_stage_choice_reward_fixture( required_count: 8, }); let guaranteed = ctx.db.game_quest_reward_item(); - guaranteed.id().delete(509_033_3); + guaranteed.id().delete(5_090_333); guaranteed.insert(QuestRewardItem { - id: 509_033_3, + id: 5_090_333, quest_entry: CHOICE_FIXTURE_QUEST, item_entry: CHOICE_FIXTURE_GUARANTEED, count: 3, }); let choices = ctx.db.game_quest_reward_choice(); - choices.id().delete(509_033_1); - choices.id().delete(509_033_2); + choices.id().delete(5_090_331); + choices.id().delete(5_090_332); choices.insert(QuestRewardChoice { - id: 509_033_1, + id: 5_090_331, quest_entry: CHOICE_FIXTURE_QUEST, choice_index: 0, item_entry: CHOICE_FIXTURE_CHOICE_0, count: 1, }); choices.insert(QuestRewardChoice { - id: 509_033_2, + id: 5_090_332, quest_entry: CHOICE_FIXTURE_QUEST, choice_index: 1, item_entry: CHOICE_FIXTURE_CHOICE_1, count: 2, }); let relations = ctx.db.game_creature_quest(); - relations.id().delete(509_033_4); + relations.id().delete(5_090_334); relations.insert(CreatureQuest { - id: 509_033_4, + id: 5_090_334, creature_entry: CHOICE_FIXTURE_GIVER_ENTRY, quest_entry: CHOICE_FIXTURE_QUEST, role: quest_role::END, @@ -1479,6 +1479,7 @@ fn eventai_credit_fixture_count( /// Exercise EventAI quest authority against durable live rows in one standalone transaction. #[cfg(feature = "debug_reducers")] #[spacetimedb::reducer] +#[allow(clippy::too_many_lines)] // One durable step per staged fixture row. pub fn debug_verify_eventai_quest_credit_fixture(ctx: &ReducerContext) -> Result<(), String> { crate::helpers::require_operator(ctx)?; diff --git a/module/src/seed.rs b/module/src/seed.rs index da25244df..cfb73b06f 100644 --- a/module/src/seed.rs +++ b/module/src/seed.rs @@ -243,6 +243,7 @@ fn seed_production_core(ctx: &ReducerContext) { /// `importer --apply` run lands for map 0 (or fenced off entirely for any other continent). This is /// a DIFFERENT fixture family from `seed/fixtures.rs`'s synthetic engine-mechanic fixtures (5xxxx /// ids, no map content) — see that file's header. +#[allow(clippy::too_many_lines)] // A seed data table written as code. fn seed_map0_demo_content(ctx: &ReducerContext) { use constants::start_human_warrior as hw; @@ -803,6 +804,7 @@ fn seed_map0_demo_content(ctx: &ReducerContext) { /// Stratum 3 — the hand-authored spell/item registry: `game_spell`/`game_spell_effect` rows, the /// crafted-consumable on-use spells, 1-10-alpha consumable breadth, the mock-seed fixture kits /// (`seed/fixtures.rs`), enchant/disenchant, talents, and the stacking-group starter set. +#[allow(clippy::too_many_lines)] // A seed data table written as code. fn seed_spell_registry(ctx: &ReducerContext) { // --- Spell registry: hand-authored `game_spell` headers + `game_spell_effect` rows (the // data-driven effect-row engine). Each spell = a header + 1..3 effect rows; effect.id is the @@ -1393,12 +1395,14 @@ fn seed_scheduler_arming(ctx: &ReducerContext) { scheduled_at: ScheduleAt::Interval(TimeDuration::from_micros(1_000_000)), }); - ctx.db.game_duel_schedule().insert(crate::duel::DuelSchedule { - scheduled_id: 0, - scheduled_at: ScheduleAt::Interval(TimeDuration::from_micros( - crate::duel::DUEL_TICK_MICROS, - )), - }); + ctx.db + .game_duel_schedule() + .insert(crate::duel::DuelSchedule { + scheduled_id: 0, + scheduled_at: ScheduleAt::Interval(TimeDuration::from_micros( + crate::duel::DUEL_TICK_MICROS, + )), + }); // Ground-AoE damage tick every 500ms (118): drives game_ground_area (Consecration/…). 500ms so a // 1s/2s area period fires within ~½ tick of due. Areas gate on their own next_tick_micros. @@ -1937,14 +1941,22 @@ mod tests { crate::skill::LEARN_JOURNEYMAN_RIDING_SPELL_ID, ]; let unique: std::collections::BTreeSet = ids.iter().copied().collect(); - assert_eq!(unique.len(), ids.len(), "fixture spell ids collide: {ids:?}"); + assert_eq!( + unique.len(), + ids.len(), + "fixture spell ids collide: {ids:?}" + ); // Creature template + item entry: the same reserved families the other fixtures use. assert!( (51_000..52_000).contains(&RIDING_TRAINER_ENTRY), "the riding trainer must use a reserved 51xxx creature entry" ); - for entry in [TEST_WOLF_ENTRY, PROFESSION_TRAINER_ENTRY, TEST_TAME_BOAR_ENTRY] { + for entry in [ + TEST_WOLF_ENTRY, + PROFESSION_TRAINER_ENTRY, + TEST_TAME_BOAR_ENTRY, + ] { assert_ne!( entry, RIDING_TRAINER_ENTRY, "the riding trainer shadows an existing fixture creature" diff --git a/module/src/seed/fixtures.rs b/module/src/seed/fixtures.rs index 7266d6a5f..ad37e2911 100644 --- a/module/src/seed/fixtures.rs +++ b/module/src/seed/fixtures.rs @@ -71,7 +71,7 @@ pub(crate) fn test_wolf_template() -> CreatureTemplate { // 0 ⇒ `skin_corpse` falls back to the flat Light Leather — the pre-210 verify flow // (debug_skin_nearest → 1x Light Leather) stays byte-identical without a seeded skin table. skin_loot_id: 0, - trainer_type: 0, // the test wolf is a beast, not a trainer + trainer_type: 0, // the test wolf is a beast, not a trainer trainer_class: 0, } } @@ -107,7 +107,7 @@ pub(crate) fn profession_trainer_template() -> CreatureTemplate { armor: 0, // a trainer never takes damage anyway pickpocket_loot_id: 0, // not imported — a friendly trainer is never pickpocketed skin_loot_id: 0, // not imported — a Humanoid trainer isn't skinnable anyway - trainer_type: 2, // TRADESKILLS — serves every class; the gate keys on trainer_class, which stays 0 + trainer_type: 2, // TRADESKILLS — serves every class; the gate keys on trainer_class, which stays 0 trainer_class: 0, } } @@ -404,7 +404,7 @@ pub(crate) fn tough_jerky_template(entry: u32) -> ItemTemplate { buy_price: 10, sell_price: 2, max_stack: 20, - food_type: 1, // Meat + food_type: 1, // Meat spellid_1: 50115, // "Eating" — A_PERIODIC_HEAL food HoT spelltrigger_1: 0, // on-use bonding: crate::items::bonding::NONE, // plain food — unbound/tradeable @@ -1341,6 +1341,7 @@ pub(crate) fn seed_fixture_catalogue(ctx: &ReducerContext) { /// unconditionally (contrast `FIXTURE_BLADE`/`FIXTURE_JERKY` above, which stay reachable from `init` /// in every build and so carry no such attribute). #[cfg_attr(not(feature = "debug_reducers"), allow(dead_code))] +#[allow(clippy::too_many_lines)] // A seed data table written as code. pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { use crate::quest::quest_role; @@ -1489,7 +1490,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { armor: 0, pickpocket_loot_id: 0, skin_loot_id: 0, - trainer_type: 0, // not a trainer + trainer_type: 0, // not a trainer trainer_class: 0, }); } @@ -1520,7 +1521,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { armor: 0, pickpocket_loot_id: 0, // not imported — a Humanoid questgiver has no pickpocket table skin_loot_id: 0, // not imported — a Humanoid questgiver isn't skinnable anyway - trainer_type: 0, // not a trainer + trainer_type: 0, // not a trainer trainer_class: 0, }); } @@ -1553,7 +1554,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { armor: 0, pickpocket_loot_id: 0, // not imported — a Humanoid vendor has no pickpocket table skin_loot_id: 0, // not imported — a Humanoid vendor isn't skinnable anyway - trainer_type: 0, // not a trainer + trainer_type: 0, // not a trainer trainer_class: 0, }); } @@ -1647,7 +1648,7 @@ pub(crate) fn seed_scenario_fixtures(ctx: &ReducerContext) { armor: 0, pickpocket_loot_id: 0, // not imported — a friendly weapon master has no pickpocket table skin_loot_id: 0, // not imported — a Humanoid trainer isn't skinnable anyway - trainer_type: 2, // TRADESKILLS: the weapon master serves every class, matching the real Woo Ping (entry 11867) in the dump + trainer_type: 2, // TRADESKILLS: the weapon master serves every class, matching the real Woo Ping (entry 11867) in the dump trainer_class: 0, }); } diff --git a/module/src/spell/cast/mod.rs b/module/src/spell/cast/mod.rs index d9b1edce4..604f5ed3c 100644 --- a/module/src/spell/cast/mod.rs +++ b/module/src/spell/cast/mod.rs @@ -1,7 +1,7 @@ //! Targeting + effect dispatch + the cast core (381 split of the old flat 2,999-line `cast.rs`): -//! - `targeting` — `select_targets` / `aura_apply` / `apply_effect` and the per-kind effect handlers. -//! - `resolve` — the cast core: `resolve_cast_at` (+ its extracted `check_cast_gates` gate sweep) / -//! `resolve_cast` / `begin_cast` / the passive-apply path. +//! - `targeting` — `select_targets` / `aura_apply` / `apply_effect` and the per-kind effect handlers. +//! - `resolve` — the cast core: `resolve_cast_at` (+ its extracted `check_cast_gates` gate sweep) / +//! `resolve_cast` / `begin_cast` / the passive-apply path. //! //! The cast-bar teardown family (`pushback_cast` / `interrupt_cast` / `break_channel` / //! `interrupt_cast_and_lock`) moved OUT of this module entirely, into `control.rs` — it already called diff --git a/module/src/spell/cast/resolve.rs b/module/src/spell/cast/resolve.rs index 2010fc660..5cf42ae91 100644 --- a/module/src/spell/cast/resolve.rs +++ b/module/src/spell/cast/resolve.rs @@ -760,6 +760,7 @@ fn check_cast_gate_prefix( } #[allow(clippy::too_many_arguments)] +#[allow(clippy::too_many_lines)] // One gate per refusal a cast can hit. fn check_cast_gate_suffix( ctx: &ReducerContext, caster: &WorldEntity, diff --git a/module/src/spell/cast/targeting.rs b/module/src/spell/cast/targeting.rs index 80fa6fc77..7f8cddb9b 100644 --- a/module/src/spell/cast/targeting.rs +++ b/module/src/spell/cast/targeting.rs @@ -328,6 +328,7 @@ fn displace_auras( /// re-freezes the magnitude + resets the next tick. The `UNIT_FIELD_AURA` slot is chosen PER-SPELL on a /// fresh apply (`pick_aura_slot` — all of a spell's effects share one slot ⇒ one icon). The cast visual is /// emitted ONCE by `resolve_cast_at`, not here. [entity] +#[allow(clippy::too_many_lines)] // The refresh and insert branches share one frozen magnitude and stay together. pub(crate) fn aura_apply( ctx: &ReducerContext, e: &SpellEffect, diff --git a/module/src/spell/math.rs b/module/src/spell/math.rs index 9cfd4308b..1abb8f329 100644 --- a/module/src/spell/math.rs +++ b/module/src/spell/math.rs @@ -778,8 +778,7 @@ pub(crate) fn aura_moves_sheet(kind: u8, p0: i32) -> bool { || p0 == STAT_INT as i32 || p0 == STAT_SPI as i32 || p0 == STAT_ALL as i32)) - || (kind == A_MOD_COMBAT - && (p0 == COMBAT_ATTACK_POWER as i32 || p0 == COMBAT_CRIT as i32)) + || (kind == A_MOD_COMBAT && (p0 == COMBAT_ATTACK_POWER as i32 || p0 == COMBAT_CRIT as i32)) } /// One shield's draw against `incoming` damage from a pool of `remaining`: returns diff --git a/module/src/spell/mod.rs b/module/src/spell/mod.rs index 7f71b0fca..b4635c1b7 100644 --- a/module/src/spell/mod.rs +++ b/module/src/spell/mod.rs @@ -12,21 +12,21 @@ //! relay reuses the proven dirty_reset discipline (the gateway reads slot/spell_id/level/flags). [event] //! //! Submodules: -//! - `tables` — the spell/aura/cooldown/cast/schedule table structs (+ their generated accessors). -//! - `taxonomy` — the KIND/MECHANIC/param/target/combat-field/stat/resistance consts + tuning. -//! - `control` — the crowd-control model (the 7 CC predicates + immunity + break-on-damage), the -//! (381) cast-bar teardown family (`pushback_cast` / `interrupt_cast` / -//! `break_channel` / `interrupt_cast_and_lock`), and the death aura cleanup. -//! - `math` — pure helpers + aura-read folds only (388: no ctx-bound orchestration — see `effects`). -//! - `effects` — (388) the ctx-bound orchestration `math` used to carry: `arm_spell_retaliation` / -//! `apply_target_damage` / `dispel_target` / `recompute_vitals` / `absorb_incoming` — -//! every one of which WRITES to a live table, split out so `math`'s own charter is true. -//! - `cast` — a directory module (381 split): `targeting` (`select_targets` / `aura_apply` / -//! `apply_effect` + per-kind handlers) and `resolve` (the cast core — -//! `resolve_cast_at` + its extracted `check_cast_gates` gate sweep / `resolve_cast` / -//! `begin_cast` / the passive-apply path). -//! - `proc` — the Proc engine: the pure proc decision plus the ONE pass, from `apply_hit`. -//! - `scheduler` — the `#[reducer]`s (`tick_auras` / `fire_pending_cast` / `cast_spell`). +//! - `tables` — the spell/aura/cooldown/cast/schedule table structs (+ their generated accessors). +//! - `taxonomy` — the KIND/MECHANIC/param/target/combat-field/stat/resistance consts + tuning. +//! - `control` — the crowd-control model (the 7 CC predicates + immunity + break-on-damage), the +//! (381) cast-bar teardown family (`pushback_cast` / `interrupt_cast` / +//! `break_channel` / `interrupt_cast_and_lock`), and the death aura cleanup. +//! - `math` — pure helpers + aura-read folds only (388: no ctx-bound orchestration — see `effects`). +//! - `effects` — (388) the ctx-bound orchestration `math` used to carry: `arm_spell_retaliation` / +//! `apply_target_damage` / `dispel_target` / `recompute_vitals` / `absorb_incoming` — +//! every one of which WRITES to a live table, split out so `math`'s own charter is true. +//! - `cast` — a directory module (381 split): `targeting` (`select_targets` / `aura_apply` / +//! `apply_effect` + per-kind handlers) and `resolve` (the cast core — +//! `resolve_cast_at` + its extracted `check_cast_gates` gate sweep / `resolve_cast` / +//! `begin_cast` / the passive-apply path). +//! - `proc` — the Proc engine: the pure proc decision plus the ONE pass, from `apply_hit`. +//! - `scheduler` — the `#[reducer]`s (`tick_auras` / `fire_pending_cast` / `cast_spell`). //! //! Everything is re-exported below so every `crate::spell::` path resolves regardless of which //! submodule actually defines it. diff --git a/module/src/spell/scheduler.rs b/module/src/spell/scheduler.rs index dc9284ecd..c901a2641 100644 --- a/module/src/spell/scheduler.rs +++ b/module/src/spell/scheduler.rs @@ -327,6 +327,7 @@ pub(crate) fn is_due_for_expiry(eff_kind: u8, expires_at: Timestamp, now: Timest /// Scheduler-only. Time indexes select due rows across the Shard; each effect operates on its /// own target, so instance count does not change an aura's cadence. #[reducer] +#[allow(clippy::too_many_lines)] // One pass per aura family the tick settles. pub fn tick_auras(ctx: &ReducerContext, _schedule: AuraSchedule) { if ctx.sender() != ctx.database_identity() { return; diff --git a/module/src/spell/tables.rs b/module/src/spell/tables.rs index 368d5f956..9fc3b4314 100644 --- a/module/src/spell/tables.rs +++ b/module/src/spell/tables.rs @@ -3,9 +3,7 @@ //! re-exports them (`pub use tables::*`) so every `crate::spell::` / `ctx.db.game_
()` //! path resolves. -use spacetimedb::{ - table, Identity, ReducerContext, ScheduleAt, Table, Timestamp, -}; +use spacetimedb::{table, Identity, ReducerContext, ScheduleAt, Table, Timestamp}; // The `scheduled(..)` table macros below reference the reducer callbacks by name; those reducers live in // `scheduler.rs` and are re-exported by `mod.rs`, so pull them into scope for the macro to resolve. diff --git a/module/src/spell/tests.rs b/module/src/spell/tests.rs index a78afb546..a82797d9a 100644 --- a/module/src/spell/tests.rs +++ b/module/src/spell/tests.rs @@ -717,7 +717,10 @@ fn aura_moves_sheet_gate() { // A combat aura on a field the sheet doesn't render (e.g. hit rating) must NOT trip it. assert!(!aura_moves_sheet(A_MOD_COMBAT, COMBAT_HIT as i32)); // A different aura kind with an AP-looking p0 still must NOT trip (the kind gate is first). - assert!(!aura_moves_sheet(A_PERIODIC_DAMAGE, COMBAT_ATTACK_POWER as i32)); + assert!(!aura_moves_sheet( + A_PERIODIC_DAMAGE, + COMBAT_ATTACK_POWER as i32 + )); } /// Cast interrupt-on-damage / CC-break flag decode: `break_auras_on_damage` (which also folds in the diff --git a/module/src/talent.rs b/module/src/talent.rs index 3335bc9b6..80382f8bd 100644 --- a/module/src/talent.rs +++ b/module/src/talent.rs @@ -33,9 +33,7 @@ //! the demo rows (idle, orphaned in tree_id 0-2 alongside real tabs) and the real tree — harmless, since //! nothing gates on "is this the only row in tree_id N". -use spacetimedb::{ - table, Identity, ReducerContext, Table, -}; +use spacetimedb::{table, Identity, ReducerContext, Table}; use crate::{game_character, game_spell, game_spell_effect, game_world_entity, Spell, SpellEffect}; diff --git a/module/src/test_scan.rs b/module/src/test_scan.rs index 71fe6f9d3..0d35732ce 100644 --- a/module/src/test_scan.rs +++ b/module/src/test_scan.rs @@ -106,7 +106,7 @@ pub(crate) fn on_comment_line(content: &str, byte_idx: usize) -> bool { /// must not treat assertion needles as live table reads. pub(crate) fn in_string_literal(content: &str, byte_idx: usize) -> bool { let line_start = content[..byte_idx].rfind('\n').map(|i| i + 1).unwrap_or(0); - let bytes = content[line_start..byte_idx].as_bytes(); + let bytes = &content.as_bytes()[line_start..byte_idx]; let mut in_string = false; let mut i = 0; while i < bytes.len() { diff --git a/module/src/transfer/harness.rs b/module/src/transfer/harness.rs index 4f0299c35..e5b2ff28c 100644 --- a/module/src/transfer/harness.rs +++ b/module/src/transfer/harness.rs @@ -475,7 +475,10 @@ fn active_taxi_refuses_transfer_before_any_source_mutation() { assert_eq!(error, "PLAYER_IN_TAXI_FLIGHT"); assert_eq!(*src.live.borrow(), before_live); - assert_eq!(src.chars.borrow().keys().copied().collect::>(), before_chars); + assert_eq!( + src.chars.borrow().keys().copied().collect::>(), + before_chars + ); assert_eq!(src.froze.get(), 0); assert!(src.out_rows.borrow().is_empty()); assert!(!src.reaper_armed.get()); diff --git a/module/src/tripwires.rs b/module/src/tripwires.rs index 41356a057..bdc807c71 100644 --- a/module/src/tripwires.rs +++ b/module/src/tripwires.rs @@ -906,12 +906,9 @@ pub(crate) mod grid_cell_tripwire { // `helpers.rs` reads the grid off rows (`grid_of`, `entity_addr`) and its ONE fixture writer // is covered; the exemptions below are files where `grid_x` appears only in a signature, // a tuple destructure or a comment. - [ - "module/src/tripwires.rs", - "module/src/region.rs", - ] - .iter() - .any(|p| path.ends_with(p) || path.replace('\\', "/").ends_with(p)) + ["module/src/tripwires.rs", "module/src/region.rs"] + .iter() + .any(|p| path.ends_with(p) || path.replace('\\', "/").ends_with(p)) } /// The AOI-scoped tables' own `#[table]` definitions declare `pub grid_x: i32` next to a `cell` @@ -919,7 +916,10 @@ pub(crate) mod grid_cell_tripwire { /// match that is a struct FIELD DECLARATION rather than an initializer or assignment. fn is_field_declaration(code: &str, idx: usize) -> bool { let line_start = code[..idx].rfind('\n').map(|i| i + 1).unwrap_or(0); - let line_end = code[idx..].find('\n').map(|i| idx + i).unwrap_or(code.len()); + let line_end = code[idx..] + .find('\n') + .map(|i| idx + i) + .unwrap_or(code.len()); let line = code[line_start..line_end].trim(); line.starts_with("pub grid_x") || line.starts_with("grid_x: i32") } @@ -1208,7 +1208,8 @@ pub(crate) mod grid_cell_tripwire { // `struct E` declares a `cell` column, so this tripwire is E's business — mirrors a real // cell-bearing table's own file, where the `#[table]` struct and every construction site // live together. - let cell_bearing_struct = "struct E {\n pub grid_x: i32,\n pub grid_y: i32,\n pub cell: i64,\n}\n"; + let cell_bearing_struct = + "struct E {\n pub grid_x: i32,\n pub grid_y: i32,\n pub cell: i64,\n}\n"; let bad = format!( "{cell_bearing_struct}fn f() -> E {{\n E {{\n grid_x,\n grid_y,\n }}\n}}\n" @@ -1272,7 +1273,8 @@ pub(crate) mod grid_cell_tripwire { /// was skipped outright — violation or not. Confirms a broken compact literal is still CAUGHT. #[test] fn the_scan_catches_a_broken_compact_one_line_shorthand() { - let cell_bearing_struct = "struct E {\n pub grid_x: i32,\n pub grid_y: i32,\n pub cell: i64,\n}\n"; + let cell_bearing_struct = + "struct E {\n pub grid_x: i32,\n pub grid_y: i32,\n pub cell: i64,\n}\n"; let bad = format!("{cell_bearing_struct}fn f() -> E {{ E {{ grid_x, grid_y }} }}\n"); assert_eq!( diff --git a/module/src/vmap.rs b/module/src/vmap.rs index 5afaed351..84d41e7a9 100644 --- a/module/src/vmap.rs +++ b/module/src/vmap.rs @@ -208,6 +208,7 @@ fn same_stage_request( /// Start a generation. Repeating the exact request resumes it; a conflicting request is refused. #[reducer] +#[allow(clippy::too_many_arguments)] // A reducer's arguments are its call signature. pub fn stage_vmap_generation( ctx: &ReducerContext, generation_id: u64, @@ -1019,12 +1020,15 @@ mod tests { use super::*; use std::collections::BTreeMap; + /// (base_z, walk, obs) for one coverage cell. + type CoverageCell = (f32, Vec, Vec); + #[derive(Default)] struct LifecycleHarness { generations: BTreeMap, chunks: BTreeMap<(u64, u64, u32), Vec>, /// (generation_id, cell_key) -> (base_z, walk, obs) — mirrors `VmapNavCoverage`. - coverage: BTreeMap<(u64, u64), (f32, Vec, Vec)>, + coverage: BTreeMap<(u64, u64), CoverageCell>, /// generation_id -> (cell_count, digest, complete) — mirrors `VmapNavCoverageManifest`. coverage_manifests: BTreeMap, } @@ -1116,7 +1120,7 @@ mod tests { /// Mirrors `finalize_vmap_nav_coverage`: cell count + digest over the sorted current rows. fn finalize_coverage(&mut self, id: u64) -> Result<(), String> { self.coverage_preparable(id)?; - let mut cells: Vec<(u64, &(f32, Vec, Vec))> = self + let mut cells: Vec<(u64, &CoverageCell)> = self .coverage .iter() .filter(|((generation_id, _), _)| *generation_id == id) diff --git a/module/src/xp.rs b/module/src/xp.rs index ad54d9ae7..8351ea213 100644 --- a/module/src/xp.rs +++ b/module/src/xp.rs @@ -4,9 +4,7 @@ //! the gateway turns into `SMSG_LEVELUP_INFO`. The XP-bar number syncs at create-object (login); //! the popup + the heal carry the live feedback (see docs/roadmap). -use spacetimedb::{ - log, table, Identity, ReducerContext, Table, Timestamp, -}; +use spacetimedb::{log, table, Identity, ReducerContext, Table, Timestamp}; use crate::game_character; use crate::game_config; diff --git a/module/tests/support/mod.rs b/module/tests/support/mod.rs index e9901dcfe..5fac679ab 100644 --- a/module/tests/support/mod.rs +++ b/module/tests/support/mod.rs @@ -121,6 +121,7 @@ impl Standalone { .expect("private Owner Token is missing") } + #[allow(dead_code)] // Only the integration tests that publish a module use this. pub fn publish_module(&mut self) { let workspace = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap(); let module_dir = workspace.join("module"); @@ -163,6 +164,7 @@ impl Standalone { command.output().expect("failed to call reducer") } + #[allow(dead_code)] // Only the integration tests that call reducers use this. pub fn assert_call(&self, reducer: &str, args: &[&str]) { self.assert_ok(&self.call(reducer, args)); } diff --git a/module/tests/typed_taxi_contracts.rs b/module/tests/typed_taxi_contracts.rs index e4550d2fb..40bbfe74b 100644 --- a/module/tests/typed_taxi_contracts.rs +++ b/module/tests/typed_taxi_contracts.rs @@ -51,7 +51,8 @@ fn gateway_taxi_gates_keep_refusals_typed_and_invariants_fatal() { standalone.assert_call("realm_group_op", &["0", "3", "5", "0", "0"]); standalone.assert_sql("DELETE FROM game_group WHERE leader_guid = 3"); - let valid_group_before = standalone.query_rows("SELECT * FROM game_group WHERE leader_guid = 1"); + let valid_group_before = + standalone.query_rows("SELECT * FROM game_group WHERE leader_guid = 1"); for args in [ &["0", "1", "3", "0", "0"][..], &["1", "5", "0", "0", "0"][..], @@ -223,7 +224,10 @@ fn build_module_bytes() -> Vec { .filter_map(|message| message["filenames"].as_array().cloned()) .flatten() .filter_map(|filename| filename.as_str().map(PathBuf::from)) - .find(|path| path.extension().is_some_and(|extension| extension == "wasm")) + .find(|path| { + path.extension() + .is_some_and(|extension| extension == "wasm") + }) .expect("Cargo did not report the built Module Wasm artifact"); let bytes = std::fs::read(&wasm).expect("the Wasm preflight output is missing"); assert!(bytes.starts_with(b"\0asm"), "the built module is not Wasm");