Deep investigation of the retail AC client's GUI subsystem, driven by 6
parallel Opus research agents, plus the first cut of a retail-faithful
retained-mode widget toolkit that scaffolds Phase D.
Research (docs/research/retail-ui/):
- 00-master-synthesis.md — cross-slice synthesis + port plan
- 01-architecture-and-init.md — WinMain, CreateMainWindow, frame loop,
Keystone bring-up (7 globals mapped)
- 02-class-hierarchy.md — key finding: UI lives in keystone.dll,
not acclient.exe; CUIManager + CUIListener
MI pattern, CFont + CSurface + CString
- 03-rendering.md — 24-byte XYZRHW+UV verts, per-font
256x256 atlas baked from RenderSurface,
TEXTUREFACTOR coloring, DrawPrimitiveUP
- 04-input-events.md — Win32 WndProc → Device (DAT_00837ff4)
→ widget OnEvent(+0x128); full event-type
table (0x01 click, 0x07 tooltip ~1000ms,
0x15 drag-begin, 0x21 enter, 0x3E drop)
- 05-panels.md — chat, attributes, skills, spells, paperdoll
(25-slot layout), inventory, fellowship,
allegiance — with wire-message bindings
- 06-hud-and-assets.md — vital orbs (scissor fill), radar
(0x06001388/0x06004CC1, 1.18× shrink),
compass strip, dat asset catalog
Key insight: keystone.dll owns the actual widget toolkit — we cannot
port a class hierarchy from the decompile because it's not there.
Instead we implement our own retained-mode toolkit with retail-faithful
behavior (event codes, focus/modal/capture, drag-drop state machine)
and will consume the same portal.dat fonts + sprites so the visual
identity is preserved.
C# scaffold (src/AcDream.App/UI/):
- UiEvent — 24-byte event struct + retail event-type constants
(0x01 click, 0x15 drag-begin, 0x201 WM_LBUTTONDOWN,
etc.) matching retail decompile switches
- UiElement — base widget: children, ZOrder, focus/capture flags,
virtual OnDraw/OnEvent/OnHitTest/OnTick; children-
first hit test + back-to-front composite
- UiPanel — panel, label, button primitives
- UiRenderContext — 2D draw context with translate stack
- UiRoot — top-of-tree + Device responsibilities (mouse/
keyboard state, focus, modal, capture, drag-drop,
tooltip timer); WorldMouseFallThrough/
WorldKeyFallThrough preserves existing camera
controls when no widget consumes
- UiHost — packages UiRoot + TextRenderer + input wiring
helpers for one-line integration into GameWindow
- README.md — orientation for future agents
Roadmap (docs/plans/2026-04-11-roadmap.md):
- D.1 marked shipped (debug overlay from 2026-04-17)
- D.2 expanded to include the retail UI framework landed here
- D.3-D.7 added: AcFont, dat sprites, core panels, HUD, CursorManager
- D.8 remains sound
All existing 470 tests pass. 0 warnings, 0 errors.
42 KiB
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 aLanguageStringby name, routed throughFUN_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), plusBitBltfallback 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 authoritativeVitalId(0x01, 0x03, 0x05) andCurVitalId(0x02, 0x04, 0x06) enums plus theSecondaryAttributeInfopacket 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:
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:
- Draw the full "empty" background sprite (the globe outline, fixed image).
- Set a
glScissor(or a D3D8 sub-rect when bitblitting) that covers only the bottomf * heightpixels of the orb. - Draw the "full" colored sprite clipped to the scissor.
- 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)
- Blue
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:
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:
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 (
Positionpacket, 24-byte LandCell + XYZ). - Every nearby object's
CreateObject/UpdatePositionwith heading. - Per-object
RadarColoroverride (hostile = red, green = friendly NPC, etc.) +ObjectFlags2bits (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:
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.headingin 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:
["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:
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:
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:
- 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. - Drop:
ItemSlot_DragOver_DropIn(0x10000046) fires, followed byItemSlot_Filled(0x1000001D). - Click: the slot's icon is looked up, and the client dispatches either:
C2S_UseItemif it's an object, orC2S_CastSpellif it's a spell.
- Keybind: F1-F12 and 1-0 map to slot indices via
MasterInputMap(0x14000000-0x1400FFFF).
Per-slot draw order:
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:
- A floating name plate above their head in 3D space (billboarded).
- An over-the-head health bar showing
current_health / max_healthof the selected target. - Below the name plate: extra text for target state (e.g. "Selected", "Talking to", monster level if allowed).
Data sources:
selected_object_idlocal UI state.- The target's
CreateObjectgave the client its name, level, and visible health fraction (NOT the exact current HP — servers usually obfuscate that). Qualities_UpdateAttribute2ndfor 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:
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(ACGameEventType.CombatDamage= 0x01AE). The packet carriesattacker_id,victim_id, damage amount, damage type, and location.- Derived client-side: the client decides whose head to anchor to.
Lifecycle per floater:
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:
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):
// 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:
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":
// 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:
- Fade in over 0.3s (alpha 0 → 1).
- Hold for message_duration (default 5s, scales with message length).
- 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:
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)
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.
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)
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:
// 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)
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)
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)
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)
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)
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)
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:
- 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.
- LayoutDesc.Elements iterates children. Each ElementDesc's default
StateDesc.Media list contains a
MediaDescImageorMediaDescCursor— these hold aFilefield which is a 0x06xxxxxx RenderSurface DataId (icon), a 0x08xxxxxx Surface DataId (material), or similar. - State transitions swap media. When
UIStateIdchanges on an element, the client re-reads the per-state StateDesc, pulls its Media list, and re-binds sprites. - 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 toFUN_004016b0(StringTable.Lookup(id)) and returns the string pointer in the currently-active StringTable. - 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.
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):
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
IUiRendererover Silk.NET with a single per-frame sprite batcher (similar toChorizite.OpenGLSDLBackend/FontRenderer.cs) — 4 verts per sprite, flush on texture change or reach ofMAX_SPRITES = 10048. - Use a single 2D orthographic projection matrix sized to the swapchain extent. Update on window resize.
- Implement
UiIconCachekeyed 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_UpdateAttribute2ndpackets. Store into aPlayerVitalscomponent; 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
UiFontusing 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.