Skip to main content

dynoffsets_macros/
lib.rs

1//! Internal proc-macro crate for `dynoffsets`.
2//!
3//! Do not use directly — depend on `dynoffsets` instead.
4
5use proc_macro::TokenStream;
6use proc_macro2::TokenStream as TokenStream2;
7use quote::{format_ident, quote};
8use syn::{
9    ext::IdentExt,
10    parse::{Parse, ParseStream},
11    parse_macro_input,
12    punctuated::Punctuated,
13    Expr, Ident, Item, ItemConst, ItemMod, LitBool, LitStr, Token,
14};
15
16struct Args {
17    dll: String,
18    enabled: bool,
19    /// True when `r#static` was passed; use `AtomicUsize` cells + register fn.
20    static_storage: bool,
21    /// True when `hashed` was passed; emit fnv1a-wrapped lookup to avoid &str in .rdata.
22    hashed: bool,
23}
24
25impl Default for Args {
26    fn default() -> Self {
27        Args {
28            dll: "client.dll".to_string(),
29            enabled: true,
30            static_storage: false,
31            hashed: false,
32        }
33    }
34}
35
36enum Arg {
37    Dll(LitStr),
38    Enabled(LitBool),
39    Static,
40    Hashed,
41}
42
43impl Parse for Arg {
44    fn parse(input: ParseStream) -> syn::Result<Self> {
45        if input.peek(LitStr) {
46            return Ok(Arg::Dll(input.parse()?));
47        }
48        if input.peek(LitBool) {
49            return Ok(Arg::Enabled(input.parse()?));
50        }
51        if input.peek(Token![static]) {
52            let _: Token![static] = input.parse()?;
53            return Ok(Arg::Static);
54        }
55        if input.peek(Ident::peek_any) {
56            let id: Ident = input.call(Ident::parse_any)?;
57            let s = id.to_string();
58            if matches!(s.as_str(), "static" | "r#static") {
59                return Ok(Arg::Static);
60            }
61            if matches!(s.as_str(), "hashed" | "hash" | "h") {
62                return Ok(Arg::Hashed);
63            }
64            return Err(syn::Error::new(
65                id.span(),
66                "unknown flag; expected `r#static`, `hashed`, a dll name string, or a bool",
67            ));
68        }
69        Err(input.error("expected dll name string, bool, `r#static`, or `hashed`"))
70    }
71}
72
73impl Parse for Args {
74    fn parse(input: ParseStream) -> syn::Result<Self> {
75        let mut args = Args::default();
76        if input.is_empty() {
77            return Ok(args);
78        }
79        let list: Punctuated<Arg, Token![,]> = Punctuated::parse_terminated(input)?;
80        for arg in list {
81            match arg {
82                Arg::Dll(s) => args.dll = s.value(),
83                Arg::Enabled(b) => args.enabled = b.value,
84                Arg::Static => args.static_storage = true,
85                Arg::Hashed => args.hashed = true,
86            }
87        }
88        Ok(args)
89    }
90}
91
92fn slot_ident(name: &Ident) -> Ident {
93    format_ident!("__DYNOFFSETS_{}", name, span = name.span())
94}
95
96struct ConstInfo {
97    fn_name: Ident,
98    vis: syn::Visibility,
99    lit_expr: TokenStream2,
100    name_str: LitStr,
101}
102
103impl ConstInfo {
104    fn from_item_const(c: &ItemConst) -> Self {
105        let fn_name = c.ident.clone();
106        let name_str = LitStr::new(&fn_name.to_string(), c.ident.span());
107        Self {
108            fn_name,
109            vis: c.vis.clone(),
110            lit_expr: expr_tokens(&c.expr),
111            name_str,
112        }
113    }
114}
115
116fn process_static_const(c: &ItemConst) -> Option<(TokenStream2, Item, Item)> {
117    if !is_pub_usize(c) {
118        return None;
119    }
120    let info = ConstInfo::from_item_const(c);
121    let slot = slot_ident(&info.fn_name);
122    let name_str = &info.name_str;
123    let entry = quote! { (#name_str, &#slot) };
124    let stat = parse_slot_static(&slot, &info.lit_expr);
125    let fun = parse_slot_fn(&info.vis, &info.fn_name, &slot);
126    Some((entry, stat, fun))
127}
128
129fn rewrite_dynamic_consts(
130    items: &mut [Item],
131    mut build_fn: impl FnMut(&ConstInfo) -> TokenStream2,
132) {
133    for item in items.iter_mut() {
134        let Item::Const(c) = item else { continue };
135        if !is_pub_usize(c) {
136            continue;
137        }
138        let info = ConstInfo::from_item_const(c);
139        *item = syn::parse2(build_fn(&info)).expect("fn");
140    }
141}
142
143fn rewrite_static_module(
144    items: &mut Vec<Item>,
145    build_register: impl FnOnce(&[TokenStream2]) -> TokenStream2,
146) {
147    let mut entries: Vec<TokenStream2> = Vec::new();
148    let mut new_items: Vec<Item> = Vec::with_capacity(items.len() * 2 + 1);
149    for item in items.iter() {
150        if let Item::Const(c) = item {
151            if let Some((entry, slot, accessor)) = process_static_const(c) {
152                entries.push(entry);
153                new_items.push(slot);
154                new_items.push(accessor);
155                continue;
156            }
157        }
158        new_items.push(item.clone());
159    }
160    new_items.push(syn::parse2(build_register(&entries)).expect("register fn"));
161    *items = new_items;
162}
163
164#[proc_macro_attribute]
165pub fn schema(attr: TokenStream, item: TokenStream) -> TokenStream {
166    let args = parse_macro_input!(attr as Args);
167    let mut module = parse_macro_input!(item as ItemMod);
168    rewrite_schema_module(&mut module, &args);
169    quote!(#module).into()
170}
171
172fn rewrite_schema_module(class_mod: &mut ItemMod, args: &Args) {
173    let Some((_, items)) = class_mod.content.as_mut() else {
174        return;
175    };
176    let class_lit = LitStr::new(&class_mod.ident.to_string(), class_mod.ident.span());
177    let dll = &args.dll;
178
179    if args.static_storage {
180        rewrite_static_module(items, |entries| {
181            quote! {
182                /// Register slots for `populate()`. Call after `init`.
183                pub fn __dynoffsets_register() {
184                    ::dynoffsets::__register_schema_static(#dll, #class_lit, &[#(#entries),*]);
185                }
186            }
187        });
188        return;
189    }
190
191    let enabled = args.enabled;
192    let hashed = args.hashed;
193    rewrite_dynamic_consts(items, |info| {
194        let ConstInfo {
195            fn_name,
196            vis,
197            lit_expr,
198            name_str: field_str,
199        } = info;
200        if enabled {
201            let lookup = if hashed {
202                let dll_len = dll.len() as u16;
203                let class_len = class_lit.value().len() as u16;
204                let field_len = field_str.value().len() as u16;
205                quote! {
206                    ::dynoffsets::lookup_or_fallback_h(
207                        ::dynoffsets::fnv1a(#dll), #dll_len,
208                        ::dynoffsets::fnv1a(#class_lit), #class_len,
209                        ::dynoffsets::fnv1a(#field_str), #field_len,
210                        #lit_expr
211                    )
212                }
213            } else {
214                quote! {
215                    ::dynoffsets::lookup_or_fallback(#dll, #class_lit, #field_str, #lit_expr)
216                }
217            };
218            cached_accessor(vis, fn_name, lit_expr, lookup)
219        } else {
220            quote! {
221                // code generated by https://github.com/H0llyW00dzZ/dynoffsets — do not edit
222                #vis fn #fn_name() -> usize { #lit_expr }
223            }
224        }
225    });
226}
227
228#[proc_macro_attribute]
229pub fn globals(attr: TokenStream, item: TokenStream) -> TokenStream {
230    let args = parse_macro_input!(attr as Args);
231    let mut module = parse_macro_input!(item as ItemMod);
232    rewrite_globals_module(&mut module, &args);
233    quote!(#module).into()
234}
235
236fn rewrite_globals_module(module: &mut ItemMod, args: &Args) {
237    let Some((_, items)) = module.content.as_mut() else {
238        return;
239    };
240
241    if args.static_storage {
242        rewrite_static_module(items, |entries| {
243            quote! {
244                /// Register slots for `populate()`. Call after `init`.
245                pub fn __dynoffsets_register() {
246                    ::dynoffsets::__register_globals_static(&[#(#entries),*]);
247                }
248            }
249        });
250        return;
251    }
252
253    rewrite_dynamic_consts(items, |info| {
254        let ConstInfo {
255            fn_name,
256            vis,
257            lit_expr,
258            ..
259        } = info;
260        cached_accessor(
261            vis,
262            fn_name,
263            lit_expr,
264            quote! {
265                ::dynoffsets::get_runtime_globals()
266                    .and_then(|g| g.#fn_name)
267                    .unwrap_or(#lit_expr)
268            },
269        )
270    });
271}
272
273#[proc_macro_attribute]
274pub fn interfaces(attr: TokenStream, item: TokenStream) -> TokenStream {
275    let args = parse_macro_input!(attr as Args);
276    let mut module = parse_macro_input!(item as ItemMod);
277    rewrite_interfaces_module(&mut module, &args);
278    quote!(#module).into()
279}
280
281fn rewrite_interfaces_module(module: &mut ItemMod, args: &Args) {
282    let Some((_, items)) = module.content.as_mut() else {
283        return;
284    };
285    let dll = &args.dll;
286
287    if args.static_storage {
288        rewrite_static_module(items, |entries| {
289            quote! {
290                /// Register slots for `populate()`. Call after `init`.
291                pub fn __dynoffsets_register() {
292                    ::dynoffsets::__register_interfaces_static(#dll, &[#(#entries),*]);
293                }
294            }
295        });
296        return;
297    }
298
299    let enabled = args.enabled;
300    rewrite_dynamic_consts(items, |info| {
301        let ConstInfo {
302            fn_name,
303            vis,
304            lit_expr,
305            name_str,
306        } = info;
307        if enabled {
308            cached_accessor(
309                vis,
310                fn_name,
311                lit_expr,
312                quote! {
313                    ::dynoffsets::get_runtime_interfaces()
314                        .and_then(|i| i.get(#dll, #name_str))
315                        .unwrap_or(#lit_expr)
316                },
317            )
318        } else {
319            quote! {
320                // code generated by https://github.com/H0llyW00dzZ/dynoffsets — do not edit
321                #[inline] #vis fn #fn_name() -> usize { #lit_expr }
322            }
323        }
324    });
325}
326
327#[proc_macro_attribute]
328pub fn buttons(attr: TokenStream, item: TokenStream) -> TokenStream {
329    let args = parse_macro_input!(attr as Args);
330    let mut module = parse_macro_input!(item as ItemMod);
331    rewrite_buttons_module(&mut module, &args);
332    quote!(#module).into()
333}
334
335fn rewrite_buttons_module(module: &mut ItemMod, args: &Args) {
336    let Some((_, items)) = module.content.as_mut() else {
337        return;
338    };
339
340    if args.static_storage {
341        rewrite_static_module(items, |entries| {
342            quote! {
343                /// Register slots for `populate()`. Call after `init`.
344                pub fn __dynoffsets_register() {
345                    ::dynoffsets::__register_buttons_static(&[#(#entries),*]);
346                }
347            }
348        });
349        return;
350    }
351
352    rewrite_dynamic_consts(items, |info| {
353        let ConstInfo {
354            fn_name,
355            vis,
356            lit_expr,
357            name_str,
358        } = info;
359        cached_accessor(
360            vis,
361            fn_name,
362            lit_expr,
363            quote! {
364                ::dynoffsets::get_runtime_buttons()
365                    .and_then(|b| b.get(#name_str))
366                    .unwrap_or(#lit_expr)
367            },
368        )
369    });
370}
371
372/// Emits a `#[inline]` accessor whose first post-`init` invocation resolves
373/// `lookup_expr` (a `usize` expression that already folds the literal fallback
374/// in via `unwrap_or`), latches the result into a per-accessor `OnceCell<usize>`,
375/// and from then on returns it via a single cheap atomic check + pointer load.
376///
377/// Pre-`init` calls deliberately *do not* write the cell — they just return
378/// `lit_expr` — so an early call before [`is_initialized`] never latches the
379/// literal fallback. The first post-`init` call wins the `OnceCell` and from
380/// then on every other call is a fast `OnceCell::get()` hit, regardless of
381/// whether the live value was found or the literal was used (so a pattern miss
382/// no longer pays the slow path on every frame).
383fn cached_accessor(
384    vis: &syn::Visibility,
385    fn_name: &Ident,
386    lit_expr: &TokenStream2,
387    lookup_expr: TokenStream2,
388) -> TokenStream2 {
389    quote! {
390        // code generated by https://github.com/H0llyW00dzZ/dynoffsets — do not edit
391        #[inline]
392        #vis fn #fn_name() -> usize {
393            static CELL: ::dynoffsets::__AccessorCell<usize> =
394                ::dynoffsets::__AccessorCell::new();
395
396            if let ::core::option::Option::Some(&v) = CELL.get() {
397                return v;
398            }
399
400            #[cold]
401            #[inline(never)]
402            fn __dynoffsets_resolve() -> usize {
403                if !::dynoffsets::is_initialized() {
404                    return #lit_expr;
405                }
406                *CELL.get_or_init(|| { let v: usize = #lookup_expr; v })
407            }
408
409            __dynoffsets_resolve()
410        }
411    }
412}
413
414fn parse_slot_static(slot: &Ident, lit_expr: &TokenStream2) -> Item {
415    syn::parse2(quote! {
416        #[doc(hidden)]
417        #[allow(non_upper_case_globals)]
418        pub static #slot: ::core::sync::atomic::AtomicUsize =
419            ::core::sync::atomic::AtomicUsize::new(#lit_expr);
420    })
421    .expect("slot static")
422}
423
424fn parse_slot_fn(vis: &syn::Visibility, fn_name: &Ident, slot: &Ident) -> Item {
425    syn::parse2(quote! {
426        #[inline]
427        #vis fn #fn_name() -> usize {
428            #slot.load(::core::sync::atomic::Ordering::Relaxed)
429        }
430    })
431    .expect("slot fn")
432}
433
434fn is_pub_usize(c: &ItemConst) -> bool {
435    matches!(c.vis, syn::Visibility::Public(_))
436        && matches!(c.ty.as_ref(), syn::Type::Path(p) if p.path.is_ident("usize"))
437}
438
439fn expr_tokens(expr: &Expr) -> TokenStream2 {
440    quote!(#expr)
441}
442
443#[cfg(test)]
444mod tests {
445    use super::*;
446
447    #[test]
448    fn args_default_is_client_dll_enabled() {
449        let a = Args::default();
450        assert_eq!(a.dll, "client.dll");
451        assert!(a.enabled);
452    }
453
454    #[test]
455    fn args_parse_empty_uses_defaults() {
456        let a: Args = syn::parse_str("").unwrap();
457        assert_eq!(a.dll, "client.dll");
458        assert!(a.enabled);
459    }
460
461    #[test]
462    fn args_parse_dll_string() {
463        let a: Args = syn::parse_str("\"server.dll\"").unwrap();
464        assert_eq!(a.dll, "server.dll");
465        assert!(a.enabled);
466    }
467
468    #[test]
469    fn args_parse_bool_only() {
470        let a: Args = syn::parse_str("false").unwrap();
471        assert!(!a.enabled);
472    }
473
474    #[test]
475    fn args_parse_dll_and_bool() {
476        let a: Args = syn::parse_str("\"engine2.dll\", false").unwrap();
477        assert_eq!(a.dll, "engine2.dll");
478        assert!(!a.enabled);
479    }
480
481    #[test]
482    fn args_parse_two_dlls_last_wins() {
483        let a: Args = syn::parse_str("\"a.dll\", \"b.dll\"").unwrap();
484        assert_eq!(a.dll, "b.dll");
485    }
486
487    #[test]
488    fn args_parse_invalid_token_errors() {
489        let r: syn::Result<Args> = syn::parse_str("123");
490        assert!(r.is_err());
491    }
492
493    #[test]
494    fn args_parse_missing_comma_errors() {
495        let r: syn::Result<Args> = syn::parse_str("\"a.dll\" false");
496        assert!(r.is_err());
497    }
498
499    #[test]
500    fn is_pub_usize_accepts_pub_usize_const() {
501        let c: ItemConst = syn::parse_str("pub const X: usize = 1;").unwrap();
502        assert!(is_pub_usize(&c));
503    }
504
505    #[test]
506    fn is_pub_usize_rejects_private_const() {
507        let c: ItemConst = syn::parse_str("const X: usize = 1;").unwrap();
508        assert!(!is_pub_usize(&c));
509    }
510
511    #[test]
512    fn is_pub_usize_rejects_non_usize_const() {
513        let c: ItemConst = syn::parse_str("pub const X: u32 = 1;").unwrap();
514        assert!(!is_pub_usize(&c));
515    }
516
517    #[test]
518    fn is_pub_usize_rejects_complex_type() {
519        let c: ItemConst = syn::parse_str("pub const X: [u8; 4] = [0; 4];").unwrap();
520        assert!(!is_pub_usize(&c));
521    }
522
523    #[test]
524    fn expr_tokens_roundtrip_simple() {
525        let e: Expr = syn::parse_str("42").unwrap();
526        assert_eq!(expr_tokens(&e).to_string(), "42");
527    }
528
529    #[test]
530    fn expr_tokens_roundtrip_arith() {
531        let e: Expr = syn::parse_str("0x10 + 0x08").unwrap();
532        assert!(!expr_tokens(&e).to_string().is_empty());
533    }
534
535    fn parse_mod(src: &str) -> ItemMod {
536        syn::parse_str(src).unwrap()
537    }
538
539    #[test]
540    fn rewrite_schema_enabled_emits_lookup_call() {
541        let mut m = parse_mod("mod C_Foo { pub const m_x: usize = 0x10; }");
542        rewrite_schema_module(&mut m, &Args::default());
543        let s = quote!(#m).to_string();
544        assert!(s.contains("lookup_or_fallback"));
545        assert!(s.contains("\"C_Foo\""));
546        assert!(s.contains("\"m_x\""));
547        // Per-accessor `OnceCell<usize>` cache machinery is in place.
548        assert!(s.contains("__AccessorCell"));
549        assert!(s.contains("is_initialized"));
550        assert!(s.contains("get_or_init"));
551    }
552
553    #[test]
554    fn rewrite_schema_disabled_emits_literal_fn() {
555        let mut m = parse_mod("mod C_Foo { pub const m_x: usize = 0x10; }");
556        let args = Args {
557            dll: "client.dll".into(),
558            enabled: false,
559            static_storage: false,
560            hashed: false,
561        };
562        rewrite_schema_module(&mut m, &args);
563        let s = quote!(#m).to_string();
564        assert!(!s.contains("lookup_or_fallback"));
565        assert!(!s.contains("__AccessorCell"));
566        assert!(s.contains("fn m_x"));
567    }
568
569    #[test]
570    fn rewrite_schema_skips_non_pub_usize_items() {
571        let mut m = parse_mod(
572            "mod C_Foo { const priv_x: usize = 1; pub const non_usize: u32 = 2; \
573             pub fn f() {} pub const ok: usize = 0x10; }",
574        );
575        rewrite_schema_module(&mut m, &Args::default());
576        let s = quote!(#m).to_string();
577        // exactly one rewritten item (the pub const usize one)
578        let count = s.matches("lookup_or_fallback").count();
579        assert_eq!(count, 1);
580    }
581
582    #[test]
583    fn rewrite_schema_hashed_emits_fnv_no_strings() {
584        let mut m = parse_mod("mod C_Foo { pub const m_x: usize = 0x10; }");
585        let mut a = Args::default();
586        a.hashed = true;
587        rewrite_schema_module(&mut m, &a);
588        let s = quote!(#m).to_string();
589        assert!(s.contains("lookup_or_fallback_h"));
590        assert!(s.contains("fnv1a"));
591        // the str version should not be called for this accessor
592        assert_eq!(s.matches("lookup_or_fallback(").count(), 0);
593        // `u16` length literals next to each `fnv1a(...)` call: "client.dll" -> 10,
594        // "C_Foo" -> 5, "m_x" -> 3.
595        assert!(s.contains("10u16"));
596        assert!(s.contains("5u16"));
597        assert!(s.contains("3u16"));
598    }
599
600    #[test]
601    fn rewrite_schema_handles_module_without_body() {
602        // `mod E;` has no inline content → function returns early without touching anything.
603        let mut m = parse_mod("mod E;");
604        rewrite_schema_module(&mut m, &Args::default());
605        rewrite_globals_module(&mut m, &Args::default());
606        rewrite_interfaces_module(&mut m, &Args::default());
607        rewrite_buttons_module(&mut m, &Args::default());
608    }
609
610    #[test]
611    fn rewrite_globals_emits_runtime_lookup() {
612        let mut m = parse_mod("mod g { pub const dw_thing: usize = 0x42; }");
613        rewrite_globals_module(&mut m, &Args::default());
614        let s = quote!(#m).to_string();
615        assert!(s.contains("get_runtime_globals"));
616        assert!(s.contains("dw_thing"));
617        // Per-accessor `OnceCell<usize>` cache machinery is in place.
618        assert!(s.contains("__AccessorCell"));
619        assert!(s.contains("is_initialized"));
620        assert!(s.contains("get_or_init"));
621    }
622
623    #[test]
624    fn rewrite_interfaces_enabled_emits_lookup() {
625        let mut m = parse_mod("mod i { pub const Source2Client002: usize = 0xAA; }");
626        rewrite_interfaces_module(&mut m, &Args::default());
627        let s = quote!(#m).to_string();
628        assert!(s.contains("get_runtime_interfaces"));
629        assert!(s.contains("\"Source2Client002\""));
630        // Per-accessor `OnceCell<usize>` cache machinery is in place.
631        assert!(s.contains("__AccessorCell"));
632        assert!(s.contains("is_initialized"));
633        assert!(s.contains("get_or_init"));
634    }
635
636    #[test]
637    fn rewrite_interfaces_disabled_emits_literal() {
638        let mut m = parse_mod("mod i { pub const Source2Client002: usize = 0xAA; }");
639        let args = Args {
640            dll: "client.dll".into(),
641            enabled: false,
642            static_storage: false,
643            hashed: false,
644        };
645        rewrite_interfaces_module(&mut m, &args);
646        let s = quote!(#m).to_string();
647        assert!(!s.contains("get_runtime_interfaces"));
648        assert!(!s.contains("__AccessorCell"));
649    }
650
651    #[test]
652    fn rewrite_buttons_emits_runtime_lookup() {
653        let mut m = parse_mod("mod b { pub const in_attack: usize = 0x100; }");
654        rewrite_buttons_module(&mut m, &Args::default());
655        let s = quote!(#m).to_string();
656        assert!(s.contains("get_runtime_buttons"));
657        assert!(s.contains("\"in_attack\""));
658        // Per-accessor `OnceCell<usize>` cache machinery is in place.
659        assert!(s.contains("__AccessorCell"));
660        assert!(s.contains("is_initialized"));
661        assert!(s.contains("get_or_init"));
662    }
663
664    #[test]
665    fn rewrite_globals_skips_non_pub_usize_items() {
666        let mut m = parse_mod(
667            "mod g { const priv_x: usize = 1; pub const ok: usize = 2; pub const non_usize: u32 = 3; }",
668        );
669        rewrite_globals_module(&mut m, &Args::default());
670        let s = quote!(#m).to_string();
671        assert_eq!(s.matches("get_runtime_globals").count(), 1);
672    }
673
674    #[test]
675    fn rewrite_interfaces_skips_non_pub_usize_items() {
676        let mut m = parse_mod(
677            "mod i { const priv_x: usize = 1; pub const ok: usize = 2; pub const non_usize: u32 = 3; }",
678        );
679        rewrite_interfaces_module(&mut m, &Args::default());
680        let s = quote!(#m).to_string();
681        assert_eq!(s.matches("get_runtime_interfaces").count(), 1);
682    }
683
684    #[test]
685    fn rewrite_buttons_skips_non_pub_usize_items() {
686        let mut m = parse_mod(
687            "mod b { const priv_x: usize = 1; pub const ok: usize = 2; pub const non_usize: u32 = 3; }",
688        );
689        rewrite_buttons_module(&mut m, &Args::default());
690        let s = quote!(#m).to_string();
691        assert_eq!(s.matches("get_runtime_buttons").count(), 1);
692    }
693
694    #[test]
695    fn rewrite_modules_with_empty_body_are_unchanged() {
696        let mut m = parse_mod("mod empty {}");
697        rewrite_schema_module(&mut m, &Args::default());
698        rewrite_globals_module(&mut m, &Args::default());
699        rewrite_interfaces_module(&mut m, &Args::default());
700        rewrite_buttons_module(&mut m, &Args::default());
701        let s = quote!(#m).to_string();
702        assert!(s.contains("mod empty"));
703    }
704
705    fn static_args(dll: &str) -> Args {
706        Args {
707            dll: dll.into(),
708            enabled: true,
709            static_storage: true,
710            hashed: false,
711        }
712    }
713
714    #[test]
715    fn args_parse_raw_static_keyword() {
716        let a: Args = syn::parse_str("r#static").unwrap();
717        assert!(a.static_storage);
718        assert_eq!(a.dll, "client.dll");
719        assert!(a.enabled);
720    }
721
722    #[test]
723    fn args_parse_bare_static_keyword() {
724        // Even though `static` alone isn't writable in user attribute source
725        // (rustc would treat it as the keyword before reaching the macro),
726        // the parser still accepts the keyword token directly.
727        let a: Args = syn::parse_str("static").unwrap();
728        assert!(a.static_storage);
729    }
730
731    #[test]
732    fn args_parse_dll_then_static() {
733        let a: Args = syn::parse_str("\"server.dll\", r#static").unwrap();
734        assert_eq!(a.dll, "server.dll");
735        assert!(a.static_storage);
736        assert!(a.enabled);
737    }
738
739    #[test]
740    fn args_parse_unknown_ident_errors() {
741        let r: syn::Result<Args> = syn::parse_str("bogus");
742        assert!(r.is_err());
743    }
744
745    #[test]
746    fn rewrite_schema_static_mode_emits_atomic_storage_and_register() {
747        let mut m = parse_mod("mod C_Foo { pub const m_x: usize = 0x10; }");
748        rewrite_schema_module(&mut m, &static_args("client.dll"));
749        let s = quote!(#m).to_string();
750        assert!(s.contains("AtomicUsize"));
751        assert!(s.contains("__DYNOFFSETS_m_x"));
752        assert!(s.contains("__register_schema_static"));
753        assert!(s.contains("\"client.dll\""));
754        assert!(s.contains("\"C_Foo\""));
755        assert!(s.contains("\"m_x\""));
756        assert!(s.contains("fn __dynoffsets_register"));
757        // The accessor body must not contain any library call or the dynamic
758        // OnceCell cache (static mode uses its own AtomicUsize slots).
759        assert!(!s.contains("lookup_or_fallback"));
760        assert!(!s.contains("__AccessorCell"));
761    }
762
763    #[test]
764    fn rewrite_globals_static_mode_emits_atomic_storage_and_register() {
765        let mut m = parse_mod("mod g { pub const dw_thing: usize = 0x42; }");
766        rewrite_globals_module(&mut m, &static_args("client.dll"));
767        let s = quote!(#m).to_string();
768        assert!(s.contains("AtomicUsize"));
769        assert!(s.contains("__DYNOFFSETS_dw_thing"));
770        assert!(s.contains("__register_globals_static"));
771        assert!(s.contains("fn __dynoffsets_register"));
772        // Accessor must not contain the dynamic lookup.
773        assert!(!s.contains("get_runtime_globals"));
774    }
775
776    #[test]
777    fn rewrite_interfaces_static_mode_emits_atomic_storage_and_register() {
778        let mut m = parse_mod("mod i { pub const Source2Client002: usize = 0xAA; }");
779        rewrite_interfaces_module(&mut m, &static_args("server.dll"));
780        let s = quote!(#m).to_string();
781        assert!(s.contains("AtomicUsize"));
782        assert!(s.contains("__DYNOFFSETS_Source2Client002"));
783        assert!(s.contains("__register_interfaces_static"));
784        assert!(s.contains("\"server.dll\""));
785        assert!(s.contains("\"Source2Client002\""));
786        assert!(!s.contains("get_runtime_interfaces"));
787    }
788
789    #[test]
790    fn rewrite_buttons_static_mode_emits_atomic_storage_and_register() {
791        let mut m = parse_mod("mod b { pub const in_attack: usize = 0x100; }");
792        rewrite_buttons_module(&mut m, &static_args("client.dll"));
793        let s = quote!(#m).to_string();
794        assert!(s.contains("AtomicUsize"));
795        assert!(s.contains("__DYNOFFSETS_in_attack"));
796        assert!(s.contains("__register_buttons_static"));
797        assert!(s.contains("\"in_attack\""));
798        assert!(!s.contains("get_runtime_buttons"));
799    }
800
801    #[test]
802    fn static_mode_skips_non_pub_usize_items() {
803        let mut m = parse_mod(
804            "mod g { const priv_x: usize = 1; pub const ok: usize = 2; pub const non_usize: u32 = 3; }",
805        );
806        rewrite_globals_module(&mut m, &static_args("client.dll"));
807        let s = quote!(#m).to_string();
808        // Exactly one offset was rewritten into a storage cell.
809        assert_eq!(s.matches("AtomicUsize :: new").count(), 1);
810        // The slot ident is referenced 3 times: static decl, accessor load, register entry.
811        assert_eq!(s.matches("__DYNOFFSETS_ok").count(), 3);
812        // Non-pub / non-usize consts are preserved unchanged.
813        assert!(s.contains("const priv_x"));
814        assert!(s.contains("const non_usize"));
815    }
816
817    #[test]
818    fn slot_ident_prefixes_input() {
819        let id = syn::Ident::new("m_iHealth", proc_macro2::Span::call_site());
820        assert_eq!(slot_ident(&id).to_string(), "__DYNOFFSETS_m_iHealth");
821    }
822}