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(&format!(
46            "\nimpl rusteal_core::UeEnum for {name} {{\n    type Repr = {repr};\n}}\n"
47        ));
48        generate_newtype_container_element(&mut out, name, repr);
49        return out;
50    }
51
52    // Determine repr: if there are negative values and the UE type is unsigned,
53    // promote to the signed equivalent. Also normalize values to avoid collisions
54    // (e.g., 255u8 == -1i8).
55    let has_negative = variants.iter().any(|(_, v)| *v < 0);
56    let repr = if has_negative {
57        underlying_to_signed_repr(&e.underlying_type)
58    } else {
59        underlying_to_repr(&e.underlying_type)
60    };
61
62    // Normalize values through the repr type to detect actual duplicates
63    if has_negative {
64        let mut seen_normalized = std::collections::HashSet::new();
65        variants.retain(|(_, v)| {
66            let normalized = normalize_to_signed(*v, &e.underlying_type);
67            seen_normalized.insert(normalized)
68        });
69    }
70
71    // Enum definition
72    out.push_str(&format!(
73        "#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]\n\
74         #[repr({repr})]\n\
75         pub enum {name} {{\n"
76    ));
77
78    for (variant_name, value) in &variants {
79        let clean_name = strip_enum_prefix(variant_name, name);
80        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
81        let display_value = if has_negative {
82            normalize_to_signed(*value, &e.underlying_type)
83        } else {
84            *value
85        };
86        out.push_str(&format!("    {safe_name} = {display_value},\n"));
87    }
88
89    out.push_str("}\n");
90
91    // from_value method
92    out.push_str(&format!(
93        "\nimpl {name} {{\n    pub fn from_value(v: {repr}) -> Option<Self> {{\n        match v {{\n"
94    ));
95    for (variant_name, value) in &variants {
96        let clean_name = strip_enum_prefix(variant_name, name);
97        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
98        let display_value = if has_negative {
99            normalize_to_signed(*value, &e.underlying_type)
100        } else {
101            *value
102        };
103        out.push_str(&format!("            {display_value} => Some({name}::{safe_name}),\n"));
104    }
105    out.push_str("            _ => None,\n        }\n    }\n\n");
106
107    // display_name method
108    out.push_str("    pub fn display_name(&self) -> &'static str {\n        match self {\n");
109    for (variant_name, _) in &variants {
110        let clean_name = strip_enum_prefix(variant_name, name);
111        let safe_name = escape_reserved(&sanitize_variant_name(clean_name));
112        out.push_str(&format!(
113            "            {name}::{safe_name} => \"{clean_name}\",\n"
114        ));
115    }
116    out.push_str("        }\n    }\n}\n");
117
118    // UeEnum impl
119    out.push_str(&format!(
120        "\nimpl rusteal_core::UeEnum for {name} {{\n    type Repr = {repr};\n}}\n"
121    ));
122
123    // ContainerElement impl — allows this enum to be used as TArray/TMap/TSet element
124    generate_enum_container_element(&mut out, name, repr);
125
126    out
127}
128
129/// Map UE underlying type string to Rust repr type.
130fn underlying_to_repr(ut: &str) -> &'static str {
131    match ut {
132        "uint8" => "u8",
133        "int8" => "i8",
134        "uint16" => "u16",
135        "int16" => "i16",
136        "uint32" => "u32",
137        "int32" => "i32",
138        "uint64" => "u64",
139        "int64" => "i64",
140        _ => "u8",
141    }
142}
143
144/// Map unsigned UE type to signed equivalent (for enums with negative values).
145fn underlying_to_signed_repr(ut: &str) -> &'static str {
146    match ut {
147        "uint8" => "i8",
148        "int8" => "i8",
149        "uint16" => "i16",
150        "int16" => "i16",
151        "uint32" => "i32",
152        "int32" => "i32",
153        "uint64" => "i64",
154        "int64" => "i64",
155        _ => "i8",
156    }
157}
158
159/// Strip enum name prefix from variant names (e.g., "EFoo::Bar" → "Bar",
160/// or if variants have EnumName_ prefix).
161fn strip_enum_prefix<'a>(variant: &'a str, enum_name: &str) -> &'a str {
162    if let Some(rest) = variant.strip_prefix(enum_name)
163        && let Some(rest) = rest.strip_prefix("::")
164    {
165        return rest;
166    }
167    variant
168}
169
170/// Normalize a value to signed representation through the type's width.
171/// e.g., 255u8 → -1i8, but 127u8 → 127i8.
172fn normalize_to_signed(value: i64, underlying_type: &str) -> i64 {
173    match underlying_type {
174        "uint8" => (value as u8) as i8 as i64,
175        "int8" => (value as i8) as i64,
176        "uint16" => (value as u16) as i16 as i64,
177        "int16" => (value as i16) as i64,
178        "uint32" => (value as u32) as i32 as i64,
179        "int32" => (value as i32) as i64,
180        _ => value,
181    }
182}
183
184/// Generate `ContainerElement` impl for a `#[repr(X)]` enum.
185fn generate_enum_container_element(out: &mut String, name: &str, repr: &str) {
186    out.push_str(&format!(
187        "\nunsafe impl rusteal_core::ContainerElement for {name} {{\n\
188         \x20   const BUF_SIZE: u32 = std::mem::size_of::<{repr}>() as u32;\n\n\
189         \x20   #[inline]\n\
190         \x20   unsafe fn read_from_buf(buf: *const u8, _written: u32) -> Self {{\n\
191         \x20       unsafe {{ std::mem::transmute::<{repr}, Self>((buf as *const {repr}).read_unaligned()) }}\n\
192         \x20   }}\n\n\
193         \x20   #[inline]\n\
194         \x20   unsafe fn write_to_buf(&self, buf: *mut u8) -> u32 {{\n\
195         \x20       unsafe {{\n\
196         \x20           let raw: {repr} = std::mem::transmute(*self);\n\
197         \x20           (buf as *mut {repr}).write_unaligned(raw);\n\
198         \x20       }}\n\
199         \x20       std::mem::size_of::<{repr}>() as u32\n\
200         \x20   }}\n\
201         }}\n"
202    ));
203}
204
205/// Generate `ContainerElement` impl for a newtype-wrapper empty enum.
206fn generate_newtype_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 {{ 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 {{ (buf as *mut {repr}).write_unaligned(self.0) }};\n\
217         \x20       std::mem::size_of::<{repr}>() as u32\n\
218         \x20   }}\n\
219         }}\n"
220    ));
221}
222
223/// Sanitize variant name to be a valid Rust identifier.
224fn sanitize_variant_name(name: &str) -> String {
225    let mut result = String::with_capacity(name.len());
226    for (i, ch) in name.chars().enumerate() {
227        if ch.is_alphanumeric() || ch == '_' {
228            if i == 0 && ch.is_ascii_digit() {
229                result.push('_');
230            }
231            result.push(ch);
232        } else {
233            result.push('_');
234        }
235    }
236    if result.is_empty() {
237        result.push_str("_Unknown");
238    }
239    result
240}