1use std::sync::atomic::{AtomicU64, Ordering};
27
28use chrono::{DateTime, Utc};
29
30use crate::events::GameEvent;
31use crate::log::entry::LogEntry;
32use crate::log::timestamp::parse_log_timestamp;
33use crate::parsers;
34use crate::util::truncate_for_log;
35
36#[derive(Debug, Default)]
47pub struct RouterStats {
48 routed: AtomicU64,
50 unknown: AtomicU64,
52 timestamp_failures: AtomicU64,
54}
55
56impl RouterStats {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn routed_count(&self) -> u64 {
64 self.routed.load(Ordering::Relaxed)
65 }
66
67 pub fn unknown_count(&self) -> u64 {
69 self.unknown.load(Ordering::Relaxed)
70 }
71
72 pub fn timestamp_failure_count(&self) -> u64 {
74 self.timestamp_failures.load(Ordering::Relaxed)
75 }
76
77 pub fn reset(&self) {
79 self.routed.store(0, Ordering::Relaxed);
80 self.unknown.store(0, Ordering::Relaxed);
81 self.timestamp_failures.store(0, Ordering::Relaxed);
82 }
83}
84
85pub struct Router {
113 stats: RouterStats,
115}
116
117impl Router {
118 pub fn new() -> Self {
120 Self {
121 stats: RouterStats::new(),
122 }
123 }
124
125 pub fn stats(&self) -> &RouterStats {
127 &self.stats
128 }
129
130 pub fn route(&self, entry: &LogEntry) -> Vec<GameEvent> {
145 let timestamp = extract_timestamp(&entry.body);
146
147 if timestamp.is_none() {
148 self.stats
149 .timestamp_failures
150 .fetch_add(1, Ordering::Relaxed);
151 ::log::debug!(
152 "No timestamp in entry header: {:?}",
153 truncate_for_log(&entry.body, 120),
154 );
155 }
156
157 let events = dispatch_to_parsers(entry, timestamp);
158
159 if events.is_empty() {
160 self.stats.unknown.fetch_add(1, Ordering::Relaxed);
161 ::log::debug!(
162 "Unrecognized entry (header={}, body={:?})",
163 entry.header,
164 truncate_for_log(&entry.body, 120),
165 );
166 } else {
167 self.stats.routed.fetch_add(1, Ordering::Relaxed);
168 }
169
170 events
171 }
172}
173
174impl Default for Router {
175 fn default() -> Self {
176 Self::new()
177 }
178}
179
180fn extract_timestamp(body: &str) -> Option<DateTime<Utc>> {
201 let first_line = body.lines().next()?;
202
203 let after_bracket = first_line.find(']').map(|pos| &first_line[pos + 1..])?;
205 let trimmed = after_bracket.trim();
206
207 if trimmed.is_empty() {
208 return None;
209 }
210
211 let words: Vec<&str> = trimmed.split_whitespace().collect();
215
216 let max_words = words.len().min(4);
219 for end in (2..=max_words).rev() {
220 let candidate = words[..end].join(" ");
221 let cleaned = candidate.trim_end_matches(|c: char| c.is_ascii_punctuation());
223 if let Ok(ts) = parse_log_timestamp(cleaned) {
224 return Some(ts);
225 }
226 }
227
228 None
229}
230
231fn dispatch_to_parsers(entry: &LogEntry, timestamp: Option<DateTime<Utc>>) -> Vec<GameEvent> {
258 if let Some(event) = parsers::metadata::try_parse(entry, timestamp) {
260 return vec![event];
261 }
262
263 let gre_events = parsers::gre::try_parse(entry, timestamp);
265 if !gre_events.is_empty() {
266 return gre_events;
267 }
268
269 let event = None
271 .or_else(|| parsers::client_actions::try_parse(entry, timestamp))
272 .or_else(|| parsers::match_state::try_parse(entry, timestamp))
273 .or_else(|| parsers::session::try_parse(entry, timestamp))
274 .or_else(|| parsers::draft::bot::try_parse(entry, timestamp))
275 .or_else(|| parsers::draft::human::try_parse(entry, timestamp))
276 .or_else(|| parsers::draft::complete::try_parse(entry, timestamp))
277 .or_else(|| parsers::event_lifecycle::try_parse(entry, timestamp))
278 .or_else(|| parsers::rank::try_parse(entry, timestamp))
279 .or_else(|| parsers::collection::try_parse(entry, timestamp))
280 .or_else(|| parsers::inventory::try_parse(entry, timestamp))
281 .or_else(|| parsers::connection_state::try_parse(entry, timestamp))
282 .or_else(|| parsers::connection_close::try_parse(entry, timestamp))
283 .or_else(|| parsers::connection_error::try_parse(entry, timestamp));
284
285 event.into_iter().collect()
286}
287
288#[cfg(test)]
293mod tests {
294 use super::*;
295 use crate::log::entry::EntryHeader;
296 use chrono::Timelike;
297
298 fn unity_entry(body: &str) -> LogEntry {
300 LogEntry {
301 header: EntryHeader::UnityCrossThreadLogger,
302 body: body.to_owned(),
303 }
304 }
305
306 fn gre_entry(body: &str) -> LogEntry {
308 LogEntry {
309 header: EntryHeader::ClientGre,
310 body: body.to_owned(),
311 }
312 }
313
314 mod extract_timestamp_tests {
317 use super::*;
318
319 #[test]
320 fn test_extract_timestamp_us_format_with_pm() {
321 let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM greToClientEvent";
322 let ts = extract_timestamp(body);
323 assert!(ts.is_some());
324 if let Some(ts) = ts {
325 assert_eq!(
326 ts.format("%Y-%m-%d %H:%M:%S").to_string(),
327 "2026-02-25 12:00:00"
328 );
329 }
330 }
331
332 #[test]
333 fn test_extract_timestamp_us_format_with_am() {
334 let body = "[UnityCrossThreadLogger]2/22/2026 11:59:51 AM";
335 let ts = extract_timestamp(body);
336 assert!(ts.is_some());
337 if let Some(ts) = ts {
338 assert_eq!(
339 ts.format("%Y-%m-%d %H:%M:%S").to_string(),
340 "2026-02-22 11:59:51"
341 );
342 }
343 }
344
345 #[test]
346 fn test_extract_timestamp_with_trailing_colon() {
347 let body = "[UnityCrossThreadLogger]3/13/2026 11:34:51 PM: Match to AAF4FC69CE47D53A";
348 let ts = extract_timestamp(body);
349 assert!(ts.is_some());
350 if let Some(ts) = ts {
351 assert_eq!(ts.hour(), 23); }
353 }
354
355 #[test]
356 fn test_extract_timestamp_24h_format() {
357 let body = "[UnityCrossThreadLogger]2026-02-25 14:30:00 some content";
358 let ts = extract_timestamp(body);
359 assert!(ts.is_some());
360 if let Some(ts) = ts {
361 assert_eq!(
362 ts.format("%Y-%m-%d %H:%M:%S").to_string(),
363 "2026-02-25 14:30:00"
364 );
365 }
366 }
367
368 #[test]
369 fn test_extract_timestamp_client_gre_header() {
370 let body = "[Client GRE]2/25/2026 12:00:00 PM GreToClientEvent";
371 let ts = extract_timestamp(body);
372 assert!(ts.is_some());
373 }
374
375 #[test]
376 fn test_extract_timestamp_no_bracket_returns_none() {
377 let body = "no bracket here";
378 let ts = extract_timestamp(body);
379 assert!(ts.is_none());
380 }
381
382 #[test]
383 fn test_extract_timestamp_empty_after_bracket_returns_none() {
384 let body = "[UnityCrossThreadLogger]";
385 let ts = extract_timestamp(body);
386 assert!(ts.is_none());
387 }
388
389 #[test]
390 fn test_extract_timestamp_no_timestamp_content_returns_none() {
391 let body = "[UnityCrossThreadLogger]Updated account. DisplayName:Player";
392 let ts = extract_timestamp(body);
393 assert!(ts.is_none());
394 }
395
396 #[test]
397 fn test_extract_timestamp_timestamp_on_own_line() {
398 let body = "[UnityCrossThreadLogger]2/22/2026 11:59:51 AM\n<== StartHook(abc-123)";
399 let ts = extract_timestamp(body);
400 assert!(ts.is_some());
401 if let Some(ts) = ts {
402 assert_eq!(
403 ts.format("%Y-%m-%d %H:%M:%S").to_string(),
404 "2026-02-22 11:59:51"
405 );
406 }
407 }
408
409 #[test]
410 fn test_extract_timestamp_with_leading_space() {
411 let body = "[UnityCrossThreadLogger] 2/25/2026 12:00:00 PM event";
412 let ts = extract_timestamp(body);
413 assert!(ts.is_some());
414 }
415 }
416
417 mod known_routing {
420 use super::*;
421
422 #[test]
423 fn test_route_gre_game_state_message() {
424 let router = Router::new();
425 let payload = serde_json::json!({
426 "greToClientEvent": {
427 "greToClientMessages": [{
428 "type": "GREMessageType_GameStateMessage",
429 "gameStateMessage": {
430 "gameInfo": { "stage": "GameStage_Play" },
431 "gameObjects": [],
432 "zones": []
433 }
434 }]
435 }
436 });
437 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{payload}");
438 let entry = unity_entry(&body);
439
440 let results = router.route(&entry);
441 assert_eq!(results.len(), 1);
442 assert!(matches!(&results[0], GameEvent::GameState(_)));
443 assert_eq!(router.stats().routed_count(), 1);
444 assert_eq!(router.stats().unknown_count(), 0);
445 }
446
447 #[test]
448 fn test_route_client_action() {
449 let router = Router::new();
450 let payload = serde_json::json!({
451 "clientToMatchServiceMessageType":
452 "ClientToMatchServiceMessageType_ClientToGREMessage",
453 "payload": {
454 "type": "ClientMessageType_MulliganResp",
455 "gameStateId": 5,
456 "respId": 1,
457 "mulliganResp": { "decision": "MulliganOption_Mulligan" }
458 },
459 "requestId": 12345,
460 "timestamp": "637123456789"
461 });
462 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{payload}");
463 let entry = unity_entry(&body);
464
465 let results = router.route(&entry);
466 assert_eq!(results.len(), 1);
467 assert!(matches!(&results[0], GameEvent::ClientAction(_)));
468 }
469
470 #[test]
471 fn test_route_match_state() {
472 let router = Router::new();
473 let payload = serde_json::json!({
474 "matchGameRoomStateChangedEvent": {
475 "gameRoomInfo": {
476 "stateType": "MatchGameRoomStateType_Playing",
477 "gameRoomConfig": {
478 "matchId": "match-123",
479 "reservedPlayers": []
480 }
481 }
482 }
483 });
484 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{payload}");
485 let entry = unity_entry(&body);
486
487 let results = router.route(&entry);
488 assert_eq!(results.len(), 1);
489 assert!(matches!(&results[0], GameEvent::MatchState(_)));
490 }
491
492 #[test]
493 fn test_route_session_account_update() {
494 let router = Router::new();
495 let body = "[UnityCrossThreadLogger]Updated account. \
496 DisplayName:TestPlayer, \
497 AccountID:abc123, \
498 Token:sometoken";
499 let entry = unity_entry(body);
500
501 let results = router.route(&entry);
502 assert_eq!(results.len(), 1);
503 assert!(matches!(&results[0], GameEvent::Session(_)));
504 }
505
506 #[test]
507 fn test_route_rank_event() {
508 let router = Router::new();
509 let payload = serde_json::json!({
510 "constructedClass": "Gold",
511 "constructedLevel": 2,
512 "limitedClass": "Silver",
513 "limitedLevel": 1
514 });
515 let body = format!(
516 "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
517 <== RankGetCombinedRankInfo(abc-123)\n{payload}",
518 );
519 let entry = unity_entry(&body);
520
521 let results = router.route(&entry);
522 assert_eq!(results.len(), 1);
523 assert!(matches!(&results[0], GameEvent::Rank(_)));
524 }
525
526 #[test]
527 fn test_route_event_lifecycle() {
528 let router = Router::new();
529 let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM \
530 ==> EventJoin {\"id\":\"abc-123\",\
531 \"request\":\"{\\\"EventName\\\":\\\"PremierDraft_MKM\\\"}\"}";
532 let entry = unity_entry(body);
533
534 let results = router.route(&entry);
535 assert_eq!(results.len(), 1);
536 assert!(matches!(&results[0], GameEvent::EventLifecycle(_)));
537 }
538
539 #[test]
540 fn test_route_draft_complete() {
541 let router = Router::new();
542 let payload = serde_json::json!({
543 "CourseId": "draft-123",
544 "InternalEventName": "PremierDraft_MKM",
545 "CardPool": [12345, 67890]
546 });
547 let body = format!(
548 "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
549 <== DraftCompleteDraft(abc-123)\n{payload}",
550 );
551 let entry = unity_entry(&body);
552
553 let results = router.route(&entry);
554 assert_eq!(results.len(), 1);
555 assert!(matches!(&results[0], GameEvent::DraftComplete(_)));
556 }
557
558 #[test]
559 fn test_route_draft_bot_pack_presentation() {
560 let router = Router::new();
561 let payload = serde_json::json!({
562 "DraftStatus": "PickNext",
563 "PackNumber": 0,
564 "PickNumber": 0,
565 "DraftPack": ["12345", "67890", "11111"],
566 "EventName": "QuickDraft_MKM_20260201"
567 });
568 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{payload}",);
569 let entry = unity_entry(&body);
570
571 let results = router.route(&entry);
572 assert_eq!(results.len(), 1);
573 assert!(matches!(&results[0], GameEvent::DraftBot(_)));
574 }
575
576 #[test]
577 fn test_route_draft_human_notify() {
578 let router = Router::new();
579 let payload = serde_json::json!({
580 "draftId": "abc-123-def",
581 "SelfPack": 0,
582 "SelfPick": 0,
583 "PackCards": "12345,67890,11111"
584 });
585 let body = format!("[UnityCrossThreadLogger]Draft.Notify\n{payload}",);
586 let entry = unity_entry(&body);
587
588 let results = router.route(&entry);
589 assert_eq!(results.len(), 1);
590 assert!(matches!(&results[0], GameEvent::DraftHuman(_)));
591 }
592
593 #[test]
594 fn test_route_collection_event() {
595 let router = Router::new();
596 let payload = serde_json::json!({
597 "PlayerCards": { "98535": 4, "12345": 2 },
598 "InventoryInfo": { "Gems": 100 }
599 });
600 let body = format!(
601 "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
602 <== StartHook(abc-123)\n{payload}",
603 );
604 let entry = unity_entry(&body);
605
606 let results = router.route(&entry);
607 assert_eq!(results.len(), 1);
608 assert!(matches!(&results[0], GameEvent::Collection(_)));
610 }
611
612 #[test]
613 fn test_route_inventory_event() {
614 let router = Router::new();
615 let payload = serde_json::json!({
616 "InventoryInfo": { "Gems": 100, "Gold": 5000 }
617 });
618 let body = format!(
619 "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
620 <== StartHook(abc-123)\n{payload}",
621 );
622 let entry = unity_entry(&body);
623
624 let results = router.route(&entry);
625 assert_eq!(results.len(), 1);
626 assert!(matches!(&results[0], GameEvent::Inventory(_)));
628 }
629 }
630
631 mod unknown_entries {
634 use super::*;
635
636 #[test]
637 fn test_route_unknown_entry_returns_empty() {
638 let router = Router::new();
639 let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
640 some unrecognized content here";
641 let entry = unity_entry(body);
642
643 let results = router.route(&entry);
644 assert!(results.is_empty());
645 }
646
647 #[test]
648 fn test_route_unknown_entry_increments_counter() {
649 let router = Router::new();
650 let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n\
651 unrecognized content";
652 let entry = unity_entry(body);
653
654 router.route(&entry);
655 assert_eq!(router.stats().unknown_count(), 1);
656 assert_eq!(router.stats().routed_count(), 0);
657 }
658
659 #[test]
660 fn test_route_multiple_unknown_entries_accumulates() {
661 let router = Router::new();
662
663 for i in 0..5 {
664 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\nunknown_{i}",);
665 let entry = unity_entry(&body);
666 router.route(&entry);
667 }
668
669 assert_eq!(router.stats().unknown_count(), 5);
670 assert_eq!(router.stats().routed_count(), 0);
671 }
672
673 #[test]
674 fn test_route_empty_body_after_header_returns_empty() {
675 let router = Router::new();
676 let body = "[UnityCrossThreadLogger]";
677 let entry = unity_entry(body);
678
679 let results = router.route(&entry);
680 assert!(results.is_empty());
682 assert_eq!(router.stats().timestamp_failure_count(), 1);
683 assert_eq!(router.stats().unknown_count(), 1);
684 }
685
686 #[test]
687 fn test_route_no_timestamp_increments_timestamp_failure() {
688 let router = Router::new();
689 let body = "[UnityCrossThreadLogger]just some text without a timestamp";
690 let entry = unity_entry(body);
691
692 let results = router.route(&entry);
693 assert!(results.is_empty());
695 assert_eq!(router.stats().timestamp_failure_count(), 1);
696 assert_eq!(router.stats().unknown_count(), 1);
697 }
698
699 #[test]
700 fn test_route_no_timestamp_session_still_routes() {
701 let router = Router::new();
702 let body = "[UnityCrossThreadLogger]Updated account. \
704 DisplayName:Player, \
705 AccountID:abc123, \
706 Token:token";
707 let entry = unity_entry(body);
708
709 let results = router.route(&entry);
710 assert_eq!(results.len(), 1);
711 assert!(matches!(&results[0], GameEvent::Session(_)));
713 assert_eq!(router.stats().timestamp_failure_count(), 1);
714 assert_eq!(router.stats().routed_count(), 1);
715 }
716
717 #[test]
718 fn test_route_no_timestamp_passes_none_to_metadata() {
719 let router = Router::new();
720 let body = "[UnityCrossThreadLogger]Updated account. \
723 DisplayName:Player, \
724 AccountID:abc123, \
725 Token:token";
726 let entry = unity_entry(body);
727
728 let results = router.route(&entry);
729 assert_eq!(results.len(), 1);
730 assert!(
731 results[0].metadata().timestamp().is_none(),
732 "entries without parseable timestamps should have None timestamp"
733 );
734 }
735
736 #[test]
737 fn test_route_with_timestamp_passes_some_to_metadata() {
738 let router = Router::new();
739 let payload = serde_json::json!({
740 "greToClientEvent": {
741 "greToClientMessages": [{
742 "type": "GREMessageType_GameStateMessage",
743 "gameStateMessage": {
744 "gameInfo": { "stage": "GameStage_Play" },
745 "gameObjects": [],
746 "zones": []
747 }
748 }]
749 }
750 });
751 let body = format!("[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\n{payload}");
752 let entry = unity_entry(&body);
753
754 let results = router.route(&entry);
755 assert_eq!(results.len(), 1);
756 assert!(
757 results[0].metadata().timestamp().is_some(),
758 "entries with parseable timestamps should have Some timestamp"
759 );
760 }
761 }
762
763 mod stats {
766 use super::*;
767
768 #[test]
769 fn test_stats_initial_values_are_zero() {
770 let router = Router::new();
771 assert_eq!(router.stats().routed_count(), 0);
772 assert_eq!(router.stats().unknown_count(), 0);
773 assert_eq!(router.stats().timestamp_failure_count(), 0);
774 }
775
776 #[test]
777 fn test_stats_reset_clears_all_counters() {
778 let router = Router::new();
779
780 let body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\nunknown";
782 let entry = unity_entry(body);
783 router.route(&entry);
784 router.route(&entry);
785
786 assert_eq!(router.stats().unknown_count(), 2);
787
788 router.stats().reset();
789
790 assert_eq!(router.stats().routed_count(), 0);
791 assert_eq!(router.stats().unknown_count(), 0);
792 assert_eq!(router.stats().timestamp_failure_count(), 0);
793 }
794
795 #[test]
796 fn test_stats_mixed_routing() {
797 let router = Router::new();
798
799 let known_body = "[UnityCrossThreadLogger]Updated account. \
801 DisplayName:Player, \
802 AccountID:abc123, \
803 Token:token";
804 router.route(&unity_entry(known_body));
805
806 let unknown_body = "[UnityCrossThreadLogger]2/25/2026 12:00:00 PM\nunknown";
808 router.route(&unity_entry(unknown_body));
809
810 let bad_ts_body = "[UnityCrossThreadLogger]";
812 router.route(&unity_entry(bad_ts_body));
813
814 assert_eq!(router.stats().routed_count(), 1);
815 assert_eq!(router.stats().unknown_count(), 2);
817 assert_eq!(router.stats().timestamp_failure_count(), 2);
819 }
820 }
821
822 mod default_impl {
825 use super::*;
826
827 #[test]
828 fn test_router_default_creates_functional_router() {
829 let router = Router::default();
830 assert_eq!(router.stats().routed_count(), 0);
831 assert_eq!(router.stats().unknown_count(), 0);
832 }
833 }
834
835 mod client_gre_entries {
838 use super::*;
839
840 #[test]
841 fn test_route_client_gre_entry() {
842 let router = Router::new();
843 let payload = serde_json::json!({
844 "greToClientEvent": {
845 "greToClientMessages": [{
846 "type": "GREMessageType_GameStateMessage",
847 "gameStateMessage": {
848 "gameInfo": { "stage": "GameStage_Play" },
849 "gameObjects": [],
850 "zones": []
851 }
852 }]
853 }
854 });
855 let body = format!("[Client GRE]2/25/2026 12:00:00 PM\n{payload}");
856 let entry = gre_entry(&body);
857
858 let results = router.route(&entry);
859 assert_eq!(results.len(), 1);
860 assert!(matches!(&results[0], GameEvent::GameState(_)));
861 }
862 }
863
864 mod metadata_entries {
867 use super::*;
868
869 fn metadata_entry(body: &str) -> LogEntry {
871 LogEntry {
872 header: EntryHeader::Metadata,
873 body: body.to_owned(),
874 }
875 }
876
877 #[test]
878 fn test_route_detailed_logs_enabled() {
879 let router = Router::new();
880 let entry = metadata_entry("DETAILED LOGS: ENABLED");
881
882 let results = router.route(&entry);
883 assert_eq!(results.len(), 1);
884 assert!(matches!(&results[0], GameEvent::DetailedLoggingStatus(_)));
885 if let GameEvent::DetailedLoggingStatus(ref e) = results[0] {
886 assert_eq!(e.enabled(), Some(true));
887 }
888 assert_eq!(router.stats().routed_count(), 1);
889 }
890
891 #[test]
892 fn test_route_detailed_logs_disabled() {
893 let router = Router::new();
894 let entry = metadata_entry("DETAILED LOGS: DISABLED");
895
896 let results = router.route(&entry);
897 assert_eq!(results.len(), 1);
898 assert!(matches!(&results[0], GameEvent::DetailedLoggingStatus(_)));
899 if let GameEvent::DetailedLoggingStatus(ref e) = results[0] {
900 assert_eq!(e.enabled(), Some(false));
901 }
902 }
903
904 #[test]
905 fn test_route_metadata_no_timestamp_failure() {
906 let router = Router::new();
907 let entry = metadata_entry("DETAILED LOGS: ENABLED");
908
909 router.route(&entry);
910 assert_eq!(router.stats().timestamp_failure_count(), 1);
913 assert_eq!(router.stats().routed_count(), 1);
915 }
916
917 #[test]
918 fn test_route_unrecognized_metadata_returns_empty() {
919 let router = Router::new();
920 let entry = metadata_entry("SOME OTHER METADATA");
921
922 let results = router.route(&entry);
923 assert!(results.is_empty());
924 assert_eq!(router.stats().unknown_count(), 1);
925 }
926 }
927}