1mod consts;
11use consts as mcmeta;
12
13use yog_data::{Res, Schema};
14
15#[derive(Schema, Debug, Clone, PartialEq)]
17pub struct TextureFile {
18 pub image: Res,
20 pub atlas: Option<String>,
24 pub animation: Option<Animation>,
26 #[data(default)]
28 pub blur: bool,
29 #[data(default)]
31 pub clamp: bool,
32}
33
34#[derive(Schema, Debug, Clone, PartialEq, Default)]
36pub struct Animation {
37 pub frame_time: Option<u32>,
39 #[data(default)]
41 pub interpolate: bool,
42 #[data(default)]
44 pub frames: Vec<Frame>,
45 pub width: Option<u32>,
47 pub height: Option<u32>,
48}
49
50#[derive(Schema, Debug, Clone, PartialEq, Default)]
52pub struct Frame {
53 pub index: u32,
54 pub time: Option<u32>,
55}
56
57impl Animation {
58 pub const DEFAULT_FRAME_TIME: u32 = 1;
60
61 #[must_use]
63 pub fn frame_time(&self) -> u32 {
64 self.frame_time.unwrap_or(Self::DEFAULT_FRAME_TIME)
65 }
66}
67
68#[must_use]
70pub fn id_of(mod_id: &str, rel: &str) -> Option<String> {
71 let path = rel
72 .strip_suffix(yog_ids::Format::Texture.ext())?
73 .strip_suffix('.')?;
74 Some(yog_ids::join(mod_id, path))
75}
76
77use serde_json::{Value, json};
78
79impl TextureFile {
80 #[must_use]
82 pub fn game_mcmeta(&self) -> Option<Value> {
83 let t = self;
84 let mut meta = serde_json::Map::new();
85 if let Some(a) = &t.animation {
86 meta.insert(mcmeta::ANIMATION.into(), animation(a));
87 }
88 if t.blur || t.clamp {
89 meta.insert(
90 mcmeta::TEXTURE.into(),
91 json!({ "blur": t.blur, "clamp": t.clamp }),
92 );
93 }
94 (!meta.is_empty()).then_some(Value::Object(meta))
95 }
96}
97
98fn animation(a: &Animation) -> Value {
100 let mut anim = serde_json::Map::new();
101 anim.insert("frametime".into(), json!(a.frame_time()));
102 if a.interpolate {
103 anim.insert("interpolate".into(), json!(true));
104 }
105 if !a.frames.is_empty() {
106 let frames = a.frames.iter().map(|f| match f.time {
107 Some(time) => json!({ "index": f.index, "time": time }),
108 None => json!(f.index),
109 });
110 anim.insert("frames".into(), Value::Array(frames.collect()));
111 }
112 if let Some(w) = a.width {
113 anim.insert("width".into(), json!(w));
114 }
115 if let Some(h) = a.height {
116 anim.insert("height".into(), json!(h));
117 }
118 Value::Object(anim)
119}
120
121impl Animation {
122 #[must_use]
124 pub fn from_mcmeta(mcmeta: &Value) -> Option<Animation> {
125 let a = mcmeta.get(mcmeta::ANIMATION)?;
126 let u32_of = |v: &Value, k: &str| {
127 v.get(k)
128 .and_then(Value::as_u64)
129 .and_then(|n| u32::try_from(n).ok())
130 };
131 Some(Animation {
132 frame_time: u32_of(a, "frametime"),
133 interpolate: a
134 .get("interpolate")
135 .and_then(Value::as_bool)
136 .unwrap_or(false),
137 frames: a
138 .get("frames")
139 .and_then(Value::as_array)
140 .map(|fs| {
141 fs.iter()
142 .filter_map(|f| match f.as_u64() {
143 Some(i) => Some(Frame {
144 index: u32::try_from(i).ok()?,
145 time: None,
146 }),
147 None => Some(Frame {
148 index: u32_of(f, "index")?,
149 time: u32_of(f, "time"),
150 }),
151 })
152 .collect()
153 })
154 .unwrap_or_default(),
155 width: u32_of(a, "width"),
156 height: u32_of(a, "height"),
157 })
158 }
159}
160
161#[must_use]
165pub fn picture_set(path: &str) -> String {
166 match path.split(yog_ids::sep::PATH).next().unwrap_or_default() {
167 GUI_DIR => GUI_SET.to_string(),
168 top => {
169 top.split('_')
170 .map(|w| w[..1].to_ascii_uppercase() + &w[1..])
171 .collect::<String>()
172 + PICTURE_SET_SUFFIX
173 }
174 }
175}
176
177const GUI_DIR: &str = "gui";
179const GUI_SET: &str = "Images";
180const PICTURE_SET_SUFFIX: &str = "Images";
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 #[test]
187 fn a_texture_round_trips_through_its_file_and_names_itself() {
188 let t = TextureFile {
189 image: Res {
190 set: "Images".into(),
191 key: "block.ruby".into(),
192 },
193 atlas: Some("minecraft:blocks".into()),
194 animation: Some(Animation {
195 frame_time: Some(2),
196 frames: vec![Frame {
197 index: 1,
198 time: Some(5),
199 }],
200 ..Animation::default()
201 }),
202 blur: false,
203 clamp: true,
204 };
205 assert_eq!(
206 yog_data::from_text::<TextureFile>(&yog_data::text::to_string_pretty(&t.write()))
207 .unwrap()
208 .value,
209 t
210 );
211 assert_eq!(
212 id_of("mymod", "block/ruby.ytexture").as_deref(),
213 Some("mymod:block/ruby")
214 );
215 assert_eq!(
216 (
217 picture_set("block/ruby"),
218 picture_set("mob_effect/x"),
219 picture_set("gui/logo")
220 ),
221 (
222 "BlockImages".into(),
223 "MobEffectImages".into(),
224 "Images".into()
225 )
226 );
227 let meta = json!({ "animation": { "frametime": 2, "interpolate": true, "frames": [1, { "index": 0, "time": 5 }] } });
228 let a = Animation::from_mcmeta(&meta).unwrap();
229 assert_eq!(
230 TextureFile {
231 animation: Some(a),
232 clamp: false,
233 ..t
234 }
235 .game_mcmeta(),
236 Some(meta)
237 );
238 }
239}