diff --git a/src/AcDream.App/UI/RetailChromeSprites.cs b/src/AcDream.App/UI/RetailChromeSprites.cs index 70a8cb4e..f2a80fd7 100644 --- a/src/AcDream.App/UI/RetailChromeSprites.cs +++ b/src/AcDream.App/UI/RetailChromeSprites.cs @@ -45,4 +45,22 @@ public static class RetailChromeSprites /// Border thickness in pixels = the corner/edge sprite size (5px). public const int Border = 5; + + // ── Resize-grip overlay ────────────────────────────────────────────── + // A second 8-piece layer drawn ON TOP of the bevel above: the gold ridged + // accents + square corner studs that frame a resizable retail window. From + // the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each + // corner is the same 5×5 stud (0x06006129); the edges are gold double-line + // strips tiled along each side. These have transparent gaps, so the bevel + // shows through — both layers are needed. + /// Corner grip stud, all four corners (5×5). + public const uint GripCorner = 0x06006129; + /// Top edge grip (10×5, tiled across). + public const uint GripTop = 0x0600612A; + /// Left edge grip (5×10, tiled down). + public const uint GripLeft = 0x0600612B; + /// Bottom edge grip (10×5). + public const uint GripBottom = 0x0600612C; + /// Right edge grip (5×10). + public const uint GripRight = 0x0600612D; } diff --git a/src/AcDream.App/UI/UiNineSlicePanel.cs b/src/AcDream.App/UI/UiNineSlicePanel.cs index 2e4465a1..9c18f095 100644 --- a/src/AcDream.App/UI/UiNineSlicePanel.cs +++ b/src/AcDream.App/UI/UiNineSlicePanel.cs @@ -72,6 +72,19 @@ public sealed class UiNineSlicePanel : UiPanel DrawStretched(ctx, RetailChromeSprites.CornerTR, r.TR); DrawStretched(ctx, RetailChromeSprites.CornerBL, r.BL); DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR); + + // Resize-grip overlay (gold ridged edges + square corner studs) drawn on + // top of the bevel — the second border layer the vitals LayoutDesc carries + // (0x1000063B–0x10000642). Edges tile; the corner stud is the same sprite + // at all four corners. + DrawTiled(ctx, RetailChromeSprites.GripTop, r.Top); + DrawTiled(ctx, RetailChromeSprites.GripBottom, r.Bottom); + DrawTiled(ctx, RetailChromeSprites.GripLeft, r.Left); + DrawTiled(ctx, RetailChromeSprites.GripRight, r.Right); + DrawStretched(ctx, RetailChromeSprites.GripCorner, r.TL); + DrawStretched(ctx, RetailChromeSprites.GripCorner, r.TR); + DrawStretched(ctx, RetailChromeSprites.GripCorner, r.BL); + DrawStretched(ctx, RetailChromeSprites.GripCorner, r.BR); } private void DrawTiled(UiRenderContext ctx, uint id, Rect d) diff --git a/src/AcDream.Cli/Program.cs b/src/AcDream.Cli/Program.cs index 0fdad988..6be503c4 100644 --- a/src/AcDream.Cli/Program.cs +++ b/src/AcDream.Cli/Program.cs @@ -43,6 +43,19 @@ if (args.Length >= 1 && args[0] == "render-vitals-mockup") return VitalsMockup.Render(rvmDatDir, rvmOut); } +if (args.Length >= 1 && args[0] == "dump-sprite-sheet") +{ + string? dssDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); + string? dssIds = args.ElementAtOrDefault(2); + string dssOut = args.ElementAtOrDefault(3) ?? "sprite-sheet.png"; + if (string.IsNullOrWhiteSpace(dssDir) || string.IsNullOrWhiteSpace(dssIds)) + { + Console.Error.WriteLine("usage: AcDream.Cli dump-sprite-sheet <0xId,0xId,...> [out.png]"); + return 2; + } + return VitalsMockup.ExportSheet(dssDir, dssIds, dssOut); +} + if (args.Length >= 1 && args[0] == "export-ui-sprite") { string? eusDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR"); diff --git a/src/AcDream.Cli/VitalsMockup.cs b/src/AcDream.Cli/VitalsMockup.cs index 90c222f4..445a918b 100644 --- a/src/AcDream.Cli/VitalsMockup.cs +++ b/src/AcDream.Cli/VitalsMockup.cs @@ -29,14 +29,14 @@ public static class VitalsMockup private static readonly Vital[] Vitals = { - new("health", 1.00f, 0x0600747E, 0x0600747F, 0x06007480, 0x06007481, 0x06007482, 0x06007483), - new("stamina", 1.00f, 0x06007484, 0x06007485, 0x06007486, 0x06007487, 0x06007488, 0x06007489), - new("mana", 1.00f, 0x0600748A, 0x0600748B, 0x0600748C, 0x0600748D, 0x0600748E, 0x0600748F), + new("health", 0.80f, 0x0600747E, 0x0600747F, 0x06007480, 0x06007481, 0x06007482, 0x06007483), + new("stamina", 0.50f, 0x06007484, 0x06007485, 0x06007486, 0x06007487, 0x06007488, 0x06007489), + new("mana", 0.65f, 0x0600748A, 0x0600748B, 0x0600748C, 0x0600748D, 0x0600748E, 0x0600748F), }; - private const int Border = 5, BarH = 16, Zoom = 4; - // Widened bars so stretch-vs-tile is obvious (native middle tile ~100px). - private const int BarW = 280; + private const uint CenterFill = 0x06004CC2; // dark interior panel (UiNineSlicePanel draws this) + private const int Border = 5, BarH = 16, Zoom = 6; + private const int BarW = 150; // default vitals window bar width (0x2100006C) private static readonly int[] BarLocalY = { 0, 16, 32 }; // flush stacked inside the interior public static int Render(string datDir, string outPath) @@ -44,23 +44,25 @@ public static class VitalsMockup if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; } using var dats = new DatCollection(datDir, DatAccessType.Read); - int winW = BarW + 2 * Border; // 290 + int winW = BarW + 2 * Border; // 160 int winH = 3 * BarH + 2 * Border; // 58 - int gap = 16; - using var canvas = new Image(winW, winH * 2 + gap, new Rgba32(20, 20, 24, 255)); + using var canvas = new Image(winW, winH, new Rgba32(20, 20, 24, 255)); - DrawWindow(canvas, dats, 0, winW, winH, tileMid: false); // top: STRETCH (current) - DrawWindow(canvas, dats, winH + gap, winW, winH, tileMid: true); // bottom: TILE (retail) + DrawWindow(canvas, dats, 0, winW, winH, tileMid: true); canvas.Mutate(c => c.Resize(canvas.Width * Zoom, canvas.Height * Zoom, KnownResamplers.NearestNeighbor)); canvas.SaveAsPng(outPath); - Console.WriteLine($"wrote {outPath} ({canvas.Width}x{canvas.Height}) — TOP=stretch(current) BOTTOM=tile(retail), widened {BarW}px bars"); + Console.WriteLine($"wrote {outPath} ({canvas.Width}x{canvas.Height}) — faithful default vitals window 0x2100006C"); return 0; } private static void DrawWindow(Image canvas, DatCollection dats, int offY, int winW, int winH, bool tileMid) { - // 8-piece chrome border (kept identical in both rows; only the bar fill varies). + // Dark interior fill (matches UiNineSlicePanel's CenterFill behind the bars). + using (var cf = Load(dats, CenterFill)) + Blit(canvas, cf, Border, offY + Border, winW - 2 * Border, winH - 2 * Border); + + // 8-piece chrome border (corners native 5x5, edges stretched for this preview). using (var tl = Load(dats, TL)) using (var top = Load(dats, TOP)) using (var tr = Load(dats, TR)) using (var le = Load(dats, LEFT)) using (var ri = Load(dats, RIGHT)) using (var bl = Load(dats, BL)) using (var bo = Load(dats, BOT)) using (var br = Load(dats, BR)) @@ -75,6 +77,23 @@ public static class VitalsMockup Blit(canvas, br, winW - Border, offY + winH - Border, Border, Border); } + // Resize-grip overlay: gold ridged edge strips + square corner studs, on + // top of the bevel (vitals LayoutDesc 0x1000063B–0x10000642). Edges shown + // stretched here for the preview; the client tiles them via UV-repeat. + using (var gc = Load(dats, 0x06006129)) + using (var gt = Load(dats, 0x0600612A)) using (var gb = Load(dats, 0x0600612C)) + using (var gl = Load(dats, 0x0600612B)) using (var gr = Load(dats, 0x0600612D)) + { + Blit(canvas, gt, Border, offY, winW - 2 * Border, Border); + Blit(canvas, gb, Border, offY + winH - Border, winW - 2 * Border, Border); + Blit(canvas, gl, 0, offY + Border, Border, winH - 2 * Border); + Blit(canvas, gr, winW - Border, offY + Border, Border, winH - 2 * Border); + Blit(canvas, gc, 0, offY, Border, Border); + Blit(canvas, gc, winW - Border, offY, Border, Border); + Blit(canvas, gc, 0, offY + winH - Border, Border, Border); + Blit(canvas, gc, winW - Border, offY + winH - Border, Border, Border); + } + for (int i = 0; i < Vitals.Length; i++) { var v = Vitals[i]; @@ -144,6 +163,35 @@ public static class VitalsMockup canvas.Mutate(c => c.DrawImage(s, new Point(x, y), 1f)); } + /// Composite a comma-separated list of sprite ids into one row, magnified, + /// on a neutral background — so the exact chrome/bar graphics can be eyeballed. + public static int ExportSheet(string datDir, string idsCsv, string outPath) + { + if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; } + using var dats = new DatCollection(datDir, DatAccessType.Read); + + var ids = idsCsv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(ParseHex).Where(x => x != 0).ToArray(); + if (ids.Length == 0) { Console.Error.WriteLine("no valid ids"); return 2; } + + var imgs = ids.Select(id => Load(dats, id)).ToArray(); + const int pad = 6, zoom = 10; + int totalW = pad + imgs.Sum(i => i.Width + pad); + int maxH = imgs.Max(i => i.Height); + using var canvas = new Image(totalW, maxH + 2 * pad, new Rgba32(64, 64, 72, 255)); + int x = pad; + foreach (var im in imgs) + { + canvas.Mutate(c => c.DrawImage(im, new Point(x, pad), 1f)); + x += im.Width + pad; + } + canvas.Mutate(c => c.Resize(canvas.Width * zoom, canvas.Height * zoom, KnownResamplers.NearestNeighbor)); + canvas.SaveAsPng(outPath); + Console.WriteLine("order (L→R): " + string.Join(" ", ids.Zip(imgs, (id, im) => $"0x{id:X8}={im.Width}x{im.Height}"))); + foreach (var im in imgs) im.Dispose(); + return 0; + } + public static int ExportSprite(string datDir, string idText, string outPath) { if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }