fix(D.2b): vitals from the real stacked-window LayoutDesc (0x2100006C)
The vitals bars were rendered from the WRONG layout. The ids in vitals.xml (0x0600113x) belong to LayoutDesc 0x21000014 -- the 800x28 floaty side-vitals ROW. The stacked vitals window the user sees is LayoutDesc 0x2100006C (160x58), which uses a different sprite set and geometry. Dumped the real tree (new dump-vitals-layout CLI, reflective) and ported it: - Sprites (#2): the stacked-window set 0x0600747E-0x0600748F (health/stamina/ mana, each back+front 3-slice; caps 10px, mid 130px). - Right cap (#1) + fill model: retail UIElement_Meter::DrawChildren draws the back 3-slice full then the front 3-slice CLIPPED to the fill fraction (its own right-cap shows at 100%, the back's shows through when partial). UiMeter now clips the front per-slice (UV-crop) instead of growing a capless slice. - Spacing (#5): three flush 150x16 bars at y=5/21/37 in a 160x58 window (16px pitch, zero gap), per the dat rects -- not the old 20px-apart guess. - Border (#3): the window is the 8-piece chrome frame (corners 0x060074C3-C6, edges 0x060074BF-C2, 5px) -- dat-confirmed identical to RetailChromeSprites. The headless render-vitals-mockup now composites this exact window (0x2100006C) from the real sprites with the same clipped-fill model, so the look was verified before launch. Font (#4, dat Font 0x40000000) is the next commit. Decomp refs: gmVitalsUI::PostInit @0x4bfce0; UIElement_Meter::DrawChildren @0x46fbd0 (scissor-fill); geometry from LayoutDesc 0x2100006C. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ada863980c
commit
ff29787f12
5 changed files with 293 additions and 89 deletions
|
|
@ -66,11 +66,14 @@ public sealed class UiMeter : UiElement
|
||||||
|
|
||||||
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
if (SpriteResolve is { } resolve && (BackLeft != 0 || BackTile != 0 || FrontTile != 0))
|
||||||
{
|
{
|
||||||
// Empty track: full-width 3-slice (left-cap + stretched gradient + right-cap).
|
// Retail meter (UIElement_Meter::DrawChildren): the BACK 3-slice is the
|
||||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, 0, 0, Width, Height, withRightCap: true);
|
// empty track, drawn full width; the FRONT 3-slice is the coloured fill,
|
||||||
// Coloured fill: grows from the left to the value, no right-cap of its own.
|
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
|
||||||
|
// The front carries its own right-cap (shown at 100%); clipping below 100%
|
||||||
|
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
||||||
|
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
||||||
if (pct is not null && p > 0f)
|
if (pct is not null && p > 0f)
|
||||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, 0, 0, Width * p, Height, withRightCap: false);
|
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -94,33 +97,43 @@ public sealed class UiMeter : UiElement
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Draws a horizontal 3-slice into <paramref name="w"/> x <paramref name="h"/> at
|
/// Draws the full-width horizontal 3-slice (native-width left-cap, stretched
|
||||||
/// (<paramref name="x"/>,<paramref name="y"/>): a native-width left-cap, a stretched
|
/// middle, native-width right-cap) over this meter's rect, horizontally CLIPPED
|
||||||
/// middle, and (when <paramref name="withRightCap"/>) a native-width right-cap. Caps
|
/// so nothing past <paramref name="clipW"/> (local px from the left) is drawn.
|
||||||
/// are clamped so a narrow bar never overdraws. A 0 id skips that slice.
|
/// The back track passes <c>clipW = Width</c>; the front fill passes
|
||||||
|
/// <c>clipW = Width * fraction</c>. Clipping UV-crops each slice proportionally,
|
||||||
|
/// so the fill ends cleanly and the back's right-cap shows through when partial.
|
||||||
|
/// A 0 id skips that slice.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static void DrawHBar(
|
private void DrawHBar(
|
||||||
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||||
uint leftId, uint tileId, uint rightId,
|
uint leftId, uint midId, uint rightId, float clipW)
|
||||||
float x, float y, float w, float h, bool withRightCap)
|
|
||||||
{
|
{
|
||||||
if (w <= 0f) return;
|
if (clipW <= 0f) return;
|
||||||
|
float w = Width, h = Height;
|
||||||
var (lt, lw, _) = resolve(leftId);
|
var (lt, lw, _) = resolve(leftId);
|
||||||
var (tt, _, _) = resolve(tileId);
|
var (mt, _, _) = resolve(midId);
|
||||||
var (rt, rw, _) = resolve(rightId);
|
var (rt, rw, _) = resolve(rightId);
|
||||||
|
|
||||||
float rcap = withRightCap && rt != 0 ? MathF.Min(rw, w) : 0f;
|
float capL = lt != 0 ? MathF.Min(lw, w) : 0f;
|
||||||
float lcap = lt != 0 ? MathF.Min(lw, w - rcap) : 0f;
|
float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f;
|
||||||
|
float midW = w - capL - capR;
|
||||||
|
|
||||||
if (lt != 0 && lcap > 0f)
|
DrawPiece(ctx, lt, 0f, capL, h, clipW);
|
||||||
ctx.DrawSprite(lt, x, y, lcap, h, 0, 0, 1, 1, Vector4.One);
|
DrawPiece(ctx, mt, capL, midW, h, clipW);
|
||||||
|
DrawPiece(ctx, rt, w - capR, capR, h, clipW);
|
||||||
|
}
|
||||||
|
|
||||||
float midX = x + lcap;
|
/// <summary>Draw one slice spanning local [<paramref name="pieceX"/>,
|
||||||
float midW = w - lcap - rcap;
|
/// pieceX+<paramref name="pieceW"/>], UV-cropped so nothing past
|
||||||
if (tt != 0 && midW > 0f)
|
/// <paramref name="clipW"/> shows.</summary>
|
||||||
ctx.DrawSprite(tt, midX, y, midW, h, 0, 0, 1, 1, Vector4.One);
|
private static void DrawPiece(
|
||||||
|
UiRenderContext ctx, uint tex, float pieceX, float pieceW, float h, float clipW)
|
||||||
if (rcap > 0f)
|
{
|
||||||
ctx.DrawSprite(rt, x + w - rcap, y, rcap, h, 0, 0, 1, 1, Vector4.One);
|
if (tex == 0 || pieceW <= 0f) return;
|
||||||
|
float visibleW = MathF.Min(pieceW, clipW - pieceX);
|
||||||
|
if (visibleW <= 0f) return;
|
||||||
|
float u1 = visibleW / pieceW; // crop the texture horizontally
|
||||||
|
ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,13 @@
|
||||||
<panel id="acdream.vitals" x="10" y="30" w="220" h="96" resize="x">
|
<!-- Retail stacked vitals window, geometry + sprite ids dat-verified from
|
||||||
<meter id="health" x="8" y="24" w="200" h="14" fill="{HealthPercent}" cur="{HealthCurrent}" max="{HealthMax}" color="#FFC70D0D" anchor="left,top,right"
|
LayoutDesc 0x2100006C (160x58, 5px chrome border, three flush 150x16 bars
|
||||||
backleft="0x06001141" backtile="0x06001140" backright="0x0600113F" frontleft="0x06001131" fronttile="0x06001132" frontright="0x06001133"/>
|
at y=5/21/37). Each bar: back 3-slice (empty track) + front 3-slice (fill,
|
||||||
<meter id="stamina" x="8" y="44" w="200" h="14" fill="{StaminaPercent}" cur="{StaminaCurrent}" max="{StaminaMax}" color="#FFD49E1F" anchor="left,top,right"
|
clipped to the fraction). Sprite ids are the STACKED-window set
|
||||||
backleft="0x06001147" backtile="0x06001146" backright="0x06001145" frontleft="0x06001137" fronttile="0x06001138" frontright="0x06001139"/>
|
(0x0600747E-0x0600748F), NOT the floaty-row set. -->
|
||||||
<meter id="mana" x="8" y="64" w="200" h="14" fill="{ManaPercent}" cur="{ManaCurrent}" max="{ManaMax}" color="#FF1F33D9" anchor="left,top,right"
|
<panel id="acdream.vitals" x="10" y="30" w="160" h="58" resize="x">
|
||||||
backleft="0x06001144" backtile="0x06001143" backright="0x06001142" frontleft="0x06001134" fronttile="0x06001135" frontright="0x06001136"/>
|
<meter id="health" x="5" y="5" w="150" h="16" fill="{HealthPercent}" cur="{HealthCurrent}" max="{HealthMax}" color="#FFC70D0D" anchor="left,top,right"
|
||||||
|
backleft="0x0600747E" backtile="0x0600747F" backright="0x06007480" frontleft="0x06007481" fronttile="0x06007482" frontright="0x06007483"/>
|
||||||
|
<meter id="stamina" x="5" y="21" w="150" h="16" fill="{StaminaPercent}" cur="{StaminaCurrent}" max="{StaminaMax}" color="#FFD49E1F" anchor="left,top,right"
|
||||||
|
backleft="0x06007484" backtile="0x06007485" backright="0x06007486" frontleft="0x06007487" fronttile="0x06007488" frontright="0x06007489"/>
|
||||||
|
<meter id="mana" x="5" y="37" w="150" h="16" fill="{ManaPercent}" cur="{ManaCurrent}" max="{ManaMax}" color="#FF1F33D9" anchor="left,top,right"
|
||||||
|
backleft="0x0600748A" backtile="0x0600748B" backright="0x0600748C" frontleft="0x0600748D" fronttile="0x0600748E" frontright="0x0600748F"/>
|
||||||
</panel>
|
</panel>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,18 @@ if (args.Length >= 1 && args[0] == "dump-vitals-bars")
|
||||||
return DumpVitalsBars(dvbDatDir);
|
return DumpVitalsBars(dvbDatDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (args.Length >= 1 && args[0] == "dump-vitals-layout")
|
||||||
|
{
|
||||||
|
string? dvlDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||||
|
string? dvlLayout = args.ElementAtOrDefault(2);
|
||||||
|
if (string.IsNullOrWhiteSpace(dvlDatDir))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("usage: AcDream.Cli dump-vitals-layout <dat-directory> [0xLayoutId]");
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return VitalsLayoutDump.Run(dvlDatDir, dvlLayout);
|
||||||
|
}
|
||||||
|
|
||||||
if (args.Length >= 1 && args[0] == "render-vitals-mockup")
|
if (args.Length >= 1 && args[0] == "render-vitals-mockup")
|
||||||
{
|
{
|
||||||
string? rvmDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
string? rvmDatDir = args.ElementAtOrDefault(1) ?? Env.GetEnvironmentVariable("ACDREAM_DAT_DIR");
|
||||||
|
|
|
||||||
152
src/AcDream.Cli/VitalsLayoutDump.cs
Normal file
152
src/AcDream.Cli/VitalsLayoutDump.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
using DatReaderWriter;
|
||||||
|
using DatReaderWriter.DBObjs;
|
||||||
|
using DatReaderWriter.Options;
|
||||||
|
using DatReaderWriter.Types;
|
||||||
|
|
||||||
|
namespace AcDream.Cli;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Full reflective dump of a vitals LayoutDesc element tree: every scalar
|
||||||
|
/// property (position/size/flags) of each ElementDesc + its state sprites,
|
||||||
|
/// so the real bar rects + spacing + window size can be read from the dat
|
||||||
|
/// instead of guessed. Uses reflection so it doesn't depend on knowing the
|
||||||
|
/// DatReaderWriter property names ahead of time.
|
||||||
|
/// </summary>
|
||||||
|
public static class VitalsLayoutDump
|
||||||
|
{
|
||||||
|
public static int Run(string datDir, string? layoutIdText)
|
||||||
|
{
|
||||||
|
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }
|
||||||
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
|
// Default to the vitals layout dump-vitals-bars found; allow override.
|
||||||
|
uint layoutId = 0x21000014u;
|
||||||
|
if (!string.IsNullOrWhiteSpace(layoutIdText))
|
||||||
|
{
|
||||||
|
var t = layoutIdText.Trim();
|
||||||
|
if (t.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) t = t[2..];
|
||||||
|
uint.TryParse(t, System.Globalization.NumberStyles.HexNumber, null, out layoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// First: scan ALL LayoutDescs that contain a vitals meter element, with root size,
|
||||||
|
// so we can tell whether 0x21000014 is the one the user sees (row vs stacked).
|
||||||
|
Console.WriteLine("=== LayoutDescs containing a vitals meter element (0x100000E6/EC/EE) ===");
|
||||||
|
foreach (var id in dats.GetAllIdsOfType<LayoutDesc>())
|
||||||
|
{
|
||||||
|
var l = dats.Get<LayoutDesc>(id);
|
||||||
|
if (l is null) continue;
|
||||||
|
if (!ContainsAny(l, 0x100000E6u, 0x100000ECu, 0x100000EEu)) continue;
|
||||||
|
Console.WriteLine($" 0x{id:X8} {RootSizeSummary(l)}");
|
||||||
|
}
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
var ld = dats.Get<LayoutDesc>(layoutId);
|
||||||
|
if (ld is null) { Console.Error.WriteLine($"layout 0x{layoutId:X8} not found"); return 1; }
|
||||||
|
|
||||||
|
Console.WriteLine($"=== FULL DUMP layout 0x{layoutId:X8} ===");
|
||||||
|
DumpScalars("LayoutDesc", ld, 0);
|
||||||
|
foreach (var kv in ld.Elements)
|
||||||
|
DumpElement(kv.Value, 1);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ContainsAny(LayoutDesc l, params uint[] ids)
|
||||||
|
{
|
||||||
|
foreach (var kv in l.Elements)
|
||||||
|
if (ElemContains(kv.Value, ids)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ElemContains(ElementDesc e, uint[] ids)
|
||||||
|
{
|
||||||
|
if (Array.IndexOf(ids, e.ElementId) >= 0) return true;
|
||||||
|
foreach (var kv in e.Children)
|
||||||
|
if (ElemContains(kv.Value, ids)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string RootSizeSummary(LayoutDesc l)
|
||||||
|
{
|
||||||
|
// Print any LayoutDesc-level scalar that looks like a size.
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (var p in l.GetType().GetProperties())
|
||||||
|
{
|
||||||
|
if (p.GetIndexParameters().Length > 0) continue;
|
||||||
|
if (p.Name is "Elements") continue;
|
||||||
|
object? v; try { v = p.GetValue(l); } catch { continue; }
|
||||||
|
if (v is null) continue;
|
||||||
|
if (IsScalar(v)) sb.Append($"{p.Name}={v} ");
|
||||||
|
}
|
||||||
|
return sb.ToString().Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpElement(ElementDesc e, int depth)
|
||||||
|
{
|
||||||
|
string ind = new string(' ', depth * 2);
|
||||||
|
Console.WriteLine($"{ind}element 0x{e.ElementId:X8}");
|
||||||
|
DumpScalars(ind + " ", e, depth);
|
||||||
|
|
||||||
|
if (e.StateDesc is not null) DumpMedia(ind + " [DirectState]", e.StateDesc);
|
||||||
|
foreach (var s in e.States)
|
||||||
|
DumpMedia($"{ind} [state {s.Key}]", s.Value);
|
||||||
|
|
||||||
|
foreach (var c in e.Children)
|
||||||
|
DumpElement(c.Value, depth + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly HashSet<string> Skip = new() { "Children", "States", "StateDesc", "Elements", "Media" };
|
||||||
|
|
||||||
|
private static void DumpScalars(string label, object o, int depth)
|
||||||
|
{
|
||||||
|
foreach (var (name, val) in Members(o))
|
||||||
|
{
|
||||||
|
if (Skip.Contains(name)) continue;
|
||||||
|
if (IsScalar(val))
|
||||||
|
Console.WriteLine($"{label} {name} = {Fmt(name, val)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DumpMedia(string label, StateDesc sd)
|
||||||
|
{
|
||||||
|
foreach (var m in sd.Media)
|
||||||
|
{
|
||||||
|
var sb = new System.Text.StringBuilder();
|
||||||
|
foreach (var (name, val) in Members(m))
|
||||||
|
if (IsScalar(val)) sb.Append($"{name}={Fmt(name, val)} ");
|
||||||
|
Console.WriteLine($"{label} {m.GetType().Name}: {sb.ToString().Trim()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enumerate public properties AND public fields (the DatReaderWriter
|
||||||
|
/// generated types expose geometry/file ids as fields, not properties).</summary>
|
||||||
|
private static IEnumerable<(string name, object val)> Members(object o)
|
||||||
|
{
|
||||||
|
var t = o.GetType();
|
||||||
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
if (p.GetIndexParameters().Length > 0) continue;
|
||||||
|
object? v; try { v = p.GetValue(o); } catch { continue; }
|
||||||
|
if (v is not null) yield return (p.Name, v);
|
||||||
|
}
|
||||||
|
foreach (var f in t.GetFields(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
object? v; try { v = f.GetValue(o); } catch { continue; }
|
||||||
|
if (v is not null) yield return (f.Name, v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Fmt(string name, object v) =>
|
||||||
|
name.Contains("File", StringComparison.OrdinalIgnoreCase) && v is uint u ? $"0x{u:X8}" : v.ToString() ?? "";
|
||||||
|
|
||||||
|
private static bool IsScalar(object v)
|
||||||
|
{
|
||||||
|
var t = v.GetType();
|
||||||
|
if (v is string) return true;
|
||||||
|
if (t.IsPrimitive || t.IsEnum) return true;
|
||||||
|
if (v is IEnumerable) return false;
|
||||||
|
// value-type structs (Rectangle/Point/etc.) — print via ToString
|
||||||
|
return t.IsValueType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,76 +9,84 @@ using SixLabors.ImageSharp.Processing;
|
||||||
namespace AcDream.Cli;
|
namespace AcDream.Cli;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Headless PNG preview of the retail vital bars. Loads the real RenderSurface
|
/// Headless PNG preview of the retail STACKED vitals window (LayoutDesc
|
||||||
/// sprites from the dats and composites them with the SAME horizontal 3-slice
|
/// 0x2100006C, 160x58), composited with the SAME model the in-client UiMeter
|
||||||
/// logic the in-client <c>UiMeter.DrawHBar</c> uses (fixed-width bevelled caps +
|
/// uses: an 8-piece chrome border, then three flush-stacked 150x16 bars, each
|
||||||
/// a stretched gradient middle; the empty "back" track full width, the coloured
|
/// drawn as a BACK 3-slice (empty track, full width) + a FRONT 3-slice
|
||||||
/// "front" fill grown from the left to the value). This lets the bar assembly be
|
/// (coloured fill) horizontally CLIPPED to the fill fraction — so the front's
|
||||||
/// verified by eye without launching the client + connecting to the server.
|
/// own right-cap shows at full, and clipping reveals the back's right-cap when
|
||||||
/// Bar sprite ids come from the vitals LayoutDesc (0x21000014) via dump-vitals-bars.
|
/// partial (matching retail's scissor-fill). All ids are dat-verified from
|
||||||
|
/// 0x2100006C via dump-vitals-layout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class VitalsMockup
|
public static class VitalsMockup
|
||||||
{
|
{
|
||||||
private readonly record struct Vital(
|
// 8-piece chrome border (RetailChromeSprites; 5px), dat-verified in 0x2100006C.
|
||||||
string Name, uint BackL, uint BackT, uint BackR, uint FrontL, uint FrontT, uint FrontR);
|
private const uint TL = 0x060074C3, TOP = 0x060074BF, TR = 0x060074C4;
|
||||||
|
private const uint LEFT = 0x060074C0, RIGHT = 0x060074C2;
|
||||||
|
private const uint BL = 0x060074C5, BOT = 0x060074C1, BR = 0x060074C6;
|
||||||
|
|
||||||
|
private readonly record struct Vital(
|
||||||
|
string Name, float Frac,
|
||||||
|
uint BackL, uint BackM, uint BackR, uint FrontL, uint FrontM, uint FrontR);
|
||||||
|
|
||||||
|
// Stacked-window (0x2100006C) sprite ids — NOT the floaty-row 0x0600113x set.
|
||||||
private static readonly Vital[] Vitals =
|
private static readonly Vital[] Vitals =
|
||||||
{
|
{
|
||||||
new("health", 0x06001141, 0x06001140, 0x0600113F, 0x06001131, 0x06001132, 0x06001133),
|
new("health", 0.80f, 0x0600747E, 0x0600747F, 0x06007480, 0x06007481, 0x06007482, 0x06007483),
|
||||||
new("stamina", 0x06001147, 0x06001146, 0x06001145, 0x06001137, 0x06001138, 0x06001139),
|
new("stamina", 0.50f, 0x06007484, 0x06007485, 0x06007486, 0x06007487, 0x06007488, 0x06007489),
|
||||||
new("mana", 0x06001144, 0x06001143, 0x06001142, 0x06001134, 0x06001135, 0x06001136),
|
new("mana", 0.65f, 0x0600748A, 0x0600748B, 0x0600748C, 0x0600748D, 0x0600748E, 0x0600748F),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly float[] Fills = { 1.0f, 0.6f, 0.25f };
|
// Window geometry from 0x2100006C: 160x58, 5px border, bars at x=5 y=5/21/37, 150x16.
|
||||||
|
private const int WinW = 160, WinH = 58, Border = 5, BarX = 5, BarW = 150, BarH = 16;
|
||||||
private const int BarW = 200, BarH = 14, PadX = 10, PadY = 10, GapY = 10, ColGap = 16, Zoom = 3;
|
private static readonly int[] BarY = { 5, 21, 37 };
|
||||||
|
private const int Zoom = 5;
|
||||||
|
|
||||||
public static int Render(string datDir, string outPath)
|
public static int Render(string datDir, string outPath)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: directory not found: {datDir}"); return 2; }
|
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
||||||
int cols = Fills.Length;
|
using var canvas = new Image<Rgba32>(WinW, WinH, new Rgba32(0, 0, 0, 0));
|
||||||
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
|
// 8-piece chrome border.
|
||||||
// dark gray so the bevels + gradient read clearly.
|
using (var tl = Load(dats, TL)) using (var top = Load(dats, TOP)) using (var tr = Load(dats, TR))
|
||||||
using var canvas = new Image<Rgba32>(canvasW, canvasH, new Rgba32(38, 38, 44, 255));
|
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))
|
||||||
for (int vi = 0; vi < Vitals.Length; vi++)
|
|
||||||
{
|
{
|
||||||
var v = Vitals[vi];
|
Blit(canvas, tl, 0, 0, Border, Border);
|
||||||
using var bl = Load(dats, v.BackL);
|
Blit(canvas, top, Border, 0, WinW - 2 * Border, Border);
|
||||||
using var bt = Load(dats, v.BackT);
|
Blit(canvas, tr, WinW - Border, 0, Border, Border);
|
||||||
using var br = Load(dats, v.BackR);
|
Blit(canvas, le, 0, Border, Border, WinH - 2 * Border);
|
||||||
using var fl = Load(dats, v.FrontL);
|
Blit(canvas, ri, WinW - Border, Border, Border, WinH - 2 * Border);
|
||||||
using var ft = Load(dats, v.FrontT);
|
Blit(canvas, bl, 0, WinH - Border, Border, Border);
|
||||||
using var fr = Load(dats, v.FrontR);
|
Blit(canvas, bo, Border, WinH - Border, WinW - 2 * Border, Border);
|
||||||
|
Blit(canvas, br, WinW - Border, WinH - Border, Border, Border);
|
||||||
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));
|
for (int i = 0; i < Vitals.Length; i++)
|
||||||
|
{
|
||||||
|
var v = Vitals[i];
|
||||||
|
int y = BarY[i];
|
||||||
|
using var bl_ = Load(dats, v.BackL); using var bm = Load(dats, v.BackM); using var br_ = Load(dats, v.BackR);
|
||||||
|
using var fl = Load(dats, v.FrontL); using var fm = Load(dats, v.FrontM); using var fr = Load(dats, v.FrontR);
|
||||||
|
Console.WriteLine($"{v.Name,-8} back[{bl_.Width}x{bl_.Height} {bm.Width}x{bm.Height} {br_.Width}x{br_.Height}] " +
|
||||||
|
$"front[{fl.Width}x{fl.Height} {fm.Width}x{fm.Height} {fr.Width}x{fr.Height}] frac={v.Frac}");
|
||||||
|
// Back track: full width.
|
||||||
|
DrawHBar(canvas, bl_, bm, br_, BarX, y, BarW, BarH, clipW: BarW);
|
||||||
|
// Front fill: full 3-slice clipped to the fraction.
|
||||||
|
DrawHBar(canvas, fl, fm, fr, BarX, y, BarW, BarH, clipW: (int)MathF.Round(BarW * v.Frac));
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.Mutate(c => c.Resize(WinW * Zoom, WinH * Zoom, KnownResamplers.NearestNeighbor));
|
||||||
canvas.SaveAsPng(outPath);
|
canvas.SaveAsPng(outPath);
|
||||||
Console.WriteLine($"wrote {outPath} ({canvasW * Zoom}x{canvasH * Zoom}; rows=vitals, cols=100%/60%/25%)");
|
Console.WriteLine($"wrote {outPath} ({WinW * Zoom}x{WinH * Zoom}; stacked window 0x2100006C, fracs h/s/m={Vitals[0].Frac}/{Vitals[1].Frac}/{Vitals[2].Frac})");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int ExportSprite(string datDir, string idText, string outPath)
|
public static int ExportSprite(string datDir, string idText, string outPath)
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: directory not found: {datDir}"); return 2; }
|
if (!Directory.Exists(datDir)) { Console.Error.WriteLine($"error: dir not found: {datDir}"); return 2; }
|
||||||
uint id = ParseHex(idText);
|
uint id = ParseHex(idText);
|
||||||
if (id == 0) { Console.Error.WriteLine($"error: bad id '{idText}'"); return 2; }
|
if (id == 0) { Console.Error.WriteLine($"error: bad id '{idText}'"); return 2; }
|
||||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||||
|
|
@ -88,20 +96,34 @@ public static class VitalsMockup
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Replicates UiMeter.DrawHBar: native-width left-cap, stretched middle,
|
/// <summary>Horizontal 3-slice (native-width left-cap, stretched middle, native-width
|
||||||
/// optional native-width right-cap; caps clamped so a narrow bar never overdraws.</summary>
|
/// right-cap) clipped so nothing past <paramref name="clipW"/> (bar-local px) draws.
|
||||||
|
/// Mirrors the in-client UiMeter: back uses clipW=full, front uses clipW=frac*width.</summary>
|
||||||
private static void DrawHBar(
|
private static void DrawHBar(
|
||||||
Image<Rgba32> canvas, Image<Rgba32> left, Image<Rgba32> tile, Image<Rgba32> right,
|
Image<Rgba32> canvas, Image<Rgba32> left, Image<Rgba32> mid, Image<Rgba32> right,
|
||||||
int x, int y, int w, int h, bool withRightCap)
|
int x, int y, int w, int h, int clipW)
|
||||||
{
|
{
|
||||||
if (w <= 0) return;
|
if (w <= 0 || clipW <= 0) return;
|
||||||
int rcap = withRightCap ? Math.Min(right.Width, w) : 0;
|
int capL = Math.Min(left.Width, w);
|
||||||
int lcap = Math.Min(left.Width, w - rcap);
|
int capR = Math.Min(right.Width, w - capL);
|
||||||
|
int midW = w - capL - capR;
|
||||||
|
DrawClippedPiece(canvas, left, x, y, 0, capL, h, clipW);
|
||||||
|
DrawClippedPiece(canvas, mid, x, y, capL, midW, h, clipW);
|
||||||
|
DrawClippedPiece(canvas, right, x, y, w - capR, capR, h, clipW);
|
||||||
|
}
|
||||||
|
|
||||||
if (lcap > 0) Blit(canvas, left, x, y, lcap, h);
|
/// <summary>Draw one slice spanning bar-local [pieceLocalX, pieceLocalX+pieceW], cropped
|
||||||
int midX = x + lcap, midW = w - lcap - rcap;
|
/// horizontally so nothing past clipW shows (UV-cropping the texture proportionally).</summary>
|
||||||
if (midW > 0) Blit(canvas, tile, midX, y, midW, h);
|
private static void DrawClippedPiece(
|
||||||
if (rcap > 0) Blit(canvas, right, x + w - rcap, y, rcap, h);
|
Image<Rgba32> canvas, Image<Rgba32> src, int x, int y, int pieceLocalX, int pieceW, int h, int clipW)
|
||||||
|
{
|
||||||
|
if (pieceW <= 0) return;
|
||||||
|
int visibleW = Math.Min(pieceW, clipW - pieceLocalX);
|
||||||
|
if (visibleW <= 0) return;
|
||||||
|
int srcCropW = Math.Max(1, (int)MathF.Round(src.Width * (visibleW / (float)pieceW)));
|
||||||
|
srcCropW = Math.Min(srcCropW, src.Width);
|
||||||
|
using var piece = src.Clone(c => c.Crop(new Rectangle(0, 0, srcCropW, src.Height)).Resize(visibleW, h));
|
||||||
|
canvas.Mutate(c => c.DrawImage(piece, new Point(x + pieceLocalX, y), 1f));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Blit(Image<Rgba32> canvas, Image<Rgba32> src, int x, int y, int dw, int dh)
|
private static void Blit(Image<Rgba32> canvas, Image<Rgba32> src, int x, int y, int dw, int dh)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue