Skip to main content

rakata_formats/
content_source.rs

1//! What a reader is allowed to ask the outside world for.
2//!
3//! Turning engine integers into typed semantics means reaching things that
4//! live outside the file being read: a 2DA row, a line of display text,
5//! whether a referenced resource is actually there. Taking a concrete
6//! resource-mounting type instead would drag that whole layer into every
7//! crate that only wanted to parse and resolve bytes.
8//!
9//! [`ContentSource`] is that seam. It lives in `rakata-formats` because it
10//! returns a [`TwoDa`], and formats is the lowest crate that can name one.
11//!
12//! ## Nothing consumes it today
13//!
14//! The decoded views were its caller and they have been removed, pending a
15//! consumer that can state requirements. What keeps the seam here rather
16//! than deleting it with them is the ruling it was built for: "no install"
17//! is a source that answers nothing, not a second code path through every
18//! reader. If the decoded layer has not been rebuilt by the time the write
19//! path lands, this goes then.
20//!
21//! ## Capability is opt-in
22//!
23//! Every method has a default body, so an implementor writes only the
24//! capabilities it has. A decode test hands in a map of tables and
25//! implements one method; an install-backed source implements all of them.
26//! A capability added here later breaks no implementor and no call site.
27//!
28//! **The defaults answer "I did not look", never "no".** That distinction is
29//! the reason [`ContentSource::resource_exists`] returns `Option<bool>`
30//! rather than a `bool`: a source with no install behind it saying `false`
31//! is a negative claim from something that never went looking, and a caller
32//! cannot tell that apart from a real absence.
33//!
34//! ## Shape
35//!
36//! Every method takes `&mut self`, including the read-only ones. Mixing
37//! `&self` in buys nothing, because the borrow conflict callers hit comes
38//! from holding a returned table, and that blocks a shared borrow exactly as
39//! hard as a mutable one. Uniform `&mut self` leaves every capability free
40//! to populate itself lazily, which is what an install-backed talk table
41//! needs.
42//!
43//! Only the table is returned borrowed. It is large and a resolver reads
44//! several cells out of one row, so the borrow earns itself. Strings are
45//! small and read once, and a second borrowing accessor would multiply the
46//! inner-block dance `Uti`'s magnitude resolution already needs across every
47//! view.
48//!
49//! The trait is not object-safe, since the table lookup takes
50//! `impl AsRef<str>`. Call sites take `&mut impl ContentSource`, so nothing
51//! needs it to be.
52
53use rakata_core::{ResRef, ResourceTypeCode, StrRef};
54
55use crate::twoda::TwoDa;
56
57/// Something a reader can resolve external references against.
58///
59/// See the [module docs](self) for why every method is defaulted and why
60/// the defaults are non-answers rather than falsy values.
61pub trait ContentSource {
62    /// Returns the named table, or `None` if it cannot be produced.
63    ///
64    /// `name` is a bare table name with no `.2da` extension, so both a
65    /// `&str` literal and a `TwoDaName` constant work at the call site.
66    ///
67    /// The `None` case merges "no such table" with "the table would not
68    /// parse". A decoder falls back to raw values either way, and a caller
69    /// that needs to tell the two apart should ask the implementation
70    /// directly rather than route that through resolution.
71    fn twoda(&mut self, name: impl AsRef<str>) -> Option<&TwoDa> {
72        let _ = name;
73        None
74    }
75
76    /// Returns the display text a `StrRef` names.
77    ///
78    /// `None` covers a source with no talk table behind it as well as a
79    /// `StrRef` that names no entry. A caller that got `None` displays the
80    /// raw `StrRef`.
81    fn text(&mut self, strref: StrRef) -> Option<String> {
82        let _ = strref;
83        None
84    }
85
86    /// Returns the voiceover resource a `StrRef`'s entry names.
87    ///
88    /// Talk table entries carry a VO resref alongside their text, so this is
89    /// a second read of the same entry rather than a separate lookup.
90    fn voiceover(&mut self, strref: StrRef) -> Option<ResRef> {
91        let _ = strref;
92        None
93    }
94
95    /// Returns whether the named resource resolves.
96    ///
97    /// `None` means this source cannot answer, which is not the same as
98    /// `Some(false)`: a decoded view flagging a broken reference must not
99    /// treat "nobody looked" as "the resource is missing".
100    fn resource_exists(
101        &mut self,
102        resref: &ResRef,
103        resource_type: ResourceTypeCode,
104    ) -> Option<bool> {
105        let _ = (resref, resource_type);
106        None
107    }
108}
109
110/// A source that answers nothing.
111///
112/// This is what a caller with no install behind it hands in, and it matters
113/// more than a convenience type would. Without it a CLI inspecting a loose
114/// file would stop at a projection while a GUI with an install reached a
115/// resolved view, and the two could not share display code. With it there is
116/// one resolved type everywhere, and having no install is a source that
117/// declines every question rather than a separate path through the program.
118#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
119pub struct NoSource;
120
121impl ContentSource for NoSource {}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126    use crate::twoda::TwoDaRow;
127
128    /// The shape a decode test double takes: one capability, three defaults.
129    #[derive(Default)]
130    struct TablesOnly(Option<TwoDa>);
131
132    impl ContentSource for TablesOnly {
133        fn twoda(&mut self, _name: impl AsRef<str>) -> Option<&TwoDa> {
134            self.0.as_ref()
135        }
136    }
137
138    fn one_table() -> TwoDa {
139        TwoDa {
140            headers: vec!["label".to_string()],
141            rows: vec![TwoDaRow {
142                label: "0".to_string(),
143                cells: vec!["alpha".to_string()],
144            }],
145        }
146    }
147
148    #[test]
149    fn implementing_one_capability_leaves_the_others_answering_nothing() {
150        // The defaults are the mechanism the whole design rests on: a double
151        // that only knows tables must not start asserting things about text
152        // or about what an install contains.
153        let mut source = TablesOnly(Some(one_table()));
154
155        assert!(source.twoda("anything").is_some(), "the one impl is live");
156        assert_eq!(source.text(StrRef::from_raw(0)), None);
157        assert_eq!(source.voiceover(StrRef::from_raw(0)), None);
158        assert_eq!(
159            source.resource_exists(
160                &ResRef::new("nwscript").expect("valid resref"),
161                ResourceTypeCode::from_raw_id(2009)
162            ),
163            None,
164            "a source with no install must not claim the resource is absent"
165        );
166    }
167
168    #[test]
169    fn the_null_source_declines_every_capability_including_tables() {
170        let mut source = NoSource;
171
172        assert!(source.twoda("appearance").is_none());
173        assert_eq!(source.text(StrRef::from_raw(0)), None);
174        assert_eq!(source.voiceover(StrRef::from_raw(0)), None);
175        assert_eq!(
176            source.resource_exists(
177                &ResRef::new("nwscript").expect("valid resref"),
178                ResourceTypeCode::from_raw_id(2009)
179            ),
180            None
181        );
182    }
183}