1use std::path::Path;
14
15use crate::Error;
16
17const TOC_INTERFACE: &str = env!("TOC_INTERFACE");
18const LOADER_TEMPLATE: &str = include_str!("../templates/loader.lua");
19const TOC_TEMPLATE: &str = include_str!("../templates/template.toc");
20
21const LIBSTUB_LUA: &str = include_str!("../vendor/libsharedmedia-3.0/LibStub/LibStub.lua");
22const CALLBACKHANDLER_LUA: &str =
23 include_str!("../vendor/libsharedmedia-3.0/CallbackHandler-1.0/CallbackHandler-1.0.lua");
24const LSM_LUA: &str = include_str!("../vendor/libsharedmedia-3.0/LibSharedMedia-3.0/LibSharedMedia-3.0.lua");
25const INNER_LIB_XML: &str = include_str!("../vendor/libsharedmedia-3.0/LibSharedMedia-3.0/lib.xml");
26
27fn generate_loader(version: &str) -> String {
28 LOADER_TEMPLATE.replace("__VERSION__", version)
29}
30
31fn generate_toc(version: &str, addon_name: &str) -> String {
32 let title = crate::addon_title(addon_name);
33 TOC_TEMPLATE
34 .replace("__VERSION__", version)
35 .replace("__INTERFACE__", TOC_INTERFACE)
36 .replace("__TITLE__", title)
37}
38
39pub fn deploy_templates(addon_dir: &Path) -> Result<(), Error> {
47 let version = env!("CARGO_PKG_VERSION");
48 let name = crate::addon_name(addon_dir);
49
50 write_file(addon_dir, "loader.lua", &generate_loader(version))?;
51 write_file(addon_dir, &format!("{name}.toc"), &generate_toc(version, name))?;
52
53 write_file(addon_dir, "libraries/LibStub/LibStub.lua", LIBSTUB_LUA)?;
54 write_file(
55 addon_dir,
56 "libraries/CallbackHandler-1.0/CallbackHandler-1.0.lua",
57 CALLBACKHANDLER_LUA,
58 )?;
59 write_file(addon_dir, "libraries/LibSharedMedia-3.0/lib.xml", INNER_LIB_XML)?;
60 write_file(
61 addon_dir,
62 "libraries/LibSharedMedia-3.0/LibSharedMedia-3.0.lua",
63 LSM_LUA,
64 )?;
65
66 Ok(())
67}
68
69fn write_file(dir: &Path, filename: &str, content: &str) -> Result<(), Error> {
70 let path = dir.join(filename);
71 if let Some(parent) = path.parent() {
72 std::fs::create_dir_all(parent).map_err(|e| Error::Io {
73 source: e,
74 path: parent.to_path_buf(),
75 })?;
76 }
77 std::fs::write(&path, content).map_err(|e| Error::Io {
78 source: e,
79 path: path.clone(),
80 })
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use std::sync::{Arc, Mutex};
87
88 use mlua::{Lua, Value, Variadic};
89 use tempfile::TempDir;
90
91 type Registration = (String, String, String, Option<i64>);
92
93 fn named_addon_dir(dir: &TempDir, name: &str) -> std::path::PathBuf {
95 let p = dir.path().join(name);
96 std::fs::create_dir_all(&p).unwrap();
97 p
98 }
99
100 #[test]
101 fn test_deploy_creates_files() {
102 let dir = TempDir::new().unwrap();
103 let addon_dir = named_addon_dir(&dir, "TestAddon");
104 deploy_templates(&addon_dir).unwrap();
105
106 assert!(addon_dir.join("loader.lua").exists());
107 assert!(addon_dir.join("TestAddon.toc").exists());
108
109 let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
110 assert!(loader.contains("Media registration loader"));
111 assert!(loader.contains("local ADDON_NAME, addon = ..."));
112 assert!(loader.contains("BASE_PATH"));
113 assert!(loader.contains("ADDON_NAME"));
114 assert!(loader.contains(&format!("Version: {}", env!("CARGO_PKG_VERSION"))));
115
116 let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
117 assert!(toc.contains("data.lua"));
118 assert!(toc.contains("loader.lua"));
119 assert!(toc.contains("## Title: TestAddon"));
120 assert!(toc.contains("## Notes: Provides textures, sounds, and other media for LibSharedMedia addons."));
121 assert!(!toc.contains("## Author:"));
122 assert!(!toc.contains("!!!WindMedia"));
123 }
124
125 #[test]
126 fn test_deploy_creates_vendor_files() {
127 let dir = TempDir::new().unwrap();
128 let addon_dir = named_addon_dir(&dir, "TestAddon");
129 deploy_templates(&addon_dir).unwrap();
130
131 assert!(addon_dir.join("libraries/LibStub/LibStub.lua").exists());
132 assert!(
133 addon_dir
134 .join("libraries/CallbackHandler-1.0/CallbackHandler-1.0.lua")
135 .exists()
136 );
137 assert!(
138 addon_dir
139 .join("libraries/LibSharedMedia-3.0/LibSharedMedia-3.0.lua")
140 .exists()
141 );
142 assert!(addon_dir.join("libraries/LibSharedMedia-3.0/lib.xml").exists());
143 }
144
145 #[test]
146 fn test_deploy_overwrites() {
147 let dir = TempDir::new().unwrap();
148 let addon_dir = named_addon_dir(&dir, "TestAddon");
149 deploy_templates(&addon_dir).unwrap();
150
151 std::fs::write(addon_dir.join("loader.lua"), "corrupted").unwrap();
152
153 deploy_templates(&addon_dir).unwrap();
154 let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
155 assert!(loader.contains("Media registration loader"));
156 assert!(loader.contains("Version: "));
157 assert!(!loader.contains("DO NOT EDIT MANUALLY"));
158 assert!(!loader.contains("Reads the data table"));
159 }
160
161 #[test]
162 fn test_toc_contains_interface_version() {
163 let dir = TempDir::new().unwrap();
164 let addon_dir = named_addon_dir(&dir, "TestAddon");
165 deploy_templates(&addon_dir).unwrap();
166
167 let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
168 assert!(toc.contains(&format!("## Interface: {}", TOC_INTERFACE)));
169 assert!(toc.contains(&format!("## Version: {}", env!("CARGO_PKG_VERSION"))));
170 assert!(toc.contains("## Title: TestAddon"));
171 assert!(toc.contains("## Notes: Provides textures, sounds, and other media for LibSharedMedia addons."));
172 assert!(toc.contains("## DefaultState: enabled"));
173 assert!(!toc.contains("## Author:"));
174 assert!(!toc.contains("!!!WindMedia"));
175 assert!(toc.contains("libraries\\LibStub\\LibStub.lua"));
176 assert!(toc.contains("LibSharedMedia-3.0\\lib.xml"));
177 }
178
179 #[test]
180 fn test_toc_orders_libraries_before_runtime_files() {
181 let dir = TempDir::new().unwrap();
182 let addon_dir = named_addon_dir(&dir, "TestAddon");
183 deploy_templates(&addon_dir).unwrap();
184
185 let toc = std::fs::read_to_string(addon_dir.join("TestAddon.toc")).unwrap();
186 let lines: Vec<&str> = toc.lines().collect();
187
188 let libstub = lines
189 .iter()
190 .position(|line| *line == "libraries\\LibStub\\LibStub.lua")
191 .unwrap();
192 let callbackhandler = lines
193 .iter()
194 .position(|line| *line == "libraries\\CallbackHandler-1.0\\CallbackHandler-1.0.lua")
195 .unwrap();
196 let lsm = lines
197 .iter()
198 .position(|line| *line == "libraries\\LibSharedMedia-3.0\\lib.xml")
199 .unwrap();
200 let data = lines.iter().position(|line| *line == "data.lua").unwrap();
201 let loader = lines.iter().position(|line| *line == "loader.lua").unwrap();
202
203 assert!(libstub < callbackhandler);
204 assert!(callbackhandler < lsm);
205 assert!(lsm < data);
206 assert!(data < loader);
207 }
208
209 #[test]
210 fn test_toc_skips_data_lua() {
211 let dir = TempDir::new().unwrap();
212 let addon_dir = named_addon_dir(&dir, "TestAddon");
213 deploy_templates(&addon_dir).unwrap();
214 assert!(!addon_dir.join("data.lua").exists());
215 }
216
217 #[test]
218 fn test_loader_uses_dynamic_addon_name() {
219 let dir = TempDir::new().unwrap();
220 let addon_dir = named_addon_dir(&dir, "TestAddon");
221 deploy_templates(&addon_dir).unwrap();
222
223 let loader = std::fs::read_to_string(addon_dir.join("loader.lua")).unwrap();
224 assert!(loader.contains("Media registration loader"));
225 assert!(loader.contains("local ADDON_NAME, addon = ..."));
226 assert!(loader.contains(r#"Interface\\AddOns\\"#));
227 assert!(loader.contains("ADDON_NAME"));
228 assert!(loader.contains("data.entries"));
229 assert!(!loader.contains("DO NOT EDIT MANUALLY"));
230 }
231
232 #[test]
233 fn test_generate_loader_reflects_version_changes() {
234 let v1 = generate_loader("1.2.3");
235 let v2 = generate_loader("9.9.9");
236
237 assert!(v1.contains("Version: 1.2.3"));
238 assert!(v2.contains("Version: 9.9.9"));
239 assert_ne!(v1, v2);
240 }
241
242 #[test]
243 fn test_generate_toc_reflects_version_changes() {
244 let v1 = generate_toc("0.1.0", "TestAddon");
245 let v2 = generate_toc("0.2.0", "TestAddon");
246
247 assert!(v1.contains("## Version: 0.1.0"));
248 assert!(v2.contains("## Version: 0.2.0"));
249 assert_ne!(v1, v2);
250 }
251
252 #[test]
253 fn test_toc_strips_bangs_from_title() {
254 let dir = TempDir::new().unwrap();
255 let addon_dir = named_addon_dir(&dir, "!!!WindMedia");
256 deploy_templates(&addon_dir).unwrap();
257
258 assert!(addon_dir.join("!!!WindMedia.toc").exists());
259 let toc = std::fs::read_to_string(addon_dir.join("!!!WindMedia.toc")).unwrap();
260 assert!(toc.contains("## Title: WindMedia"));
261 assert!(!toc.contains("## Title: !!!"));
262 }
263
264 #[test]
265 fn test_loader_executes_in_lua51_style_runtime() {
266 let lua = Lua::new();
267 let registrations: Arc<Mutex<Vec<Registration>>> = Arc::new(Mutex::new(Vec::new()));
268
269 let lsm = lua.create_table().unwrap();
270 lsm.set("LOCALE_BIT_koKR", 1).unwrap();
271 lsm.set("LOCALE_BIT_ruRU", 2).unwrap();
272 lsm.set("LOCALE_BIT_zhCN", 4).unwrap();
273 lsm.set("LOCALE_BIT_zhTW", 8).unwrap();
274 lsm.set("LOCALE_BIT_western", 16).unwrap();
275
276 let regs = registrations.clone();
277 let register = lua
278 .create_function_mut(move |_, args: Variadic<Value>| {
279 let kind = match &args[1] {
280 Value::String(s) => s.to_str()?.to_string(),
281 other => panic!("unexpected type arg: {other:?}"),
282 };
283 let key = match &args[2] {
284 Value::String(s) => s.to_str()?.to_string(),
285 other => panic!("unexpected key arg: {other:?}"),
286 };
287 let file = match &args[3] {
288 Value::String(s) => s.to_str()?.to_string(),
289 other => panic!("unexpected file arg: {other:?}"),
290 };
291 let mask = args.get(4).and_then(|v| match v {
292 Value::Integer(i) => Some(*i),
293 _ => None,
294 });
295 regs.lock().unwrap().push((kind, key, file, mask));
296 Ok(())
297 })
298 .unwrap();
299 lsm.set("Register", register).unwrap();
300
301 let globals = lua.globals();
302 let libstub_lsm = lsm.clone();
303 let libstub = lua
304 .create_function(move |_, (_name, _silent): (String, bool)| Ok(libstub_lsm.clone()))
305 .unwrap();
306 globals.set("LibStub", libstub).unwrap();
307
308 let addon = lua.create_table().unwrap();
309 let data = lua.create_table().unwrap();
310 let entries = lua.create_table().unwrap();
311
312 let font = lua.create_table().unwrap();
313 font.set("type", "font").unwrap();
314 font.set("key", "Body Font").unwrap();
315 font.set("file", "media/font/body.ttf").unwrap();
316 let metadata = lua.create_table().unwrap();
317 let locales = lua.create_table().unwrap();
318 locales.set(1, "western").unwrap();
319 locales.set(2, "zhCN").unwrap();
320 metadata.set("locales", locales).unwrap();
321 font.set("metadata", metadata).unwrap();
322
323 let statusbar = lua.create_table().unwrap();
324 statusbar.set("type", "statusbar").unwrap();
325 statusbar.set("key", "Smooth").unwrap();
326 statusbar.set("file", "media/statusbar/smooth.tga").unwrap();
327
328 entries.set(1, font).unwrap();
329 entries.set(2, statusbar).unwrap();
330 data.set("entries", entries).unwrap();
331 addon.set("data", data).unwrap();
332
333 let loader = generate_loader("1.2.3");
334 let wrapped = format!("return function(...)\n{}\nend", loader);
335 let func: mlua::Function = lua.load(&wrapped).eval().unwrap();
336 func.call::<()>(("TestAddon".to_string(), addon)).unwrap();
337
338 let regs = registrations.lock().unwrap();
339 assert_eq!(regs.len(), 2);
340 assert_eq!(regs[0].0, "font");
341 assert_eq!(regs[0].1, "Body Font");
342 assert_eq!(regs[0].2, r#"Interface\AddOns\TestAddon\media/font/body.ttf"#);
343 assert_eq!(regs[0].3, Some(20));
344 assert_eq!(regs[1].0, "statusbar");
345 assert_eq!(regs[1].1, "Smooth");
346 assert_eq!(regs[1].2, r#"Interface\AddOns\TestAddon\media/statusbar/smooth.tga"#);
347 assert_eq!(regs[1].3, None);
348 }
349}