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