Skip to main content

manasight_parser/
lib.rs

1//! MTG Arena log file parser.
2//!
3//! This library crate reads Arena's `Player.log`, parses raw log entries
4//! into typed game events, and distributes them via an async broadcast
5//! channel. It is designed to run on the caller's Tokio runtime — it does
6//! not initialize its own runtime or logger.
7//!
8//! # Quick start (async, desktop)
9//!
10//! Requires the `tailer` feature (enabled by default):
11//!
12//! ```rust,no_run,ignore
13//! use std::path::Path;
14//! use manasight_parser::MtgaEventStream;
15//!
16//! # async fn example() -> Result<(), Box<dyn std::error::Error>> {
17//! let (stream, mut subscriber) = MtgaEventStream::start(Path::new("Player.log")).await?;
18//!
19//! while let Some(event) = subscriber.recv().await {
20//!     println!("got event: {event:?}");
21//! }
22//! # Ok(())
23//! # }
24//! ```
25//!
26//! # Quick start (sync, WASM)
27//!
28//! With `--no-default-features --features brace_depth_flush` (no `tailer`),
29//! only the pure-sync subset of the crate is available:
30//!
31//! ```rust
32//! use manasight_parser::parse_whole_log;
33//!
34//! let input = ""; // replace with actual Player.log content
35//! let events = parse_whole_log(input);
36//! println!("parsed {} events", events.len());
37//! ```
38//!
39//! # Architecture
40//!
41//! ```text
42//! Player.log → File Tailer → Entry Buffer → Router → Parsers → Event Bus
43//! ```
44//!
45//! - **`log`** module: file discovery, polling tailer, entry accumulation, timestamps
46//! - **`router`**: dispatches raw entries to the correct category parser
47//! - **`parsers`**: one sub-module per event category
48//! - **`events`**: public event type enums/structs (the parser's output contract)
49//! - **`event_bus`**: `tokio::broadcast` channel for fan-out to subscribers (requires `tailer` feature)
50//! - **`stream`**: public entry point ([`MtgaEventStream`]) (requires `tailer` feature)
51
52#[cfg(feature = "tailer")]
53pub mod event_bus;
54pub mod events;
55pub mod log;
56pub mod parsers;
57pub mod router;
58pub mod sanitize;
59#[cfg(feature = "tailer")]
60pub mod stream;
61pub mod util;
62#[cfg(feature = "wasm")]
63pub mod wasm;
64
65// ---------------------------------------------------------------------------
66// Re-exports — public API surface
67// ---------------------------------------------------------------------------
68
69#[cfg(feature = "tailer")]
70pub use event_bus::Subscriber;
71pub use events::{
72    ClientActionEvent, DeckCollectionEvent, DeckSubmissionEvent, DetailedLoggingStatusEvent,
73    DraftBotEvent, DraftCompleteEvent, DraftHumanEvent, EventLifecycleEvent, EventMetadata,
74    GameEvent, GameResultEvent, GameStateEvent, InventoryEvent, LogFileRotatedEvent,
75    MatchStateEvent, PerformanceClass, RankEvent, SessionEvent, TruncationEvent,
76};
77pub use sanitize::{scrub_raw_log, scrub_raw_log_with, ScrubOptions};
78#[cfg(feature = "tailer")]
79pub use stream::{MtgaEventStream, StreamError};
80pub use util::{compress_log, content_hash};
81
82// ---------------------------------------------------------------------------
83// Sync entry point — WASM-compatible
84// ---------------------------------------------------------------------------
85
86/// Parses an entire MTG Arena `Player.log` string into a [`Vec`] of [`GameEvent`]s.
87///
88/// This is a pure, synchronous function with no I/O and no async dependencies.
89/// It is suitable for use in WASM environments or any context where the file
90/// content has already been loaded into memory.
91///
92/// # How it works
93///
94/// Lines are fed through [`log::entry::LineBuffer`] to reassemble log entries,
95/// then dispatched via [`router::Router`] — the same core pipeline the async
96/// desktop path uses. A final [`LineBuffer::flush`](log::entry::LineBuffer::flush)
97/// drains any trailing entry that was not followed by a new header.
98///
99/// # Example
100///
101/// ```rust
102/// use manasight_parser::parse_whole_log;
103///
104/// let input = "DETAILED LOGS: ENABLED\n";
105/// let events = parse_whole_log(input);
106/// assert_eq!(events.len(), 1);
107/// ```
108pub fn parse_whole_log(input: &str) -> Vec<GameEvent> {
109    let mut buffer = log::entry::LineBuffer::new();
110    let router = router::Router::new();
111    let mut events = Vec::new();
112
113    for line in input.lines() {
114        for entry in buffer.push_line(line) {
115            events.extend(router.route(&entry));
116        }
117    }
118
119    // Drain any trailing entry that was not followed by a new header.
120    if let Some(entry) = buffer.flush() {
121        events.extend(router.route(&entry));
122    }
123
124    events
125}