From b69598cdded3e90c163981f6a1a6337e221f469a Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 04:45:08 -0400 Subject: [PATCH 1/2] Harness: the world login reads past the family packets of entering the game Every entry now ends with the Family window's INFO and entitlements (since #699), which the login's map-setup drain stopped at and left for the tests, so twelve tests that read the next packet strictly failed on opcode 95. The drain now waits for them and keeps them in WorldSession::family, leaving anything else in order; families.rs reads them from there. --- integration-harness/src/packets.rs | 3 ++ integration-harness/src/protocol.rs | 64 +++++++++++++++++++++------ integration-harness/tests/families.rs | 30 +++++++++---- 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/integration-harness/src/packets.rs b/integration-harness/src/packets.rs index 2c429f202..c9509078a 100644 --- a/integration-harness/src/packets.rs +++ b/integration-harness/src/packets.rs @@ -275,6 +275,9 @@ pub fn opcode_name(opcode: i16) -> &'static str { x if x == SendOpcode::SpawnReactor as i16 => "SpawnReactor", x if x == SendOpcode::HitReactor as i16 => "HitReactor", x if x == SendOpcode::RemoveReactor as i16 => "RemoveReactor", + x if x == SendOpcode::FamilyInfo as i16 => "FamilyInfo", + x if x == SendOpcode::FamilyPrivilegeList as i16 => "FamilyPrivilegeList", + x if x == SendOpcode::FamilySetPrivilege as i16 => "FamilySetPrivilege", _ => "Unknown", } } diff --git a/integration-harness/src/protocol.rs b/integration-harness/src/protocol.rs index 7c24a3299..ccaf3cd8c 100644 --- a/integration-harness/src/protocol.rs +++ b/integration-harness/src/protocol.rs @@ -17,7 +17,7 @@ use crate::packets::{ use net::packet::op::SendOpcode; use std::net::Ipv4Addr; use std::net::SocketAddr; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::time::timeout; #[derive(Debug, Clone, PartialEq, Eq)] @@ -73,6 +73,10 @@ pub struct WorldSession { pub quick_slots: Option<[i32; 8]>, /// The name Blessing of the Fairy's tooltip shows, if any. pub linked_name: Option, + /// The family packets of entering the game the login read past (INFO, + /// the entitlements, and a working entitlement's icon), as (opcode, + /// body). Any that came after another packet are still to be read. + pub family: Vec<(i16, Vec)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -258,7 +262,7 @@ pub async fn login_to_world_session(config: &HarnessConfig) -> Result Result Vec<(i16, Vec)> { + let mut family = Vec::new(); + let mut held = Vec::new(); + let mut listed = false; + let deadline = Instant::now() + Duration::from_secs(5); + loop { + let wait = if listed { + Duration::from_millis(100) + } else { + deadline.saturating_duration_since(Instant::now()) + }; + let Ok(Ok(envelope)) = timeout(wait, connection.read_packet("drain spawn npcs")).await + else { + break; + }; + let opcode = envelope.opcode(); + // The Family window's INFO and entitlements, and a working + // entitlement's icon. + if [ + SendOpcode::FamilyInfo as i16, + SendOpcode::FamilyPrivilegeList as i16, + SendOpcode::FamilySetPrivilege as i16, + ] + .contains(&opcode) + { + listed |= opcode == SendOpcode::FamilyPrivilegeList as i16; + family.push((opcode, envelope.packet.bytes[2..].to_vec())); + continue; + } // Map setup: NPC spawns and control, and the map entry banner. // ...and the map's mobs, drops, and reactors, and the skill update // that gives Blessing of the Fairy its master level (SET_FIELD // already carries its level). - if ![ + let setup = [ SendOpcode::SpawnNpc as i16, SendOpcode::SpawnNpcRequestController as i16, SendOpcode::FieldEffect as i16, @@ -513,12 +544,19 @@ async fn drain_spawn_npcs(connection: &mut MapleTestConnection) { SendOpcode::SpawnReactor as i16, SendOpcode::UpdateSkills as i16, ] - .contains(&envelope.opcode()) - { - connection.push_back_packet(envelope); + .contains(&opcode); + if setup && held.is_empty() { + continue; + } + held.push(envelope); + if listed { break; } } + for envelope in held.into_iter().rev() { + connection.push_back_packet(envelope); + } + family } /// Log in again right after dropping a session. The server closes the old diff --git a/integration-harness/tests/families.rs b/integration-harness/tests/families.rs index 62b87ea31..22567da79 100644 --- a/integration-harness/tests/families.rs +++ b/integration-harness/tests/families.rs @@ -125,12 +125,22 @@ async fn new_character(config: &HarnessConfig, level: i16) -> i32 { /// Log in and go to `map`; the family packets of entering the game. async fn enter(config: &HarnessConfig, map: i32) -> (WorldSession, Trace) { let mut session = relogin(config).await; - let mut t = Trace::default(); - until(&mut session.connection, &mut t, |t| t.family.len() >= 2).await; + let mut t = Trace { + family: entered(&mut session, 2).await, + ..Trace::default() + }; assert_eq!(warp(&mut session.connection, map, "", &mut t).await, map); (session, t) } +/// The family packets of `session` entering the game, at least `count`: +/// those the login read past, then any still to come. +async fn entered(session: &mut WorldSession, count: usize) -> Vec<(i16, Vec)> { + let mut packets = std::mem::take(&mut session.family); + packets.extend(family(session, count.saturating_sub(packets.len())).await); + packets +} + /// The next `count` family packets `session` gets. async fn family(session: &mut WorldSession, count: usize) -> Vec<(i16, Vec)> { let mut t = Trace::default(); @@ -157,9 +167,10 @@ async fn a_senior_takes_a_junior_on_their_map_who_says_yes() { // Out of a family: no leader and no name, and nothing else on login. let mut a = relogin(&a_config).await; - let mut at = Trace::default(); - until(&mut a.connection, &mut at, |t| t.family.len() >= 2).await; - read_for(&mut a.connection, &mut at, 300).await; + let mut at = Trace { + family: entered(&mut a, 2).await, + ..Trace::default() + }; assert_eq!(at.family.len(), 2); assert_eq!(at.family[0], info(0, 0, "")); // The eleven entitlements, the first two to a relative named next. @@ -255,8 +266,8 @@ async fn a_senior_takes_a_junior_on_their_map_who_says_yes() { assert_eq!(family(&mut a, 1).await, [online(0)]); let mut b = relogin(&b_config).await; assert_eq!(family(&mut a, 1).await, [online(1)]); - let entered = family(&mut b, 2).await; - assert_eq!(entered[0], info(0, a_id, &an)); + let packets = entered(&mut b, 2).await; + assert_eq!(packets[0], info(0, a_id, &an)); } /// `senior` asks `junior`, both on one map, who says yes. @@ -639,7 +650,10 @@ async fn members_buy_rates_and_see_their_icon_until_they_end() { tokio::time::sleep(Duration::from_millis(500)).await; end_family_buff_in(b_id, 8).await.expect("buff"); let mut b = relogin(&b_config).await; - let mut t = Trace::default(); + let mut t = Trace { + family: std::mem::take(&mut b.family), + ..Trace::default() + }; until(&mut b.connection, &mut t, |t| { t.family .iter() From 963a0ba7895d49a3fac4cdc164939175249eab76 Mon Sep 17 00:00:00 2001 From: David Goldstein Date: Thu, 22 Oct 2026 04:54:32 -0400 Subject: [PATCH 2/2] Client queue: room for entering the busiest maps (1024) Entering a map queues one message per NPC, player, mob and its control, reactor and drop while the client actor is still busy with the map change: about 190 for Kerning City's subway (103000103, 91 mobs), so a fresh field's spawns could pass the 256 cap and disconnect the player as a slow client (rank_rooms' 'connection closed during packet read'). --- runtime/src/message.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/runtime/src/message.rs b/runtime/src/message.rs index 34f5462a5..f7754a145 100644 --- a/runtime/src/message.rs +++ b/runtime/src/message.rs @@ -30,8 +30,11 @@ use tokio_util::sync::CancellationToken; use tracing::warn; /// Capacity of each client's inbound queue. A client this far behind is -/// considered dead weight and disconnected. -pub const CLIENT_QUEUE_CAPACITY: usize = 256; +/// considered dead weight and disconnected. Entering a map queues a packet +/// for each of its NPCs, players, mobs (and control of them), reactors and +/// drops while the client is still busy with the map change, close to 200 +/// in Kerning City's subway, so this leaves room for the busiest maps. +pub const CLIENT_QUEUE_CAPACITY: usize = 1024; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct FieldKey {