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 }