using AcDream.Core.Textures; using DatReaderWriter; using DatReaderWriter.DBObjs; using DatReaderWriter.Options; using SixLabors.ImageSharp; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; namespace AcDream.Cli; /// /// Headless PNG preview of the retail vital bars. Loads the real RenderSurface /// sprites from the dats and composites them with the SAME horizontal 3-slice /// logic the in-client UiMeter.DrawHBar uses (fixed-width bevelled caps + /// a stretched gradient middle; the empty "back" track full width, the coloured /// "front" fill grown from the left to the value). This lets the bar assembly be /// verified by eye without launching the client + connecting to the server. /// Bar sprite ids come from the vitals LayoutDesc (0x21000014) via dump-vitals-bars. /// public static class VitalsMockup { private readonly record struct Vital( string Name, uint BackL, uint BackT, uint BackR, uint FrontL, uint FrontT, uint FrontR); private static readonly Vital[] Vitals = { new("health", 0x06001141, 0x06001140, 0x0600113F, 0x06001131, 0x06001132, 0x06001133), new("stamina", 0x06001147, 0x06001146, 0x06001145, 0x06001137, 0x06001138, 0x06001139), new("mana", 0x06001144, 0x06001143, 0x06001142, 0x06001134, 0x06001135, 0x06001136), }; private static readonly float[] Fills = { 1.0f, 0.6f, 0.25f }; private const int BarW = 200, BarH = 14, PadX = 10, PadY = 10, GapY = 10, ColGap = 16, Zoom = 3; public static int Render(string datDir, string outPath) { if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: directory not found: {datDir}"); return 2; } using var dats = new DatCollection(datDir, DatAccessType.Read); int cols = Fills.Length; int canvasW = PadX * 2 + cols * BarW + (cols - 1) * ColGap; int canvasH = PadY * 2 + Vitals.Length * BarH + (Vitals.Length - 1) * GapY; // Retail vitals window backdrop is a dark translucent panel; pick a neutral // dark gray so the bevels + gradient read clearly. using var canvas = new Image(canvasW, canvasH, new Rgba32(38, 38, 44, 255)); for (int vi = 0; vi < Vitals.Length; vi++) { var v = Vitals[vi]; using var bl = Load(dats, v.BackL); using var bt = Load(dats, v.BackT); using var br = Load(dats, v.BackR); using var fl = Load(dats, v.FrontL); using var ft = Load(dats, v.FrontT); using var fr = Load(dats, v.FrontR); Console.WriteLine($"{v.Name,-8} back[{bl.Width}x{bl.Height} {bt.Width}x{bt.Height} {br.Width}x{br.Height}] " + $"front[{fl.Width}x{fl.Height} {ft.Width}x{ft.Height} {fr.Width}x{fr.Height}]"); int y = PadY + vi * (BarH + GapY); for (int ci = 0; ci < Fills.Length; ci++) { int x = PadX + ci * (BarW + ColGap); DrawHBar(canvas, bl, bt, br, x, y, BarW, BarH, withRightCap: true); int fw = (int)(BarW * Fills[ci]); if (fw > 0) DrawHBar(canvas, fl, ft, fr, x, y, fw, BarH, withRightCap: false); } } canvas.Mutate(c => c.Resize(canvasW * Zoom, canvasH * Zoom, KnownResamplers.NearestNeighbor)); canvas.SaveAsPng(outPath); Console.WriteLine($"wrote {outPath} ({canvasW * Zoom}x{canvasH * Zoom}; rows=vitals, cols=100%/60%/25%)"); return 0; } public static int ExportSprite(string datDir, string idText, string outPath) { if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: directory not found: {datDir}"); return 2; } uint id = ParseHex(idText); if (id == 0) { Console.Error.WriteLine($"error: bad id '{idText}'"); return 2; } using var dats = new DatCollection(datDir, DatAccessType.Read); using var img = Load(dats, id); img.SaveAsPng(outPath); Console.WriteLine($"wrote {outPath} (0x{id:X8} {img.Width}x{img.Height})"); return 0; } /// Replicates UiMeter.DrawHBar: native-width left-cap, stretched middle, /// optional native-width right-cap; caps clamped so a narrow bar never overdraws. private static void DrawHBar( Image canvas, Image left, Image tile, Image right, int x, int y, int w, int h, bool withRightCap) { if (w <= 0) return; int rcap = withRightCap ? Math.Min(right.Width, w) : 0; int lcap = Math.Min(left.Width, w - rcap); if (lcap > 0) Blit(canvas, left, x, y, lcap, h); int midX = x + lcap, midW = w - lcap - rcap; if (midW > 0) Blit(canvas, tile, midX, y, midW, h); if (rcap > 0) Blit(canvas, right, x + w - rcap, y, rcap, h); } private static void Blit(Image canvas, Image src, int x, int y, int dw, int dh) { if (dw <= 0 || dh <= 0) return; using var s = src.Clone(c => c.Resize(dw, dh)); canvas.Mutate(c => c.DrawImage(s, new Point(x, y), 1f)); } private static Image Load(DatCollection dats, uint id) { var rs = dats.Get(id); if (rs is null) { Console.Error.WriteLine($" missing RenderSurface 0x{id:X8}"); return new Image(1, 1); } var dt = SurfaceDecoder.DecodeRenderSurface(rs); return Image.LoadPixelData(dt.Rgba8, dt.Width, dt.Height); } private static uint ParseHex(string s) { s = s.Trim(); if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) s = s[2..]; return uint.TryParse(s, System.Globalization.NumberStyles.HexNumber, System.Globalization.CultureInfo.InvariantCulture, out var v) ? v : 0u; } }