1use 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 static_storage: bool,
21 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 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 #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 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 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 #[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 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
372fn cached_accessor(
384 vis: &syn::Visibility,
385 fn_name: &Ident,
386 lit_expr: &TokenStream2,
387 lookup_expr: TokenStream2,
388) -> TokenStream2 {
389 quote! {
390 #[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 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 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 assert_eq!(s.matches("lookup_or_fallback(").count(), 0);
593 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 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 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 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 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 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 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 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 assert_eq!(s.matches("AtomicUsize :: new").count(), 1);
810 assert_eq!(s.matches("__DYNOFFSETS_ok").count(), 3);
812 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}