Skip to main content

rakata_formats/vis/
mod.rs

1//! VIS ASCII reader and writer.
2//!
3//! VIS (visibility) resources define which rooms are visible from each other
4//! and are used for renderer culling decisions.
5//!
6//! ## Format Layout
7//! ```text
8//! <observer_room> <child_count>
9//!   <visible_room>
10//!   <visible_room>
11//! <observer_room> <child_count>
12//!   ...
13//! ```
14//!
15//! Empty lines are ignored. Room names are treated case-insensitively and are
16//! normalized to lowercase in-memory.
17//! Text is decoded/encoded as Windows-1252.
18
19mod reader;
20mod writer;
21
22pub use reader::{read_vis, read_vis_from_bytes};
23pub use writer::{write_vis, write_vis_to_vec};
24
25use std::collections::{BTreeMap, BTreeSet};
26use thiserror::Error;
27
28use rakata_core::{DecodeTextError, EncodeTextError};
29
30use crate::binary::{DecodeBinary, EncodeBinary};
31
32/// In-memory VIS graph.
33#[derive(Debug, Clone, PartialEq, Eq, Default)]
34pub struct Vis {
35    visibility: BTreeMap<String, BTreeSet<String>>,
36}
37
38impl Vis {
39    /// Creates an empty VIS graph.
40    pub fn new() -> Self {
41        Self::default()
42    }
43
44    /// Returns all known rooms.
45    pub fn all_rooms(&self) -> BTreeSet<String> {
46        self.visibility.keys().cloned().collect()
47    }
48
49    /// Returns visibility edges keyed by observer room.
50    pub fn visibility(&self) -> &BTreeMap<String, BTreeSet<String>> {
51        &self.visibility
52    }
53
54    /// Returns `true` if `room` exists in the graph.
55    pub fn room_exists(&self, room: &str) -> bool {
56        self.visibility.contains_key(&canonical_room(room))
57    }
58
59    /// Adds a room if it does not already exist.
60    pub fn add_room(&mut self, room: impl AsRef<str>) {
61        let room = canonical_room(room.as_ref());
62        self.visibility.entry(room).or_default();
63    }
64
65    /// Removes a room and all references to it.
66    pub fn remove_room(&mut self, room: &str) {
67        let room = canonical_room(room);
68        self.visibility.remove(&room);
69        for observed in self.visibility.values_mut() {
70            observed.remove(&room);
71        }
72    }
73
74    /// Renames a room and updates all references to it.
75    ///
76    /// # Errors
77    ///
78    /// [`VisError::MissingRoom`] when `old` names no room, carrying the name.
79    /// Nothing is renamed in that case. `new` is not checked for whitespace,
80    /// which the grammar cannot represent; the writer refuses that instead.
81    pub fn rename_room(&mut self, old: &str, new: &str) -> Result<(), VisError> {
82        let old = canonical_room(old);
83        let new = canonical_room(new);
84        if old == new {
85            return Ok(());
86        }
87        let observed = self
88            .visibility
89            .remove(&old)
90            .ok_or_else(|| VisError::MissingRoom(old.clone()))?;
91        self.visibility.insert(new.clone(), observed);
92        for room_observed in self.visibility.values_mut() {
93            if room_observed.remove(&old) {
94                room_observed.insert(new.clone());
95            }
96        }
97        Ok(())
98    }
99
100    /// Sets visibility between two rooms.
101    ///
102    /// # Errors
103    ///
104    /// [`VisError::MissingRoom`] when either name is not a room in the graph,
105    /// carrying the one that is missing. Visibility is directional, so both
106    /// have to exist and only the named direction changes.
107    pub fn set_visible(
108        &mut self,
109        when_inside: &str,
110        show: &str,
111        visible: bool,
112    ) -> Result<(), VisError> {
113        let when_inside = canonical_room(when_inside);
114        let show = canonical_room(show);
115
116        if !self.visibility.contains_key(&when_inside) {
117            return Err(VisError::MissingRoom(when_inside));
118        }
119        if !self.visibility.contains_key(&show) {
120            return Err(VisError::MissingRoom(show));
121        }
122
123        if visible {
124            self.visibility
125                .entry(when_inside)
126                .or_default()
127                .insert(show.clone());
128        } else if let Some(observed) = self.visibility.get_mut(&when_inside) {
129            observed.remove(&show);
130        }
131        Ok(())
132    }
133
134    /// Returns whether one room is visible from another.
135    ///
136    /// # Errors
137    ///
138    /// [`VisError::MissingRoom`] when `when_inside` names no room. A `show`
139    /// that is not visible from it is `Ok(false)` rather than an error, so
140    /// this distinguishes "not visible" from "not a room".
141    pub fn get_visible(&self, when_inside: &str, show: &str) -> Result<bool, VisError> {
142        let when_inside = canonical_room(when_inside);
143        let show = canonical_room(show);
144        let observed = self
145            .visibility
146            .get(&when_inside)
147            .ok_or_else(|| VisError::MissingRoom(when_inside.clone()))?;
148        if !self.visibility.contains_key(&show) {
149            return Err(VisError::MissingRoom(show));
150        }
151        Ok(observed.contains(&show))
152    }
153
154    /// Marks all rooms visible from each other (excluding self-visibility).
155    pub fn set_all_visible(&mut self) {
156        // Take the map by value to iterate keys while inserting new entries.
157        // The borrow checker prevents iterating `self.visibility.keys()` while
158        // calling `self.visibility.entry()`, so we operate on an owned map
159        // and rebuild in place.
160        let rooms: Vec<String> = self.visibility.keys().cloned().collect();
161        for observer in &rooms {
162            let observed = self
163                .visibility
164                .get_mut(observer)
165                .expect("observer was just collected from the map's keys");
166            observed.clear();
167            observed.extend(rooms.iter().filter(|r| *r != observer).cloned());
168        }
169    }
170}
171
172impl DecodeBinary for Vis {
173    type Error = VisError;
174
175    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
176        read_vis_from_bytes(bytes)
177    }
178}
179
180impl EncodeBinary for Vis {
181    type Error = VisError;
182
183    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
184        write_vis_to_vec(self)
185    }
186}
187
188/// Errors produced while parsing or serializing VIS ASCII data.
189#[derive(Debug, Error)]
190pub enum VisError {
191    /// I/O read/write failure.
192    #[error(transparent)]
193    Io(#[from] std::io::Error),
194    /// Text content is malformed.
195    #[error("invalid VIS data: {0}")]
196    InvalidData(String),
197    /// Text cannot be represented in Windows-1252 output.
198    #[error("VIS text encoding failed for {context}: {source}")]
199    TextEncoding {
200        /// Value context.
201        context: String,
202        /// Encoding error details.
203        #[source]
204        source: EncodeTextError,
205    },
206    /// Input bytes could not be decoded losslessly as Windows-1252.
207    #[error("VIS text decoding failed for {context}: {source}")]
208    TextDecoding {
209        /// Value context.
210        context: String,
211        /// Decoding error details.
212        #[source]
213        source: DecodeTextError,
214    },
215    /// A room referenced by an API call does not exist.
216    #[error("VIS room `{0}` does not exist")]
217    MissingRoom(String),
218    /// A room token contains disallowed whitespace.
219    #[error("invalid VIS room `{0}` (whitespace is not allowed)")]
220    InvalidRoom(String),
221}
222
223fn canonical_room(room: &str) -> String {
224    room.to_ascii_lowercase()
225}