using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Windows.Media.Imaging; namespace Character_Builder; /// Finds the Characters_Build_Docs root, lists characters, and scans their part PNGs. public static class AssetScanner { /// Walk up from the exe to find the "Characters_Build_Docs" folder. public static string? FindRoot() { var dir = new DirectoryInfo(AppContext.BaseDirectory); for (int i = 0; i < 10 && dir != null; i++) { if (string.Equals(dir.Name, "Characters_Build_Docs", StringComparison.OrdinalIgnoreCase)) return dir.FullName; // also: a child named Characters_Build_Docs var child = Path.Combine(dir.FullName, "Characters_Build_Docs"); if (Directory.Exists(child)) return child; dir = dir.Parent; } return null; } /// Character = a subfolder that contains a Reference/ folder. public static List GetCharacters(string root) { var list = new List(); if (!Directory.Exists(root)) return list; foreach (var d in Directory.GetDirectories(root)) { var refDir = Path.Combine(d, "Reference"); if (!Directory.Exists(refDir)) continue; var name = Path.GetFileName(d); var sheet = PickSheet(refDir, name); list.Add(new CharacterInfo { Name = name, FolderPath = d, SheetPath = sheet, Thumb = LoadBitmap(sheet) }); } return list.OrderBy(c => c.Name).ToList(); } private static string? PickSheet(string refDir, string charName) { var sheets = Directory.GetFiles(refDir, "*_sheet.png"); if (sheets.Length == 0) return Directory.GetFiles(refDir, "*.png").FirstOrDefault(); bool duo = charName.Contains("and", StringComparison.OrdinalIgnoreCase) || charName.Contains("Duo", StringComparison.OrdinalIgnoreCase) || charName.Contains("&"); // duo -> prefer combined; single -> prefer non-combined var ordered = sheets.OrderBy(s => { bool combined = Path.GetFileName(s).Contains("combined", StringComparison.OrdinalIgnoreCase); return duo ? (combined ? 0 : 1) : (combined ? 1 : 0); }).ThenBy(s => s); return ordered.First(); } /// Scan every PNG under the character's Images/ folders. public static List GetParts(string charFolder) { var result = new List(); if (!Directory.Exists(charFolder)) return result; var pngs = Directory.GetFiles(charFolder, "*.png", SearchOption.AllDirectories) .Where(p => p.Replace('\\', '/').Contains("/Images/")) .OrderBy(p => p); foreach (var p in pngs) { var fn = Path.GetFileName(p); var stem = Path.GetFileNameWithoutExtension(fn); var item = new PartItem { FileName = fn, FilePath = p, Display = stem }; var low = stem.ToLowerInvariant(); if (low.Contains("_hairmask_")) { item.Shape = AfterToken(stem, "hairmask"); } else if (low.Contains("_head_")) { var parts = stem.Split('_'); int hi = Array.FindIndex(parts, t => t.Equals("head", StringComparison.OrdinalIgnoreCase)); if (hi >= 0 && hi + 1 < parts.Length) { item.Shape = parts[hi + 1]; item.Expr = (hi + 2 < parts.Length) ? string.Join("_", parts.Skip(hi + 2)) : "(base)"; } } result.Add(item); } return result; } private static string AfterToken(string stem, string token) { var parts = stem.Split('_'); int i = Array.FindIndex(parts, t => t.Equals(token, StringComparison.OrdinalIgnoreCase)); return (i >= 0 && i + 1 < parts.Length) ? string.Join("_", parts.Skip(i + 1)) : ""; } public static BitmapImage? LoadBitmap(string? path) { if (string.IsNullOrEmpty(path) || !File.Exists(path)) return null; try { var b = new BitmapImage(); b.BeginInit(); b.CacheOption = BitmapCacheOption.OnLoad; b.CreateOptions = BitmapCreateOptions.IgnoreImageCache; b.UriSource = new Uri(path); b.EndInit(); b.Freeze(); return b; } catch { return null; } } private static readonly Dictionary _partCache = new(StringComparer.OrdinalIgnoreCase); /// Load a part PNG with its baked-in background keyed out to transparency (cached). public static BitmapSource? LoadPart(string? path) { if (string.IsNullOrEmpty(path)) return null; if (_partCache.TryGetValue(path, out var cached)) return cached; var raw = LoadBitmap(path); BitmapSource? keyed = raw; bool isHead = Path.GetFileName(path).ToLowerInvariant().Contains("_head_"); try { if (raw != null) keyed = BgKey.KeyOut(raw, isHead); } catch { keyed = raw; } _partCache[path] = keyed; return keyed; } }