1use crate::{Component, Entity, Geometry, JsonError, World};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(deny_unknown_fields)]
10pub enum NameOrigin {
11 Game,
12 Community { credit: String },
13}
14impl NameOrigin {
15 pub fn verify(&self) -> Result<(), String> {
17 if let Self::Community { credit } = self
18 && credit.trim().is_empty()
19 {
20 return Err("community name has no credit".into());
21 }
22 Ok(())
23 }
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31pub struct ContentNote {
32 pub key: String,
33 pub reason: String,
34 pub unsupported: bool,
35 pub at: Option<[f32; 3]>,
36}
37impl Component for ContentNote {
38 const NAME: &'static str = "core.content_note";
39 const GEOMETRY: Geometry = Geometry::None;
40 fn to_json(&self) -> Result<Option<Value>, JsonError> {
41 crate::json::to_value(self).map(Some)
42 }
43 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
45 crate::world::Linearity::Invariant
46 }
47 fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
48 if self.key.trim().is_empty() {
49 return Err("content note has no machine key".into());
50 }
51 if self.reason.trim().is_empty() {
52 return Err("content note has no reason".into());
53 }
54 if self.at.is_some_and(|at| !at.iter().all(|v| v.is_finite())) {
55 return Err("content note position is not finite".into());
56 }
57 Ok(())
58 }
59}
60
61#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
64#[serde(deny_unknown_fields)]
65pub struct Record {
66 pub category: String,
67 pub kind: String,
68}
69impl Component for Record {
70 const NAME: &'static str = "core.record";
71 const GEOMETRY: Geometry = Geometry::None;
72 fn to_json(&self) -> Result<Option<Value>, JsonError> {
73 crate::json::to_value(self).map(Some)
74 }
75 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
76 crate::world::Linearity::Invariant
77 }
78 fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
79 if !crate::kind_is_valid(&self.category) {
80 return Err(format!(
81 "record category {:?} is not a word of a-z, 0-9 and _",
82 self.category
83 ));
84 }
85 if !crate::kind_is_valid(&self.kind) {
86 return Err(format!(
87 "record kind {:?} is not a word of a-z, 0-9 and _",
88 self.kind
89 ));
90 }
91 Ok(())
92 }
93}
94
95#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
97#[serde(deny_unknown_fields)]
98pub struct DisplayName {
99 pub name: String,
100 pub origin: NameOrigin,
101}
102impl Component for DisplayName {
103 const NAME: &'static str = "core.display_name";
104 const GEOMETRY: Geometry = Geometry::None;
105 fn to_json(&self) -> Result<Option<Value>, JsonError> {
106 crate::json::to_value(self).map(Some)
107 }
108 fn apply_linear(&self, _l: &crate::Linear) -> crate::world::Linearity<Self> {
109 crate::world::Linearity::Invariant
110 }
111 fn verify(&self, _e: Entity, _w: &World) -> Result<(), String> {
112 if self.name.trim().is_empty() {
113 return Err("empty display name".into());
114 }
115 self.origin.verify()
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 fn world() -> (World, Entity) {
123 let mut w = World::new();
124 let e = w.spawn();
125 (w, e)
126 }
127 #[test]
128 fn notes_require_key_reason_and_finite_world_position() {
129 let (w, e) = world();
130 let note = ContentNote {
131 key: "source.unresolved_light".into(),
132 reason: "Lighting is retained, not lowered".into(),
133 unsupported: true,
134 at: Some([1., 2., 3.]),
135 };
136 assert!(note.verify(e, &w).is_ok());
137 let mut bad = note.clone();
138 bad.key.clear();
139 assert!(bad.verify(e, &w).is_err());
140 let mut bad = note.clone();
141 bad.reason = " ".into();
142 assert!(bad.verify(e, &w).is_err());
143 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
144 let mut bad = note.clone();
145 bad.at = Some([value, 0., 0.]);
146 assert!(bad.verify(e, &w).is_err());
147 assert!(bad.to_json().is_err());
148 }
149 assert_eq!(
150 serde_json::from_value::<ContentNote>(note.to_json().unwrap().unwrap()).unwrap(),
151 note
152 );
153 assert!(note.remap_primitives(&[0]).is_none());
154 }
155 #[test]
156 fn records_have_valid_words_and_need_no_placement() {
157 let (w, e) = world();
158 let r = Record {
159 category: "event".into(),
160 kind: "environment_light".into(),
161 };
162 assert!(r.verify(e, &w).is_ok());
163 for text in ["", "Region", "with space", "a:b"] {
164 let mut bad = r.clone();
165 bad.kind = text.into();
166 assert!(bad.verify(e, &w).is_err());
167 let mut bad = r.clone();
168 bad.category = text.into();
169 assert!(bad.verify(e, &w).is_err());
170 }
171 assert_eq!(
172 serde_json::from_value::<Record>(r.to_json().unwrap().unwrap()).unwrap(),
173 r
174 );
175 assert!(r.remap_primitives(&[0]).is_none());
176 }
177 #[test]
178 fn names_keep_their_origin_and_community_credit() {
179 let (w, e) = world();
180 for origin in [
181 NameOrigin::Game,
182 NameOrigin::Community {
183 credit: "The source-name project".into(),
184 },
185 ] {
186 let name = DisplayName {
187 name: "A human name".into(),
188 origin,
189 };
190 assert!(name.verify(e, &w).is_ok());
191 assert_eq!(
192 serde_json::from_value::<DisplayName>(name.to_json().unwrap().unwrap()).unwrap(),
193 name
194 );
195 assert!(name.remap_primitives(&[0]).is_none());
196 }
197 let mut bad = DisplayName {
198 name: "A name".into(),
199 origin: NameOrigin::Community { credit: " ".into() },
200 };
201 assert!(bad.verify(e, &w).is_err());
202 bad.origin = NameOrigin::Game;
203 bad.name.clear();
204 assert!(bad.verify(e, &w).is_err());
205 }
206}