Skip to main content

rusteal_codegen/
filter.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::config::Blocklist;
4use crate::context::CodegenContext;
5use crate::schema::*;
6use crate::type_map;
7
8pub fn apply_filters(ctx: &mut CodegenContext, blocklist: &Blocklist, editor: bool) {
9    let blocked_classes: HashSet<&str> = blocklist.classes.iter().map(|s| s.as_str()).collect();
10    let blocked_structs: HashSet<&str> = blocklist.structs.iter().map(|s| s.as_str()).collect();
11    let blocked_functions: Vec<(String, String)> = blocklist.function_tuples();
12
13    for cls in &blocklist.classes {
14        ctx.classes.remove(cls);
15    }
16
17    let available_types: HashSet<String> = ctx
18        .module_classes
19        .iter()
20        .filter(|(module, _)| ctx.enabled_modules.contains(*module))
21        .flat_map(|(_, classes)| classes.iter().map(|c| c.name.clone()))
22        .filter(|name| !blocked_classes.contains(name.as_str()))
23        .chain(
24            ctx.module_structs
25                .iter()
26                .filter(|(module, _)| ctx.enabled_modules.contains(*module))
27                .flat_map(|(_, structs)| structs.iter().map(|s| s.name.clone()))
28                .filter(|name| !blocked_structs.contains(name.as_str())),
29        )
30        .chain(
31            ctx.module_enums
32                .iter()
33                .filter(|(module, _)| ctx.enabled_modules.contains(*module))
34                .flat_map(|(_, enums)| enums.iter().map(|e| e.name.clone())),
35        )
36        .collect();
37
38    let unreachable_headers: HashSet<String> = ctx
39        .module_classes
40        .values()
41        .flat_map(|classes| {
42            classes
43                .iter()
44                .filter(|c| !c.header_public)
45                .map(|c| c.name.clone())
46        })
47        .chain(ctx.module_structs.values().flat_map(|structs| {
48            structs
49                .iter()
50                .filter(|s| !s.header_public)
51                .map(|s| s.name.clone())
52        }))
53        .collect();
54
55    for classes in ctx.module_classes.values_mut() {
56        classes.retain(|c| !blocked_classes.contains(c.name.as_str()));
57
58        for class in classes.iter_mut() {
59            class
60                .props
61                .retain(|p| is_property_exportable(p, &available_types));
62
63            if !class.header_public {
64                class.funcs.clear();
65            }
66
67            class
68                .funcs
69                .retain(|f| !references_any(f, &unreachable_headers));
70
71            filter_functions(
72                &class.name,
73                &mut class.funcs,
74                &available_types,
75                &blocked_structs,
76                &blocked_functions,
77                editor,
78            );
79        }
80    }
81}
82
83fn references_any(func: &FunctionInfo, types: &HashSet<String>) -> bool {
84    let names = |p: &PropertyInfo| {
85        [
86            p.class_name.clone(),
87            p.meta_class_name.clone(),
88            p.struct_name.clone(),
89        ]
90        .into_iter()
91        .flatten()
92        .collect::<Vec<_>>()
93    };
94
95    func.params.iter().any(|param| {
96        let direct = [
97            param.class_name.as_ref(),
98            param.meta_class_name.as_ref(),
99            param.struct_name.as_ref(),
100        ];
101
102        let inner = [
103            param.inner_prop.as_deref(),
104            param.key_prop.as_deref(),
105            param.value_prop.as_deref(),
106            param.element_prop.as_deref(),
107        ];
108
109        direct
110            .into_iter()
111            .flatten()
112            .any(|name| types.contains(name))
113            || inner
114                .into_iter()
115                .flatten()
116                .flat_map(names)
117                .any(|name| types.contains(&name))
118    })
119}
120
121fn is_property_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
122    if !type_map::is_supported_type(&prop.prop_type) {
123        return false;
124    }
125
126    if prop.array_dim > 1 {
127        match prop.prop_type.as_str() {
128            "StrProperty" | "NameProperty" | "TextProperty" => return false,
129            _ => {}
130        }
131    }
132
133    if prop.prop_flags & (CPF_NATIVE_ACCESS_PRIVATE | CPF_NATIVE_ACCESS_PROTECTED) != 0
134        && prop.prop_flags & (CPF_BLUEPRINT_VISIBLE | CPF_EDIT | CPF_BLUEPRINT_ASSIGNABLE) == 0
135    {
136        return false;
137    }
138
139    if is_delegate_type(&prop.prop_type) {
140        return is_delegate_exportable(prop, available);
141    }
142
143    if let Some(ref cls) = prop.class_name
144        && !available.contains(cls)
145    {
146        return false;
147    }
148
149    if let Some(ref sn) = prop.struct_name
150        && !available.contains(sn)
151    {
152        return false;
153    }
154
155    if let Some(ref en) = prop.enum_name
156        && !available.contains(en)
157    {
158        return false;
159    }
160
161    if let Some(ref iface) = prop.interface_name
162        && !available.contains(iface)
163    {
164        return false;
165    }
166
167    if prop.prop_type == "ClassProperty"
168        && let Some(ref meta_cls) = prop.meta_class_name
169        && !available.contains(meta_cls)
170    {
171        return false;
172    }
173
174    true
175}
176
177fn is_delegate_type(prop_type: &str) -> bool {
178    matches!(
179        prop_type,
180        "DelegateProperty" | "MulticastInlineDelegateProperty" | "MulticastSparseDelegateProperty"
181    )
182}
183
184fn is_delegate_exportable(prop: &PropertyInfo, available: &HashSet<String>) -> bool {
185    let func_info = match &prop.func_info {
186        Some(fi) => fi,
187        None => return false,
188    };
189
190    let params = match func_info.get("params").and_then(|p| p.as_array()) {
191        Some(params) => params,
192        None => return true,
193    };
194
195    for param_value in params {
196        let param_type = match param_value.get("type").and_then(|t| t.as_str()) {
197            Some(t) => t,
198            None => return false,
199        };
200
201        if !type_map::is_supported_type(param_type) {
202            return false;
203        }
204
205        if is_delegate_type(param_type) {
206            return false;
207        }
208
209        if matches!(param_type, "ArrayProperty" | "MapProperty" | "SetProperty") {
210            return false;
211        }
212
213        if let Some(cls) = param_value.get("class_name").and_then(|v| v.as_str())
214            && !available.contains(cls)
215        {
216            return false;
217        }
218
219        if let Some(sn) = param_value.get("struct_name").and_then(|v| v.as_str())
220            && !available.contains(sn)
221        {
222            return false;
223        }
224
225        if let Some(en) = param_value.get("enum_name").and_then(|v| v.as_str())
226            && !available.contains(en)
227        {
228            return false;
229        }
230    }
231
232    true
233}
234
235fn is_container_param_exportable(param: &ParamInfo, available: &HashSet<String>) -> bool {
236    match param.prop_type.as_str() {
237        "ArrayProperty" => {
238            if let Some(ref inner) = param.inner_prop {
239                is_inner_type_exportable(inner, available)
240            } else {
241                false
242            }
243        }
244        "MapProperty" => {
245            let key_ok = param
246                .key_prop
247                .as_ref()
248                .map(|k| is_inner_type_exportable(k, available))
249                .unwrap_or(false);
250
251            let val_ok = param
252                .value_prop
253                .as_ref()
254                .map(|v| is_inner_type_exportable(v, available))
255                .unwrap_or(false);
256
257            key_ok && val_ok
258        }
259        "SetProperty" => {
260            if let Some(ref elem) = param.element_prop {
261                is_inner_type_exportable(elem, available)
262            } else {
263                false
264            }
265        }
266        _ => false,
267    }
268}
269
270fn is_inner_type_exportable(inner: &PropertyInfo, available: &HashSet<String>) -> bool {
271    if !type_map::is_supported_type(&inner.prop_type) {
272        return false;
273    }
274
275    if matches!(
276        inner.prop_type.as_str(),
277        "ArrayProperty" | "MapProperty" | "SetProperty"
278    ) {
279        return false;
280    }
281
282    if let Some(ref cls) = inner.class_name
283        && !available.contains(cls)
284    {
285        return false;
286    }
287
288    if let Some(ref sn) = inner.struct_name
289        && !available.contains(sn)
290    {
291        return false;
292    }
293
294    if let Some(ref en) = inner.enum_name
295        && !available.contains(en)
296    {
297        return false;
298    }
299
300    if let Some(ref iface) = inner.interface_name
301        && !available.contains(iface)
302    {
303        return false;
304    }
305
306    true
307}
308
309fn filter_functions(
310    class_name: &str,
311    funcs: &mut Vec<FunctionInfo>,
312    available: &HashSet<String>,
313    blocked_structs: &HashSet<&str>,
314    blocked_functions: &[(String, String)],
315    editor: bool,
316) {
317    let all_names: HashSet<String> = funcs.iter().map(|f| f.name.clone()).collect();
318
319    funcs.retain(|f| {
320        if blocked_functions
321            .iter()
322            .any(|(c, func)| c == class_name && func == &f.name)
323        {
324            return false;
325        }
326
327        if f.func_flags & FUNC_EDITOR_ONLY != 0 && !editor {
328            return false;
329        }
330
331        if f.func_flags & FUNC_NATIVE == 0 {
332            return false;
333        }
334
335        if f.name.starts_with("K2_") {
336            let base_name = &f.name[3..];
337
338            if all_names.contains(base_name) {
339                return false;
340            }
341        }
342
343        for param in &f.params {
344            if !type_map::is_supported_type(&param.prop_type) {
345                return false;
346            }
347
348            if is_delegate_type(&param.prop_type) {
349                return false;
350            }
351
352            if matches!(
353                param.prop_type.as_str(),
354                "ArrayProperty" | "MapProperty" | "SetProperty"
355            ) && !is_container_param_exportable(param, available)
356            {
357                return false;
358            }
359
360            if let Some(ref cls) = param.class_name
361                && !available.contains(cls)
362            {
363                return false;
364            }
365
366            if let Some(ref sn) = param.struct_name
367                && (!available.contains(sn) || blocked_structs.contains(sn.as_str()))
368            {
369                return false;
370            }
371
372            if let Some(ref en) = param.enum_name
373                && !available.contains(en)
374            {
375                return false;
376            }
377
378            if let Some(ref iface) = param.interface_name
379                && !available.contains(iface)
380            {
381                return false;
382            }
383
384            if param.prop_type == "ClassProperty"
385                && let Some(ref meta_cls) = param.meta_class_name
386                && !available.contains(meta_cls)
387            {
388                return false;
389            }
390        }
391
392        true
393    });
394
395    for f in funcs.iter_mut() {
396        f.ue_name = f.name.clone();
397    }
398
399    let mut name_counts: HashMap<String, usize> = HashMap::new();
400
401    for f in funcs.iter() {
402        *name_counts.entry(f.name.clone()).or_default() += 1;
403    }
404
405    let mut name_indices: HashMap<String, usize> = HashMap::new();
406
407    for f in funcs.iter_mut() {
408        if let Some(&count) = name_counts.get(&f.name)
409            && count > 1
410        {
411            let idx = name_indices.entry(f.name.clone()).or_insert(0);
412            *idx += 1;
413            f.name = format!("{}_{}", f.name, idx);
414        }
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use rusteal_ue_flags::{CPF_BLUEPRINT_READ_ONLY, CPF_BLUEPRINT_VISIBLE};
422
423    fn object_prop(name: &str, flags: u64) -> PropertyInfo {
424        serde_json::from_value(serde_json::json!({
425            "name": name,
426            "type": "ObjectProperty",
427            "prop_flags": flags,
428            "class_name": "CharacterMovementComponent",
429        }))
430        .unwrap()
431    }
432
433    #[test]
434    fn private_properties_only_when_blueprint_can_reach_them() {
435        let available: HashSet<String> = ["CharacterMovementComponent".to_string()].into();
436        let read_only = CPF_BLUEPRINT_VISIBLE | CPF_BLUEPRINT_READ_ONLY;
437
438        assert!(is_property_exportable(
439            &object_prop("Public", 0),
440            &available
441        ));
442
443        assert!(!is_property_exportable(
444            &object_prop("Hidden", CPF_NATIVE_ACCESS_PRIVATE),
445            &available
446        ));
447
448        assert!(!is_property_exportable(
449            &object_prop("Guarded", CPF_NATIVE_ACCESS_PROTECTED),
450            &available
451        ));
452
453        assert!(is_property_exportable(
454            &object_prop("CharacterMovement", CPF_NATIVE_ACCESS_PRIVATE | read_only),
455            &available
456        ));
457
458        assert!(is_property_exportable(
459            &object_prop(
460                "Guarded",
461                CPF_NATIVE_ACCESS_PROTECTED | CPF_BLUEPRINT_VISIBLE
462            ),
463            &available
464        ));
465
466        assert!(is_property_exportable(
467            &object_prop("Editable", CPF_NATIVE_ACCESS_PROTECTED | CPF_EDIT),
468            &available
469        ));
470
471        assert!(is_property_exportable(
472            &object_prop(
473                "Assignable",
474                CPF_NATIVE_ACCESS_PROTECTED | CPF_BLUEPRINT_ASSIGNABLE
475            ),
476            &available
477        ));
478    }
479
480    fn function(params: serde_json::Value) -> FunctionInfo {
481        serde_json::from_value(serde_json::json!({
482            "name": "Probe",
483            "func_flags": FUNC_NATIVE,
484            "params": params,
485        }))
486        .unwrap()
487    }
488
489    fn param(fields: serde_json::Value) -> serde_json::Value {
490        let mut param = serde_json::json!({
491            "name": "P", "type": "ObjectProperty", "prop_flags": 0,
492            "enum_name": null, "enum_cpp_name": null, "enum_cpp_form": null,
493            "enum_underlying_type": null, "class_name": null, "meta_class_name": null,
494            "struct_name": null, "interface_name": null, "func_info": null,
495            "inner_prop": null, "key_prop": null, "value_prop": null,
496            "element_prop": null, "default": null,
497        });
498
499        for (key, value) in fields.as_object().unwrap() {
500            param[key] = value.clone();
501        }
502
503        param
504    }
505
506    #[test]
507    fn functions_naming_unreachable_headers_drop_out() {
508        let internal: HashSet<String> = ["NiagaraDataInterfaceArrayMesh".to_string()].into();
509        let takes = |fields| function(serde_json::json!([param(fields)]));
510
511        assert!(references_any(
512            &takes(serde_json::json!({"class_name": "NiagaraDataInterfaceArrayMesh"})),
513            &internal
514        ));
515
516        assert!(references_any(
517            &takes(serde_json::json!({
518                "type": "ArrayProperty",
519                "inner_prop": {"name": "P", "type": "ObjectProperty", "prop_flags": 0,
520                               "class_name": "NiagaraDataInterfaceArrayMesh"},
521            })),
522            &internal
523        ));
524
525        assert!(!references_any(
526            &takes(serde_json::json!({"class_name": "NiagaraComponent"})),
527            &internal
528        ));
529
530        assert!(!references_any(&function(serde_json::json!([])), &internal));
531    }
532}