summaryrefslogtreecommitdiff
path: root/tools/ai_show_nontrivial_hierarchies.py
blob: c652225e0e8c4f86e6474365afee85160d15f5da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
#!/usr/bin/env python3
import argparse
from pathlib import Path
from typing import Union

import yaml

import ai_common
from ai_common import BaseClasses
from common.util.graph import Graph

_known_vtables = {
    0x71024d8d68: "ActionBase",
    0x71025129f0: "Action",
    0x7102513278: "Ai",
    0x71024d8ef0: "Behavior",
    0x710243c9b8: "Query",
}


def get_name_for_vtable(vtable: Union[str, int]):
    if isinstance(vtable, str):
        return vtable

    known_name = _known_vtables.get(vtable, None)
    if known_name is not None:
        return f"[V] {known_name}"

    return f"[V] {vtable:#x}"


def guess_vtable_names(reverse_graph: Graph):
    for u in reverse_graph.nodes:
        targets = list(reverse_graph.nodes[u])
        known_targets = list(filter(lambda x: isinstance(x, str), targets))
        if len(known_targets) == 1:
            # Leaves can be named pretty easily.
            _known_vtables[u] = known_targets[0]


def build_graph(all_vtables: dict, type_: str, graph: Graph, reverse_graph: Graph):
    for name, vtables in all_vtables[type_].items():
        classes = [name] + list(reversed(vtables))
        # Each class has at least one parent, so the -1 is fine.
        for i in range(len(classes) - 1):
            from_ = classes[i]
            to_ = classes[i + 1]
            # Skip base classes to reduce noise.
            if to_ in BaseClasses:
                break
            reverse_graph.add_edge(to_, from_)

    guess_vtable_names(reverse_graph)

    for name, vtables in all_vtables[type_].items():
        classes = [name] + list(reversed(vtables))
        for i in range(len(classes) - 1):
            if classes[i + 1] in BaseClasses:
                break
            from_ = get_name_for_vtable(classes[i])
            to_ = get_name_for_vtable(classes[i + 1])
            graph.add_edge(from_, to_)


def main() -> None:
    parser = argparse.ArgumentParser(description="Shows AI classes with non-trivial class hierarchies.")
    parser.add_argument("--type", help="AI class type to visualise", choices=["Action", "AI", "Behavior", "Query"],
                        required=True)
    parser.add_argument("--out-names", help="Path to which a vtable -> name map will be written", required=True)
    args = parser.parse_args()

    all_vtables = ai_common.get_vtables()

    graph = Graph()
    reverse_graph = Graph()
    build_graph(all_vtables, args.type, graph, reverse_graph)

    interesting_nodes = set()
    node_colors = dict()

    colors = ["#c7dcff", "#ffc7c7", "#ceffc7", "#dcc7ff", "#fffdc9", "#c9fff3", "#ffe0cc", "#ffcffe", "#96a8ff"]
    components = graph.find_connected_components()
    num_nontrivial_cc = 0
    for i, comp in enumerate(components):
        if len(comp) == 2:
            continue
        for node in comp:
            node_colors[node] = colors[i % len(colors)]
        num_nontrivial_cc += 1
        interesting_nodes |= set(comp)

    print("digraph {")
    print("node [shape=rectangle]")
    for u in graph.nodes:
        if u not in interesting_nodes:
            continue
        for v in graph.nodes[u]:
            shape_u = "shape=component," if "[V]" not in u else ""
            shape_v = "shape=component," if "[V]" not in v else ""
            print(f'"{u}" [{shape_u}style=filled, fillcolor="{node_colors[u]}"]')
            print(f'"{v}" [{shape_v}style=filled, fillcolor="{node_colors[v]}"]')
            print(f'"{u}" -> "{v}"')
    print("}")
    print(f"# {len(components)} connected components")
    print(f"# {num_nontrivial_cc} non-trivial connected components")

    yaml.add_representer(int, lambda dumper, data: yaml.ScalarNode('tag:yaml.org,2002:int', f"{data:#x}"),
                         Dumper=yaml.CSafeDumper)
    with Path(args.out_names).open("w") as f:
        yaml.dump(_known_vtables, f, Dumper=yaml.CSafeDumper)


if __name__ == '__main__':
    main()