Skip to main content

yog_cli/design/
service.rs

1//! The designers as `yog serve` methods: what a tool (Yog-IDLE) asks to draw,
2//! list or change, by the name it calls it and with its parameters.
3
4use std::path::Path;
5
6use base64::Engine as _;
7use serde::Serialize;
8use serde::de::DeserializeOwned;
9use serde_json::{Value, json};
10
11use super::{advancements, book, codebehind, content, custom, images, project, recipe_kinds, refs, tags, worldgen, yui_codegen};
12
13type Reply = Result<Value, Value>;
14
15/// A parameter, read as its type.
16fn param<T: DeserializeOwned>(params: &Value, name: &str) -> Result<T, Value> {
17    serde_json::from_value(params.get(name).cloned().unwrap_or(Value::Null)).map_err(|e| json!(format!("`{name}`: {e}")))
18}
19
20fn ok<T: Serialize>(v: T) -> Reply {
21    serde_json::to_value(v).map_err(|e| json!(e.to_string()))
22}
23
24fn done<T: Serialize>(r: Result<T, String>) -> Reply {
25    r.map_err(Value::from).and_then(ok)
26}
27
28/// A picture's `width` and `height` parameters.
29fn size(params: &Value) -> Result<(u32, u32), Value> {
30    Ok((param(params, "width")?, param(params, "height")?))
31}
32
33/// The texture the `.ytexture` at `file` is: `@texture:<mod>:<path>`.
34fn texture_file_id(file: &Path) -> Option<String> {
35    let root = project::mod_root(file)?;
36    let rel = file.strip_prefix(root.join(yog_ids::project::RESOURCES)).ok()?.to_string_lossy().replace('\\', yog_ids::sep::PATH);
37    let id = yog_formats::texture::id_of(&project::mod_id(&root)?, &rel)?;
38    Some(format!("{}{id}", yog_ids::reference::TEXTURE))
39}
40
41/// What design method `method` does with `params`; `None`: no such method.
42pub fn dispatch(method: &str, params: &Value) -> Option<Reply> {
43    let path = || param::<String>(params, "path");
44    let file = |f: fn(&Path) -> Reply| path().and_then(|p| f(Path::new(&p)));
45    let with = |name: &str, f: &dyn Fn(&Path, &str) -> Reply| -> Reply {
46        let (p, v) = (path()?, param::<String>(params, name)?);
47        f(Path::new(&p), &v)
48    };
49    Some(match method {
50        "recipe_kinds" => file(|p| ok(recipe_kinds::all(super::mod_root(p).as_deref()))),
51        "recipe_kind_create" => with("name", &|p, name| done(recipe_kinds::create(p, name))),
52        "worldgen_preview" => param::<String>(params, "text").and_then(|t| done(worldgen::render(&t, param(params, "size")?))),
53        "yui_codegen" => path().and_then(|p| done(yui_codegen::codegen(&p))),
54        "content_mod_id" => file(|p| ok(super::mod_root(p).and_then(|r| project::mod_id(&r)))),
55        "content_texture" | "body_texture" => with("texture", &|p, t| done(content::texture(p, t))),
56        "content_save_texture" => with("texture", &|p, t| {
57            let png = base64::engine::general_purpose::STANDARD.decode(param::<String>(params, "png")?).map_err(|e| json!(e.to_string()))?;
58            done(content::write_texture(p, t, png))
59        }),
60        "content_save_texture_animation" => with("texture", &|p, t| done(content::write_animation(p, t, param::<Option<content::Animation>>(params, "animation")?.as_ref()))),
61        "content_textures" => file(|p| ok(content::textures(p))),
62        "content_strings" => file(|p| ok(content::strings(p))),
63        "content_sound_groups" => file(|p| ok(content::sound_groups(p))),
64        "content_tool_materials" => file(|p| ok(content::tool_materials(p))),
65        "content_armor_materials" => file(|p| ok(content::armor_materials(p))),
66        "content_refs" => with("kind", &|p, k| ok(refs::refs(p, k))),
67        "content_defs" => with("kind", &|p, k| ok(refs::own_defs(p, k))),
68        "content_block_kinds" => Ok(refs::block_kinds()),
69        "content_tags" => with("registry", &|p, r| ok(content::tags(p, r))),
70        "content_parent_model" => with("id", &|p, id| done(content::parent_model(p, id))),
71        "content_item_potion_color" => file(|p| ok(content::item_potion_color(p))),
72        "content_import" => with("kind", &|p, kind| {
73            let model = param::<Option<String>>(params, "model")?;
74            done(content::import(p, kind, &param::<String>(params, "source")?, model.as_deref(), param(params, "force")?))
75        }),
76        // The texture a `.ytexture` file is (`@texture:<mod>:<path>`), or null outside a mod's resources.
77        "texture_file_id" => file(|p| ok(texture_file_id(p))),
78        // A `.ytexture`'s `animation` (as the file has it) of a `width × height` picture, every frame resolved.
79        "texture_animation_resolve" => size(params).and_then(|size| {
80            let a = yog_data::read_value::<yog_formats::texture::Animation>(&yog_data::json::from_json(&params["animation"])).map_err(|e| json!(format!("{e:?}")))?;
81            ok(content::Animation::of(&a, size))
82        }),
83        // A resolved animation (as `texture_animation_resolve` gives it) as the file keeps it.
84        "texture_animation_format" => size(params).and_then(|size| {
85            let a: content::Animation = param(params, "animation")?;
86            ok(yog_data::json::to_json(&yog_data::Schema::write(&a.to_format(size))))
87        }),
88        "item_tag_items" => with("tag", &|p, t| ok(tags::item_tag(p, t))),
89        "yui_image_sources" => file(|p| ok(images::candidates(p))),
90        "yui_image_preview" => with("source", &|p, s| ok(images::preview(p, s, param(params, "size")?))),
91        "yui_designer_render" => param(params, "args").and_then(|a| done(super::render(&a))),
92        "yui_designer_event" => param(params, "args").and_then(|a| ok(super::event(&a))),
93        "yui_designer_reset" => path().map(|p| {
94            super::reset(&p);
95            Value::Null
96        }),
97        "yui_designer_components" => file(|p| {
98            let root = if project::path(p).is_file() { p.to_path_buf() } else { super::mod_root(p).ok_or("not inside a mod (no src/ folder)")? };
99            ok(custom::status(&root))
100        }),
101        "yui_event_handler" => with("event", &|p, e| done(codebehind::ensure_handler(p, e))),
102        "book_page_preview" => param(params, "args").and_then(|a| ok(book::page_preview(&a))),
103        "book_designer_render" => param(params, "args").and_then(|a| ok(book::render(&a))),
104        "book_designer_close" => path().map(|p| {
105            book::close(&p);
106            Value::Null
107        }),
108        "advancement_render" => param(params, "args").and_then(|a| ok(advancements::render(&a))),
109        "ydata_text" => file(|p| {
110            let bytes = std::fs::read(p).map_err(|e| json!(e.to_string()))?;
111            let value = yog_data::binary::decode(&bytes).map_err(|e| json!(e.to_string()))?;
112            ok(yog_data::text::to_string_pretty(&value))
113        }),
114        _ => return None,
115    })
116}