1pub mod layout;
9pub mod page_type;
10pub mod refs;
11
12use std::collections::BTreeMap;
13
14use yog_data::Schema;
15use yog_data::kinds::{Block, Category, Entity, Entry, Item, Recipe};
16use yog_data::with::{List, Opt, RefTo, Values};
17
18use refs::{BookText, Icon};
19
20pub fn multiblock_blocks(
23 pattern: &[Vec<String>],
24 mapping: &BTreeMap<String, String>,
25) -> Vec<((i32, i32, i32), String)> {
26 let mut out = Vec::new();
27 let layers = pattern.len() as i32;
28 for (li, layer) in pattern.iter().enumerate() {
29 for (z, row) in layer.iter().enumerate() {
30 for (x, ch) in row.chars().enumerate() {
31 if ch == ' ' {
32 continue;
33 }
34 if let Some(state) = mapping.get(&ch.to_string()) {
35 out.push(((x as i32, layers - 1 - li as i32, z as i32), state.clone()));
36 }
37 }
38 }
39 }
40 out
41}
42
43#[derive(Debug, Clone, Schema)]
47#[data(tag = "type")]
48pub enum BookPage {
49 Text {
53 #[data(with = "BookText")]
54 text: String,
55 #[data(with = "Opt<BookText>")]
56 title: Option<String>,
57 font: Option<String>,
59 font_size: Option<f32>,
61 },
62 Spotlight {
64 #[data(with = "RefTo<(Item, Block)>")]
65 item: String,
66 #[data(with = "Opt<BookText>")]
67 title: Option<String>,
68 #[data(with = "Opt<BookText>")]
69 text: Option<String>,
70 },
71 Recipe {
73 #[data(with = "RefTo<Recipe>")]
74 recipe: String,
75 #[data(with = "Opt<BookText>")]
76 text: Option<String>,
77 },
78 Image {
80 texture: String,
82 #[data(with = "Opt<BookText>")]
83 title: Option<String>,
84 #[data(with = "Opt<BookText>")]
85 text: Option<String>,
86 },
87 Entity {
89 #[data(with = "RefTo<Entity>")]
90 entity: String,
91 #[data(with = "Opt<BookText>")]
92 name: Option<String>,
93 #[data(with = "Opt<BookText>")]
94 text: Option<String>,
95 },
96 Multiblock {
100 #[data(with = "Opt<BookText>")]
101 name: Option<String>,
102 pattern: Vec<Vec<String>>,
104 #[data(with = "Values<RefTo<Block>>")]
106 mapping: BTreeMap<String, String>,
107 #[data(with = "Opt<BookText>")]
108 text: Option<String>,
109 },
110 Relations {
112 #[data(with = "List<RefTo<Entry>>")]
113 entries: Vec<String>,
114 #[data(with = "Opt<BookText>")]
115 text: Option<String>,
116 },
117 Drawn {
122 drawer: String,
124 #[data(with = "Opt<BookText>")]
125 title: Option<String>,
126 #[data(default, range = "0..=150")]
128 height: f32,
129 #[data(with = "Opt<BookText>")]
131 caption: Option<String>,
132 #[data(with = "Opt<BookText>")]
133 text: Option<String>,
134 #[data(default)]
136 data: yog_data::Value,
137 },
138 Empty,
140 #[data(other)]
143 Custom(page_type::CustomPage),
144}
145
146#[derive(Debug, Clone, Default, Schema)]
150pub struct BookCategory {
151 pub id: String,
152 #[data(with = "BookText")]
153 pub name: String,
154 #[data(with = "Opt<BookText>")]
155 pub description: Option<String>,
156 #[data(with = "Opt<Icon>")]
158 pub icon: Option<String>,
159 #[data(default)]
161 pub sortnum: i32,
162}
163
164#[derive(Debug, Clone, Default, Schema)]
166pub struct BookEntry {
167 pub id: String,
168 #[data(with = "BookText")]
169 pub name: String,
170 #[data(with = "RefTo<Category>")]
171 pub category: String,
172 #[data(default)]
173 pub pages: Vec<BookPage>,
174 #[data(with = "Opt<Icon>")]
176 pub icon: Option<String>,
177 #[data(default)]
179 pub priority: i32,
180 #[data(default)]
182 pub secret: bool,
183 #[data(default)]
185 pub read_by_default: bool,
186 #[data(default)]
189 pub requires: Vec<String>,
190 pub advancement: Option<String>,
192}
193
194#[derive(Debug, Clone, Schema)]
198pub struct Book {
199 pub id: String,
201 #[data(with = "BookText")]
202 pub name: String,
203 #[data(with = "Opt<BookText>")]
205 pub subtitle: Option<String>,
206 #[data(default)]
209 pub version: u32,
210 #[data(with = "BookText", default)]
211 pub landing_text: String,
212 #[data(default = "default_nameplate_color")]
214 pub nameplate_color: String,
215 #[data(default = "default_accent")]
217 pub accent: String,
218 #[data(default = "default_book_texture")]
220 pub book_texture: String,
221 #[data(default)]
222 pub categories: Vec<BookCategory>,
223 #[data(default)]
224 pub entries: Vec<BookEntry>,
225 #[data(default)]
228 pub i18n: bool,
229 #[data(default)]
231 pub layouts: Vec<layout::RecipeLayout>,
232 #[data(default)]
235 pub page_types: Vec<page_type::PageType>,
236}
237
238fn default_nameplate_color() -> String {
239 "FFDD98".into()
240}
241
242fn default_accent() -> String {
243 "#8d6acc".into()
244}
245
246fn default_book_texture() -> String {
247 yog_ids::yog::BOOK_TEXTURE.into()
248}
249
250impl Default for Book {
251 fn default() -> Self {
252 Book {
253 id: String::new(),
254 name: String::new(),
255 subtitle: None,
256 version: 0,
257 landing_text: String::new(),
258 nameplate_color: default_nameplate_color(),
259 accent: default_accent(),
260 book_texture: default_book_texture(),
261 categories: Vec::new(),
262 entries: Vec::new(),
263 i18n: false,
264 layouts: Vec::new(),
265 page_types: Vec::new(),
266 }
267 }
268}
269
270impl Book {
271 pub fn localized(&self, tr: impl Fn(&str) -> String) -> Book {
276 let opt = |o: &mut Option<String>| {
277 if let Some(t) = o {
278 *t = tr(t);
279 }
280 };
281 let mut b = self.clone();
282 let types = std::mem::take(&mut b.page_types);
283 b.name = tr(&b.name);
284 b.landing_text = tr(&b.landing_text);
285 opt(&mut b.subtitle);
286 for c in &mut b.categories {
287 c.name = tr(&c.name);
288 opt(&mut c.description);
289 }
290 for e in &mut b.entries {
291 e.name = tr(&e.name);
292 for p in &mut e.pages {
293 match p {
294 BookPage::Text { text, title, .. } => {
295 *text = tr(text);
296 opt(title);
297 }
298 BookPage::Spotlight { title, text, .. }
299 | BookPage::Image { title, text, .. } => {
300 opt(title);
301 opt(text);
302 }
303 BookPage::Entity { name, text, .. }
304 | BookPage::Multiblock { name, text, .. } => {
305 opt(name);
306 opt(text);
307 }
308 BookPage::Recipe { text, .. } | BookPage::Relations { text, .. } => opt(text),
309 BookPage::Drawn {
310 title,
311 caption,
312 text,
313 ..
314 } => {
315 opt(title);
316 opt(caption);
317 opt(text);
318 }
319 BookPage::Empty => {}
320 BookPage::Custom(c) => {
322 let Some(t) = types.iter().find(|t| t.id == c.kind) else {
323 continue;
324 };
325 for prop in t.properties.iter().filter(|p| {
326 matches!(
327 p.kind,
328 page_type::PropKind::Text | page_type::PropKind::BookText
329 )
330 }) {
331 if let Some(yog_data::Value::Str(s)) = c.fields.get_mut(&prop.name) {
332 *s = tr(s);
333 }
334 }
335 }
336 }
337 }
338 }
339 b.page_types = types;
340 b
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347
348 fn text(t: &str) -> BookPage {
349 BookPage::Text {
350 text: t.into(),
351 title: None,
352 font: None,
353 font_size: None,
354 }
355 }
356
357 #[test]
358 fn books_round_trip_through_their_json() {
359 let book = Book {
360 id: "yog:test".into(),
361 name: "Test Book".into(),
362 version: 1,
363 categories: vec![BookCategory {
364 id: "yog:c1".into(),
365 name: "Cat".into(),
366 icon: Some("yog:ruby".into()),
367 ..Default::default()
368 }],
369 entries: vec![BookEntry {
370 id: "yog:e1".into(),
371 name: "Entry".into(),
372 category: "yog:c1".into(),
373 pages: vec![
374 text("plain"),
375 BookPage::Spotlight {
376 item: "yog:ruby".into(),
377 title: None,
378 text: None,
379 },
380 BookPage::Recipe {
381 recipe: "yog:r1".into(),
382 text: None,
383 },
384 BookPage::Empty,
385 ],
386 ..Default::default()
387 }],
388 ..Default::default()
389 };
390 let parsed: Book = yog_data::read_bytes(&yog_data::to_bytes(&book)).expect("it reads back");
391 assert_eq!(parsed.version, 1);
392 assert_eq!(parsed.entries[0].pages.len(), 4);
393 assert!(
394 matches!(&parsed.entries[0].pages[1], BookPage::Spotlight { item, .. } if item == "yog:ruby")
395 );
396 assert!(
397 matches!(&parsed.entries[0].pages[2], BookPage::Recipe { recipe, .. } if recipe == "yog:r1")
398 );
399 }
400
401 #[test]
402 fn localized_translates_every_text() {
403 let book = Book {
404 id: "m:b".into(),
405 name: "m.book.name".into(),
406 landing_text: "m.book.landing".into(),
407 categories: vec![BookCategory {
408 id: "m:basics".into(),
409 name: "m.cat".into(),
410 ..Default::default()
411 }],
412 entries: vec![BookEntry {
413 id: "m:e".into(),
414 name: "m.entry".into(),
415 category: "m:basics".into(),
416 pages: vec![
417 BookPage::Text {
418 text: "m.text".into(),
419 title: Some("m.title".into()),
420 font: None,
421 font_size: None,
422 },
423 text("raw text"),
424 ],
425 ..Default::default()
426 }],
427 ..Default::default()
428 };
429 let out = book.localized(|k| {
430 if k.starts_with("m.") {
431 k.to_uppercase()
432 } else {
433 k.to_string()
434 }
435 });
436 assert_eq!(out.name, "M.BOOK.NAME");
437 assert_eq!(out.landing_text, "M.BOOK.LANDING");
438 assert_eq!(out.categories[0].name, "M.CAT");
439 assert_eq!(out.entries[0].name, "M.ENTRY");
440 assert!(
441 matches!(&out.entries[0].pages[0], BookPage::Text { text, title, .. } if text == "M.TEXT" && title.as_deref() == Some("M.TITLE"))
442 );
443 assert!(
444 matches!(&out.entries[0].pages[1], BookPage::Text { text, .. } if text == "raw text")
445 );
446 }
447}