Skip to main content

banished_ui/
dialog.rs

1//! Dialog instances resolve named template substitutions and controller attachment points.
2//! Controller type IDs describe application code, so callers register their own handlers.
3use crate::*;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone)]
7/// A controller attached to an element of a dialog.
8pub struct Binding {
9    /// The controller's type id (CRC-32 of its class name).
10    pub controller: u32,
11    /// The controller's configuration resource, if it has one.
12    pub config: Option<String>,
13    /// The element it is attached to.
14    pub element: NodeId,
15    /// The element's name.
16    pub name: String,
17}
18
19#[derive(Debug, Clone)]
20/// A dialog made from its resource: the element tree and its controllers.
21pub struct DialogInstance {
22    /// The root element: lay it out and draw it.
23    pub root: NodeId,
24    /// The controllers attached to its elements.
25    pub bindings: Vec<Binding>,
26    /// The dialogs it was built from: the dialog and those it includes.
27    pub dialogs: Vec<String>,
28    /// The background sprite drawn behind the dialog, if any.
29    pub background: Option<SpriteRef>,
30}
31
32impl DialogInstance {
33    /// Deliver an event to each controller attached to the element or one of its ancestors.
34    /// The application implements the game-specific controller using its type ID and config.
35    pub fn dispatch(&self, ui: &Ui, event: Event, mut handler: impl FnMut(&Binding, Event)) {
36        let source = input::event_node(event);
37        for b in &self.bindings {
38            if ui.descendant(b.element, source) {
39                handler(b, event);
40            }
41        }
42    }
43}
44
45impl Ui {
46    /// Build a dialog from its resource, for example `Dialog/MainMenu.rsc:resource`.
47    pub fn instantiate_dialog(&mut self, reference: &str) -> Option<DialogInstance> {
48        let d = banished_formats::ui::dialog(&self.res.archive.read_ref(reference).ok()?).ok()?;
49        // Ghidra Dialog::CreateElement (0x180115760): match the requested path identifier
50        // against the named list; a null replacement suppresses that slot.
51        let overrides: HashMap<_, _> = d
52            .named
53            .iter()
54            .map(|(r, name)| (name.clone(), r.clone()))
55            .collect();
56        self.overrides.push(overrides);
57        let root = d.root.as_deref().and_then(|r| self.instantiate(r));
58        self.overrides.pop();
59        let root = root?;
60        for (name, key) in [("buttonOk", 13), ("buttonCancel", 27)] {
61            if let Some(id) = self.find(root, name) {
62                self.set_shortcut(id, key);
63            }
64        }
65        let mut bindings = Vec::new();
66        for b in d.bindings {
67            let element = if b.name.is_empty() {
68                Some(root)
69            } else {
70                self.find(root, &b.name)
71            };
72            let Some(element) = element else {
73                log::warn!("unresolved dialog controller host: {}", b.name);
74                continue;
75            };
76            // Ghidra AddController (0x180115d00) optionally creates an element description
77            // under the named host before attaching its controller.
78            let attached = if let Some(reference) = b.source.as_deref() {
79                if let Some(c) = self.instantiate(reference) {
80                    self.add_child(element, c);
81                    c
82                } else {
83                    element
84                }
85            } else {
86                element
87            };
88            bindings.push(Binding {
89                controller: b.unknown,
90                config: b.target,
91                element: attached,
92                name: b.name,
93            });
94        }
95        let background = self
96            .res
97            .sprite(d.background_sheet.as_deref(), &d.background_sprite);
98        Some(DialogInstance {
99            root,
100            bindings,
101            dialogs: d.dialogs.into_iter().flatten().collect(),
102            background,
103        })
104    }
105
106    /// Whether `node` is `root` or inside it.
107    pub fn descendant(&self, root: NodeId, node: NodeId) -> bool {
108        root == node
109            || self
110                .child_ids(root)
111                .into_iter()
112                .any(|child| self.descendant(child, node))
113    }
114}