Skip to main content

yog_cli/design/
mod.rs

1//! The `.yui` screen designer's backend: the canvas drawn by yog-ui itself
2//! (the crate mods render with in game) through a recording `YogGfxApi`,
3//! rasterized with Minecraft's own font and textures from the local game
4//! files; a preview mode driving the same tree with real UI events
5//! (`click:X:Y`, `scroll:DY`, scrollbar drags); and code-behind handlers.
6
7pub(crate) mod advancements;
8pub(crate) mod content;
9pub(crate) mod data;
10pub(crate) mod mod_ui;
11pub(crate) mod project;
12pub(crate) mod recipe_kinds;
13pub(crate) mod refs;
14pub(crate) mod service;
15pub(crate) mod worldgen;
16pub(crate) mod yui_codegen;
17
18/// This CLI as a command, for a design call that runs one of its
19/// subcommands: the running binary when it's `yog`, else the `yog` on `PATH`.
20pub(crate) fn this_cli() -> std::process::Command {
21    let exe = std::env::current_exe()
22        .ok()
23        .filter(|e| e.file_stem().is_some_and(|s| s == yog_ids::program::CLI));
24    std::process::Command::new(exe.unwrap_or_else(|| yog_ids::program::CLI.into()))
25}
26pub(crate) mod assets;
27pub(crate) mod blocks3d;
28pub(crate) mod book;
29pub(crate) mod codebehind;
30pub(crate) mod components;
31pub(crate) mod custom;
32pub(crate) mod font;
33pub(crate) mod font_copy;
34mod font_yog;
35pub(crate) mod gfx;
36pub mod host;
37pub(crate) mod images;
38pub(crate) mod item_models;
39pub(crate) mod markup;
40pub(crate) mod raster;
41pub(crate) mod recipe_area;
42pub(crate) mod sound;
43pub(crate) mod video;
44pub(crate) mod tags;
45
46use std::collections::HashMap;
47use std::path::{Path, PathBuf};
48use yog_sync::{Mutex, OnceLock};
49
50use base64::Engine as _;
51use yog_gfx::Vec2;
52use yog_ui::layout::LayoutNode;
53use yog_ui::{ScrollState, UiRoot, Widget};
54
55use self::assets::Resources;
56use self::markup::YuiNode;
57
58/// A laid-out widget: its border box and its content box (inside padding), GUI px.
59#[derive(Debug, Clone, yog_data::Schema)]
60pub(crate) struct NodeRect {
61    pub(crate) uid: String,
62    pub(crate) rect: [f32; 4],
63    pub(crate) content: [f32; 4],
64    /// A grid's rows and columns, `[start, size]` in GUI px (for its adorners).
65    pub(crate) grid: Option<GridRects>,
66}
67
68#[derive(Debug, Clone, yog_data::Schema)]
69pub(crate) struct GridRects {
70    pub(crate) rows: Vec<[f32; 2]>,
71    pub(crate) cols: Vec<[f32; 2]>,
72}
73
74/// Where the game resources came from (`None`: no client jar found).
75#[derive(Debug, Clone, yog_data::Schema)]
76#[data(rename_all = "camelCase")]
77pub(crate) struct AssetsInfo {
78    pub(crate) version: String,
79    pub(crate) jar: String,
80}
81
82#[derive(Debug, yog_data::Schema)]
83#[data(rename_all = "camelCase")]
84/// A screen drawn by [`render`]: its picture and where each element went.
85pub struct Rendered {
86    /// `data:image/png;base64,…` — `scale` physical px per GUI px.
87    pub(crate) image: String,
88    pub(crate) scale: u32,
89    pub(crate) rects: Vec<NodeRect>,
90    pub(crate) assets: Option<AssetsInfo>,
91    /// Text uses the game's font (else a stand-in glyph box).
92    pub(crate) real_font: bool,
93}
94
95/// The mod a file belongs to: the folder with its `yog.toml` (else the
96/// folder holding the `src/` it's in).
97pub(crate) fn mod_root(file: &Path) -> Option<PathBuf> {
98    project::mod_root(file).or_else(|| {
99        file.ancestors()
100            .find(|p| p.file_name().is_some_and(|n| n == "src"))
101            .and_then(|p| p.parent())
102            .map(Path::to_path_buf)
103    })
104}
105
106/// Built and laid out like the generated `root_with`.
107type Laid = UiRoot;
108
109fn lay_out(root: &YuiNode, screen_w: f32, screen_h: f32, scroll: &HashMap<String, f32>) -> Laid {
110    let mut ui = UiRoot::new(
111        "yog-idle:designer",
112        markup::screen(root, screen_w, screen_h, scroll),
113    );
114    // Text widths and image sizes come from the "host" — the designer's own
115    // font and image resolution — as they do from the game in `UiRoot::render`.
116    let table = gfx::api(screen_w as i32, screen_h as i32, 1.0);
117    // SAFETY: every function in `table` is sound to call; only fn pointers are kept.
118    let ctx = unsafe { yog_gfx::GfxContext::from_raw(&table) };
119    yog_ui::text::use_host_metrics(&ctx);
120    yog_ui::image::use_host_images(&ctx);
121    ui.layout(screen_w, screen_h);
122    ui
123}
124
125/// Pairs each `.yui` node with its widget and layout node (the screen's
126/// wrapper skipped; a `TextBox`'s inner parts aren't elements).
127fn walk<'a>(
128    node: &'a YuiNode,
129    w: &'a Widget,
130    l: &'a LayoutNode,
131    visit: &mut impl FnMut(&'a YuiNode, &'a Widget, &'a LayoutNode),
132) {
133    visit(node, w, l);
134    if !markup::is_container(&node.kind) {
135        return;
136    }
137    for (i, child) in markup::widget_children(node).enumerate() {
138        if let (Some(cw), Some(cl)) = (w.children.get(i), l.children.get(i)) {
139            walk(child, cw, cl, visit);
140        }
141    }
142}
143
144fn with_content<'a>(
145    root: &'a YuiNode,
146    laid: &'a Laid,
147    mut visit: impl FnMut(&'a YuiNode, &'a Widget, &'a LayoutNode),
148) {
149    if let (Some(w), Some(l)) = (
150        laid.root.children.first(),
151        laid.layout_root.children.first(),
152    ) {
153        walk(root, w, l, &mut visit);
154    }
155}
156
157fn rects(root: &YuiNode, laid: &Laid) -> Vec<NodeRect> {
158    let mut out = Vec::new();
159    with_content(root, laid, |n, w, l| {
160        let r = l.rect;
161        let p = w.style.pad;
162        out.push(NodeRect {
163            uid: n.uid.clone(),
164            rect: [r.x, r.y, r.w, r.h],
165            content: [
166                r.x + p[3],
167                r.y + p[0],
168                (r.w - p[1] - p[3]).max(0.0),
169                (r.h - p[0] - p[2]).max(0.0),
170            ],
171            grid: l.grid.as_ref().map(|g| GridRects {
172                rows: g.rows.iter().map(|&(a, b)| [a, b]).collect(),
173                cols: g.cols.iter().map(|&(a, b)| [a, b]).collect(),
174            }),
175        });
176    });
177    out
178}
179
180/// Per-file preview state: the scroll views' offsets/drags.
181#[derive(Default)]
182struct Preview {
183    scroll: HashMap<String, ScrollState>,
184}
185
186fn previews() -> &'static Mutex<HashMap<String, Preview>> {
187    static P: OnceLock<Mutex<HashMap<String, Preview>>> = OnceLock::new();
188    P.get_or_init(Default::default)
189}
190
191fn offsets(path: &str, preview: bool) -> HashMap<String, f32> {
192    if !preview {
193        return HashMap::new();
194    }
195    let p = previews().lock();
196    p.get(path)
197        .map(|s| {
198            s.scroll
199                .iter()
200                .map(|(k, v)| (k.clone(), v.offset))
201                .collect()
202        })
203        .unwrap_or_default()
204}
205
206#[derive(Debug, yog_data::Schema)]
207#[data(rename_all = "camelCase")]
208/// What [`render`] draws: a `.yui` screen's tree at a screen size and scale.
209pub struct RenderArgs {
210    pub(crate) path: String,
211    pub(crate) root: YuiNode,
212    pub(crate) screen_w: u32,
213    pub(crate) screen_h: u32,
214    pub(crate) scale: u32,
215    #[data(default)]
216    pub(crate) preview: bool,
217}
218
219/// Draws a screen as the game would: its picture and its elements' boxes.
220pub fn render(args: &RenderArgs) -> Result<Rendered, String> {
221    let (sw, sh) = (args.screen_w.clamp(1, 4096), args.screen_h.clamp(1, 4096));
222    let scroll = offsets(&args.path, args.preview);
223    let root = mod_root(Path::new(&args.path));
224    images::set_document(root.as_deref());
225    // The mod's own components: expanded from markup, or marked spacers here drawn by the designer host below.
226    let mut themed = args.root.clone();
227    markup::apply_resources(&mut themed);
228    markup::apply_theme(&mut themed);
229    recipe_area::expand(&mut themed, root.as_deref());
230    let prepared = custom::prepare(&themed, root.as_deref(), sw as f32, sh as f32);
231    let laid = lay_out(&prepared.root, sw as f32, sh as f32, &scroll);
232    let mut ops = gfx::record(sw as i32, sh as i32, |ctx| laid.render(ctx));
233    let mut node_rects = rects(&prepared.root, &laid);
234    if !prepared.is_empty() {
235        custom::splice(
236            &mut ops,
237            &prepared,
238            &node_rects.iter().map(|r| (r.uid.clone(), r.rect)).collect(),
239        );
240    }
241    // The inside of an expanded component instance isn't the document's.
242    node_rects.retain(|r| !r.uid.contains(components::INNER));
243    let res = Resources::for_mod(root.as_deref());
244    let scale = raster::fit_scale(sw, sh, args.scale);
245    let canvas = raster::rasterize(&ops, sw, sh, scale, &res);
246    let real_font = res.vanilla.is_some() || canvas_font_real(&res);
247    let png = canvas.png()?;
248    Ok(Rendered {
249        image: format!(
250            "data:image/png;base64,{}",
251            base64::engine::general_purpose::STANDARD.encode(png)
252        ),
253        scale,
254        rects: node_rects,
255        assets: res.vanilla.as_ref().map(|v| AssetsInfo {
256            version: v.version.clone(),
257            jar: v.jar.to_string_lossy().into_owned(),
258        }),
259        real_font,
260    })
261}
262
263fn canvas_font_real(res: &Resources) -> bool {
264    font::Font::load(res, yog_ids::yog::DEFAULT_FONT).is_real()
265}
266
267#[derive(Debug, yog_data::Schema)]
268#[data(rename_all = "camelCase")]
269pub(crate) struct EventArgs {
270    pub(crate) path: String,
271    pub(crate) root: YuiNode,
272    pub(crate) screen_w: u32,
273    pub(crate) screen_h: u32,
274    /// A Yog UI event: `click:X:Y`, `drag:X:Y`, `release:X:Y`, `scroll:DY`, `move:X:Y` (hover only).
275    pub(crate) event: String,
276    /// The cursor, for the wheel (which carries no position).
277    #[data(default)]
278    pub(crate) x: f32,
279    #[data(default)]
280    pub(crate) y: f32,
281}
282
283#[derive(Debug, Default, yog_data::Schema)]
284#[data(rename_all = "camelCase")]
285pub(crate) struct EventResult {
286    /// A scroll view used the event (wheel, scrollbar).
287    pub(crate) scrolled: bool,
288    /// The clickable element under the cursor (`hit_test`: deepest enabled with `on_click`).
289    pub(crate) hit: Option<String>,
290    /// Its `on_click`, for a click.
291    pub(crate) clicked: Option<String>,
292}
293
294/// `hit_test` (yog-ui `layout::hit_test`), answering with the `.yui` uid.
295fn hit<'a>(node: &'a YuiNode, l: &'a LayoutNode, mx: f32, my: f32) -> Option<&'a YuiNode> {
296    if !l.rect.contains(Vec2::new(mx, my)) {
297        return None;
298    }
299    if markup::is_container(&node.kind) {
300        // Laid out from the widget children only — property elements have no layout node.
301        let children: Vec<_> = markup::widget_children(node).zip(&l.children).collect();
302        for (child, cl) in children.into_iter().rev() {
303            if let Some(found) = hit(child, cl, mx, my) {
304                return Some(found);
305            }
306        }
307    }
308    (l.on_click.is_some() && l.enabled).then_some(node)
309}
310
311fn xy(rest: &str) -> Option<(f32, f32)> {
312    let mut it = rest.split(':');
313    Some((it.next()?.parse().ok()?, it.next()?.parse().ok()?))
314}
315
316pub(crate) fn event(args: &EventArgs) -> EventResult {
317    let (sw, sh) = (
318        args.screen_w.clamp(1, 4096) as f32,
319        args.screen_h.clamp(1, 4096) as f32,
320    );
321    let scroll = offsets(&args.path, true);
322    let mod_root = mod_root(Path::new(&args.path));
323    // Laid out like `render` does: images size by the same mod's resources.
324    images::set_document(mod_root.as_deref());
325    let mut themed = args.root.clone();
326    markup::apply_resources(&mut themed);
327    markup::apply_theme(&mut themed);
328    recipe_area::expand(&mut themed, mod_root.as_deref());
329    let prepared = custom::prepare(&themed, mod_root.as_deref(), sw, sh);
330    let root = &prepared.root;
331    let laid = lay_out(root, sw, sh, &scroll);
332    let mut out = EventResult::default();
333    let pos = args
334        .event
335        .split_once(':')
336        .and_then(|(_, rest)| xy(rest))
337        .unwrap_or((args.x, args.y));
338
339    // Scroll views first, as a screen's code-behind does (`ScrollState::handle`).
340    if !args.event.starts_with("move:") {
341        let mut views: Vec<(&YuiNode, &LayoutNode)> = Vec::new();
342        with_content(root, &laid, |n, _, l| {
343            if n.kind == yog_ids::yui::SCROLL_VIEW {
344                views.push((n, l));
345            }
346        });
347        let mut all = previews().lock();
348        let state = all.entry(args.path.clone()).or_default();
349        let wheel = args.event.starts_with("scroll:");
350        // The wheel goes to the innermost view under the cursor; the rest to every view (each checks its own scrollbar).
351        for (n, l) in views.iter().rev() {
352            if wheel && !l.rect.contains(Vec2::new(pos.0, pos.1)) {
353                continue;
354            }
355            let s = state.scroll.entry(n.uid.clone()).or_default();
356            if s.handle(&args.event, l, yog_ui::scroll::WHEEL_STEP) {
357                out.scrolled = true;
358                break;
359            }
360        }
361    }
362    if let Some(l) = laid.layout_root.children.first() {
363        let found = hit(root, l, pos.0, pos.1);
364        // Inside a component instance: the instance.
365        out.hit = found.map(|n| {
366            n.uid
367                .split(components::INNER)
368                .next()
369                .unwrap_or_default()
370                .to_string()
371        });
372        if args.event.starts_with("click:") && !out.scrolled {
373            out.clicked = found
374                .and_then(|n| n.attr(yog_ids::yui_attr::ON_CLICK))
375                .map(str::to_string);
376        }
377    }
378    out
379}
380
381/// Forgets a file's preview state (scroll positions).
382pub(crate) fn reset(path: &str) {
383    previews().lock().remove(path);
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::design::raster::DrawOp;
390
391    fn node(uid: &str, kind: &str, attrs: &[(&str, &str)], children: Vec<YuiNode>) -> YuiNode {
392        YuiNode {
393            uid: uid.into(),
394            kind: kind.into(),
395            attrs: attrs
396                .iter()
397                .map(|(k, v)| (k.to_string(), v.to_string()))
398                .collect(),
399            children,
400        }
401    }
402
403    fn screen() -> YuiNode {
404        node(
405            "s",
406            "Screen",
407            &[
408                ("id", "m:s"),
409                ("width", "100"),
410                ("height", "60"),
411                ("bg", "#FF101010"),
412            ],
413            vec![
414                node(
415                    "b",
416                    "Button",
417                    &[
418                        ("x", "10"),
419                        ("y", "10"),
420                        ("w", "40"),
421                        ("h", "20"),
422                        ("text", "Play"),
423                        ("on_click", "play"),
424                    ],
425                    vec![],
426                ),
427                node(
428                    "sv",
429                    "ScrollView",
430                    &[("x", "60"), ("y", "0"), ("w", "30"), ("h", "30")],
431                    (0..10)
432                        .map(|i| {
433                            node(
434                                &format!("r{i}"),
435                                "Spacer",
436                                &[("w", "20"), ("h", "10")],
437                                vec![],
438                            )
439                        })
440                        .collect(),
441                ),
442            ],
443        )
444    }
445
446    #[test]
447    fn rects_come_from_yog_ui_layout() {
448        let laid = lay_out(&screen(), 200.0, 100.0, &HashMap::new());
449        let r = rects(&screen(), &laid);
450        // Centered: ((200 - 100) / 2, (100 - 60) / 2).
451        assert_eq!(r[0].uid, "s");
452        assert_eq!(r[0].rect, [50.0, 20.0, 100.0, 60.0]);
453        let b = r.iter().find(|n| n.uid == "b").unwrap();
454        assert_eq!(b.rect, [60.0, 30.0, 40.0, 20.0]);
455    }
456
457    #[test]
458    fn grids_place_children_in_cells() {
459        let defs = node(
460            "d",
461            "Grid.ColumnDefinitions",
462            &[],
463            vec![
464                node("c0", "ColumnDefinition", &[("width", "30")], vec![]),
465                node("c1", "ColumnDefinition", &[], vec![]),
466            ],
467        );
468        let root = node(
469            "s",
470            "Screen",
471            &[("width", "100"), ("height", "60")],
472            vec![node(
473                "g",
474                "Grid",
475                &[
476                    ("w", "100"),
477                    ("h", "60"),
478                    ("rows", "Auto,*"),
479                    ("column_spacing", "10"),
480                ],
481                vec![
482                    defs,
483                    node(
484                        "a",
485                        "Spacer",
486                        &[("h", "12"), ("Grid.ColumnSpan", "2")],
487                        vec![],
488                    ),
489                    node(
490                        "b",
491                        "Spacer",
492                        &[("Grid.Row", "1"), ("Grid.Column", "1"), ("margin", "2")],
493                        vec![],
494                    ),
495                ],
496            )],
497        );
498        let laid = lay_out(&root, 100.0, 60.0, &HashMap::new());
499        let r = rects(&root, &laid);
500        let at = |uid: &str| r.iter().find(|n| n.uid == uid).unwrap();
501        let g = at("g").grid.as_ref().unwrap();
502        assert_eq!(g.rows, [[0.0, 12.0], [12.0, 48.0]]);
503        assert_eq!(g.cols, [[0.0, 30.0], [40.0, 60.0]]);
504        assert_eq!(at("a").rect, [0.0, 0.0, 100.0, 12.0]);
505        assert_eq!(at("b").rect, [42.0, 14.0, 56.0, 44.0]);
506    }
507
508    #[test]
509    fn renders_through_the_recorder() {
510        let args = RenderArgs {
511            path: "/nowhere/src/x.yui".into(),
512            root: screen(),
513            screen_w: 200,
514            screen_h: 100,
515            scale: 2,
516            preview: false,
517        };
518        let laid = lay_out(&args.root, 200.0, 100.0, &HashMap::new());
519        let ops = gfx::record(200, 100, |ctx| laid.render(ctx));
520        assert!(
521            matches!(
522                ops.first(),
523                Some(DrawOp::Rect {
524                    color: 0xFF10_1010,
525                    ..
526                })
527            ),
528            "{ops:?}"
529        );
530        assert!(
531            ops.iter()
532                .any(|o| matches!(o, DrawOp::Text { text, .. } if text == "Play"))
533        );
534        let out = render(&args).unwrap();
535        assert!(out.image.starts_with("data:image/png;base64,"));
536        assert_eq!(out.rects.len(), 13);
537    }
538
539    #[test]
540    fn preview_clicks_and_scrolls() {
541        let path = "/nowhere/src/preview_test.yui";
542        let ev = |e: &str, x: f32, y: f32| {
543            event(&EventArgs {
544                path: path.into(),
545                root: screen(),
546                screen_w: 200,
547                screen_h: 100,
548                event: e.into(),
549                x,
550                y,
551            })
552        };
553        let r = ev("click:70:40", 0.0, 0.0);
554        assert_eq!(r.clicked.as_deref(), Some("play"));
555        assert_eq!(r.hit.as_deref(), Some("b"));
556        assert!(ev("click:5:5", 0.0, 0.0).clicked.is_none());
557        let r = ev("scroll:-1", 120.0, 30.0);
558        assert!(r.scrolled);
559        assert!(offsets(path, true)["sv"] > 0.0);
560        reset(path);
561        assert!(offsets(path, true).is_empty());
562    }
563
564    #[test]
565    fn clicks_skip_property_elements() {
566        let defs = node(
567            "d",
568            "Grid.ColumnDefinitions",
569            &[],
570            vec![node("c0", "ColumnDefinition", &[], vec![])],
571        );
572        let button = node(
573            "b",
574            "Button",
575            &[("w", "40"), ("h", "20"), ("on_click", "go")],
576            vec![],
577        );
578        let root = node(
579            "s",
580            "Screen",
581            &[("width", "100"), ("height", "60")],
582            vec![node(
583                "g",
584                "Grid",
585                &[("w", "100"), ("h", "60")],
586                vec![defs, button],
587            )],
588        );
589        let r = event(&EventArgs {
590            path: "/nowhere/src/grid_click.yui".into(),
591            root,
592            screen_w: 100,
593            screen_h: 60,
594            event: "click:50:30".into(),
595            x: 0.0,
596            y: 0.0,
597        });
598        assert_eq!(r.hit.as_deref(), Some("b"));
599        assert_eq!(r.clicked.as_deref(), Some("go"));
600    }
601}
602
603#[cfg(test)]
604mod visual {
605    use super::*;
606
607    /// `YOG_DESIGNER_PNG=out.png cargo test --lib designer::visual -- --ignored`:
608    /// a sample screen drawn with the local game files, for eyeballing.
609    #[test]
610    #[ignore]
611    fn sample_png() {
612        let Ok(out) = std::env::var(yog_ids::env::DESIGNER_PNG) else {
613            return;
614        };
615        let n = |uid: &str, kind: &str, attrs: &[(&str, &str)]| YuiNode {
616            uid: uid.into(),
617            kind: kind.into(),
618            attrs: attrs
619                .iter()
620                .map(|(k, v)| (k.to_string(), v.to_string()))
621                .collect(),
622            children: vec![],
623        };
624        let mut root = n(
625            "s",
626            "Screen",
627            &[("width", "200"), ("height", "120"), ("bg", "#C0101010")],
628        );
629        root.children = vec![
630            n(
631                "l",
632                "Label",
633                &[
634                    ("x", "6"),
635                    ("y", "6"),
636                    ("text", "Привет, Minecraft! §cRed §lBold"),
637                    ("color", "#FFFFFF"),
638                    ("no_wrap", "true"),
639                ],
640            ),
641            n(
642                "b",
643                "Button",
644                &[
645                    ("x", "6"),
646                    ("y", "24"),
647                    ("w", "80"),
648                    ("h", "20"),
649                    ("text", "Play"),
650                    ("bg", "#FF555555"),
651                    ("color", "#FFFFFF"),
652                ],
653            ),
654            n(
655                "t",
656                "TextBox",
657                &[
658                    ("x", "96"),
659                    ("y", "26"),
660                    ("w", "90"),
661                    ("h", "16"),
662                    ("placeholder", "Search…"),
663                ],
664            ),
665            n(
666                "i",
667                "InvSlot",
668                &[
669                    ("x", "6"),
670                    ("y", "50"),
671                    ("item", "minecraft:diamond"),
672                    ("count", "64"),
673                ],
674            ),
675            n(
676                "g",
677                "Label",
678                &[
679                    ("x", "6"),
680                    ("y", "76"),
681                    ("text", "Galactic"),
682                    ("font", "yog:font/alt"),
683                    ("no_wrap", "true"),
684                ],
685            ),
686            n(
687                "u",
688                "Label",
689                &[
690                    ("x", "6"),
691                    ("y", "90"),
692                    ("text", "日本語 ✓ ♥"),
693                    ("no_wrap", "true"),
694                ],
695            ),
696        ];
697        let args = RenderArgs {
698            path: "/nowhere/src/x.yui".into(),
699            root,
700            screen_w: 220,
701            screen_h: 130,
702            scale: 4,
703            preview: false,
704        };
705        let r = render(&args).unwrap();
706        let b64 = r.image.trim_start_matches("data:image/png;base64,");
707        std::fs::write(
708            out,
709            base64::engine::general_purpose::STANDARD
710                .decode(b64)
711                .unwrap(),
712        )
713        .unwrap();
714    }
715}