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}