Skip to main content

delvewright_dsl/
viewdistance.rs

1//! **The view distance a campaign declares, and what it serves** (spec-0091).
2//!
3//! `world.view_distance` is the server's `view-distance`, in chunks. The
4//! engine's floor is what every proof is written against; a campaign whose far
5//! views need more declares more, and the build states what the declaration
6//! costs the host. The radius a body is sure to see in every direction is a
7//! function of the number — [`served_radius_blocks`] — and it is the one fact
8//! every far-view check reads: a sightline, a view, a showcase camera or a
9//! cutscene shot aimed past it is refused under `DW0956`, because what it aims
10//! at is never sent to the client.
11//!
12//! The document-tier shapes live here, with no world built: the declared
13//! number's range, and the site plan's `sightlines[]` and `views[]`, whose two
14//! ends are world coordinates the document already holds. The build-tier
15//! shapes — a showcase camera's subject and a cutscene shot's aim — are the
16//! compiler's, in `delvec`, against the assembled world.
17//!
18//! Measured on the pinned server (`tools/spike-view-distance/ at 80f1a72135e9`, spec-0091 §2):
19//! the server sends one more ring of chunks than `view-distance`, and the
20//! pinned client draws `min(its render-distance option, the server's
21//! view-distance)` chunks around the camera's section, so the radius a player
22//! can count on is the declared number of chunks, not the server's margin.
23
24use crate::diagnostic::Diagnostic;
25use crate::envelope::Campaign;
26
27/// The engine's floor, in chunks: what every delve is served when it declares
28/// nothing, and the least a campaign may declare. 10 chunks reach the far side
29/// of the largest scene built to date from any standpoint inside it, and the
30/// loop kernel's reach (128 blocks) and the horizon library's vista arithmetic
31/// are written against it.
32pub const FLOOR: u8 = 10;
33/// The most the pinned server serves (vanilla clamps `view-distance` to 32).
34pub const CEILING: u8 = 32;
35/// Blocks per chunk, on the horizontal axes.
36pub const CHUNK_BLOCKS: u32 = 16;
37
38/// The view distance a campaign is served, in chunks: the declared number, or
39/// the floor. The one reading of `world.view_distance` every consumer takes.
40pub fn chunks(c: &Campaign) -> u8 {
41    c.world.content.view_distance.unwrap_or(FLOOR)
42}
43
44/// The radius, in blocks, a body standing anywhere is sure to see in every
45/// direction at `chunks`: the client draws a square of `chunks` sections around
46/// the camera's own section, so along an axis a camera at the far edge of its
47/// section sees `16 × chunks` blocks before the first undrawn section, and on
48/// every other bearing farther.
49pub fn served_radius_blocks(chunks: u8) -> f64 {
50    f64::from(u32::from(chunks) * CHUNK_BLOCKS)
51}
52
53/// The fewest chunks that serve a view `distance` blocks long: the number the
54/// refusal prescribes.
55pub fn chunks_for(distance: f64) -> u8 {
56    let n = (distance / f64::from(CHUNK_BLOCKS)).ceil();
57    if n < f64::from(FLOOR) {
58        FLOOR
59    } else if n > f64::from(CEILING) {
60        CEILING
61    } else {
62        n as u8
63    }
64}
65
66/// Euclidean distance between two world points.
67pub fn distance(a: [i64; 3], b: [i64; 3]) -> f64 {
68    let d = [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
69    ((d[0] * d[0] + d[1] * d[1] + d[2] * d[2]) as f64).sqrt()
70}
71
72/// What the document-tier check examined, stated on every run, zeroes included.
73#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
74pub struct Binding {
75    /// The served view distance, in chunks.
76    pub chunks: u8,
77    /// Whether the campaign declared it (else the floor).
78    pub declared: bool,
79    /// Site-plan sightlines whose length was judged.
80    pub sightlines: usize,
81    /// Site-plan views whose eye-to-subject distance was judged.
82    pub views: usize,
83    /// Of those, the ones beyond the served radius (`DW0956`).
84    pub beyond: usize,
85}
86
87impl Binding {
88    /// One line, for stderr.
89    #[must_use]
90    pub fn line(&self) -> String {
91        format!(
92            "view distance binding: {n} chunk(s) ({how}) serve {r} blocks in every direction; \
93             {s} sightline(s) and {v} view(s) judged against it, {b} beyond it.",
94            n = self.chunks,
95            how = if self.declared {
96                "declared"
97            } else {
98                "the engine's floor, undeclared"
99            },
100            r = served_radius_blocks(self.chunks),
101            s = self.sightlines,
102            v = self.views,
103            b = self.beyond,
104        )
105    }
106}
107
108/// The document-tier shapes of `DW0956`, and the binding they state.
109///
110/// A declared number outside `FLOOR..=CEILING` is refused on the field, and the
111/// site plan's lines are then judged against the floor, so one bad number does
112/// not hide every far line behind it. A sightline's or a view's length is the
113/// straight distance between its two ends; the message names the number of
114/// chunks that would serve it, and that number is one edit to the one field.
115pub fn checks(c: &Campaign, d: &mut Vec<Diagnostic>) -> Binding {
116    let declared = c.world.content.view_distance;
117    let mut b = Binding {
118        chunks: FLOOR,
119        declared: declared.is_some(),
120        ..Binding::default()
121    };
122    match declared {
123        Some(n) if (FLOOR..=CEILING).contains(&n) => b.chunks = n,
124        Some(n) => d.push(Diagnostic::error(
125            crate::diagnostic::codes::VIEW_BEYOND_SERVED,
126            "world",
127            "/content/view_distance".to_string(),
128            format!(
129                "`view_distance` = {n} chunks cannot be served: the pinned server serves at most \
130                 {CEILING} chunks, and the engine's floor is {FLOOR} (every proof is written \
131                 against it, and a smaller number only takes views away). Declare a value in \
132                 {FLOOR}..={CEILING}, or drop the field for the floor."
133            ),
134        )),
135        None => {}
136    }
137    let radius = served_radius_blocks(b.chunks);
138    let Some(plan) = c.site_plan.as_ref().map(|p| &p.content) else {
139        return b;
140    };
141    for (i, s) in plan.sightlines.iter().enumerate() {
142        b.sightlines += 1;
143        let len = distance(s.from, s.to);
144        if len > radius {
145            b.beyond += 1;
146            d.push(Diagnostic::error(
147                crate::diagnostic::codes::VIEW_BEYOND_SERVED,
148                "site-plan",
149                format!("/content/sightlines/{i}"),
150                beyond_message(
151                    &format!("the vista `{}`", s.edge),
152                    len,
153                    b.chunks,
154                    b.declared,
155                ),
156            ));
157        }
158    }
159    for (i, v) in plan.views.iter().enumerate() {
160        b.views += 1;
161        let len = distance(v.eye, v.look_at);
162        if len > radius {
163            b.beyond += 1;
164            d.push(Diagnostic::error(
165                crate::diagnostic::codes::VIEW_BEYOND_SERVED,
166                "site-plan",
167                format!("/content/views/{i}"),
168                beyond_message(&format!("the view `{}`", v.id), len, b.chunks, b.declared),
169            ));
170        }
171    }
172    b
173}
174
175/// The one sentence every far-view refusal says: what is aimed, how far, what
176/// is served, and the declaration that would serve it.
177pub fn beyond_message(what: &str, length: f64, chunks: u8, declared: bool) -> String {
178    let radius = served_radius_blocks(chunks);
179    let need = chunks_for(length);
180    let how = if declared {
181        format!("the declared `world.view_distance` of {chunks} chunks")
182    } else {
183        format!("the engine's floor of {chunks} chunks (nothing declared)")
184    };
185    let remedy = if need > chunks {
186        format!(
187            "Declare `world.view_distance: {need}` (the fewest chunks that serve {length:.0} \
188             blocks), or bring the two ends within {radius:.0} blocks of each other."
189        )
190    } else {
191        format!(
192            "Bring the two ends within {radius:.0} blocks of each other: {length:.0} blocks is \
193             past what the pinned server can serve at any view distance ({CEILING} chunks)."
194        )
195    };
196    format!(
197        "{what} reaches {length:.1} blocks, and {how} serves {radius:.0} blocks in every \
198         direction — what it looks at is never sent to the client, so the view cannot render. \
199         {remedy}"
200    )
201}