# 06 — HUD Elements and Dat-File UI Assets

**Slice 6 of 6: Heads-Up Display + the catalog of every UI-related DBObj.**

This document covers the always-on-screen HUD elements (vital orbs, radar, compass,
quickbar, selection indicator, damage numbers, cursors, announcements, hover
name) and maps the complete set of dat-file data-types used by the AC UI.

The retail client's in-game HUD is built on top of the same UI layer used by
all dialogs and panels (see slices 01–05). The defining property of the HUD is
that it is always rendered last, over the 3D world, with transparency and the
camera-cursor coupling that distinguishes "in-game" from "in a dialog".

Sources:

- `chunk_00400000.c` – options/settings UI wiring (tooltip, font-face, chat
  size). Every control references a `LanguageString` by name, routed through
  `FUN_004016b0` (a localized-string lookup).
- `chunk_00410000.c` – **the DBObj ID-range dispatcher** (`FUN_0041xxxx`),
  which maps a 32-bit DataId prefix to an internal DBObjType constant. This
  is the ground truth for every range below.
- `chunk_00430000.c` – **Win32 cursor plumbing** (`SetCursor`, `ShowCursor`,
  `SetClassLongA`, HCURSOR ownership), plus `BitBlt` fallback paths for
  software rendering.
- `chunk_005A0000.c` – D3D render-state toggles including
  `"RenderDeviceD3D.AllowDrawPrimUP"` — the legacy 2D UI primitive-up path.
- `chunk_005C0000.c` – vital-name string resolution (`Strength`, `Health`,
  `Maximum Stamina`, …), used to populate tooltip text and attribute labels.
- `chunk_00680000.c` – cursor mode switching: `SetCursorPos`,
  `ClientToScreen`, `ScreenToClient`, mouse-look mode (cursor recenter) —
  the boundary between HUD-cursor and look-cursor states.
- `references/AC2D/cInterface.cpp` + `cCustomWindows.h` — a contemporaneous
  C++ reimplementation of the AC UI with **exact retail dat icon IDs baked
  into the code**. Treated as reference for which assets are which.
- `references/DatReaderWriter/` — the canonical C# model of every portal and
  local DBObj with ID ranges from the generator XML.

Related retail / cross-check:

- `references/Chorizite.ACProtocol/protocol.xml` – the authoritative
  `VitalId` (0x01, 0x03, 0x05) and `CurVitalId` (0x02, 0x04, 0x06) enums
  plus the `SecondaryAttributeInfo` packet shape.
- `references/ACE/Source/ACE.Server/Network/GameMessages/Messages/GameMessagePublicUpdateVital.cs`
  – server side of the vital-update path.
- `references/holtburger/apps/holtburger-cli/src/pages/game/hud/` – a
  working Rust client's HUD data model (what it actually pulls from game
  state).

---

## Part A — HUD Elements

AC's HUD is composed of a fixed set of **movable, transparent windows**
stacked on top of the 3D render. In retail they are: the three vital orbs
(health/stamina/mana), the radar dish, the compass arc, the hotbar, the chat
panel, the selection/target name plate, announcement strip, 3D world hover
name, damage floaters, and the status icon strip (combat mode, "connection
good" indicator, encumbrance state).

The underlying "movable window" UI is driven by the ElementDesc / StateDesc /
LayoutDesc data described in slices 01–04. Each HUD element has a
`LayoutDesc` in the `0x21xxxxxx` range and swaps its child Sprite IDs
(`0x06xxxxxx`) through `UIStateId` transitions.

### A.1 Health / Stamina / Mana globes

**Purpose:** bottom-left (or bottom-center, depending on the UI profile)
three vertical orbs that show the fraction `current / max` for each vital.
Clicking one opens the vital's detail panel; hovering shows the numeric
`current/max` and rate-of-regen tooltip.

**Data sources** (from `protocol.xml`):

| Enum | Value | Meaning |
|---|---|---|
| `VitalId.MaximumHealth` | 0x01 | Max HP (from `Qualities_UpdateAttribute2nd`) |
| `CurVitalId.CurrentHealth` | 0x02 | Current HP (from `PrivateUpdateAttribute2nd`) |
| `VitalId.MaximumStamina` | 0x03 | Max stamina |
| `CurVitalId.CurrentStamina` | 0x04 | Current stamina |
| `VitalId.MaximumMana` | 0x05 | Max mana |
| `CurVitalId.CurrentMana` | 0x06 | Current mana |

Each vital arrives as a `SecondaryAttributeInfo` structure:

```text
SecondaryAttributeInfo {
    AttributeInfo Attribute;  // ranks, base, investment
    uint Current;             // current value
}
```

The client keeps a local `cCharInfo` store (see AC2D's `cCharInfo.cpp` for
naming). The orb renderer subscribes to changes and recomputes a fill
fraction `f = min(1, current / buffed_max)`.

**Rendering — how the partial fill is drawn**

Retail uses a **textured quad with a clip rectangle** (scissor rect), not a
colored gradient. AC2D's `cProgressBar` (which `cVitalsWindow` uses)
implements the pattern:

1. Draw the full "empty" background sprite (the globe outline, fixed image).
2. Set a `glScissor` (or a D3D8 sub-rect when bitblitting) that covers only
   the bottom `f * height` pixels of the orb.
3. Draw the "full" colored sprite clipped to the scissor.
4. Restore scissor.

Retail's exact IDs (per AC2D's `cInterface.cpp::cInterface()`):

- `0x060013B2` — icon for the "Vitals" window titlebar.
- Interior globe colors:
  - Blue `0x0000FF` (mana)
  - Cyan `0x10F0F0` (stamina; AC2D uses this, retail appears similar)
  - Red `0xFF0000` (health)

In retail the orbs are visually three-dimensional (specular highlight +
shaped alpha). They are not procedural fills — they are pre-rendered RGBA
sprites loaded from `RenderSurface` (`0x06xxxxxx`) through `SurfaceTexture`
(`0x05xxxxxx`).

**Text overlay.** A `Font` (`0x40000000`–`0x40000FFF`) renders the numeric
value centered over the orb when the mouse hovers (or when the user enables
"always show values" in settings).

**Render-path pseudocode:**

```text
function DrawVitalOrb(vital_type, current, buffed_max, x, y, w, h):
    sprite_empty  = GetOrbSpriteForVital(vital_type, state = Empty)
    sprite_filled = GetOrbSpriteForVital(vital_type, state = Filled)

    DrawSprite(sprite_empty, x, y, w, h)

    fill_fraction = clamp(current / buffed_max, 0, 1)
    fill_px       = round(fill_fraction * h)

    PushScissor(x, y + h - fill_px, w, fill_px)
    DrawSprite(sprite_filled, x, y, w, h)
    PopScissor()

    if mouse_over or always_show:
        text = format("{0}/{1}", current, buffed_max)
        DrawTextCentered(font = GetUIFont(), text, x + w/2, y + h/2, color = #FFFFFF)
```

**C# port sketch:**

```csharp
public sealed class VitalOrb
{
    readonly uint _spriteFrameId;    // 0x06xxxxxx
    readonly uint _spriteFilledId;   // 0x06xxxxxx
    readonly VitalKind _kind;

    public void Draw(IUiRenderer r, Rect bounds, uint current, uint buffedMax)
    {
        r.DrawSprite(_spriteFrameId, bounds);

        var fill = buffedMax == 0 ? 0f : MathF.Min(1f, (float)current / buffedMax);
        var fillPx = (int)MathF.Round(fill * bounds.Height);
        var clipRect = new Rect(bounds.X, bounds.Y + bounds.Height - fillPx, bounds.Width, fillPx);

        using (r.PushScissor(clipRect))
            r.DrawSprite(_spriteFilledId, bounds);

        if (r.HoverContains(bounds))
            r.DrawText(_uiFont, $"{current}/{buffedMax}",
                       bounds.Center, Color.White, TextAlign.Center);
    }
}
```

### A.2 Radar / compass

The radar is the canonical "small circular polar plot of nearby creatures
with the player at the center". The compass is a thin bar across the top of
the screen showing the 16 cardinal directions as the camera rotates.

**Radar — data sources**

- Player world position (`Position` packet, 24-byte LandCell + XYZ).
- Every nearby object's `CreateObject` / `UpdatePosition` with heading.
- Per-object `RadarColor` override (hostile = red, green = friendly NPC,
  etc.) + `ObjectFlags2` bits (`0x08` = item, `0x10` = blue-book NPC).

**Radar — retail dat IDs** (AC2D, `cInterface.cpp:139-144`):

- `0x06001388` — radar window titlebar/toolbar icon.
- `0x06004CC1` — radar background art (the circular bezel).

**Radar — the player arrow + blip placement**

AC2D `cRadar::OnRender` (`cCustomWindows.h:1004-1070`) is the clearest
retail-equivalent. The math:

```text
for each nearby object obj:
    delta      = obj.pos - player.pos
    delta      = delta.RotateAround(Z, -player.heading)   // align to radar-up = camera-forward
    screen.x   = radar.left   + radar.w/2 + (delta.x / (1.18 * range)) * (radar.w/2)
    screen.y   = radar.top    + radar.h/2 - (delta.y / (1.18 * range)) * (radar.h/2)
    color      = PickRadarColor(obj.radar_override, obj.flags2)
    DrawQuad2x2(screen, color)
```

The `1.18` factor is retail-observed — it shrinks the effective range
slightly so blips near the edge stay visible before the bezel clips them.
Player's own arrow is NOT in the blip loop; it is drawn as a fixed
centered sprite (the "player dot") rotated by `player.heading`.

**Compass — data sources**

- `player.heading` in radians.

**Compass — how the rose is drawn**

The rose is a **seamless horizontal strip texture** where 360° is tiled
across some multiple of the screen width. The U-offset is
`heading_normalized * strip_u_period`, with the visible portion cropped to
a narrow strip at the top of the screen. This is the classic "scrolling
texture" approach used by most 3D clients; retail AC follows it.

Holtburger's TUI compass (`hud/status.rs:11-18`) enumerates the 16
cardinal-direction labels that retail paints onto the strip:

```text
["W", "WNW", "NW", "NNW", "N", "NNE", "NE", "ENE",
 "E", "ESE", "SE", "SSE", "S", "SSW", "SW", "WSW"]
```

22.5° per segment, first label centered on 11.25°.

**Render-path pseudocode:**

```text
function DrawCompassStrip(heading_rad, bar_x, bar_y, bar_w, bar_h):
    heading_deg = (heading_rad * 180 / PI) mod 360
    // Texture is 360° wide in world-space; we crop to bar_w centered on heading
    u_center  = heading_deg / 360           // 0..1
    u_half    = bar_w / strip_texture_w / 2
    u_left    = u_center - u_half
    u_right   = u_center + u_half
    DrawSpriteUV(compass_strip_tex, bar_x, bar_y, bar_w, bar_h,
                 u0=u_left, u1=u_right, v0=0, v1=1)
```

**C# port sketch:**

```csharp
public void DrawRadar(IUiRenderer r, Rect bounds, float playerHeading,
                      Vector3 playerPos, IEnumerable<WorldEntity> nearby, float range)
{
    r.DrawSprite(/*0x06004CC1*/ _radarBgId, bounds);

    var cx = bounds.X + bounds.Width  * 0.5f;
    var cy = bounds.Y + bounds.Height * 0.5f;

    foreach (var e in nearby)
    {
        var d = e.Position - playerPos;
        d    = Vector3.Transform(d, Matrix4x4.CreateRotationZ(-playerHeading));
        var sx = cx + (d.X / (1.18f * range)) * (bounds.Width  * 0.5f);
        var sy = cy - (d.Y / (1.18f * range)) * (bounds.Height * 0.5f);
        var col = PickRadarColor(e);
        r.DrawFilledQuad(sx - 1, sy - 1, 2, 2, col);
    }

    // Player arrow - always on top
    r.DrawSprite(_playerArrowId, cx - 5, cy - 5, 10, 10, rotation: playerHeading);
}
```

### A.3 Quickbar / hotbar

The hotbar is the horizontal strip of spell-and-item slots (traditionally 7
main bars with 8–12 slots each) at the bottom of the screen.

**Data:** each slot holds either:

- An `ObjectId` (for items: a potion, a healer kit), OR
- A spell `uint` (for spells).

The client persists the mapping in `acclient.cfg` plus server-side character
options.

**Dat IDs in retail** (AC2D `cCustomWindows.h:395-509`):

- `0x060011D2` — selection highlight ring (drawn under selected slot).
- `0x06001AB0` / `0x06001AB2` — spell-bar tab in "unselected" / "selected"
  state.
- Spell-level icons (7 tiers): `0x060013F4`, `0x060013F5`, `0x060013F6`,
  `0x060013F7`, `0x060013F8`, `0x060013F9`, `0x06001F63`.
- Slot background `0x06001AB2` (48px wide slot frame).

**Slot interaction:**

1. Drag from inventory → hover over slot → `UIStateId.Drag_rollover_accept`
   (0x09) if droppable, `Drag_rollover_reject` (0x0A) if not. This is a
   per-slot StateDesc transition in the LayoutDesc.
2. Drop: `ItemSlot_DragOver_DropIn` (0x10000046) fires, followed by
   `ItemSlot_Filled` (0x1000001D).
3. Click: the slot's icon is looked up, and the client dispatches either:
   - `C2S_UseItem` if it's an object, or
   - `C2S_CastSpell` if it's a spell.
4. Keybind: F1-F12 and 1-0 map to slot indices via `MasterInputMap`
   (`0x14000000`-`0x1400FFFF`).

**Per-slot draw order:**

```text
for each slot:
    DrawSprite(slot.bg, rect)              # 0x06001AB2 (frame)
    if slot.occupant:
        DrawSprite(slot.occupant.icon, rect)  # the item's 0x06xxxxxx icon
        if slot.occupant is Spell:
            DrawSprite(spell_tier_icon[slot.occupant.level-1], corner_rect)
    if slot.is_focused:
        DrawSprite(0x060011D2, rect)       # selection ring
    if slot.keybind:
        DrawText(ui_font, slot.keybind_label, corner)
```

### A.4 Selection target indicator

When the player clicks a creature or another player, the client draws:

1. A **floating name plate** above their head in 3D space (billboarded).
2. An **over-the-head health bar** showing `current_health / max_health` of
   the selected target.
3. Below the name plate: extra text for target state (e.g. "Selected",
   "Talking to", monster level if allowed).

**Data sources:**

- `selected_object_id` local UI state.
- The target's `CreateObject` gave the client its name, level, and visible
  health fraction (NOT the exact current HP — servers usually obfuscate
  that).
- `Qualities_UpdateAttribute2nd` for the target's vitals (when selected; the
  server streams public vital updates for the selected target).

**Retail UIStateId transitions:**

- `UIStateId.ObjectSelected` (0x1000000B) — when a world object becomes
  the primary selection.
- `UIStateId.Unselected` / `Selected` (0x10000016 / 0x10000017) — for UI
  chip and menu items referencing the same object.

**Render path:**

```text
function DrawSelectionHealthBar(target, world_to_screen, camera):
    if target == null: return

    head_world = target.pos + (0, 0, target.height + 0.2)
    head_screen = world_to_screen(head_world)
    if head_screen.z < 0 or head_screen.off_screen: return

    // Fixed 96x8 bar
    bar_x = head_screen.x - 48
    bar_y = head_screen.y - 24

    // Full bar background
    DrawFilledRect(bar_x, bar_y, 96, 8, color = #40202020)

    // Health fill
    frac = clamp(target.health_pct, 0, 1)
    fill_w = round(96 * frac)
    fill_color = health_color_for_fraction(frac)  // green→yellow→red
    DrawFilledRect(bar_x, bar_y, fill_w, 8, fill_color)

    // Name below the bar
    DrawTextCentered(ui_font, target.display_name,
                     head_screen.x, bar_y + 12, target.name_color)
```

`health_color_for_fraction`: retail interpolates linearly between
#00FF00 (100%) → #FFFF00 (50%) → #FF0000 (0%).

### A.5 Damage numbers (floating text)

When the player or the currently-selected target takes damage, retail shows
a short-lived floating text at the head of the hit target: red for damage,
yellow-green for heal.

**Data source:**

- `GameEventCombatDamage` (AC `GameEventType.CombatDamage` = 0x01AE). The
  packet carries `attacker_id`, `victim_id`, damage amount, damage type,
  and location.
- Derived client-side: the client decides whose head to anchor to.

**Lifecycle per floater:**

```text
FloatingNumber {
    world_anchor: Vector3,
    offset_y: float,       // starts at 0, grows over life
    alpha: float,          // 1 → 0 over life
    life_remaining: float, // typically 1.5s
    text: string,          // "127" or "Heal 40"
    color: RGBA
}
```

**Update:**

```text
per frame:
    for each f in floaters:
        f.offset_y += 40 * dt            # pixels per second, upward
        f.life_remaining -= dt
        f.alpha = clamp(f.life_remaining / 0.4, 0, 1)
        if f.life_remaining <= 0: remove

    for each f:
        head_screen = world_to_screen(f.world_anchor)
        DrawTextOutlined(font, f.text,
                         head_screen.x, head_screen.y - f.offset_y,
                         f.color with alpha = f.alpha,
                         outline = black)
```

Retail does the outline as a 4-corner black stamp then white fill, not
SDF — the pixel-perfect AC font system pre-dates SDF.

### A.6 Cursor customization

**Decompiled evidence** (`chunk_00430000.c:7854-8024`):

```c
// FUN_00439320: restore default system cursor
HCURSOR hCursor = LoadCursorA(0, (LPCSTR)0x7f00);   // IDC_ARROW
SetCursor(hCursor);

// FUN_004395d0: install a custom cursor on the window class
HICON hIcon = (HICON)GetClassLongA(hwnd, -0xc /* GCL_HCURSOR */);
if (hIcon != param_1) DestroyIcon(hIcon);
SetClassLongA(hwnd, -0xc, (LONG)param_1);
SetCursor(param_1);
```

Retail uses Win32 cursors for the base arrow and custom-shaped cursors for
contextual modes. The cursor shape is **driven by the HUD hit-test** of the
current frame: moving over an NPC yields the "talk" cursor, over a
monster with the melee cursor, over inventory with the "drag" cursor.

**Cursor asset source (dat):** retail stores custom cursors as
`MediaDescCursor` sub-records inside StateDesc.Media for specific UI
states. Per `MediaDescCursor.generated.cs`:

```csharp
public class MediaDescCursor : MediaDesc {
    public uint File;        // underlying SurfaceTexture (0x05xxxxxx)
    public uint XHotspot;    // pixels from image top-left
    public uint YHotspot;
}
```

The client converts these into Win32 HCURSOR at load time (CreateIconIndirect
on a monochrome mask + color bitmap pair), then swaps them via
`SetCursor(hcursor)` as the UI state transitions happen.

**Mouse-look mode** (`chunk_00680000.c:9789-9804`) reveals the cursor-recenter
pattern retail uses to implement "right-click-hold to mouse-look":

```c
// Pseudocode of FUN_0068a930 mouse-look
center_x = client_width / 2;
center_y = client_height / 2;
client_to_screen(hwnd, &center_x, &center_y);
SetCursorPos(center_x, center_y);
```

Each frame, mouse delta = `actual_pos - last_center`; then snap back to
center. The HUD cursor sprite is hidden during this (ShowCursor(FALSE)).

**UIStateId cursor contexts** (from `UIStateId.generated.cs`):

| State | Use |
|---|---|
| `Drag_rollover_accept` | green-tinted drag cursor |
| `Drag_rollover_reject` | red X cursor |
| `ObjectSelected` | selection pointer |
| `JumpMode` | jump cursor (space held) |
| `MeleeMode` | combat crosshair |
| `MissileMode` | ranged combat crosshair |
| `DDDMode` | dialog-pointing cursor |
| `Csm_highlight` / `Csm_normal` / `Csm_ghosted` | context-sensitive movement cursor |

### A.7 Announcement / status bar

The announcement strip appears near the top-center when "big" events happen:
death messages, level-up, server-wide broadcasts ("Server will shut down in
15 minutes"), and the MOTD on first login.

**Data source:** `GameEventEvent` with subtype SystemBroadcast /
AdminBroadcast / CombatDeath, plus the server's `GameMessageMOTD` on
entering-the-world.

**Visible lifecycle:**

1. Fade in over 0.3s (alpha 0 → 1).
2. Hold for message_duration (default 5s, scales with message length).
3. Fade out over 0.5s.

The strip is a single horizontal `LayoutDesc`-backed panel; it loads the
bordered-panel background sprite and lays one or two `cStaticText`-style
children centered.

**Retail dat ID:** the connecting/MOTD panel uses `0x06004CB2` (AC2D
`cInterface.cpp:196`) as the "Enter Game" button/panel art. Most
announcement strips use the same bordered-panel sprite family.

### A.8 3D item hover name

When the mouse hovers over a world object (without clicking), a floating
text tag appears above the object: the object's display name, colored by
faction/allegiance.

**Hit test:** the client does a picking ray from the screen cursor, runs it
through the physics BSP (see `src/AcDream.Core/Physics/BSPQuery.cs`), finds
the hit object, and checks its `ObjectDescriptionFlag` to decide whether
hover names should be shown. The `"Maximum Tooltip Distance"` setting
(`chunk_00400000.c`, around the `ID_Misc_TooltipDelay` binding) gates the
display.

**Render:**

```text
function DrawHoverName(target, world_to_screen):
    if target == null: return
    if distance(player, target) > hover_max_range: return

    head = target.pos + (0, 0, target.height + 0.15)
    ss = world_to_screen(head)
    if ss.z < 0 or off_screen: return

    text = target.display_name
    color = hover_color_for_relation(target)
    size = measure_text(font, text)

    DrawFilledRect(ss.x - size.x/2 - 3, ss.y - size.y - 2,
                   size.x + 6, size.y + 4, color = #C0000000)
    DrawTextCentered(font, text, ss.x, ss.y - size.y, color)
```

The `size + 6` padding is for the pill-shaped background. `ss.y - size.y`
stacks the name above the object's head; the health bar (if target is
selected) sits below.

---

## Part B — Dat-File UI Assets

This is the catalog of every DBObj type used by the UI subsystem, derived
from `references/DatReaderWriter/DatReaderWriter/Generated/DBObjs/*.cs` and
verified against the decompiled ID-range dispatcher in `chunk_00410000.c`.

### B.1 Master ID-range table

The retail client's `FUN_0041ccc0` dispatcher (which returns a
DBObjType-equivalent integer from a DataId prefix) confirms the following
ranges. **Every table entry is load-bearing — do not paraphrase.**

| DataId prefix | Name | DBObjType | Dat file | Notes |
|---|---|---|---|---|
| `0x01xxxxxx` | GfxObj | 2 | Portal | 3D mesh/geometry (not UI) |
| `0x02xxxxxx` | Setup | 3 | Portal | Multi-part rig (not UI) |
| `0x03xxxxxx` | Animation | 4 | Portal | Keyframed anim (not UI) |
| **`0x04xxxxxx`** | **Palette** | 5 | Portal | **ARGB lookup; UI tinted sprites** |
| **`0x05xxxxxx`** | **SurfaceTexture** | 6 | Portal | **Mip chain of RenderSurface** |
| **`0x06xxxxxx`** | **RenderSurface (Icon)** | 7 | Portal | **THE UI icon/sprite space** |
| **`0x07xxxxxx`** | **RenderSurface** | 7 | Portal | **HiRes dat overflow for icons** |
| **`0x08xxxxxx`** | **Surface** | 8 | Portal | **Material pointing at tex+palette** |
| `0x09xxxxxx` | MotionTable | 9 | Portal | (not UI) |
| `0x0Axxxxxx` | Wave | 10 | Portal | Audio (not UI but UI *plays* them) |
| `0x0Dxxxxxx` | Environment | 11 | Portal | (not UI) |
| `0x0Exxxxxx` | Table singletons | mixed | Portal | CharGen, ChatPoseTable, etc. |
| `0x0Fxxxxxx` | PaletteSet / PalSet | 17 | Portal | Subpalette swaps for heritage tinting |
| `0x10xxxxxx` | Clothing / ClothingTable | 18 | Portal | (not UI directly) |
| `0x11xxxxxx` | GfxObjDegradeInfo | 19 | Portal | (not UI) |
| `0x12xxxxxx` | Scene | 20 | Portal | (not UI) |
| `0x13xxxxxx` | Region | 21 | Portal | Skybox — UI reads daylight from it |
| `0x14xxxxxx` | **MasterInputMap** | 22 | Portal | **UI keybinding map** |
| `0x15xxxxxx` | RenderTexture | 23 | Portal | Material-system texture (not UI) |
| `0x16xxxxxx` | RenderMaterial | 24 | Portal | Material system (not UI) |
| `0x17xxxxxx` | MaterialModifier | 25 | Portal | |
| `0x18xxxxxx` | MaterialInstance | 26 | Portal | |
| `0x20xxxxxx` | SoundTable | 27 | Portal | UI sounds (click, error) |
| **`0x21xxxxxx`** | **LayoutDesc** | 47 | **Local** | **THE UI LAYOUT FILE** |
| `0x22xxxxxx` | EnumMapper | 28 | Portal | Id→string helpers |
| **`0x23xxxxxx` / `0x24xxxxxx`** | **StringTable** | 48 | **Local** | **Localized strings for UI** |
| `0x25xxxxxx` | EnumIDMap | 29 | Portal | |
| `0x27xxxxxx` | DualEnumIDMap / DualDataIdMapper | 32 | Portal | |
| `0x30xxxxxx` | CombatTable | 34 | Portal | |
| **`0x31xxxxxx`** | **LanguageString** | 33 | Portal | **Loose localized string** |
| `0x32xxxxxx` | ParticleEmitter(Info) | 35 | Portal | Spell FX particles (seen in UI previews) |
| `0x33xxxxxx` | PhysicsScript | 36 | Portal | |
| `0x34xxxxxx` | PhysicsScriptTable | 37 | Portal | |
| **`0x40000000`–`0x40000FFF`** | **Font** | 38 | Portal | **BITMAP FONTS for the UI** |
| **`0x41xxxxxx`** | **LanguageInfo** | 49 | **Local** | **IME + text formatting config** |

Legend: bolded rows are what a UI layer needs.

### B.2 UI-critical DBObj types in depth

#### Font (0x40000000 – 0x40000FFF)

```csharp
public class Font : DBObj {
    public uint MaxCharHeight;
    public uint MaxCharWidth;
    public List<FontCharDesc> CharDescs;
    public uint NumHorizontalBorderPixels;
    public uint NumVerticalBorderPixels;
    public uint BaselineOffset;
    public uint ForegroundSurfaceDataId;    // -> 0x06xxxxxx RenderSurface
    public uint BackgroundSurfaceDataId;    // -> 0x06xxxxxx RenderSurface (outline/shadow)
}

public class FontCharDesc {
    public ushort Unicode;                  // codepoint
    public ushort OffsetX, OffsetY;         // position within atlas image
    public byte   Width, Height;            // glyph bbox
    public sbyte  HorizontalOffsetBefore;   // pre-advance (kerning-ish)
    public sbyte  HorizontalOffsetAfter;    // post-advance
    public sbyte  VerticalOffsetBefore;     // baseline adjust
}
```

Retail has **two surfaces per font**: `ForegroundSurfaceDataId` (the glyph
pixels, typically white A8) and `BackgroundSurfaceDataId` (an outline
stroke). The renderer blits background first with the text color shifted
darker, then foreground with the fill color, giving AC's characteristic
outlined UI text.

**Glyph lookup is linear in `CharDescs`**, sorted by `Unicode`. Retail
does a binary search. For modern C# the port should build a
`Dictionary<int, FontCharDesc>` at load time.

**Typical font IDs** (observed in retail settings dialog via
`chunk_00400000.c::FUN_004037b0`): the chat font is chosen from a fixed
list hardcoded by face name — "Arial", "CourierNew", "PalatinoLinotype",
"Tahoma", "TimesNewRoman". Each face maps to a Font DataId at runtime via
a mapping stored in `StringTable`.

#### LayoutDesc (0x21000000 – 0x21FFFFFF, Local dat)

**This is the single most important UI dat type.** Every HUD panel, every
dialog, every chat window layout, is a `LayoutDesc`. The file resides in
the **Local dat** (`client_local_English.dat`), not the Portal dat.

```csharp
public class LayoutDesc : DBObj {
    public uint Width;
    public uint Height;
    public HashTable<uint, ElementDesc> Elements;   // top-level elements by ElementId
}

public class ElementDesc {
    public StateDesc StateDesc;                      // default state
    public uint ReadOrder;                           // render order within parent
    public uint ElementId;                           // unique per LayoutDesc
    public uint Type;                                // element type (button/text/panel/etc.)
    public uint BaseElement;                         // inheritance ref
    public uint BaseLayoutId;                        // parent LayoutDesc for inheritance
    public UIStateId DefaultState;
    public uint X, Y;                                // relative to parent
    public uint Width, Height;
    public uint ZLevel;
    public uint LeftEdge, TopEdge, RightEdge, BottomEdge;  // anchor margins
    public Dictionary<UIStateId, StateDesc> States;  // per-state visuals
    public Dictionary<uint, ElementDesc> Children;
}

public class StateDesc {
    public uint StateId;
    public bool PassToChildren;
    public IncorporationFlags IncorporationFlags;   // which fields the child overrides
    public Dictionary<uint, BaseProperty> Properties;
    public List<MediaDesc> Media;                   // Image/Cursor/Sound/Anim per state
}
```

An ElementDesc has a **default StateDesc** + a **dictionary of per-state
StateDescs**. When the UI's logical state changes (e.g. mouse enters a
button → UIStateId.Normal_rollover), the renderer looks up the matching
StateDesc and re-reads Properties + Media. This is how buttons change
their sprite on hover, how vital orbs switch color when the player is
poisoned, and how the drag-drop target shows a green/red highlight.

**The `IncorporationFlags` enum** determines whether the state override
applies X, Y, Width, Height, ZLevel etc. Most per-state overrides only
change Media (the sprite behind the element), not geometry.

**ElementDesc.Type is an integer code.** From protocol-adjacent evidence
and ACViewer naming, the known values are:
- 0 = Root (panel)
- 1 = Picture (sprite blitter)
- 2 = Text (string label)
- 3 = Button (picture + label + state)
- 4 = EditBox
- 5 = ScrollBar
- 6 = ListBox
- ...

These are the primitive widget kinds the UI renderer dispatches on.

#### StringTable (0x23000000 – 0x24FFFFFF, Local dat)

```csharp
public class StringTable : DBObj {
    public uint Language;    // 1 = English
    public HashTable<uint, StringTableString> Strings;
}
```

Every localized UI string lives here, keyed by a stable internal ID. The
decompiled settings-UI wiring in `chunk_00400000.c` shows the retail
access pattern:

```c
// FUN_004037b0 (the settings dialog construction)
uVar2 = FUN_004016b0("ID_Sound_DisableSound_Help");        // look up help text
uVar2 = FUN_004016b0("ID_Sound_DisableSound", uVar2);      // bind label + help
FUN_005dee50(&DAT_008375b0, 4, 0x10000003, uVar2);         // install into control
```

`FUN_004016b0` is the **StringTable lookup by name-hash**. It returns a
StringId that points into the current StringTable. When the user switches
language, a different StringTable is loaded but the same hash still
resolves.

**Retail ID prefixes observed:**

| Prefix | Domain |
|---|---|
| `ID_UI_*` | UI widget labels |
| `ID_Misc_*` | misc UI (tooltips) |
| `ID_Sound_*` | audio settings |
| `ID_Graphics_*` | graphics settings |
| `ID_Chat_*` | chat system |
| `ID_Inventory_*` | inventory panel |

The hash function (from ACE's `DatLoader`) is a variant of Pearson hashing
over the lowercased byte string. Port this exactly; computed IDs are
compared against dat values.

#### LanguageString (0x31000000 – 0x3100FFFF, Portal dat)

```csharp
public class LanguageString : DBObj {
    public PStringBase<byte> Value;    // raw ASCII/UTF-8 string
}
```

A free-standing single string, separate from StringTable. Retail uses this
for very large strings or for strings that don't belong to a table (help
text, tutorial content, quest descriptions shown in panels).

#### Palette (0x04000000 – 0x0400FFFF)

```csharp
public class Palette : DBObj {
    public List<ColorARGB> Colors;   // up to 256 entries
}
```

Paired with `RenderSurface.Format = PFID_INDEX16 | PFID_P8`. UI uses this
to tint a single icon by swapping palettes — e.g. heritage-colored UI
buttons in character creation cycle between Aluvian / Gharundim / Sho
palettes.

#### PaletteSet / PalSet (0x0F000000 – 0x0F00FFFF)

```csharp
public class PaletteSet : DBObj { public List<uint> Palettes; }
```

A list of `Palette` DataIds. Used when a UI element can be in multiple
tinted variants (e.g. the "connection quality" traffic light cycling
between good/uncertain/bad by swapping the active palette index).

#### RenderSurface (0x06000000 – 0x07FFFFFF)

```csharp
public class RenderSurface : DBObj {
    public int Width, Height;
    public PixelFormat Format;
    public byte[] SourceData;
    public uint DefaultPaletteId;   // only if Format is INDEX16 or P8
}
```

This is **the bytes behind every UI icon in the game**. A few properties
matter for UI:

- `Format = PFID_INDEX16` — 16-bit indexed, paired with DefaultPaletteId.
  Typical for tintable icons.
- `Format = PFID_A8R8G8B8` — 32-bit ARGB. Direct blit, premultiplied if
  the owning Surface has nonzero Translucency.
- `Format = PFID_CUSTOM_RAW_JPEG` — compressed raw JPEG bytes. Retail's
  portrait-rendered images (e.g. splash art).
- `Format = PFID_DXT1`/`PFID_DXT5` — compressed. Usually world textures
  but occasionally used for high-res UI backdrops.

Width/Height for UI icons are predominantly:
- 16×16 (hotbar slot icon, small skill icon)
- 32×32 (inventory icon)
- 64×64 (spell book icon)
- Various (panel background sprites, titlebar icons, 9-slice edges)

#### SurfaceTexture (0x05000000 – 0x05FFFFFF)

```csharp
public class SurfaceTexture : DBObj {
    public TextureType Type;
    public List<QualifiedDataId<RenderSurface>> Textures;  // mip chain
}
```

Wraps one or more `RenderSurface` into a mipmapped texture. UI usually uses
**only mip 0** (no mips for 2D pixels), but the wrapper is still required.

#### Surface (0x08000000 – 0x0800FFFF)

```csharp
public class Surface : DBObj {
    public SurfaceType Type;                               // bitfield
    public QualifiedDataId<SurfaceTexture> OrigTextureId;  // set if Base1Image or Base1ClipMap
    public QualifiedDataId<Palette> OrigPaletteId;
    public ColorARGB ColorValue;                           // set if no texture
    public float Translucency;
    public float Luminosity;
    public float Diffuse;
}
```

A Surface is a **material descriptor**: "use this SurfaceTexture with this
Palette, at this translucency, with this luminosity glow". UI-side,
`Translucency` controls the window transparency setting the player picks
in AC ("move the slider to make chat window 60% opaque" sets the
Translucency on the chat LayoutDesc's backing Surface).

#### MasterInputMap (0x14000000 – 0x1400FFFF)

Stores the keybinding map: "F1 → action Cast", "I → open inventory", etc.
Port this verbatim; it's a lookup table the UI event handler walks on
every key press.

#### SoundTable (0x20000000 – 0x2000FFFF)

UI sound cues (click, error, level-up, unlock) live here. Not strictly
"visual UI" but the HUD dispatches them alongside draw calls. See
`MediaDescSound` inside StateDesc.Media for the per-state sound triggers.

### B.3 How the client chooses which dat asset to load

For a given HUD element the data-flow is:

1. **UI layer starts with a known LayoutDesc DataId.** For example the
   "main game HUD" layout is a fixed DataId the client knows at compile
   time — it's a 0x21xxxxxx constant baked in.
2. **LayoutDesc.Elements iterates children.** Each ElementDesc's default
   StateDesc.Media list contains a `MediaDescImage` or
   `MediaDescCursor` — these hold a `File` field which is a 0x06xxxxxx
   RenderSurface DataId (icon), a 0x08xxxxxx Surface DataId (material),
   or similar.
3. **State transitions swap media.** When `UIStateId` changes on an
   element, the client re-reads the per-state StateDesc, pulls its Media
   list, and re-binds sprites.
4. **Text is resolved via hash.** Every text label's string-content isn't
   stored in the layout; the layout stores a `StringId` (hash), which is
   passed to `FUN_004016b0` (`StringTable.Lookup(id)`) and returns the
   string pointer in the currently-active StringTable.
5. **Icons-from-server vs icons-from-dats.** Most HUD icons are
   **hardcoded dat IDs** (see AC2D's table). But inventory icons and spell
   icons are **data-driven**: each WorldObject's CreateObject packet
   carries its `IconId` (0x06xxxxxx) from the server, which the client
   blits at its designated slot. This is the one case where server drives
   icon choice; for the HUD chrome itself (frame art, globe art, compass
   strip, selection ring) the IDs are fixed.

### B.4 Pseudocode: loading a UI icon and blitting it

This is the reference flow for "take a 0x06xxxxxx DataId and draw it into
a rect on screen". Follow it exactly when porting.

```text
function DrawUIIcon(iconDataId, rect):
    assert (iconDataId & 0xFF000000) in {0x06000000, 0x07000000}

    // 1. Resolve RenderSurface
    rs = PortalDat.Read<RenderSurface>(iconDataId)
    if rs == null:
        rs = HighResDat.Read<RenderSurface>(iconDataId)   // optional fallback
    if rs == null: return Error

    // 2. Decode pixels into a GPU-uploadable byte[] (BGRA8)
    pixels = DecodeToBGRA8(rs, rs.Format, rs.SourceData, rs.DefaultPaletteId)

    // 3. Upload (or reuse from cache)
    tex = TextureCache.GetOrUpload(iconDataId, rs.Width, rs.Height, pixels)

    // 4. Draw textured quad at rect, with straight alpha-blending
    PushBlendMode(AlphaBlend)
    DrawQuad(tex, rect, uv = (0, 0, 1, 1), tint = White)
    PopBlendMode()

function DecodeToBGRA8(rs, format, raw, defaultPaletteId):
    switch format:
        case PFID_A8R8G8B8:
            out = copy raw
            SwapRedAndBlue(out)                # AC stores ARGB, GL wants BGRA
            return out
        case PFID_R5G6B5:
            return Expand565ToBGRA(raw)
        case PFID_A4R4G4B4:
            return Expand4444ToBGRA(raw)
        case PFID_INDEX16:
            pal = PortalDat.Read<Palette>(defaultPaletteId).Colors
            out = new byte[rs.Width * rs.Height * 4]
            for each 16-bit index i in raw:
                color = pal[i & 0x7FF]         # 11-bit index; high bits mean 'paletted'
                out.AppendBGRA(color)
            return out
        case PFID_P8:
            pal = PortalDat.Read<Palette>(defaultPaletteId).Colors
            out = new byte[rs.Width * rs.Height * 4]
            for each 8-bit index i in raw:
                out.AppendBGRA(pal[i])
            return out
        case PFID_DXT1, PFID_DXT3, PFID_DXT5:
            return DecodeDXT(raw, format, rs.Width, rs.Height)
        case PFID_A8:
            # grayscale alpha - expand with white RGB
            return ExpandA8ToBGRA(raw, fill = #FFFFFF)
        case PFID_CUSTOM_RAW_JPEG:
            bmp = DecodeJPEG(raw)
            return BitmapToBGRA(bmp)
        default:
            return Error("unsupported format 0x%X" % format)
```

Cache key: `(iconDataId, paletteOverrideId)`. Palette overrides are needed
when a UI subsystem wants to tint an indexed icon differently (e.g. the
same base sprite shown in green for "trained" and red for "specialized"
skills — retail uses Palette overrides, not color-mul).

**C# port sketch (Silk.NET + .NET 10):**

```csharp
public sealed class UiIconCache
{
    readonly PortalDat _portal;
    readonly PortalDat? _highRes;
    readonly GL _gl;
    readonly Dictionary<(uint Id, uint Palette), GpuTexture> _cache = new();

    public GpuTexture Load(uint iconId, uint paletteOverride = 0)
    {
        var key = (iconId, paletteOverride);
        if (_cache.TryGetValue(key, out var tex)) return tex;

        var rs = _portal.Read<RenderSurface>(iconId)
              ?? _highRes?.Read<RenderSurface>(iconId)
              ?? throw new UiAssetMissing(iconId);

        var paletteId = paletteOverride != 0 ? paletteOverride : rs.DefaultPaletteId;
        var pixels = DecodeToBgra8(rs, paletteId);

        tex = GpuTexture.Upload(_gl, rs.Width, rs.Height, pixels);
        _cache[key] = tex;
        return tex;
    }

    byte[] DecodeToBgra8(RenderSurface rs, uint paletteId)
        => rs.Format switch
        {
            PixelFormat.PFID_A8R8G8B8       => SwapArgbToBgra(rs.SourceData),
            PixelFormat.PFID_R5G6B5         => Expand565(rs.SourceData),
            PixelFormat.PFID_A4R4G4B4       => Expand4444(rs.SourceData),
            PixelFormat.PFID_INDEX16        => ExpandIndex16(rs, _portal.Read<Palette>(paletteId)!),
            PixelFormat.PFID_P8             => ExpandP8(rs, _portal.Read<Palette>(paletteId)!),
            PixelFormat.PFID_DXT1
              or PixelFormat.PFID_DXT3
              or PixelFormat.PFID_DXT5      => DxtDecoder.Decode(rs),
            PixelFormat.PFID_A8             => ExpandA8(rs.SourceData),
            PixelFormat.PFID_CUSTOM_RAW_JPEG => JpegToBgra(rs.SourceData),
            _ => throw new NotSupportedException($"UI format {rs.Format}")
        };
}
```

### B.5 Known hardcoded HUD dat IDs (from AC2D)

These are the specific DataIds AC2D hardcodes for each HUD window, copied
verbatim from the retail `.dat` files. Use them as **golden values** for
conformance tests — if acdream can load these IDs and render them, the
HUD is wired.

| Element | IconId (titlebar) | Other IDs |
|---|---|---|
| Radar | `0x06001388` | background `0x06004CC1` |
| Minimap | `0x06001065` | map tile `0x06000261`, cursor `0x060011F9`, sub-cursor `0x06001377` |
| Chat window | `0x0600137D` | |
| Vitals window | `0x060013B2` | |
| Stats window | `0x0600138C` | |
| Skills window | `0x0600138E` | |
| Selection spell highlight | `0x060011D2` | |
| Spell-tier icons (levels I–VII) | — | `0x060013F4`, `0x060013F5`, `0x060013F6`, `0x060013F7`, `0x060013F8`, `0x060013F9`, `0x06001F63` |
| Hotbar slot frame | — | `0x06001AB2` (selected), `0x06001AB0` (unselected) |
| Skill level headers | — | `0x06000F90` (Specialized), `0x06000F86` (Trained), `0x06000F89` (Untrained + Unusable) |
| Skill line row bg | `0x06000F98` | |
| Stats line row bg | `0x06000F98` | |
| "Enter Game" button art | `0x06004CB2` | |
| Character-selection highlight | `0x06001125` | |

The UI-critical dat IDs can be verified by opening the retail
`client_portal.dat` and searching for these DataIds — they should all
be `RenderSurface` records in `0x06xxxxxx` with sensible dimensions (16×16
to 256×256).

---

## Integration notes for acdream

**Architecture:**

- Implement `IUiRenderer` over Silk.NET with a single per-frame sprite
  batcher (similar to `Chorizite.OpenGLSDLBackend/FontRenderer.cs`) —
  4 verts per sprite, flush on texture change or reach of
  `MAX_SPRITES = 10048`.
- Use a single 2D orthographic projection matrix sized to the swapchain
  extent. Update on window resize.
- Implement `UiIconCache` keyed on `(DataId, PaletteOverride)` with LRU
  eviction; HUD working-set is small (<1 MB VRAM total).
- Parse LayoutDesc lazily. When the HUD is opened, read the top-level
  LayoutDesc, recurse Elements, resolve default state's Media, submit
  sprite draws in `ReadOrder`.

**State changes:**

- Every element stores its current `UIStateId`. On mouse/keyboard/game-state
  change, recompute the target state, swap media, reissue draws.
- Vital updates arrive as `Qualities_UpdateAttribute2nd` packets. Store
  into a `PlayerVitals` component; the orb renderer subscribes and
  recomputes fill fractions (no allocation per frame).

**Ordering:**

- HUD renders **after** the 3D world. Use a separate framebuffer or simply
  a clear-depth / disable-depth pass. Retail draws HUD last into the same
  swapchain buffer, which is what we should do.

**Text:**

- Port Font decoding into `UiFont` using Font.ForegroundSurfaceDataId +
  Font.BackgroundSurfaceDataId. Build a glyph dictionary at load time.
  Use atlas textures the first time the font is used.
- Outline = background blitted first at the color-darkened-by-0.5; fill =
  foreground blitted at the requested color.

**Cursor:**

- Build HCURSOR (Windows) / SDL cursors at UI state load time; swap via
  the GLFW / Silk.NET window callback when the hit-tested UI element's
  active state specifies a MediaDescCursor.

**Phase ordering:**

- Fonts, icon cache, LayoutDesc loader — implement as a shared UI core
  library.
- Vitals HUD comes first (simplest, clear user-visible acceptance).
- Radar second (needs world-to-screen; simple polar plot).
- Chat panel + hotbar third (need proper widget primitives: scrollbox,
  button with state).
- Selection indicator + hover names + damage floaters integrate last
  (depend on the world renderer's camera and the physics picking ray).

**Conformance test targets:**

- Load `Font 0x40000001` (retail's primary UI font), decode its
  ForegroundSurfaceDataId, verify glyph count and baselineoffset match
  recorded golden values from retail dat (can be extracted with ACME).
- Load `RenderSurface 0x06001388` (radar icon), decode to BGRA,
  byte-compare against known-good BGRA from ACME.
- Load `LayoutDesc 0x21000001` (or whatever the main HUD layout is),
  parse, verify element count + default-state media IDs.

Once these three pass, the HUD foundation is stable and the rest of the
slices (01–05) plug in on top of the same ElementDesc machinery.
