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}