# 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 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, ¢er_x, ¢er_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 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` 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 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 States; // per-state visuals public Dictionary Children; } public class StateDesc { public uint StateId; public bool PassToChildren; public IncorporationFlags IncorporationFlags; // which fields the child overrides public Dictionary Properties; public List 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 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 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 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 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> 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 OrigTextureId; // set if Base1Image or Base1ClipMap public QualifiedDataId 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(iconDataId) if rs == null: rs = HighResDat.Read(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(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(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(iconId) ?? _highRes?.Read(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(paletteId)!), PixelFormat.PFID_P8 => ExpandP8(rs, _portal.Read(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.