Skip to main content

rusteal_codegen/rust_gen/
enums.rs

1// Rust enum generation from UHT JSON.
2
3use crate::naming::escape_reserved;
4use crate::schema::EnumInfo;
5
6/// Generate Rust code for a single UE enum.
7pub fn generate_enum(e: &EnumInfo) -> String {
8    let mut out = String::with_capacity(2048);
9
10    let name = &e.name;
11
12    // Filter out MAX/Count sentinel values and duplicates
13    let mut seen_values = std::collections::HashSet::new();
14    let mut seen_names = std::collections::HashSet::new();
15    let mut variants: Vec<(&str, i64)> = Vec::new();
16    for (variant_name, value) in &e.pairs {
17        // Skip MAX sentinel and _MAX patterns
18        if variant_name.ends_with("_MAX")
19            || variant_name == "MAX"
20            || variant_name.contains("__MAX")
21        {
22            continue;
23        }
24        // Skip duplicate values
25        if !seen_values.insert(*value) {
26            continue;
27        }
28        // Skip duplicate names (after prefix stripping)
29        let clean = strip_enum_prefix(variant_name, &e.name);
30        let sanitized = sanitize_variant_name(clean);
31        if !seen_names.insert(sanitized) {
32            continue;
33        }
34        variants.push((variant_name, *value));
35    }
36
37    if variants.is_empty() {
38        // Empty enum — generate as a newtype wrapper
39        let repr = underlying_to_repr(&e.underlying_type);
40        out.push_str(&format!(
41            "// Empty enum {name}\n\
42             #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n\
43             pub struct {name}(pub {repr});\n"
44        ));
45        out.push_str(&ue_enum_impl(name, repr, "self.0 as i64", "Some(Self(value as {repr}))"));
46        generate_newtype_container_element(&mut out, name, repr);
47        return out;
48    }
49
50    // Determine repr: if there are negative values and the UE type is unsigned,
51    // promote to the signed equivalent. Also normalize values to avoid collisions
52    // (e.g., 255u8 == -1i8).
53    let has_negative = variants.iter().any(|(_, v)| *v < 0);
54    let repr = if has_negative {
55        underlying_to_signed_repr(&e.underlying_type)
56    } else {
57        underlying_to_repr(&e.underlying_type)
58    };
59
60    // Normalize values through the repr type to detect actual duplicates
61    if has_negative {
62        let mut seen_normalized = std::collections::HashSet::new();
63        variants.retain(|(_, v)| {
64            let normalized = normalize_to_signed(*v, &e.underlying_type);
65            seen_normalized.insert(normalized)
66        });
67    }
68
69    // Enum definition
70    out.push_str(&format!(
71        "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n\
72         #[repr({repr})]\n\
73         pub enum {name} {{\n"
74    ));
75
76    for (variant_name, value) in &variants {
77        let clean_name = strip_enum_prefix(variant_name, name);
78        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
79        let display_value = if has_negative {
80            normalize_to_signed(*value, &e.underlying_type)
81        } else {
82            *value
83        };
84        out.push_str(&format!("    {safe_name} = {display_value},\n"));
85    }
86
87    out.push_str("}\n");
88
89    // from_value method
90    out.push_str(&format!(
91        "\nimpl {name} {{\n    pub fn from_value(v: {repr}) -> Option<Self> {{\n        match v {{\n"
92    ));
93    for (variant_name, value) in &variants {
94        let clean_name = strip_enum_prefix(variant_name, name);
95        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
96        let display_value = if has_negative {
97            normalize_to_signed(*value, &e.underlying_type)
98        } else {
99            *value
100        };
101        out.push_str(&format!("            {display_value} => Some({name}::{safe_name}),\n"));
102    }
103    out.push_str("            _ => None,\n        }\n    }\n\n");
104
105    // display_name method
106    out.push_str("    pub fn display_name(&self) -> &'static str {\n        match self {\n");
107    for (variant_name, _) in &variants {
108        let clean_name = strip_enum_prefix(variant_name, name);
109        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
110        out.push_str(&format!(
111            "            {name}::{safe_name} => \"{clean_name}\",\n"
112        ));
113    }
114    out.push_str("        }\n    }\n}\n");
115
116    // UeEnum impl
117    out.push_str(&ue_enum_impl(name, repr, "self as {repr} as i64", "Self::from_value(value as {repr})"));
118
119    // ContainerElement impl — allows this enum to be used as TArray/TMap/TSet element
120    generate_enum_container_element(&mut out, name, repr);
121
122    out
123}
124
125/// `impl UeEnum`: the repr, the UEnum by name and the i64 conversions (the
126/// expressions may name `{repr}`).
127fn ue_enum_impl(name: &str, repr: &str, to_i64: &str, from_i64: &str) -> String {
128    let to_i64 = to_i64.replace("{repr}", repr);
129    let from_i64 = from_i64.replace("{repr}", repr);
130    let name_len = name.len();
131    format!(
132        "\nimpl rusteal_core::UeEnum for {name} {{\n\
133         \x20   type Repr = {repr};\n\n\
134         \x20   fn static_enum() -> rusteal_core::UClassHandle {{\n\
135         \x20       static CACHE: std::sync::OnceLock<rusteal_core::UClassHandle> = std::sync::OnceLock::new();\n\
136         \x20       *CACHE.get_or_init(|| unsafe {{\n\
137         \x20           rusteal_core::ffi_dispatch::reflection_find_enum(b\"{name}\\0\".as_ptr(), {name_len})\n\
138         \x20       }})\n\
139         \x20   }}\n\n\
140         \x20   fn to_i64(self) -> i64 {{\n\
141         \x20       {to_i64}\n\
142         \x20   }}\n\n\
143         \x20   fn from_i64(value: i64) -> Option<Self> {{\n\
144         \x20       {from_i64}\n\
145         \x20   }}\n\
146         }}\n"
147    )
148}
149
150/// Map UE underlying type string to Rust repr type.
151fn underlying_to_repr(ut: &str) -> &'static str {
152    match ut {
153        "uint8" => "u8",
154        "int8" => "i8",
155        "uint16" => "u16",
156        "int16" => "i16",
157        "uint32" => "u32",
158        "int32" => "i32",
159        "uint64" => "u64",
160        "int64" => "i64",
161        _ => "u8",
162    }
163}
164
165/// Map unsigned UE type to signed equivalent (for enums with negative values).
166fn underlying_to_signed_repr(ut: &str) -> &'static str {
167    match ut {
168        "uint8" => "i8",
169        "int8" => "i8",
170        "uint16" => "i16",
171        "int16" => "i16",
172        "uint32" => "i32",
173        "int32" => "i32",
174        "uint64" => "i64",
175        "int64" => "i64",
176        _ => "i8",
177    }
178}
179
180/// Strip enum name prefix from variant names (e.g., "EFoo::Bar" → "Bar",
181/// or if variants have EnumName_ prefix).
182fn strip_enum_prefix<'a>(variant: &'a str, enum_name: &str) -> &'a str {
183    if let Some(rest) = variant.strip_prefix(enum_name)
184        && let Some(rest) = rest.strip_prefix("::")
185    {
186        return rest;
187    }
188    variant
189}
190
191/// Normalize a value to signed representation through the type's width.
192/// e.g., 255u8 → -1i8, but 127u8 → 127i8.
193fn normalize_to_signed(value: i64, underlying_type: &str) -> i64 {
194    match underlying_type {
195        "uint8" => (value as u8) as i8 as i64,
196        "int8" => (value as i8) as i64,
197        "uint16" => (value as u16) as i16 as i64,
198        "int16" => (value as i16) as i64,
199        "uint32" => (value as u32) as i32 as i64,
200        "int32" => (value as i32) as i64,
201        _ => value,
202    }
203}
204
205/// Generate `ContainerElement` impl for a `#[repr(X)]` enum.
206fn generate_enum_container_element(out: &mut String, name: &str, repr: &str) {
207    out.push_str(&format!(
208        "\nunsafe impl rusteal_core::ContainerElement for {name} {{\n\
209         \x20   const BUF_SIZE: u32 = std::mem::size_of::<{repr}>() as u32;\n\n\
210         \x20   #[inline]\n\
211         \x20   unsafe fn read_from_buf(buf: *const u8, _written: u32) -> Self {{\n\
212         \x20       unsafe {{ std::mem::transmute::<{repr}, Self>((buf as *const {repr}).read_unaligned()) }}\n\
213         \x20   }}\n\n\
214         \x20   #[inline]\n\
215         \x20   unsafe fn write_to_buf(&self, buf: *mut u8) -> u32 {{\n\
216         \x20       unsafe {{\n\
217         \x20           let raw: {repr} = std::mem::transmute(*self);\n\
218         \x20           (buf as *mut {repr}).write_unaligned(raw);\n\
219         \x20       }}\n\
220         \x20       std::mem::size_of::<{repr}>() as u32\n\
221         \x20   }}\n\
222         }}\n"
223    ));
224}
225
226/// Generate `ContainerElement` impl for a newtype-wrapper empty enum.
227fn generate_newtype_container_element(out: &mut String, name: &str, repr: &str) {
228    out.push_str(&format!(
229        "\nunsafe impl rusteal_core::ContainerElement for {name} {{\n\
230         \x20   const BUF_SIZE: u32 = std::mem::size_of::<{repr}>() as u32;\n\n\
231         \x20   #[inline]\n\
232         \x20   unsafe fn read_from_buf(buf: *const u8, _written: u32) -> Self {{\n\
233         \x20       unsafe {{ Self((buf as *const {repr}).read_unaligned()) }}\n\
234         \x20   }}\n\n\
235         \x20   #[inline]\n\
236         \x20   unsafe fn write_to_buf(&self, buf: *mut u8) -> u32 {{\n\
237         \x20       unsafe {{ (buf as *mut {repr}).write_unaligned(self.0) }};\n\
238         \x20       std::mem::size_of::<{repr}>() as u32\n\
239         \x20   }}\n\
240         }}\n"
241    ));
242}
243
244/// Sanitize variant name to be a valid Rust identifier.
245fn sanitize_variant_name(name: &str) -> String {
246    let mut result = String::with_capacity(name.len());
247    for (i, ch) in name.chars().enumerate() {
248        if ch.is_alphanumeric() || ch == '_' {
249            if i == 0 && ch.is_ascii_digit() {
250                result.push('_');
251            }
252            result.push(ch);
253        } else {
254            result.push('_');
255        }
256    }
257    if result.is_empty() {
258        result.push_str("_Unknown");
259    }
260    result
261}