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    pub fn rename_room(&mut self, old: &str, new: &str) -> Result<(), VisError> {
76        let old = canonical_room(old);
77        let new = canonical_room(new);
78        if old == new {
79            return Ok(());
80        }
81        let observed = self
82            .visibility
83            .remove(&old)
84            .ok_or_else(|| VisError::MissingRoom(old.clone()))?;
85        self.visibility.insert(new.clone(), observed);
86        for room_observed in self.visibility.values_mut() {
87            if room_observed.remove(&old) {
88                room_observed.insert(new.clone());
89            }
90        }
91        Ok(())
92    }
93
94    /// Sets visibility between two rooms.
95    pub fn set_visible(
96        &mut self,
97        when_inside: &str,
98        show: &str,
99        visible: bool,
100    ) -> Result<(), VisError> {
101        let when_inside = canonical_room(when_inside);
102        let show = canonical_room(show);
103
104        if !self.visibility.contains_key(&when_inside) {
105            return Err(VisError::MissingRoom(when_inside));
106        }
107        if !self.visibility.contains_key(&show) {
108            return Err(VisError::MissingRoom(show));
109        }
110
111        if visible {
112            self.visibility
113                .entry(when_inside)
114                .or_default()
115                .insert(show.clone());
116        } else if let Some(observed) = self.visibility.get_mut(&when_inside) {
117            observed.remove(&show);
118        }
119        Ok(())
120    }
121
122    /// Returns whether one room is visible from another.
123    pub fn get_visible(&self, when_inside: &str, show: &str) -> Result<bool, VisError> {
124        let when_inside = canonical_room(when_inside);
125        let show = canonical_room(show);
126        let observed = self
127            .visibility
128            .get(&when_inside)
129            .ok_or_else(|| VisError::MissingRoom(when_inside.clone()))?;
130        if !self.visibility.contains_key(&show) {
131            return Err(VisError::MissingRoom(show));
132        }
133        Ok(observed.contains(&show))
134    }
135
136    /// Marks all rooms visible from each other (excluding self-visibility).
137    pub fn set_all_visible(&mut self) {
138        // Take the map by value to iterate keys while inserting new entries.
139        // The borrow checker prevents iterating `self.visibility.keys()` while
140        // calling `self.visibility.entry()`, so we operate on an owned map
141        // and rebuild in place.
142        let rooms: Vec<String> = self.visibility.keys().cloned().collect();
143        for observer in &rooms {
144            let observed = self
145                .visibility
146                .get_mut(observer)
147                .expect("observer was just collected from the map's keys");
148            observed.clear();
149            observed.extend(rooms.iter().filter(|r| *r != observer).cloned());
150        }
151    }
152}
153
154impl DecodeBinary for Vis {
155    type Error = VisError;
156
157    fn decode_binary(bytes: &[u8]) -> Result<Self, Self::Error> {
158        read_vis_from_bytes(bytes)
159    }
160}
161
162impl EncodeBinary for Vis {
163    type Error = VisError;
164
165    fn encode_binary(&self) -> Result<Vec<u8>, Self::Error> {
166        write_vis_to_vec(self)
167    }
168}
169
170/// Errors produced while parsing or serializing VIS ASCII data.
171#[derive(Debug, Error)]
172pub enum VisError {
173    /// I/O read/write failure.
174    #[error(transparent)]
175    Io(#[from] std::io::Error),
176    /// Text content is malformed.
177    #[error("invalid VIS data: {0}")]
178    InvalidData(String),
179    /// Text cannot be represented in Windows-1252 output.
180    #[error("VIS text encoding failed for {context}: {source}")]
181    TextEncoding {
182        /// Value context.
183        context: String,
184        /// Encoding error details.
185        #[source]
186        source: EncodeTextError,
187    },
188    /// Input bytes could not be decoded losslessly as Windows-1252.
189    #[error("VIS text decoding failed for {context}: {source}")]
190    TextDecoding {
191        /// Value context.
192        context: String,
193        /// Decoding error details.
194        #[source]
195        source: DecodeTextError,
196    },
197    /// A room referenced by an API call does not exist.
198    #[error("VIS room `{0}` does not exist")]
199    MissingRoom(String),
200    /// A room token contains disallowed whitespace.
201    #[error("invalid VIS room `{0}` (whitespace is not allowed)")]
202    InvalidRoom(String),
203}
204
205fn canonical_room(room: &str) -> String {
206    room.to_ascii_lowercase()
207}