1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8use std::time::SystemTime;
9use yog_sync::{Arc, Mutex, OnceLock};
10
11use yog_data::Res;
12use yog_formats::texture::{Animation, TextureFile};
13use yog_formats::yres::{Entry, Kind, Value, split_file_name};
14use yog_ids::Format;
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct Picture {
19 pub set: String,
21 pub name: String,
22 pub file: PathBuf,
24 pub bytes: Arc<Vec<u8>>,
26}
27
28impl Picture {
29 pub fn path(&self) -> String {
31 self.name.replace('.', yog_ids::sep::PATH)
32 }
33}
34
35#[derive(Debug, Clone)]
37pub struct Texture {
38 pub id: String,
39 pub file: PathBuf,
41 pub texture: TextureFile,
42}
43
44impl Texture {
45 pub fn picture(&self, root: &Path) -> Option<Picture> {
47 picture(root, &self.texture.image.set, &self.texture.image.key)
48 }
49}
50
51pub fn mod_id(root: &Path) -> Option<String> {
53 crate::manifest::YogToml::read(&root.join(yog_ids::file::MANIFEST))
54 .ok()
55 .map(|m| m.id)
56}
57
58type Cache = Mutex<HashMap<PathBuf, (SystemTime, Arc<Vec<Picture>>)>>;
59static PICTURES: OnceLock<Cache> = OnceLock::new();
60
61fn pictures_of(file: &Path, set: &str) -> Arc<Vec<Picture>> {
63 let modified = std::fs::metadata(file)
64 .and_then(|m| m.modified())
65 .unwrap_or(SystemTime::UNIX_EPOCH);
66 let cache = PICTURES.get_or_init(Default::default);
67 if let Some((at, pictures)) = cache.lock().get(file).cloned()
68 && at == modified
69 {
70 return pictures;
71 }
72 let entries = crate::files::read_source(file)
73 .ok()
74 .and_then(|t| yog_formats::yres::parse(&t).ok())
75 .unwrap_or_default();
76 let pictures: Arc<Vec<Picture>> = Arc::new(
77 entries
78 .into_iter()
79 .filter_map(|e| match (e.kind, e.value) {
80 (Kind::Image, Value::Bytes(b)) => Some(Picture {
81 set: set.to_string(),
82 name: e.name,
83 file: file.to_path_buf(),
84 bytes: Arc::new(b),
85 }),
86 _ => None,
87 })
88 .collect(),
89 );
90 let mut c = cache.lock();
91 c.insert(file.to_path_buf(), (modified, pictures.clone()));
92 pictures
93}
94
95pub fn pictures(root: &Path) -> Vec<Picture> {
97 let mut out = Vec::new();
98 for file in crate::files::with_ext(&root.join(yog_ids::project::RESOURCES), Format::Res.ext()) {
99 let Some((set, None)) = file
101 .file_name()
102 .and_then(|n| n.to_str())
103 .and_then(split_file_name)
104 else {
105 continue;
106 };
107 out.extend(pictures_of(&file, set).iter().cloned());
108 }
109 out
110}
111
112pub fn picture(root: &Path, set: &str, name: &str) -> Option<Picture> {
114 let file = root
115 .join(yog_ids::project::RESOURCES)
116 .join(Format::Res.file(set));
117 pictures_of(&file, set)
118 .iter()
119 .find(|p| p.name == name)
120 .cloned()
121}
122
123pub fn save_picture(root: &Path, set: &str, name: &str, bytes: Vec<u8>) -> Result<PathBuf, String> {
126 let file = root
127 .join(yog_ids::project::RESOURCES)
128 .join(Format::Res.file(set));
129 let mut entries = match crate::files::read_source(&file) {
130 Ok(text) => {
131 yog_formats::yres::parse(&text).map_err(|e| format!("{}: {e}", file.display()))?
132 }
133 Err(_) => Vec::new(),
134 };
135 match entries.iter_mut().find(|e| e.name == name) {
136 Some(e) => e.value = Value::Bytes(bytes),
137 None => entries.push(Entry::picture(name, bytes)),
138 }
139 if let Some(dir) = file.parent() {
140 std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
141 }
142 crate::files::write_source(&file, &yog_formats::yres::write(&entries))?;
143 Ok(file)
144}
145
146fn texture_file(root: &Path, id: &str) -> Option<PathBuf> {
148 let (ns, path) = yog_ids::split(id);
149 (mod_id(root)? == ns).then(|| {
150 root.join(yog_ids::project::RESOURCES)
151 .join(Format::Texture.file(path))
152 })
153}
154
155pub fn texture(root: &Path, id: &str) -> Option<Texture> {
157 let file = texture_file(root, id)?;
158 let texture = yog_data::from_text::<TextureFile>(&crate::files::read_source(&file).ok()?)
159 .ok()?
160 .value;
161 Some(Texture {
162 id: yog_ids::qualify(id),
163 file,
164 texture,
165 })
166}
167
168pub fn textures(root: &Path) -> Vec<Texture> {
170 let Some(ns) = mod_id(root) else {
171 return Vec::new();
172 };
173 let resources = root.join(yog_ids::project::RESOURCES);
174 crate::files::with_ext(&resources, Format::Texture.ext())
175 .into_iter()
176 .filter_map(|f| {
177 let rel = f
178 .strip_prefix(&resources)
179 .ok()?
180 .to_string_lossy()
181 .replace('\\', yog_ids::sep::PATH);
182 texture(root, &yog_formats::texture::id_of(&ns, &rel)?)
183 })
184 .collect()
185}
186
187pub fn is_own_texture(root: &Path, id: &str) -> bool {
189 texture_file(root, id).is_some()
190}
191
192pub fn save_texture_picture(root: &Path, id: &str, png: Vec<u8>) -> Result<PathBuf, String> {
196 let file = texture_file(root, id).ok_or_else(|| format!("{id} isn't a texture of this mod"))?;
197 if let Some(own) = texture(root, id) {
198 return save_picture(root, &own.texture.image.set, &own.texture.image.key, png);
199 }
200 let path = yog_ids::split(id).1;
201 let image = Res {
202 set: yog_formats::texture::picture_set(path),
203 key: path.replace(yog_ids::sep::PATH, "."),
204 };
205 let written = save_picture(root, &image.set, &image.key, png)?;
206 let texture = TextureFile {
207 image,
208 atlas: yog_registry::Atlas::of_path(path).map(|a| a.id.clone()),
209 animation: None,
210 blur: false,
211 clamp: false,
212 };
213 write_texture(&file, &texture)?;
214 Ok(written)
215}
216
217pub fn save_texture_animation(
219 root: &Path,
220 id: &str,
221 animation: Option<Animation>,
222) -> Result<PathBuf, String> {
223 let mut own = texture(root, id)
224 .ok_or_else(|| format!("{id} has no .{} in this mod", Format::Texture.ext()))?;
225 own.texture.animation = animation;
226 write_texture(&own.file, &own.texture)?;
227 Ok(own.file)
228}
229
230fn write_texture(file: &Path, texture: &TextureFile) -> Result<(), String> {
231 if let Some(dir) = file.parent() {
232 std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
233 }
234 std::fs::write(file, yog_data::to_bytes(texture))
235 .map_err(|e| format!("{}: {e}", file.display()))
236}
237
238pub fn built_asset(root: &Path, ns: &str, path: &str) -> Option<Vec<u8>> {
243 if mod_id(root)? != ns {
244 return None;
245 }
246 let file = path.strip_prefix(yog_ids::res::TEXTURES)?;
247 if let Some(stem) = file
248 .strip_suffix(yog_ids::res::MCMETA)
249 .and_then(|f| f.strip_suffix(yog_ids::res::PNG))
250 {
251 let meta = texture(root, &yog_ids::join(ns, stem))?
252 .texture
253 .game_mcmeta()?;
254 return serde_json::to_vec(&meta).ok();
255 }
256 let stem = file.strip_suffix(yog_ids::res::PNG)?;
257 let picture = match texture(root, &yog_ids::join(ns, stem)) {
258 Some(own) => own.picture(root)?,
259 None => pictures(root).into_iter().find(|p| p.path() == stem)?,
260 };
261 let image = yog_media::image::decode(&picture.bytes).ok()?;
263 yog_media::image::encode_png(&image).ok()
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269
270 fn png() -> Vec<u8> {
271 yog_media::image::encode_png(&yog_media::image::Image {
272 width: 1,
273 height: 1,
274 rgba: vec![9, 9, 9, 255],
275 })
276 .unwrap()
277 }
278
279 #[test]
280 fn a_painted_texture_becomes_a_picture_and_a_ytexture() {
281 let dir = std::env::temp_dir().join(format!("yog-media-{}", std::process::id()));
282 let _ = std::fs::remove_dir_all(&dir);
283 std::fs::create_dir_all(&dir).unwrap();
284 std::fs::write(dir.join(yog_ids::file::MANIFEST), "[mod]\nid = \"mymod\"\n").unwrap();
285 save_texture_picture(&dir, "mymod:block/ruby", png()).unwrap();
286 let own = texture(&dir, "mymod:block/ruby").unwrap();
287 assert_eq!(own.texture.atlas.as_deref(), Some("minecraft:blocks"));
288 assert_eq!(
289 own.picture(&dir).unwrap().bytes.as_slice(),
290 png().as_slice()
291 );
292 assert_eq!(
293 built_asset(&dir, "mymod", "textures/block/ruby.png"),
294 Some(png())
295 );
296 save_texture_animation(
297 &dir,
298 "mymod:block/ruby",
299 Some(Animation {
300 frame_time: Some(4),
301 ..Animation::default()
302 }),
303 )
304 .unwrap();
305 assert!(built_asset(&dir, "mymod", "textures/block/ruby.png.mcmeta").is_some());
306 assert_eq!(textures(&dir).len(), 1);
307 assert!(!is_own_texture(&dir, "minecraft:block/stone"));
308 assert!(pictures(&dir).iter().any(|p| p.path() == "block/ruby"));
309 let _ = std::fs::remove_dir_all(&dir);
310 }
311}