From 0f55599ba5ed0115027082cb77d5a3cb7a402db4 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 15 Jun 2026 11:05:18 +0200 Subject: [PATCH] feat(D.2b): draw the window resize-grip overlay (gold ridges + corner studs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The retail vitals window border is TWO layers, not one: the bevel chrome (0x060074BF-C6) PLUS a resize-grip overlay on top — gold ridged edge strips and a square corner stud at each corner. acdream only drew the bevel, so the border looked plainer than retail and the corners lacked the little square sprite the user spotted. The overlay ids come from the vitals LayoutDesc 0x2100006C (elements 0x1000063B-0x10000642): corner stud 0x06006129 (same 5x5 at all four corners), edge strips 0x0600612A/2C (top/bottom) and 0x0600612B/2D (left/right). They have transparent gaps so the bevel shows through — both layers are drawn. UiNineSlicePanel now draws the grip overlay (edges tiled via the existing UV-repeat, corner studs 1:1) after the bevel, so every retail-chrome window (vitals + chat) gets it. Verified the grip sprites + the composited result headlessly: dump-sprite-sheet (new CLI: composite arbitrary sprite ids magnified) showed 0x06006129 is a gold stud and 0x0600612A-D are gold ridged strips; render-vitals-mockup now renders the faithful default window with the overlay. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/AcDream.App/UI/RetailChromeSprites.cs | 18 ++++++ src/AcDream.App/UI/UiNineSlicePanel.cs | 13 ++++ src/AcDream.Cli/Program.cs | 13 ++++ src/AcDream.Cli/VitalsMockup.cs | 74 +++++++++++++++++++---- 4 files changed, 105 insertions(+), 13 deletions(-) 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; }