Skip to main content

source2_demo_macros/
lib.rs

1#![doc = include_str!("../README.md")]
2//!
3//! # Procedural Macros for Source 2 Replay Parser
4//!
5//! This crate provides procedural macros that simplify implementing the Observer trait.
6//!
7//! ## Overview
8//!
9//! The macros automate much of the boilerplate required for handling replay events:
10//! - Automatically implement the Observer trait
11//! - Generate protobuf message decoding
12//! - Set up interest flags based on used methods
13//! - Handle optional `Context` parameters
14//!
15//! ## Main Macros
16//!
17//! ### `#[observer]` - Main implementation macro
18//!
19//! Automatically implements the Observer trait with all necessary methods.
20//! Takes an impl block with event handlers and generates the full implementation.
21//!
22//! ```no_run
23//! # use source2_demo::prelude::*;
24//! #[derive(Default)]
25//! struct MyObserver;
26//!
27//! #[observer]
28//! impl MyObserver {
29//!     #[on_tick_start]
30//!     fn on_tick(&mut self, ctx: &Context) -> ObserverResult {
31//!         println!("Tick: {}", ctx.tick());
32//!         Ok(())
33//!     }
34//! }
35//! ```
36//!
37//! ### Event Handler Macros
38//!
39//! These attributes mark methods as event handlers within an #[observer] impl:
40//!
41//! - `#[on_message]` - Handles protobuf messages
42//! - `#[on_tick_start]` - Called at tick start
43//! - `#[on_tick_end]` - Called at tick end
44//! - `#[on_entity]` - Called for entity changes
45//! - `#[on_game_event]` - Called for game events
46//! - `#[on_string_table]` - Called for string table updates
47//! - `#[on_stop]` - Called when replay ends
48//! - `#[on_combat_log]` - Called for combat log entries (Dota 2 only)
49//!
50//! ### Trait Markers
51//!
52//! These mark impl blocks with which data types to track:
53//!
54//! - `#[uses_entities]` - Track entities
55//! - `#[uses_string_tables]` - Track string tables
56//! - `#[uses_game_events]` - Track game events
57//! - `#[uses_combat_log]` - Track combat log (Dota 2 only)
58//!
59//! ## How the Macros Work
60//!
61//! The `#[observer]` macro:
62//!
63//! 1. Scans all methods for event handler attributes
64//! 2. Collects parameter types to generate interest flags
65//! 3. Creates decoding logic for protobuf messages
66//! 4. Generates the full `Observer` trait implementation
67//! 5. Handles optional parameters (Context is optional)
68//!
69//! ## Complete Example
70//!
71//! ```no_run
72//! use source2_demo::prelude::*;
73//!
74//! #[derive(Default)]
75//! struct GameAnalyzer {
76//!     tick_count: u32,
77//!     entity_count: u32,
78//! }
79//!
80//! #[observer]
81//! #[uses_entities]
82//! impl GameAnalyzer {
83//!     #[on_tick_start]
84//!     fn on_tick(&mut self, ctx: &Context) -> ObserverResult {
85//!         self.tick_count += 1;
86//!         Ok(())
87//!     }
88//!
89//!     #[on_entity]
90//!     fn on_entity_create(&mut self, event: EntityEvents, entity: &Entity) -> ObserverResult {
91//!         if event == EntityEvents::Created {
92//!             self.entity_count += 1;
93//!         }
94//!         Ok(())
95//!     }
96//!
97//!     #[on_message]
98//!     fn on_chat(&mut self, msg: CDotaUserMsgChatMessage) -> ObserverResult {
99//!         println!("Chat: {}", msg.message_text());
100//!         Ok(())
101//!     }
102//! }
103//! ```
104//!
105//! ## Key Features
106//!
107//! - **Automatic Interest Management**: Macro automatically determines which interests
108//!   to set based on which handler methods are defined
109//! - **Optional Context**: Pass `ctx: &Context` to any handler or omit it
110//! - **Automatic Message Decoding**: `#[on_message]` handlers automatically decode
111//!   protobuf messages based on parameter type
112//! - **Filtering Attributes**: Filter events with string arguments:
113//!   - `#[on_entity("CDOTA_Unit_Hero_Axe")]` - Only heroes named Axe
114//!   - `#[on_game_event("player_death")]` - Only death events
115//!   - `#[on_string_table("userinfo")]` - Only userinfo table updates
116//! - **Game-Specific Handlers**: Use `#[on_dota_user_message]`, `#[on_citadel_user_message]`,
117//!   etc. for game-specific message types
118
119mod protobuf_map;
120
121use crate::protobuf_map::get_enum_from_struct;
122use proc_macro::{TokenStream};
123use quote::{quote, ToTokens};
124use syn::{parse_macro_input, parse::Parser, punctuated::Punctuated, Ident, ItemImpl, Token, Type, FnArg};
125
126#[allow(unused_mut)]
127#[proc_macro_attribute]
128/// Implements the Observer trait for your struct.
129///
130/// This is the main macro that ties everything together. Apply it to an `impl` block
131/// that contains event handler methods marked with `#[on_*]` attributes.
132///
133/// # What It Does
134///
135/// - Automatically implements the Observer trait
136/// - Generates code to call all handler methods at appropriate times
137/// - Sets up interest flags based on which handlers are defined
138/// - Decodes protobuf messages and passes them to handlers
139/// - Filters events based on optional string arguments
140///
141/// # Handler Attributes
142///
143/// Use these attributes inside the impl block to mark event handlers:
144///
145/// - `#[on_tick_start]` - Called at the start of each tick
146/// - `#[on_tick_end]` - Called at the end of each tick
147/// - `#[on_entity]` - Called when entities change
148/// - `#[on_entity("ClassName")]` - Only for specific entity classes
149/// - `#[on_message]` - Called for protobuf messages (type inferred from param)
150/// - `#[on_game_event]` - Called for all game events
151/// - `#[on_game_event("event_name")]` - Only for specific events
152/// - `#[on_string_table]` - Called when string tables update
153/// - `#[on_string_table("table_name")]` - Only for specific tables
154/// - `#[on_stop]` - Called when replay ends
155/// - `#[on_combat_log]` - Called for combat log entries (Dota 2 only)
156///
157/// # Trait Attributes
158///
159/// Apply these to the `impl` block or individual methods to enable tracking:
160///
161/// - `#[uses_entities]` - Enable entity tracking
162/// - `#[uses_string_tables]` - Enable string table tracking
163/// - `#[uses_game_events]` - Enable game event tracking
164/// - `#[uses_combat_log]` - Enable combat log tracking (Dota 2 only)
165///
166/// # Parameter Guidelines
167///
168/// Handlers can have these parameters (all optional except `&mut self`):
169/// - `ctx: &Context` - Current replay state (always optional)
170/// - Specific parameters depending on the handler:
171///   - `event: EntityEvents` (on_entity)
172///   - `entity: &Entity` (on_entity)
173///   - `ge: &GameEvent` (on_game_event)
174///   - `table: &StringTable` (on_string_table)
175///   - `modified: &[i32]` (on_string_table)
176///   - `cle: &CombatLogEntry` (on_combat_log)
177///   - Protobuf message types (on_message)
178///
179/// # Examples
180///
181/// ## Basic observer
182///
183/// ```no_run
184/// # use source2_demo::prelude::*;
185/// #[derive(Default)]
186/// struct BasicObserver;
187///
188/// #[observer]
189/// impl BasicObserver {
190///     #[on_tick_start]
191///     fn on_tick_start(&mut self, ctx: &Context) -> ObserverResult {
192///         println!("Tick: {}", ctx.tick());
193///         Ok(())
194///     }
195/// }
196/// ```
197///
198/// ## With entity tracking
199///
200/// ```no_run
201/// # use source2_demo::prelude::*;
202/// #[derive(Default)]
203/// struct EntityTracker;
204///
205/// #[observer]
206/// #[uses_entities]
207/// impl EntityTracker {
208///     #[on_entity]
209///     fn on_hero_created(&mut self, event: EntityEvents, entity: &Entity) -> ObserverResult {
210///         if event == EntityEvents::Created && entity.class().name().starts_with("CDOTA_Unit_Hero_") {
211///             println!("Hero created: {}", entity.class().name());
212///         }
213///         Ok(())
214///     }
215/// }
216/// ```
217///
218/// ## With multiple handlers
219///
220/// ```no_run
221/// # use source2_demo::prelude::*;
222/// #[derive(Default)]
223/// struct ComplexObserver {
224///     ticks: u32,
225///     messages: u32,
226/// }
227///
228/// #[observer]
229/// impl ComplexObserver {
230///     #[on_tick_start]
231///     fn on_tick(&mut self, ctx: &Context) -> ObserverResult {
232///         self.ticks += 1;
233///         Ok(())
234///     }
235///
236///     #[on_message]
237///     fn on_chat(&mut self, msg: CDotaUserMsgChatMessage) -> ObserverResult {
238///         self.messages += 1;
239///         println!("Message: {}", msg.message_text());
240///         Ok(())
241///     }
242///
243///     #[on_stop]
244///     fn on_replay_end(&mut self) -> ObserverResult {
245///         println!("Total ticks: {}, messages: {}", self.ticks, self.messages);
246///         Ok(())
247///     }
248/// }
249/// ```
250///
251/// ## With game event filtering
252///
253/// ```no_run
254/// # use source2_demo::prelude::*;
255/// #[derive(Default)]
256/// struct DeathTracker {
257///     deaths: u32,
258/// }
259///
260/// #[observer]
261/// impl DeathTracker {
262///     #[on_game_event("player_death")]
263///     fn on_death(&mut self, ctx: &Context, ge: &GameEvent) -> ObserverResult {
264///         self.deaths += 1;
265///         Ok(())
266///     }
267/// }
268/// ```
269///
270/// # Interest Flags
271///
272/// The macro automatically determines which interest flags to set based on which
273/// handlers are defined. For example:
274/// - If you have `#[on_tick_start]`, `Interests::TICK_START` is added
275/// - If you have `#[on_entity]`, both `ENABLE_ENTITY` and `TRACK_ENTITY` are added
276/// - If you have `#[on_message]` handlers, appropriate message interest flags are added
277///
278/// You can also use trait attributes like `#[uses_entities]` to manually ensure
279/// certain interests are set.
280pub fn observer(attr: TokenStream, item: TokenStream) -> TokenStream {
281    let mut mode_all = false;
282
283    if !attr.is_empty() {
284        let parser = Punctuated::<Ident, Token![,]>::parse_terminated;
285        if let Ok(idents) = parser.parse(attr.clone()) {
286            for id in idents {
287                if id == "manual" { mode_all = false; }
288                if id == "all" { mode_all = true; }
289            }
290        }
291    }
292
293    let input = parse_macro_input!(item as ItemImpl);
294    let struct_name = &input.self_ty;
295
296    let mut interests = quote!(::source2_demo::Interests::empty());
297    macro_rules! add_flag {
298        ($flag:ident) => {
299            interests = quote!(#interests | ::source2_demo::Interests::$flag);
300        };
301    }
302
303    for a in &input.attrs {
304        if a.path().is_ident("uses_entities") { add_flag!(ENABLE_ENTITY); }
305        if a.path().is_ident("uses_string_tables") { add_flag!(ENABLE_STRINGTAB); }
306        if a.path().is_ident("uses_game_events") { add_flag!(BASE_GE); }
307        #[cfg(feature = "dota")]
308        if a.path().is_ident("uses_combat_log") {
309            add_flag!(ENABLE_STRINGTAB);
310            add_flag!(COMBAT_LOG);
311        }
312    }
313
314    let mut has_demo = false;
315    let mut has_net = false;
316    let mut has_svc = false;
317    let mut has_base_um = false;
318    let mut has_base_ge = false;
319    let mut has_tick_start = false;
320    let mut has_tick_end = false;
321    let mut has_entity = false;
322    let mut has_entity_track = false;
323    let mut has_string_table = false;
324    let mut has_string_table_track = false;
325    let mut has_stop = false;
326
327    #[cfg(feature = "dota")]
328    let mut has_dota_um = false;
329    #[cfg(feature = "dota")]
330    let mut has_combat_log = false;
331
332    #[cfg(feature = "citadel")]
333    let mut has_cita_um = false;
334    #[cfg(feature = "citadel")]
335    let mut has_cita_ge = false;
336
337    #[cfg(feature = "cs2")]
338    let mut has_cs2_um = false;
339    #[cfg(feature = "cs2")]
340    let mut has_cs2_ge = false;
341
342    #[cfg(feature = "dota")]
343    let mut on_combat_log_body = quote!();
344    #[cfg(feature = "dota")]
345    let mut on_dota_user_message_body = quote!();
346
347    #[cfg(feature = "citadel")]
348    let mut on_citadel_user_message_body = quote!();
349    #[cfg(feature = "citadel")]
350    let mut on_citadel_game_event_body = quote!();
351
352    #[cfg(feature = "cs2")]
353    let mut on_cs2_user_message_body = quote!();
354    #[cfg(feature = "cs2")]
355    let mut on_cs2_game_event_body = quote!();
356
357    let mut on_svc_message_body = quote!();
358    let mut on_net_message_body = quote!();
359    let mut on_base_game_event_body = quote!();
360    let mut on_demo_command_body = quote!();
361    let mut on_base_user_message_body = quote!();
362    let mut on_tick_start_body = quote!();
363    let mut on_tick_end_body = quote!();
364    let mut on_entity_body = quote!();
365    let mut on_game_event_body = quote!();
366    let mut on_string_table_body = quote!();
367    let mut on_stop_body = quote!();
368
369    for item in &input.items {
370        if let syn::ImplItem::Fn(method) = item {
371            for attr in &method.attrs {
372                for a in &method.attrs {
373                    if a.path().is_ident("uses_entities") { add_flag!(ENABLE_ENTITY); }
374                    if a.path().is_ident("uses_string_tables") { add_flag!(ENABLE_STRINGTAB); }
375                    if a.path().is_ident("uses_game_events") { add_flag!(BASE_GE); }
376                    #[cfg(feature = "dota")]
377                    if a.path().is_ident("uses_combat_log") { add_flag!(DOTA_UM); }
378                }
379
380                let method_name = method.sig.ident.clone();
381                let mut args = vec![];
382
383                let (arg_type, _) = get_arg_type(method, 1);
384                if arg_type.to_token_stream().to_string() == "Context" {
385                    args.push(quote! { ctx })
386                }
387
388                if let Some(ident) = attr.path().get_ident() {
389                    match ident.to_string().as_str() {
390                        "on_tick_start" => {
391                            has_tick_start = true;
392                            on_tick_start_body.extend(quote! {
393                            self.#method_name(#(#args),*)?;
394                        })
395                        }
396                        "on_tick_end" => {
397                            has_tick_end = true;
398                            on_tick_end_body.extend(quote! {
399                            self.#method_name(#(#args),*)?;
400                        })
401                        }
402                        "on_stop" => {
403                            has_stop = true;
404                            on_stop_body.extend(quote! {
405                                self.#method_name(#(#args),*)?;
406                            });
407                        }
408                        #[cfg(feature = "dota")]
409                        "on_combat_log" => {
410                            args.push(quote! { cle });
411
412                            on_combat_log_body.extend(quote! {
413                                self.#method_name(#(#args),*)?;
414                            })
415                        }
416                        "on_entity" => {
417                            has_entity_track = true;
418                            let (arg_type, is_ref) = get_arg_type(method, args.len() + 1);
419
420                            if arg_type.to_token_stream().to_string() == "EntityEvents" {
421                                if is_ref {
422                                    args.push(quote! { &event });
423                                } else {
424                                    args.push(quote! { event });
425                                }
426                            }
427
428                            args.push(quote! { entity });
429
430                            on_entity_body.extend(if let Ok(entity_class) = attr.parse_args::<syn::LitStr>() {
431                                quote! {
432                                    if entity.class().name() == #entity_class {
433                                        self.#method_name(#(#args),*)?;
434                                    }
435                                }
436                            } else {
437                                quote! {
438                                    self.#method_name(#(#args),*)?;
439                                }
440                            });
441                        }
442                        "on_game_event" => {
443                            args.push(quote! { ge });
444                            on_game_event_body.extend(if let Ok(event_name) = attr.parse_args::<syn::LitStr>() {
445                                quote! {
446                                    if ge.name() == #event_name {
447                                        self.#method_name(#(#args),*)?;
448                                    }
449                                }
450                            } else {
451                                quote! {
452                                    self.#method_name(#(#args),*)?;
453                                }
454                            });
455                        }
456                        "on_string_table" => {
457                            has_string_table_track = true;
458                            args.push(quote! { table });
459                            args.push(quote! { modified });
460                            on_string_table_body.extend(if let Ok(table_name) = attr.parse_args::<syn::LitStr>() {
461                                quote! {
462                                    if table.name() == #table_name {
463                                        self.#method_name(#(#args),*)?;
464                                    }
465                                }
466                            } else {
467                                quote! {
468                                    self.#method_name(#(#args),*)?;
469                                }
470                            });
471                        }
472                        "on_message" => {
473                            let (arg_type, is_ref) = get_arg_type(method, args.len() + 1);
474                            let enum_type = get_enum_from_struct(arg_type.to_token_stream().to_string().as_str());
475                            let type_string = enum_type.to_token_stream().to_string();
476                            let root = type_string.split("::").collect::<Vec<_>>()[0].trim();
477
478                            args.push(if is_ref {
479                                quote! { &message }
480                            } else {
481                                quote! { message }
482                            });
483
484                            macro_rules! extend {
485                                ($body: ident) => {
486                                    $body.extend(quote! {
487                                        if msg_type == #enum_type {
488                                            if let Ok(message) = #arg_type::decode(msg) {
489                                                self.#method_name(#(#args),*)?;
490                                            }
491                                        }
492                                    })
493                                };
494                            }
495
496                            match root {
497                                "EDemoCommands" => has_demo = true,
498                                "EBaseUserMessages" => has_base_um = true,
499                                "EBaseGameEvents" => has_base_ge = true,
500                                "SvcMessages" => has_svc = true,
501                                "NetMessages" => has_net = true,
502                                #[cfg(feature = "dota")]
503                                "EDotaUserMessages" => has_dota_um = true,
504                                #[cfg(feature = "citadel")]
505                                "CitadelUserMessageIds" => has_cita_um = true,
506                                #[cfg(feature = "citadel")]
507                                "ECitadelGameEvents" => has_cita_ge = true,
508                                #[cfg(feature = "cs2")]
509                                "ECstrike15UserMessages" => has_cs2_um = true,
510                                #[cfg(feature = "cs2")]
511                                "ECsgoGameEvents" => has_cs2_ge = true,
512                                _ => {}
513                            }
514
515
516                            match root {
517                                "EDemoCommands" => extend!(on_demo_command_body),
518                                "EBaseUserMessages" => extend!(on_base_user_message_body),
519                                "EBaseGameEvents" => extend!(on_base_game_event_body),
520                                "SvcMessages" => extend!(on_svc_message_body),
521                                "NetMessages" => extend!(on_net_message_body),
522
523                                #[cfg(feature = "dota")]
524                                "EDotaUserMessages" => extend!(on_dota_user_message_body),
525                                #[cfg(feature = "citadel")]
526                                "CitadelUserMessageIds" => extend!(on_citadel_user_message_body),
527                                #[cfg(feature = "citadel")]
528                                "ECitadelGameEvents" => extend!(on_citadel_game_event_body),
529                                #[cfg(feature = "cs2")]
530                                "ECstrike15UserMessages" => extend!(on_cs2_user_message_body),
531                                #[cfg(feature = "cs2")]
532                                "ECsgoGameEvents" => extend!(on_cs2_game_event_body),
533
534                                x => unreachable!("{}", x),
535                            }
536                        }
537                        _ => {}
538                    }
539                }
540            }
541        }
542    }
543
544    let mut obs_body = quote! {
545        fn on_base_user_message(
546            &mut self,
547            ctx: &Context,
548            msg_type: EBaseUserMessages,
549            msg: &[u8],
550        ) -> ObserverResult {
551            #on_base_user_message_body
552            Ok(())
553        }
554
555        fn on_svc_message(
556            &mut self,
557            ctx: &Context,
558            msg_type: SvcMessages,
559            msg: &[u8],
560        ) -> ObserverResult {
561            #on_svc_message_body
562            Ok(())
563        }
564
565        fn on_net_message(
566            &mut self,
567            ctx: &Context,
568            msg_type: NetMessages,
569            msg: &[u8],
570        ) -> ObserverResult {
571            #on_net_message_body
572            Ok(())
573        }
574
575        fn on_base_game_event(
576            &mut self,
577            ctx: &Context,
578            msg_type: EBaseGameEvents,
579            msg: &[u8],
580        ) -> ObserverResult {
581            #on_base_game_event_body
582            Ok(())
583        }
584
585        fn on_demo_command(
586            &mut self,
587            ctx: &Context,
588            msg_type: EDemoCommands,
589            msg: &[u8],
590        ) -> ObserverResult {
591            #on_demo_command_body
592            Ok(())
593        }
594
595        fn on_tick_start(
596            &mut self,
597            ctx: &Context,
598        ) -> ObserverResult {
599            #on_tick_start_body
600            Ok(())
601        }
602
603        fn on_tick_end(
604            &mut self,
605            ctx: &Context,
606        ) -> ObserverResult {
607            #on_tick_end_body
608            Ok(())
609        }
610
611        fn on_entity(
612            &mut self,
613            ctx: &Context,
614            event: EntityEvents,
615            entity: &Entity,
616        ) -> ObserverResult {
617            #on_entity_body
618            Ok(())
619        }
620
621        fn on_game_event(
622            &mut self,
623            ctx: &Context,
624            ge: &GameEvent
625        ) -> ObserverResult {
626            #on_game_event_body
627            Ok(())
628        }
629
630        fn on_string_table(
631            &mut self,
632            ctx: &Context,
633            table: &StringTable,
634            modified: &[i32]
635        ) -> ObserverResult {
636            #on_string_table_body
637            Ok(())
638        }
639
640        fn on_stop(
641            &mut self,
642            ctx: &Context,
643        ) -> ObserverResult {
644            #on_stop_body
645            Ok(())
646        }
647    };
648
649    #[cfg(feature = "dota")]
650    obs_body.extend(quote! {
651        fn on_combat_log(
652            &mut self,
653            ctx: &Context,
654            cle: &CombatLogEntry
655        ) -> ObserverResult {
656            #on_combat_log_body
657            Ok(())
658        }
659
660        fn on_dota_user_message(
661            &mut self,
662            ctx: &Context,
663            msg_type: EDotaUserMessages,
664            msg: &[u8],
665        ) -> ObserverResult {
666            #on_dota_user_message_body
667            Ok(())
668        }
669    });
670
671    #[cfg(feature = "citadel")]
672    obs_body.extend(quote! {
673        fn on_citadel_user_message(
674            &mut self,
675            ctx: &Context,
676            msg_type: CitadelUserMessageIds,
677            msg: &[u8],
678        ) -> ObserverResult {
679            #on_citadel_user_message_body
680            Ok(())
681        }
682
683        fn on_citadel_game_event(
684            &mut self,
685            ctx: &Context,
686            msg_type: ECitadelGameEvents,
687            msg: &[u8],
688        ) -> ObserverResult {
689            #on_citadel_game_event_body
690            Ok(())
691        }
692    });
693
694    #[cfg(feature = "cs2")]
695    obs_body.extend(quote! {
696        fn on_cs2_user_message(
697            &mut self,
698            ctx: &Context,
699            msg_type: ECstrike15UserMessages,
700            msg: &[u8],
701        ) -> ObserverResult {
702            #on_cs2_user_message_body
703            Ok(())
704        }
705
706        fn on_cs2_game_event(
707            &mut self,
708            ctx: &Context,
709            msg_type: ECsgoGameEvents,
710            msg: &[u8],
711        ) -> ObserverResult {
712            #on_cs2_game_event_body
713            Ok(())
714        }
715    });
716
717    macro_rules! add_if { ($cond:expr, $flag:ident) => {
718        if $cond { interests = quote!(#interests | ::source2_demo::Interests::$flag); }
719    }}
720
721    if mode_all {
722        interests = quote!(::source2_demo::Interests::all());
723    }
724
725    add_if!(has_demo,       DEMO);
726    add_if!(has_net,        NET);
727    add_if!(has_svc,        SVC);
728    add_if!(has_base_um,    BASE_UM);
729    add_if!(has_base_ge,    BASE_GE);
730    add_if!(has_tick_start, TICK_START);
731    add_if!(has_tick_end,   TICK_END);
732    add_if!(has_entity,     ENABLE_ENTITY);
733    add_if!(has_entity_track,     TRACK_ENTITY);
734    add_if!(has_string_table,  ENABLE_STRINGTAB);
735    add_if!(has_string_table_track,  TRACK_STRINGTAB);
736    add_if!(has_stop,       STOP);
737
738    #[cfg(feature = "dota")]
739    add_if!(has_dota_um,    DOTA_UM);
740    #[cfg(feature = "dota")]
741    add_if!(has_combat_log, COMBAT_LOG);
742
743    #[cfg(feature = "citadel")]
744    add_if!(has_cita_um,    CITA_UM);
745    #[cfg(feature = "citadel")]
746    add_if!(has_cita_ge,    CITA_GE);
747
748    #[cfg(feature = "cs2")]
749    add_if!(has_cs2_um,     CS2_UM);
750    #[cfg(feature = "cs2")]
751    add_if!(has_cs2_ge,     CS2_GE);
752
753    let ret = quote! {
754        impl Observer for #struct_name {
755            fn interests(&self) -> ::source2_demo::Interests { #interests }
756            #obs_body
757        }
758        #input
759    };
760
761    TokenStream::from(ret)
762}
763
764fn get_arg_type(method: &syn::ImplItemFn, n: usize) -> (Type, bool) {
765    if let Some(FnArg::Typed(pat_type)) = method.sig.inputs.iter().nth(n) {
766        if let Type::Reference(x) = pat_type.ty.as_ref() {
767            (*x.elem.clone(), true)
768        } else {
769            (*pat_type.ty.clone(), false)
770        }
771    } else {
772        panic!("Expected argument")
773    }
774}
775
776/// Marks a method as a protobuf message handler.
777///
778/// Use this to handle specific protobuf message types. The message type is inferred
779/// from the method's parameter type, which should be a protobuf message struct.
780///
781/// The method will automatically decode binary message data and call your handler
782/// with the decoded message object.
783///
784/// # Parameters
785///
786/// The handler can receive:
787/// - `ctx: &Context` (optional) - Access to current replay state
788/// - Message parameter - The decoded protobuf message (inferred from parameter type)
789/// - Message can be taken by value or reference
790///
791/// # Supported Message Types
792///
793/// - `CDotaUserMsgChatMessage` and other Dota 2 messages
794/// - `CCitadelUserMsgChatMsg` and other Deadlock messages
795/// - `CCSUserMessage_*` and other CS2 messages
796/// - Any protobuf message type with a `decode` method
797///
798/// # Examples
799///
800/// ## Handle Dota 2 chat messages
801///
802/// ```no_run
803/// # use source2_demo::prelude::*;
804/// # struct MyObs;
805/// # impl MyObs {
806/// #[on_message]
807/// fn on_chat(&mut self, ctx: &Context, msg: CDotaUserMsgChatMessage) -> ObserverResult {
808///     println!("[{}] {}", ctx.tick(), msg.message_text());
809///     Ok(())
810/// }
811/// # }
812/// ```
813///
814/// ## Handle without context
815///
816/// ```no_run
817/// # use source2_demo::prelude::*;
818/// # struct MyObs;
819/// # impl MyObs {
820/// #[on_message]
821/// fn on_chat(&mut self, msg: CDotaUserMsgChatMessage) -> ObserverResult {
822///     println!("Message: {}", msg.message_text());
823///     Ok(())
824/// }
825/// # }
826/// ```
827///
828/// ## Handle by reference
829///
830/// ```no_run
831/// # use source2_demo::prelude::*;
832/// # struct MyObs;
833/// # impl MyObs {
834/// #[on_message]
835/// fn on_chat(&mut self, msg: &CDotaUserMsgChatMessage) -> ObserverResult {
836///     println!("Message: {}", msg.message_text());
837///     Ok(())
838/// }
839/// # }
840/// ```
841#[proc_macro_attribute]
842pub fn on_message(_attr: TokenStream, item: TokenStream) -> TokenStream {
843    item
844}
845
846/// Marks a method as a tick-start handler.
847///
848/// This handler is called at the beginning of each game tick. Use it for
849/// per-tick logic like updating game state, tracking changes, or generating output.
850///
851/// # Parameters
852///
853/// The handler can receive:
854/// - `ctx: &Context` (optional) - Current replay state including tick number
855///
856/// # When It's Called
857///
858/// Called right after the tick number is incremented in the Context, but before
859/// any other tick-specific events are processed.
860///
861/// # Examples
862///
863/// ## Track entity state every tick
864///
865/// ```no_run
866/// # use source2_demo::prelude::*;
867/// # struct MyObs;
868/// # impl MyObs {
869/// #[on_tick_start]
870/// fn on_tick_start(&mut self, ctx: &Context) -> ObserverResult {
871///     if ctx.tick() % 30 == 0 {  // Every second (30 ticks/sec)
872///         println!("Ticks processed: {}", ctx.tick());
873///     }
874///     Ok(())
875/// }
876/// # }
877/// ```
878///
879/// ## Without context
880///
881/// ```no_run
882/// # use source2_demo::prelude::*;
883/// # struct MyObs;
884/// # impl MyObs {
885/// #[on_tick_start]
886/// fn on_tick_start(&mut self) -> ObserverResult {
887///     // Do something without needing context
888///     Ok(())
889/// }
890/// # }
891/// ```
892#[proc_macro_attribute]
893pub fn on_tick_start(_attr: TokenStream, item: TokenStream) -> TokenStream {
894    item
895}
896
897/// Marks a method as a tick-end handler.
898///
899/// This handler is called at the end of each game tick. Use it to finalize
900/// per-tick calculations, output results, or reset temporary state.
901///
902/// # Parameters
903///
904/// The handler can receive:
905/// - `ctx: &Context` (optional) - Current replay state
906///
907/// # When It's Called
908///
909/// Called after all events for the current tick have been processed.
910///
911/// # Examples
912///
913/// ## Flush buffered data at end of tick
914///
915/// ```no_run
916/// # use source2_demo::prelude::*;
917/// # struct MyObs {
918/// #     buffer: Vec<String>,
919/// # }
920/// # impl MyObs {
921/// #[on_tick_end]
922/// fn on_tick_end(&mut self, ctx: &Context) -> ObserverResult {
923///     // Output buffered data
924///     for item in self.buffer.drain(..) {
925///         println!("[{}] {}", ctx.tick(), item);
926///     }
927///     Ok(())
928/// }
929/// # }
930/// ```
931#[proc_macro_attribute]
932pub fn on_tick_end(_attr: TokenStream, item: TokenStream) -> TokenStream {
933    item
934}
935
936/// Marks a method as an entity event handler.
937///
938/// This handler is called when entities are created, updated, or deleted.
939/// Optionally filter to specific entity class names.
940///
941/// # Parameters
942///
943/// The handler can receive:
944/// - `ctx: &Context` (optional) - Current replay state
945/// - `event: EntityEvents` (optional) - Type of entity event (Created, Updated, Deleted)
946/// - `entity: &Entity` - The entity that changed
947///
948/// # Filtering
949///
950/// Pass a class name to only handle entities of that type:
951/// ```no_run
952/// # use source2_demo::prelude::*;
953/// # struct MyObs;
954/// # impl MyObs {
955/// #[on_entity("CDOTA_Unit_Hero_Axe")]
956/// fn on_axe(&mut self, entity: &Entity) -> ObserverResult {
957///     // Only called for Axe hero entity
958///     Ok(())
959/// }
960/// # }
961/// ```
962///
963/// # Examples
964///
965/// ## Track all entity changes
966///
967/// ```no_run
968/// # use source2_demo::prelude::*;
969/// # struct MyObs {
970/// #     created: usize,
971/// # }
972/// # impl MyObs {
973/// #[on_entity]
974/// fn on_entity(&mut self, event: EntityEvents, entity: &Entity) -> ObserverResult {
975///     if event == EntityEvents::Created {
976///         self.created += 1;
977///     }
978///     Ok(())
979/// }
980/// # }
981/// ```
982///
983/// ## Track specific entity type
984///
985/// ```no_run
986/// # use source2_demo::prelude::*;
987/// # struct MyObs;
988/// # impl MyObs {
989/// #[on_entity("CDOTA_PlayerResource")]
990/// fn on_player_resource(&mut self, ctx: &Context, entity: &Entity) -> ObserverResult {
991///     // Only called when player resource entity updates
992///     Ok(())
993/// }
994/// # }
995/// ```
996#[proc_macro_attribute]
997pub fn on_entity(_attr: TokenStream, item: TokenStream) -> TokenStream {
998    item
999}
1000
1001/// Marks a method as a game event handler.
1002///
1003/// This handler is called when game events occur (kills, deaths, item purchases, etc.).
1004/// Optionally filter to specific event names.
1005///
1006/// # Parameters
1007///
1008/// The handler can receive:
1009/// - `ctx: &Context` (optional) - Current replay state
1010/// - `ge: &GameEvent` - The game event
1011///
1012/// # Filtering
1013///
1014/// Pass an event name to only handle that event type:
1015/// ```no_run
1016/// # use source2_demo::prelude::*;
1017/// # struct MyObs;
1018/// # impl MyObs {
1019/// #[on_game_event("player_death")]
1020/// fn on_death(&mut self, ge: &GameEvent) -> ObserverResult {
1021///     // Only called for player_death events
1022///     Ok(())
1023/// }
1024/// # }
1025/// ```
1026///
1027/// # Examples
1028///
1029/// ## Handle all game events
1030///
1031/// ```no_run
1032/// # use source2_demo::prelude::*;
1033/// # struct MyObs;
1034/// # impl MyObs {
1035/// #[on_game_event]
1036/// fn on_event(&mut self, ctx: &Context, ge: &GameEvent) -> ObserverResult {
1037///     println!("[{}] Event: {}", ctx.tick(), ge.name());
1038///     Ok(())
1039/// }
1040/// # }
1041/// ```
1042///
1043/// ## Handle specific event type
1044///
1045/// ```no_run
1046/// # use source2_demo::prelude::*;
1047/// # struct MyObs {
1048/// #     kill_count: u32,
1049/// # }
1050/// # impl MyObs {
1051/// #[on_game_event("dota_player_kill")]
1052/// fn on_kill(&mut self, ge: &GameEvent) -> ObserverResult {
1053///     self.kill_count += 1;
1054///     Ok(())
1055/// }
1056/// # }
1057/// ```
1058#[proc_macro_attribute]
1059pub fn on_game_event(_attr: TokenStream, item: TokenStream) -> TokenStream {
1060    item
1061}
1062
1063/// Marks a method as a string table update handler.
1064///
1065/// This handler is called when string tables are updated. String tables contain
1066/// game data like player names, modifiers, effects, etc.
1067/// Optionally filter to specific table names.
1068///
1069/// # Parameters
1070///
1071/// The handler can receive:
1072/// - `ctx: &Context` (optional) - Current replay state
1073/// - `table: &StringTable` - The updated string table
1074/// - `modified: &[i32]` - Indices of rows that were modified
1075///
1076/// # Filtering
1077///
1078/// Pass a table name to only handle that table:
1079/// ```no_run
1080/// # use source2_demo::prelude::*;
1081/// # struct MyObs;
1082/// # impl MyObs {
1083/// #[on_string_table("userinfo")]
1084/// fn on_userinfo(&mut self, table: &StringTable, modified: &[i32]) -> ObserverResult {
1085///     // Only called when userinfo table updates
1086///     Ok(())
1087/// }
1088/// # }
1089/// ```
1090///
1091/// # Examples
1092///
1093/// ## Track all string table updates
1094///
1095/// ```no_run
1096/// # use source2_demo::prelude::*;
1097/// # struct MyObs;
1098/// # impl MyObs {
1099/// #[on_string_table]
1100/// fn on_table_update(&mut self, ctx: &Context, table: &StringTable, modified: &[i32]) -> ObserverResult {
1101///     println!("[{}] Table {} updated: {} rows", ctx.tick(), table.name(), modified.len());
1102///     Ok(())
1103/// }
1104/// # }
1105/// ```
1106///
1107/// ## Monitor specific table
1108///
1109/// ```no_run
1110/// # use source2_demo::prelude::*;
1111/// # struct MyObs;
1112/// # impl MyObs {
1113/// #[on_string_table("ActiveModifiers")]
1114/// fn on_modifiers(&mut self, table: &StringTable, modified: &[i32]) -> ObserverResult {
1115///     println!("Active modifiers changed: {} rows", modified.len());
1116///     Ok(())
1117/// }
1118/// # }
1119/// ```
1120#[proc_macro_attribute]
1121pub fn on_string_table(_attr: TokenStream, item: TokenStream) -> TokenStream {
1122    item
1123}
1124
1125/// Marks a method as a replay stop handler.
1126///
1127/// This handler is called when the replay ends (CDemoStop message).
1128/// Use it to finalize results, output statistics, or clean up resources.
1129///
1130/// # Parameters
1131///
1132/// The handler can receive:
1133/// - `ctx: &Context` (optional) - Final replay state
1134///
1135/// # Examples
1136///
1137/// ## Output final statistics
1138///
1139/// ```no_run
1140/// # use source2_demo::prelude::*;
1141/// # struct MyObs {
1142/// #     ticks: u32,
1143/// # }
1144/// # impl MyObs {
1145/// #[on_stop]
1146/// fn on_stop(&mut self, ctx: &Context) -> ObserverResult {
1147///     println!("Replay ended at tick {}", ctx.tick());
1148///     println!("Total ticks processed: {}", self.ticks);
1149///     Ok(())
1150/// }
1151/// # }
1152/// ```
1153///
1154/// ## Without context
1155///
1156/// ```no_run
1157/// # use source2_demo::prelude::*;
1158/// # struct MyObs;
1159/// # impl MyObs {
1160/// #[on_stop]
1161/// fn on_stop(&mut self) -> ObserverResult {
1162///     println!("Replay complete");
1163///     Ok(())
1164/// }
1165/// # }
1166/// ```
1167#[proc_macro_attribute]
1168pub fn on_stop(_attr: TokenStream, item: TokenStream) -> TokenStream {
1169    item
1170}
1171
1172/// Marks a method as a combat log handler (Dota 2 only).
1173///
1174/// This handler is called whenever a combat log entry is generated.
1175/// Combat log entries include damage, healing, kills, abilities, items, etc.
1176///
1177/// # Parameters
1178///
1179/// The handler can receive:
1180/// - `ctx: &Context` (optional) - Current replay state
1181/// - `cle: &CombatLogEntry` - The combat log entry
1182///
1183/// # Requires Feature
1184///
1185/// Only available when the `dota` feature is enabled.
1186///
1187/// # Examples
1188///
1189/// ## Track damage in real-time
1190///
1191/// ```no_run
1192/// # use source2_demo::prelude::*;
1193/// # use source2_demo::proto::DotaCombatlogTypes;
1194/// # struct MyObs {
1195/// #     total_damage: u32,
1196/// # }
1197/// # impl MyObs {
1198/// #[on_combat_log]
1199/// fn on_damage(&mut self, ctx: &Context, cle: &CombatLogEntry) -> ObserverResult {
1200///     if cle.r#type() == DotaCombatlogTypes::DotaCombatlogDamage {
1201///         if let Ok(damage) = cle.value() {
1202///             self.total_damage += damage;
1203///         }
1204///     }
1205///     Ok(())
1206/// }
1207/// # }
1208/// ```
1209///
1210/// ## Track kills
1211///
1212/// ```no_run
1213/// # use source2_demo::prelude::*;
1214/// # use source2_demo::proto::DotaCombatlogTypes;
1215/// # struct MyObs {
1216/// #     kills: u32,
1217/// # }
1218/// # impl MyObs {
1219/// #[on_combat_log]
1220/// fn on_kill(&mut self, cle: &CombatLogEntry) -> ObserverResult {
1221///     if cle.r#type() == DotaCombatlogTypes::DotaCombatlogDeath {
1222///         self.kills += 1;
1223///     }
1224///     Ok(())
1225/// }
1226/// # }
1227/// ```
1228#[cfg(feature = "dota")]
1229#[proc_macro_attribute]
1230pub fn on_combat_log(_attr: TokenStream, item: TokenStream) -> TokenStream {
1231    item
1232}
1233
1234/// Marks the impl block to enable entity tracking.
1235///
1236/// When applied to an impl block or individual method, automatically enables
1237/// the `ENABLE_ENTITY` interest flag so entities are tracked during parsing.
1238///
1239/// # Examples
1240///
1241/// ```no_run
1242/// # use source2_demo::prelude::*;
1243/// #[observer]
1244/// #[uses_entities]
1245/// impl MyObs {
1246///     // Now you can use #[on_entity] handlers
1247/// # fn dummy(&mut self) -> ObserverResult { Ok(()) }
1248/// }
1249/// ```
1250#[proc_macro_attribute]
1251pub fn uses_entities(_attr: TokenStream, item: TokenStream) -> TokenStream { item }
1252
1253/// Marks the impl block to enable string table tracking.
1254///
1255/// When applied to an impl block or individual method, automatically enables
1256/// the `ENABLE_STRINGTAB` interest flag so string tables are tracked during parsing.
1257///
1258/// # Examples
1259///
1260/// ```no_run
1261/// # use source2_demo::prelude::*;
1262/// #[observer]
1263/// #[uses_string_tables]
1264/// impl MyObs {
1265///     // Now you can use #[on_string_table] handlers
1266/// # fn dummy(&mut self) -> ObserverResult { Ok(()) }
1267/// }
1268/// ```
1269#[proc_macro_attribute]
1270pub fn uses_string_tables(_attr: TokenStream, item: TokenStream) -> TokenStream { item }
1271
1272/// Marks the impl block to enable game event tracking.
1273///
1274/// When applied to an impl block or individual method, automatically enables
1275/// the `BASE_GE` interest flag so game events are tracked during parsing.
1276///
1277/// # Examples
1278///
1279/// ```no_run
1280/// # use source2_demo::prelude::*;
1281/// #[observer]
1282/// #[uses_game_events]
1283/// impl MyObs {
1284///     // Now you can use #[on_game_event] handlers
1285/// # fn dummy(&mut self) -> ObserverResult { Ok(()) }
1286/// }
1287/// ```
1288#[proc_macro_attribute]
1289pub fn uses_game_events(_attr: TokenStream, item: TokenStream) -> TokenStream { item }
1290
1291/// Marks the impl block to enable combat log tracking (Dota 2 only).
1292///
1293/// When applied to an impl block, automatically enables the `COMBAT_LOG` and
1294/// `ENABLE_STRINGTAB` interest flags for combat log parsing.
1295///
1296/// # Requires Feature
1297///
1298/// Only available when the `dota` feature is enabled.
1299///
1300/// # Examples
1301///
1302/// ```no_run
1303/// # use source2_demo::prelude::*;
1304/// #[observer]
1305/// #[uses_combat_log]
1306/// impl MyObs {
1307///     // Now you can use #[on_combat_log] handlers
1308/// # fn dummy(&mut self) -> ObserverResult { Ok(()) }
1309/// }
1310/// ```
1311#[cfg(feature = "dota")]
1312#[proc_macro_attribute]
1313pub fn uses_combat_log(_attr: TokenStream, item: TokenStream) -> TokenStream { item }