1use yog_registry::ItemDef;
5
6#[derive(Debug, Clone)]
10pub struct BookMacro(pub String, pub String);
11
12#[derive(Debug, Clone)]
16pub enum BookPage {
17 Text {
19 text: String,
20 },
21 Spotlight {
23 item: ItemDef,
24 title: Option<String>,
25 text: Option<String>,
26 },
27 Crafting {
29 recipe_id: String,
30 text: Option<String>,
31 },
32 Smelting {
34 recipe_id: String,
35 text: Option<String>,
36 },
37 Image {
39 texture: String,
40 title: Option<String>,
41 text: Option<String>,
42 border: bool,
43 },
44 Entity {
46 entity_type: String,
47 name: Option<String>,
48 text: Option<String>,
49 },
50 Relations {
52 entries: Vec<String>,
53 text: Option<String>,
54 },
55 Empty,
57 Pattern {
59 op_id: String,
60 anchor: String,
61 input: String,
62 output: String,
63 text: String,
64 },
65}
66
67#[derive(Debug, Clone)]
71pub struct BookCategory {
72 pub id: String,
73 pub name: String,
74 pub description: Option<String>,
75 pub icon: Option<String>,
77 pub sortnum: i32,
79}
80
81#[derive(Debug, Clone, Default)]
85pub struct BookEntry {
86 pub id: String,
87 pub name: String,
88 pub category: String,
89 pub pages: Vec<BookPage>,
90 pub icon: Option<String>,
92 pub secret: bool,
94 pub priority: i32,
96 pub read_by_default: bool,
98 pub advancement: Option<String>,
100}
101
102#[derive(Debug, Clone)]
106pub struct Book {
107 pub id: String,
108 pub name: String,
109 pub nameplate_color: String,
110 pub landing_text: String,
111 pub author: Option<String>,
112 pub book_texture: String,
113 pub filler_texture: String,
114 pub model: String,
115 pub categories: Vec<BookCategory>,
116 pub entries: Vec<BookEntry>,
117 pub macros: Vec<BookMacro>,
118 pub use_resource_pack: bool,
119 pub show_progress: bool,
120 pub i18n: bool,
121 pub creative_tab: Option<String>,
122}
123
124impl Book {
125 pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
126 Self {
127 id: id.into(),
128 name: name.into(),
129 nameplate_color: "000000".into(),
130 landing_text: String::new(),
131 author: None,
132 book_texture: "yog:textures/gui/book.png".into(),
133 filler_texture: "yog:textures/gui/book_filler.png".into(),
134 model: "minecraft:book".into(),
135 categories: Vec::new(),
136 entries: Vec::new(),
137 macros: Vec::new(),
138 use_resource_pack: false,
139 show_progress: true,
140 i18n: false,
141 creative_tab: None,
142 }
143 }
144
145 pub fn author(mut self, author: impl Into<String>) -> Self {
146 self.author = Some(author.into());
147 self
148 }
149
150 pub fn book_texture(mut self, tex: impl Into<String>) -> Self {
151 self.book_texture = tex.into();
152 self
153 }
154
155 pub fn filler_texture(mut self, tex: impl Into<String>) -> Self {
156 self.filler_texture = tex.into();
157 self
158 }
159
160 pub fn nameplate(mut self, color: impl Into<String>) -> Self {
161 self.nameplate_color = color.into();
162 self
163 }
164
165 pub fn landing_text(mut self, text: impl Into<String>) -> Self {
166 self.landing_text = text.into();
167 self
168 }
169
170 pub fn model(mut self, model: impl Into<String>) -> Self {
171 self.model = model.into();
172 self
173 }
174
175 pub fn creative_tab(mut self, tab: impl Into<String>) -> Self {
176 self.creative_tab = Some(tab.into());
177 self
178 }
179
180 pub fn show_progress(mut self, show: bool) -> Self {
181 self.show_progress = show;
182 self
183 }
184
185 pub fn i18n(mut self, val: bool) -> Self {
186 self.i18n = val;
187 self
188 }
189
190 pub fn use_resource_pack(mut self, val: bool) -> Self {
191 self.use_resource_pack = val;
192 self
193 }
194
195 pub fn add_macro(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
196 self.macros.push(BookMacro(key.into(), value.into()));
197 self
198 }
199
200 pub fn add_category(mut self, category: BookCategory) -> Self {
201 self.categories.push(category);
202 self
203 }
204
205 pub fn add_entry(mut self, entry: BookEntry) -> Self {
206 self.entries.push(entry);
207 self
208 }
209}
210
211impl Default for Book {
212 fn default() -> Self {
213 Self::new("yog:default", "Unknown Book")
214 }
215}
216
217#[derive(Debug, Default)]
221pub struct BookRegistry {
222 books: std::collections::HashMap<String, Book>,
223}
224
225impl BookRegistry {
226 pub fn register(&mut self, book: Book) {
227 self.books.insert(book.id.clone(), book);
228 }
229
230 pub fn get(&self, id: &str) -> Option<&Book> {
231 self.books.get(id)
232 }
233
234 pub fn all(&self) -> impl Iterator<Item = &Book> {
235 self.books.values()
236 }
237}
238
239pub fn text_page(text: impl Into<String>) -> BookPage {
242 BookPage::Text { text: text.into() }
243}
244
245pub fn spotlight_page(item: ItemDef) -> BookPage {
246 BookPage::Spotlight { item, title: None, text: None }
247}
248
249pub fn crafting_page(recipe_id: impl Into<String>) -> BookPage {
250 BookPage::Crafting { recipe_id: recipe_id.into(), text: None }
251}
252
253pub fn crafting_page_with_text(recipe_id: impl Into<String>, text: impl Into<String>) -> BookPage {
254 BookPage::Crafting { recipe_id: recipe_id.into(), text: Some(text.into()) }
255}
256
257pub fn smelting_page(recipe_id: impl Into<String>) -> BookPage {
258 BookPage::Smelting { recipe_id: recipe_id.into(), text: None }
259}
260
261pub fn image_page(texture: impl Into<String>) -> BookPage {
262 BookPage::Image { texture: texture.into(), title: None, text: None, border: true }
263}
264
265pub fn entity_page(entity_type: impl Into<String>) -> BookPage {
266 BookPage::Entity { entity_type: entity_type.into(), name: None, text: None }
267}
268
269pub fn relations_page(entries: Vec<String>) -> BookPage {
270 BookPage::Relations { entries, text: None }
271}
272
273pub fn pattern_page(op_id: impl Into<String>, anchor: impl Into<String>, input: impl Into<String>, output: impl Into<String>, text: impl Into<String>) -> BookPage {
274 BookPage::Pattern {
275 op_id: op_id.into(),
276 anchor: anchor.into(),
277 input: input.into(),
278 output: output.into(),
279 text: text.into(),
280 }
281}
282
283#[cfg(feature = "yog-ui")]
285pub mod book_ui {
286 use crate::{Book, BookEntry, BookPage};
287 use yog_ui::widget::{self, Widget};
288 use yog_ui::{Align, FlexDir, UiRoot};
289
290 pub fn build_book_ui(book: &Book, selected_cat: usize, selected_entry: usize, current_page: usize) -> UiRoot {
294 let mut cats: Vec<Widget> = Vec::new();
295 for (i, cat) in book.categories.iter().enumerate() {
296 let color = if i == selected_cat { 0xFF_FFFF55 } else { 0xFF_CCCCCC };
297 cats.push(widget::button(&cat.name)
298 .color(color)
299 .on_click(format!("cat:{}", i)));
300 }
301
302 let cat = book.categories.get(selected_cat);
303 let mut entries: Vec<Widget> = Vec::new();
304 if let Some(cat) = cat {
305 let cat_entries: Vec<&BookEntry> = book.entries.iter()
306 .filter(|e| e.category == cat.id).collect();
307 for (i, entry) in cat_entries.iter().enumerate() {
308 let color = if i == selected_entry { 0xFF_FFFF55 } else { 0xFF_CCCCCC };
309 let label = if entry.name.len() > 14 { &entry.name[..14] } else { &entry.name };
310 entries.push(widget::button(label)
311 .color(color)
312 .on_click(format!("entry:{}", i)));
313 }
314 }
315
316 let mut pages: Vec<Widget> = Vec::new();
317 if let Some(cat) = cat {
318 let cat_entries: Vec<&BookEntry> = book.entries.iter()
319 .filter(|e| e.category == cat.id).collect();
320 if let Some(entry) = cat_entries.get(selected_entry) {
321 if let Some(page) = entry.pages.get(current_page) {
322 pages.push(render_page(page));
323 }
324 }
325 }
326
327 let nav = widget::panel(FlexDir::Row).gap(4.0)
328 .child(widget::button("<").w(28.0).on_click("prev_page"))
329 .child(widget::label(&format!("{}/{}", current_page + 1,
330 cat.map_or(0, |c| {
331 book.entries.iter().filter(|e| e.category == c.id).nth(selected_entry)
332 .map_or(0, |e| e.pages.len())
333 }))).color(0xFF_888888).flex(1.0).align(Align::Center))
334 .child(widget::button(">").w(28.0).on_click("next_page"));
335
336 UiRoot::new(&book.id,
337 widget::panel(FlexDir::Row).gap(2.0)
338 .padding(2.0, 2.0, 2.0, 2.0).bg(0xFF_2A1A0E)
339 .child(
340 widget::panel(FlexDir::Column).w(104.0)
341 .child(widget::label("Categories").color(0xFF_888888))
342 .child(widget::panel(FlexDir::Column).gap(1.0)
343 .child_many(cats))
344 .child(widget::label("Entries").color(0xFF_888888))
345 .child(widget::panel(FlexDir::Column).gap(1.0)
346 .child_many(entries))
347 )
348 .child(
349 widget::panel(FlexDir::Column).flex(1.0).gap(2.0)
350 .child(widget::panel(FlexDir::Column).flex(1.0)
351 .child_many(pages))
352 .child(nav)
353 )
354 )
355 }
356
357 fn render_page(page: &BookPage) -> Widget {
358 match page {
359 BookPage::Text { text } =>
360 widget::label(text).color(0xFF_CCCCAA),
361 BookPage::Spotlight { item, title, text } => {
362 let mut p = widget::panel(FlexDir::Column).gap(2.0);
363 if let Some(t) = title { p = p.child(widget::label(t).color(0xFF_FFFF55)); }
364 p = p.child(widget::item_slot(&item.id));
365 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
366 p
367 }
368 BookPage::Crafting { recipe_id, text } => {
369 let mut p = widget::panel(FlexDir::Column).gap(2.0);
370 p = p.child(widget::label(format!("Crafting: {}", recipe_id)).color(0xFF_888888));
371 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
372 p
373 }
374 BookPage::Smelting { recipe_id, text } => {
375 let mut p = widget::panel(FlexDir::Column).gap(2.0);
376 p = p.child(widget::label(format!("Smelting: {}", recipe_id)).color(0xFF_888888));
377 if let Some(t) = text { p = p.child(widget::label(t).color(0xFF_CCCCAA)); }
378 p
379 }
380 BookPage::Empty => widget::spacer(),
381 _ => widget::label("(unsupported page)").color(0xFF_888888),
382 }
383 }
384
385 trait WidgetExt {
387 fn child_many(self, children: Vec<Widget>) -> Self;
388 }
389 impl WidgetExt for Widget {
390 fn child_many(mut self, children: Vec<Widget>) -> Self {
391 for c in children { self = self.child(c); }
392 self
393 }
394 }
395}
396
397fn esc(s: &str) -> String {
400 s.replace('\\', "\\\\").replace('"', "\\\"")
401}
402
403impl BookPage {
404 pub fn to_json(&self) -> String {
405 match self {
406 Self::Text { text } =>
407 format!(r#"{{"type":"text","text":"{}"}}"#, esc(text)),
408 Self::Spotlight { item, title, text } => {
409 let t = title.as_deref().map(|s| format!(r#","title":"{}""#, esc(s))).unwrap_or_default();
410 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
411 format!(r#"{{"type":"spotlight","item":"{id}"{t}{tx}}}"#, id = esc(&item.id))
412 }
413 Self::Crafting { recipe_id, text } => {
414 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
415 format!(r#"{{"type":"crafting","recipe":"{}"{}}}"#, esc(recipe_id), tx)
416 }
417 Self::Smelting { recipe_id, text } => {
418 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
419 format!(r#"{{"type":"smelting","recipe":"{}"{}}}"#, esc(recipe_id), tx)
420 }
421 Self::Image { texture, title, text, border } => {
422 let t = title.as_deref().map(|s| format!(r#","title":"{}""#, esc(s))).unwrap_or_default();
423 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
424 format!(r#"{{"type":"image","texture":"{}","border":{}{}{}}}"#,
425 esc(texture), border, t, tx)
426 }
427 Self::Entity { entity_type, name, text } => {
428 let n = name.as_deref().map(|s| format!(r#","name":"{}""#, esc(s))).unwrap_or_default();
429 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
430 format!(r#"{{"type":"entity","entity":"{}"{}{}}}"#, esc(entity_type), n, tx)
431 }
432 Self::Relations { entries, text } => {
433 let e: String = entries.iter().map(|s| format!(r#""{}""#, esc(s))).collect::<Vec<_>>().join(",");
434 let tx = text.as_deref().map(|s| format!(r#","text":"{}""#, esc(s))).unwrap_or_default();
435 format!(r#"{{"type":"relations","entries":[{}]{}}}"#, e, tx)
436 }
437 Self::Empty => r#"{"type":"empty"}"#.to_string(),
438 Self::Pattern { op_id, anchor, input, output, text } =>
439 format!(r#"{{"type":"pattern","op_id":"{}","anchor":"{}","input":"{}","output":"{}","text":"{}"}}"#,
440 esc(op_id), esc(anchor), esc(input), esc(output), esc(text)),
441 }
442 }
443}
444
445impl BookEntry {
446 pub fn to_json(&self) -> String {
447 let pages: String = self.pages.iter().map(|p| p.to_json()).collect::<Vec<_>>().join(",");
448 let icon = self.icon.as_deref().map(|s| format!(r#","icon":"{}""#, esc(s))).unwrap_or_default();
449 let adv = self.advancement.as_deref().map(|s| format!(r#","advancement":"{}""#, esc(s))).unwrap_or_default();
450 format!(
451 r#"{{"id":"{}","name":"{}","category":"{}","pages":[{}],"secret":{},"priority":{},"read_by_default":{}{}{}}}"#,
452 esc(&self.id), esc(&self.name), esc(&self.category), pages,
453 self.secret, self.priority, self.read_by_default, icon, adv
454 )
455 }
456}
457
458impl BookCategory {
459 pub fn to_json(&self) -> String {
460 let desc = self.description.as_deref().map(|s| format!(r#","description":"{}""#, esc(s))).unwrap_or_default();
461 let icon = self.icon.as_deref().map(|s| format!(r#","icon":"{}""#, esc(s))).unwrap_or_default();
462 format!(
463 r#"{{"id":"{}","name":"{}","sortnum":{}{}{}}}"#,
464 esc(&self.id), esc(&self.name), self.sortnum, desc, icon
465 )
466 }
467}
468
469impl Book {
470 pub fn to_json(&self) -> String {
471 let cats: String = self.categories.iter().map(|c| c.to_json()).collect::<Vec<_>>().join(",");
472 let entries: String = self.entries.iter().map(|e| e.to_json()).collect::<Vec<_>>().join(",");
473 let author = self.author.as_deref().map(|s| format!(r#","author":"{}""#, esc(s))).unwrap_or_default();
474 let tab = self.creative_tab.as_deref().map(|s| format!(r#","creative_tab":"{}""#, esc(s))).unwrap_or_default();
475 format!(
476 r#"{{"id":"{}","name":"{}","nameplate_color":"{}","landing_text":"{}","book_texture":"{}","filler_texture":"{}","model":"{}","show_progress":{},"i18n":{},"use_resource_pack":{},"categories":[{}],"entries":[{}]{}{}}}"#,
477 esc(&self.id), esc(&self.name), esc(&self.nameplate_color), esc(&self.landing_text),
478 esc(&self.book_texture), esc(&self.filler_texture), esc(&self.model),
479 self.show_progress, self.i18n, self.use_resource_pack,
480 cats, entries, author, tab
481 )
482 }
483}