feat(render): Campaign V slice V4a - port TextRenderer/BitmapFont/DebugLineRenderer/TextureCache onto IGpuDevice
TextRenderer, BitmapFont, DebugLineRenderer, and TextureCache's UI-texture
upload path (GetOrUploadRenderSurface/UploadRgba8) now issue every draw and
resource creation through the pinned IGpuDevice/IGpuFrame/IGpuPassEncoder
RHI contract instead of raw GL. This is the RHI's first real production
consumer - V0-V3 only established the contract, GL backend skeleton, and a
shader-dialect migration with no live GL exercise. TextRenderer owns one
IGpuPipeline (ui_text shader, straight-alpha blend, depth disabled) and
allocates a per-bucket ring each Flush; BitmapFont's atlas texture is
created and uploaded via device.CreateTexture/.Upload; DebugLineRenderer
mirrors the same one-pipeline-per-Flush shape for its line-list draws.
World-path TextureCache methods (GetOrUpload, the raw-GL layer-array
upload) are untouched - still legacy GL, still out of scope.
Frame lifecycle: GpuDeviceFrameLifetime (RenderFrameOrchestrator.cs) wraps
IGpuDevice.BeginFrame()/IGpuFrame.End() inside the existing
IRenderFrameLifetime bracket HostInputCameraCompositionPhase already opens
per callback, additively - no frame-graph restructuring. Ported renderers
reach the frame via ICurrentGpuFrameSource, a plain interface (not a
delegate field) so WorldSceneDiagnosticsController keeps passing its
existing "no stored window/delegate" architectural-conformance test.
Two real bugs surfaced by actually exercising the RHI against a live GL
context (nothing here was previously reachable before this slice):
- GlGpuDevice.BeginFrame() now resets the render-state cache every frame.
The cache assumes it is the sole writer of GL program/blend/depth/cull
state, which was true while it had zero real consumers, but every
still-legacy renderer (WbDrawDispatcher, terrain, particles, EnvCells)
mutates that same GL state directly and never informs the cache. Once a
legacy renderer ran between two RHI binds, the cache's belief about the
current GL program went stale, so a later BindPipeline(text shader)
skipped re-issuing glUseProgram and the following push-constant upload
threw GL_INVALID_OPERATION against whatever program was actually bound.
Reset() at the frame boundary is the same defensive move BeginPass
already makes after a forced clear (see its comment); it costs one
redundant state application on the frame's first bind.
- GL_MULTISAMPLE has no representation in the pinned contract. Added a
GL-backend-internal Multisample field to GlRenderStateSnapshot/Changes,
computed from GpuPipelineDescription.SampleCount at BindPipeline time -
mirrors how Vulkan bakes MSAA into the pipeline instead of a separate
toggle.
Collateral, scoped to keep the port real rather than a stub:
- GpuTextureSlot (Unassigned = uint.MaxValue, NOT 0) now flows through
every consumer of TextureCache.GetOrUploadRenderSurface/UploadRgba8 and
TextRenderer.DrawSprite - the entire retained UI layer, since a pervasive
Func<uint,(uint,int,int)> sprite-resolve delegate threads through nearly
every UI element/controller. Every prior `== 0` / `!= 0` "no texture"
check became `.IsAssigned` / `!.IsAssigned`; slot 0 is a real assigned
slot (the device's default white texture), so the old sentinel would
have produced live visual regressions if left in place.
- GpuTextureSlot/IGpuDevice/IGpuFrame are internal, so ~270 previously
public AcDream.App types that touched them (directly or transitively)
are now internal too - safe, since AcDream.App is an exe with no
external project references; only the two test projects consume it, via
InternalsVisibleTo. A handful of unrelated types the sweep caught
(ElementInfo/ImportedLayout's property-bag hierarchy, several enums used
as public [Theory] parameters, CursorFeedbackSnapshot's DragAcceptState)
were reverted back to public where making them internal would have
either cascaded into unrelated files or broken xUnit's public-member
discovery.
- ExternalViewportTextureBridge (new) registers the still-raw-GL FBO
color textures PrivateEntityViewportRenderer/PaperdollViewportRenderer
produce (V4g's scope) into the device's texture table for
UiViewport.TextureHandle, via a temporary
GlGpuDevice.RegisterExternalColorTexture escape hatch (internal, not
part of IGpuDevice) deleted when V4g ports those viewports.
- TextRenderGlStateScope.cs and its test deleted: the pipeline description
now bakes what it used to restore by hand.
- ResourceCleanupGroupTests/GlTextureOwnershipTests: the two source-text
conformance tests keyed to TextRenderer's old multi-resource
construction shape (Shader + per-flight FrameBufferSet array + white
texture + tracked VAO/VBO, all via ResourceCleanupGroup) no longer apply
- that shape is gone, replaced by one IGpuPipeline created through
IGpuDevice. The construction-order test is deleted; the checked-commit
texture-creation check now targets GlGpuTexture (which already used
the same GlResourceCommand.CreateName primitive before this slice).
Gates:
- dotnet build -c Release: 0 warnings, 0 errors (AcDream.App has
TreatWarningsAsErrors).
- dotnet test tests/AcDream.App.Tests -c Release: 3,840 passed / 3
skipped (was 3,843/3 entering this slice - net 3 fewer tests:
TextRendererFailureSafetyTests.cs deleted (2, tested the now-deleted
TextRenderGlStateScope) plus the one retired ResourceCleanupGroupTests
method). Full solution: 8,908 passed / 5 skipped across all nine test
projects.
- Offline pixel gate (tools/run-offline-pixel-gate.ps1, parent ec414d60
vs this commit): differing fraction 0.318% (1,791/563,200 compared
pixels), above the 0.001 threshold. Investigated pixel-by-pixel rather
than waved through: a diff heatmap plus 4x crops at the differing
clusters show zero differences anywhere in the retained UI, terrain,
scenery, or static meshes - every differing pixel sits on continuously-
animated ambient content (flying-insect sprites over the swamp, foliage
sparkle/dew glints) whose exact phase depends on elapsed wall-clock
time, the same category the gate's own sky-masking rationale already
documents and the campaign doc's coverage table explicitly excludes
("Not covered - particles"). Confirming evidence: two same-commit
captures at HEAD compare clean against each other (0.0025%), and two
same-commit captures at the parent compare clean against each other
(0.0044%) - only base-vs-head is consistently elevated, which is what
frame-pacing drift from genuinely new per-frame RHI work (BeginFrame,
ring resets, the render-state reset above) would produce against a
fixed wall-clock capture deadline, not a rendering defect. Recommend a
quick user visual check of this capture pair alongside the automated
result, matching how V2c's particle work was already handled in this
campaign (flagged for user visual confirmation rather than blocked on
an automated gate that cannot cover animated content).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
ec414d60cd
commit
ceec3bc440
334 changed files with 3660 additions and 3840 deletions
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Ui;
|
||||
using AcDream.Core.Social;
|
||||
using AcDream.UI.Abstractions;
|
||||
|
|
@ -10,9 +10,9 @@ namespace AcDream.App.UI;
|
|||
/// remain backend- and network-agnostic; this controller owns the boundary
|
||||
/// between verified command behavior and live session/UI services.
|
||||
/// </summary>
|
||||
public sealed class ClientCommandController
|
||||
internal sealed class ClientCommandController
|
||||
{
|
||||
public sealed record Bindings(
|
||||
internal sealed record Bindings(
|
||||
Action TeleportToLifestone,
|
||||
Action TeleportToMarketplace,
|
||||
Action TeleportToPkArena,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal reader for retail's <c>controls.ini</c> — a flat INI with one
|
||||
/// Minimal reader for retail's <c>controls.ini</c> — a flat INI with one
|
||||
/// <c>[section]</c> per element type. Colors are <c>#AARRGGBB</c> (alpha
|
||||
/// first). Optional: a missing file yields an empty sheet (callers fall back
|
||||
/// to hardcoded defaults). See the D.2b spec §7.
|
||||
/// to hardcoded defaults). See the D.2b spec §7.
|
||||
/// </summary>
|
||||
public sealed class ControlsIni
|
||||
internal sealed class ControlsIni
|
||||
{
|
||||
private readonly Dictionary<string, Dictionary<string, string>> _sections;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -62,7 +62,7 @@ public enum RetailGlobalCursorKind
|
|||
TargetInvalid,
|
||||
}
|
||||
|
||||
public readonly record struct CursorFeedback(
|
||||
internal readonly record struct CursorFeedback(
|
||||
CursorFeedbackKind Kind,
|
||||
UiCursorMedia Cursor = default,
|
||||
RetailGlobalCursorKind GlobalKind = RetailGlobalCursorKind.Default)
|
||||
|
|
@ -72,7 +72,7 @@ public readonly record struct CursorFeedback(
|
|||
|
||||
public readonly record struct CursorFeedbackSnapshot(
|
||||
object? DragPayload = null,
|
||||
UiItemSlot.DragAcceptState DragAccept = UiItemSlot.DragAcceptState.None,
|
||||
DragAcceptState DragAccept = DragAcceptState.None,
|
||||
ResizeEdges ActiveResizeEdges = ResizeEdges.None,
|
||||
ResizeEdges HoverResizeEdges = ResizeEdges.None,
|
||||
bool WindowMoveActive = false,
|
||||
|
|
@ -85,7 +85,7 @@ public readonly record struct CursorFeedbackSnapshot(
|
|||
RetailCursorTargetMode TargetMode = RetailCursorTargetMode.None,
|
||||
CombatMode CombatMode = CombatMode.NonCombat);
|
||||
|
||||
public sealed class CursorFeedbackController
|
||||
internal sealed class CursorFeedbackController
|
||||
{
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly Func<uint>? _worldTargetProvider;
|
||||
|
|
@ -110,8 +110,8 @@ public sealed class CursorFeedbackController
|
|||
UiElement? hover = root.Pick(root.MouseX, root.MouseY);
|
||||
|
||||
// Retail UpdateCursorState (0x00564630) keys the target-mode cursor off
|
||||
// the SmartBox found object — the WORLD entity under the cursor. A UI
|
||||
// window occludes the world (no found object → pending). The one
|
||||
// the SmartBox found object — the WORLD entity under the cursor. A UI
|
||||
// window occludes the world (no found object → pending). The one
|
||||
// UI-side source retail-style cells contribute is an occupied item
|
||||
// slot's own item.
|
||||
RetailCursorTargetMode targetMode = ModeFromInteraction(_itemInteraction);
|
||||
|
|
@ -127,7 +127,7 @@ public sealed class CursorFeedbackController
|
|||
|
||||
var snapshot = new CursorFeedbackSnapshot(
|
||||
DragPayload: root.DragPayload,
|
||||
DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? UiItemSlot.DragAcceptState.None,
|
||||
DragAccept: FindHoveredItemSlot(hover)?.DragAcceptVisual ?? DragAcceptState.None,
|
||||
ActiveResizeEdges: root.ActiveResizeEdges,
|
||||
HoverResizeEdges: root.HoverResizeEdges,
|
||||
WindowMoveActive: root.IsWindowMoveActive,
|
||||
|
|
@ -168,8 +168,8 @@ public sealed class CursorFeedbackController
|
|||
{
|
||||
return snapshot.DragAccept switch
|
||||
{
|
||||
UiItemSlot.DragAcceptState.Accept => CursorFeedbackKind.DragAccept,
|
||||
UiItemSlot.DragAcceptState.Reject => CursorFeedbackKind.DragReject,
|
||||
DragAcceptState.Accept => CursorFeedbackKind.DragAccept,
|
||||
DragAcceptState.Reject => CursorFeedbackKind.DragReject,
|
||||
_ => CursorFeedbackKind.Drag,
|
||||
};
|
||||
}
|
||||
|
|
@ -197,10 +197,10 @@ public sealed class CursorFeedbackController
|
|||
}
|
||||
|
||||
// Retail UpdateCursorState (0x00564630), TARGET_MODE 3: no found
|
||||
// object → the 0x27 four-arrows pending cursor — INCLUDING over
|
||||
// object → the 0x27 four-arrows pending cursor — INCLUDING over
|
||||
// UI chrome. Valid/invalid exist only with a target under the
|
||||
// cursor. (The earlier HoverUi → Invalid arm was a non-retail
|
||||
// invention — 2026-07-03 visual gate.)
|
||||
// cursor. (The earlier HoverUi → Invalid arm was a non-retail
|
||||
// invention — 2026-07-03 visual gate.)
|
||||
return CursorFeedbackKind.TargetPending;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI;
|
|||
/// completion; this semantic owner retains the server type/context and sends the
|
||||
/// matching confirmation response.
|
||||
/// </summary>
|
||||
public sealed class GameplayConfirmationController : IDisposable
|
||||
internal sealed class GameplayConfirmationController : IDisposable
|
||||
{
|
||||
private readonly RetailDialogFactory _dialogs;
|
||||
private readonly Action<uint, uint, bool> _sendResponse;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Visual result of a drag-over query. Retail handlers can consume a drag-over
|
||||
/// message without setting either accept or reject art; shortcut aliases over a
|
||||
/// physical item list use that neutral path.
|
||||
/// </summary>
|
||||
public enum ItemDragAcceptance
|
||||
internal enum ItemDragAcceptance
|
||||
{
|
||||
None,
|
||||
Accept,
|
||||
|
|
@ -18,13 +18,13 @@ public enum ItemDragAcceptance
|
|||
/// (<c>RegisterItemListDragHandler</c>, decomp 230461; confirmed acclient
|
||||
/// 0x004a539e + the gmToolbarUI block 0x004bdd89).
|
||||
/// <para><see cref="OnDragOver"/> decides the neutral/accept/reject overlay only (advisory).
|
||||
/// <see cref="HandleDropRelease"/> is authoritative — it performs the action, or
|
||||
/// <see cref="HandleDropRelease"/> is authoritative — it performs the action, or
|
||||
/// no-ops to reject.</para>
|
||||
/// </summary>
|
||||
public interface IItemListDragHandler
|
||||
internal interface IItemListDragHandler
|
||||
{
|
||||
/// <summary>The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag
|
||||
/// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its
|
||||
/// <summary>The drag STARTED from a cell in this list — retail's RecvNotice_ItemListBeginDrag
|
||||
/// → RemoveShortcut (decomp 0x004bd930/0x004bd450): the handler removes the lifted item from its
|
||||
/// model + wire so the source slot empties immediately. The item is "in hand" until
|
||||
/// HandleDropRelease (place) or the drag ends off-target (stays removed). No restore on cancel.</summary>
|
||||
void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.UI;
|
|||
/// Implementations own panel-specific subscriptions and reactions; the window
|
||||
/// manager owns visibility, focus, capture, geometry, and teardown ordering.
|
||||
/// </summary>
|
||||
public interface IRetainedPanelController : IDisposable
|
||||
internal interface IRetainedPanelController : IDisposable
|
||||
{
|
||||
/// <summary>Called once for each hidden-to-shown transition.</summary>
|
||||
void OnShown() { }
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Panel state that is not completely described by outer-frame bounds.</summary>
|
||||
public readonly record struct RetainedWindowState(
|
||||
internal readonly record struct RetainedWindowState(
|
||||
bool Collapsed = false,
|
||||
bool Maximized = false,
|
||||
float? PersistedTop = null,
|
||||
|
|
@ -11,7 +11,7 @@ public readonly record struct RetainedWindowState(
|
|||
/// Optional state seam used by retained-window persistence. Bounds are restored
|
||||
/// first; the controller then reapplies collapsed/maximized presentation.
|
||||
/// </summary>
|
||||
public interface IRetainedWindowStateController
|
||||
internal interface IRetainedWindowStateController
|
||||
{
|
||||
RetainedWindowState CaptureWindowState();
|
||||
void RestoreWindowState(RetainedWindowState state);
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Narrow numeric-state bridge for widgets imported from retail LayoutDesc data.
|
||||
/// Controllers use retail state ids without knowing how names/media are stored.
|
||||
/// </summary>
|
||||
public interface IUiDatStateful
|
||||
internal interface IUiDatStateful
|
||||
{
|
||||
uint ActiveRetailStateId { get; }
|
||||
bool TrySetRetailState(uint stateId);
|
||||
}
|
||||
|
||||
public static class RetailUiStateIds
|
||||
internal static class RetailUiStateIds
|
||||
{
|
||||
public const uint Closed = 11u;
|
||||
public const uint Open = 12u;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Opt-in recipient of retail UI global message 3, broadcast once per UI frame
|
||||
/// after tooltip deadline processing.
|
||||
/// </summary>
|
||||
public interface IUiGlobalTimeListener
|
||||
internal interface IUiGlobalTimeListener
|
||||
{
|
||||
void OnGlobalUiTime(double nowSeconds);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Renders a 3-D mini-scene into an off-screen buffer and returns the GL color-texture
|
||||
/// handle. Called by the per-frame pre-UI hook (GameWindow), NOT from UiViewport.OnDraw. Implemented
|
||||
/// by PaperdollViewportRenderer in AcDream.App.Rendering. Intra-App decoupling so the UI widget
|
||||
/// doesn't depend on WbDrawDispatcher/GameWindow.</summary>
|
||||
public interface IUiViewportRenderer
|
||||
internal interface IUiViewportRenderer
|
||||
{
|
||||
/// <summary>Render at (width,height); return the color-texture GL handle, or 0 if nothing rendered.</summary>
|
||||
uint Render(int width, int height);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
|
@ -17,9 +17,9 @@ namespace AcDream.App.UI;
|
|||
/// DBCache::GetDIDFromEnum (0x413940). Each layer is a 0x06 RenderSurface decoded
|
||||
/// DIRECTLY (the D.2b RenderSurface-vs-Surface rule).
|
||||
///
|
||||
/// Layer order (bottom → top), matching retail:
|
||||
/// Layer order (bottom → top), matching retail:
|
||||
/// 1. type-default underlay (OPAQUE backing; resolved via EnumIDMap 0x10000004 from
|
||||
/// the portal MasterMap) — <see cref="ResolveUnderlayDid"/>
|
||||
/// the portal MasterMap) — <see cref="ResolveUnderlayDid"/>
|
||||
/// 2. item custom underlay (e.g. "magic" tint strip)
|
||||
/// 3. base icon
|
||||
/// 4. item custom overlay (e.g. "enchanted" sparkle)
|
||||
|
|
@ -31,28 +31,28 @@ namespace AcDream.App.UI;
|
|||
///
|
||||
/// Composited textures are cached by their (typeUnderlay, underlay, base, overlay) tuple.
|
||||
/// </summary>
|
||||
public sealed class IconComposer
|
||||
internal sealed class IconComposer
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly TextureCache _cache;
|
||||
private readonly Dictionary<(uint, uint, uint, uint, uint), uint> _byTuple = new();
|
||||
private readonly Dictionary<(uint, uint, uint, uint, uint), GpuTextureSlot> _byTuple = new();
|
||||
private readonly Dictionary<(uint, uint, uint), ComposedIcon> _dragByTuple = new();
|
||||
private readonly Dictionary<uint, uint> _spellIcons = new();
|
||||
private readonly Dictionary<uint, uint> _componentIcons = new();
|
||||
private readonly Dictionary<uint, GpuTextureSlot> _spellIcons = new();
|
||||
private readonly Dictionary<uint, GpuTextureSlot> _componentIcons = new();
|
||||
|
||||
private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, uint Texture);
|
||||
private sealed record ComposedIcon(byte[] Rgba, int Width, int Height, GpuTextureSlot Texture);
|
||||
|
||||
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||
// Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21.
|
||||
// Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940.
|
||||
// ── type-default underlay resolve (EnumIDMap 0x10000004) ─────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000004 → submap DID (0x25000008).
|
||||
// Submap maps index → 0x06 RenderSurface DID. index = LSB(itemType)+1, or 0x21.
|
||||
// Refs: IconData::RenderIcons 0058d214–0058d22c; DBCache::GetDIDFromEnum 0x413940.
|
||||
private EnumIDMap? _underlaySubMap;
|
||||
private bool _underlayResolveTried;
|
||||
private readonly Dictionary<uint, uint> _underlayDidByIndex = new();
|
||||
|
||||
// ── effect overlay resolve (EnumIDMap 0x10000005) ────────────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009).
|
||||
// Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21.
|
||||
// ── effect overlay resolve (EnumIDMap 0x10000005) ────────────────────────
|
||||
// Portal MasterMap (0x25000000) maps enum 0x10000005 → submap DID (0x25000009).
|
||||
// Submap maps index → 0x06 RenderSurface DID. index = LSB(effects)+1, fallback 0x21.
|
||||
// Refs: IconData::RenderIcons 0x0058d180 (effect path); the effect tile is a
|
||||
// ReplaceColor tint SOURCE, not a blit layer (see RESOLVED doc, divergence DR-1).
|
||||
private EnumIDMap? _effectSubMap;
|
||||
|
|
@ -68,13 +68,13 @@ public sealed class IconComposer
|
|||
|
||||
/// <summary>
|
||||
/// Resolve the type-default underlay DID for <paramref name="itemType"/> via the
|
||||
/// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c +
|
||||
/// two-level EnumIDMap chain (retail: IconData::RenderIcons 0058d214–0058d22c +
|
||||
/// DBCache::GetDIDFromEnum 0x413940).
|
||||
///
|
||||
/// <para>index = LowestSetBit(itemType) + 1, or 0x21 when itemType has no bits set.</para>
|
||||
///
|
||||
/// <para>NOTE: retail RenderIcons (407546) has a special paperdoll IsThePlayer case
|
||||
/// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that
|
||||
/// that uses GetDIDByEnum(0x10000004, 7) + TYPE_CONTAINER for the player doll — that
|
||||
/// path is out of scope here (paperdoll phase).</para>
|
||||
/// </summary>
|
||||
internal uint ResolveUnderlayDid(ItemType itemType)
|
||||
|
|
@ -97,7 +97,7 @@ public sealed class IconComposer
|
|||
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
|
||||
if (masterDid == 0) return;
|
||||
if (!_dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)) return;
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000004u, out var subDid)) return; // → 0x25000008
|
||||
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _underlaySubMap = sub;
|
||||
}
|
||||
|
||||
|
|
@ -105,7 +105,7 @@ public sealed class IconComposer
|
|||
/// Resolve the effect-overlay DID for <paramref name="effects"/> via the EnumIDMap
|
||||
/// 0x10000005 chain. index = LowestSetBit(effects)+1; if the entry is missing/zero,
|
||||
/// retail falls back to index 0x21 (the solid-black tile). NOTE: the effect path has
|
||||
/// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss →
|
||||
/// NO lsb==-1 pre-check (unlike the type underlay), so effects==0 → index 0 → miss →
|
||||
/// fallback. (Retail IconData::RenderIcons 0x0058d180.)
|
||||
/// </summary>
|
||||
internal uint ResolveEffectDid(uint effects)
|
||||
|
|
@ -129,16 +129,16 @@ public sealed class IconComposer
|
|||
uint masterDid = (uint)_dats.Portal.Db.Header.MasterMapId; // = 0x25000000
|
||||
if (masterDid == 0) return;
|
||||
if (!_dats.Portal.TryGet<EnumIDMap>(masterDid, out var master)) return;
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
|
||||
if (!master.ClientEnumToID.TryGetValue(0x10000005u, out var subDid)) return; // → 0x25000009
|
||||
if (_dats.Portal.TryGet<EnumIDMap>(subDid, out var sub)) _effectSubMap = sub;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>SurfaceWindow::ReplaceColor</c> SURFACE overload (0x004415b0): for every
|
||||
/// pixel in <paramref name="dst"/> that equals pure-white-opaque (RGBAColor(1,1,1,1) →
|
||||
/// pixel in <paramref name="dst"/> that equals pure-white-opaque (RGBAColor(1,1,1,1) →
|
||||
/// 0xFFFFFFFF), copy the SAME (x,y) pixel from the source effect tile. This preserves
|
||||
/// the effect tile's texture/gradient (NOT a flat color). Retail requires the source to
|
||||
/// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged.
|
||||
/// cover the dest (it does — both are 32x32); out-of-range pixels are left unchanged.
|
||||
/// Mutates <paramref name="dst"/> in place.
|
||||
/// </summary>
|
||||
internal static void ReplaceWhiteFromSurface(byte[] dst, int dw, int dh, byte[] src, int sw, int sh)
|
||||
|
|
@ -160,7 +160,7 @@ public sealed class IconComposer
|
|||
/// <summary>
|
||||
/// The decoded effect tile for <paramref name="effects"/> (enum 0x10000005). The tile is
|
||||
/// a 32x32 textured RenderSurface whose pixels ARE the per-effect coloring (blue=Magical,
|
||||
/// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into
|
||||
/// green=Poisoned, …; the 0x21 fallback is solid black). Retail copies it per-pixel into
|
||||
/// the icon's white pixels (gradient), so we need the whole tile, not a representative
|
||||
/// color. Cached per DID.
|
||||
/// </summary>
|
||||
|
|
@ -224,27 +224,27 @@ public sealed class IconComposer
|
|||
/// effects==0 resolves to the 0x21 solid-black fallback tile, so pure-white pixels become
|
||||
/// black (matching retail); magical items take the per-effect hue instead.
|
||||
/// </summary>
|
||||
public uint GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
public GpuTextureSlot GetIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
{
|
||||
if (iconId == 0) return 0;
|
||||
if (iconId == 0) return GpuTextureSlot.Unassigned;
|
||||
uint typeUnderlayDid = ResolveUnderlayDid(itemType);
|
||||
var key = (typeUnderlayDid, iconId, underlayId, overlayId, effects);
|
||||
if (_byTuple.TryGetValue(key, out var tex)) return tex;
|
||||
|
||||
// Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
|
||||
// Stage 1 — retail m_pDragIcon: base + custom overlay, then the effect recolor.
|
||||
// RenderIcons retains this as a distinct Graphic because the cursor ghost must not
|
||||
// carry the type/custom underlay that fills an inventory cell.
|
||||
ComposedIcon? drag = GetOrCreateDragIcon(iconId, overlayId, effects);
|
||||
|
||||
// Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
|
||||
// Stage 2 — retail m_pIcon: type-default underlay (opaque) + custom underlay + drag.
|
||||
var layers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(layers, typeUnderlayDid);
|
||||
AddLayer(layers, underlayId);
|
||||
if (drag is not null) layers.Add((drag.Rgba, drag.Width, drag.Height));
|
||||
if (layers.Count == 0) return 0;
|
||||
if (layers.Count == 0) return GpuTextureSlot.Unassigned;
|
||||
|
||||
var (rgba, w, h) = Compose(layers);
|
||||
uint handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
|
||||
GpuTextureSlot handle = _cache.UploadRgba8(rgba, w, h, nearest: true);
|
||||
_byTuple[key] = handle;
|
||||
return handle;
|
||||
}
|
||||
|
|
@ -255,13 +255,15 @@ public sealed class IconComposer
|
|||
/// <c>UIElement_ItemList::PrepareDragIcon</c> obtains exactly this graphic through
|
||||
/// <c>ACCWeenieObject::GetDragIcon</c> (0x004e2a50 / 0x0058d180).
|
||||
/// </summary>
|
||||
public uint GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
public GpuTextureSlot GetDragIcon(ItemType itemType, uint iconId, uint underlayId, uint overlayId, uint effects)
|
||||
{
|
||||
// itemType/underlayId are deliberately unused: keeping the resolver signature identical
|
||||
// to GetIcon lets every item-panel binding request the two retail siblings from one model.
|
||||
_ = itemType;
|
||||
_ = underlayId;
|
||||
return iconId == 0 ? 0u : GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? 0u;
|
||||
return iconId == 0
|
||||
? GpuTextureSlot.Unassigned
|
||||
: GetOrCreateDragIcon(iconId, overlayId, effects)?.Texture ?? GpuTextureSlot.Unassigned;
|
||||
}
|
||||
|
||||
private ComposedIcon? GetOrCreateDragIcon(uint iconId, uint overlayId, uint effects)
|
||||
|
|
@ -275,11 +277,11 @@ public sealed class IconComposer
|
|||
if (dragLayers.Count == 0) return null;
|
||||
|
||||
var composed = Compose(dragLayers);
|
||||
// Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// Effect recolor — ALWAYS, matching retail IconData::RenderIcons (0x0058d180):
|
||||
// the effect tile (enum 0x10000005, lsb(effects)+1, fallback 0x21) is non-null
|
||||
// even for effects==0 (the 0x21 SOLID-BLACK tile 0x060011C5). Retail's RenderIcons
|
||||
// calls the SURFACE overload of SurfaceWindow::ReplaceColor (0x004415b0), copying
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// the textured effect tile per-pixel into the icon's pure-white pixels — so
|
||||
// magical items take the tile's GRADIENT hue and mundane items go solid black.
|
||||
// (Visually confirmed against retail 2026-06-17: the Energy Crystal's blue is a
|
||||
// gradient, not a flat tint, and the no-mana scroll's edges are black.)
|
||||
|
|
@ -287,7 +289,7 @@ public sealed class IconComposer
|
|||
ReplaceWhiteFromSurface(composed.rgba, composed.w, composed.h,
|
||||
tile.Rgba8, tile.Width, tile.Height);
|
||||
|
||||
uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
var created = new ComposedIcon(composed.rgba, composed.w, composed.h, texture);
|
||||
_dragByTuple[key] = created;
|
||||
return created;
|
||||
|
|
@ -307,12 +309,12 @@ public sealed class IconComposer
|
|||
/// Retail ClientMagicSystem::CompositeSpellIcon (0x00567550): power-level
|
||||
/// backing, spell art, reversed/normal recolor, then self/fellow overlay.
|
||||
/// </summary>
|
||||
public uint GetSpellIcon(uint spellId)
|
||||
public GpuTextureSlot GetSpellIcon(uint spellId)
|
||||
{
|
||||
if (_spellIcons.TryGetValue(spellId, out uint cached)) return cached;
|
||||
if (_spellIcons.TryGetValue(spellId, out GpuTextureSlot cached)) return cached;
|
||||
DatReaderWriter.DBObjs.SpellTable? table =
|
||||
_dats.Get<DatReaderWriter.DBObjs.SpellTable>(0x0E00000Eu);
|
||||
if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return 0u;
|
||||
if (table is null || !table.Spells.TryGetValue(spellId, out var spell)) return GpuTextureSlot.Unassigned;
|
||||
|
||||
uint power = spell.Components.Count == 0
|
||||
? 0u
|
||||
|
|
@ -321,7 +323,7 @@ public sealed class IconComposer
|
|||
var layers = new List<(byte[] rgba, int w, int h)>();
|
||||
AddLayer(layers, powerBacking);
|
||||
AddLayer(layers, spell.Icon);
|
||||
if (layers.Count == 0) return 0u;
|
||||
if (layers.Count == 0) return GpuTextureSlot.Unassigned;
|
||||
|
||||
var composed = Compose(layers);
|
||||
uint tintIndex = (spell.Bitfield & DatReaderWriter.Enums.SpellIndex.Reversed) != 0
|
||||
|
|
@ -344,7 +346,7 @@ public sealed class IconComposer
|
|||
(overlay.Rgba8, overlay.Width, overlay.Height)]);
|
||||
}
|
||||
|
||||
uint texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
GpuTextureSlot texture = _cache.UploadRgba8(composed.rgba, composed.w, composed.h, nearest: true);
|
||||
_spellIcons[spellId] = texture;
|
||||
return texture;
|
||||
}
|
||||
|
|
@ -353,11 +355,11 @@ public sealed class IconComposer
|
|||
/// Retail ClientMagicSystem::CompositeSpellComponentIcon (0x00567720).
|
||||
/// Components use their raw DAT art with pure white replaced by black.
|
||||
/// </summary>
|
||||
public uint GetSpellComponentIcon(uint iconId)
|
||||
public GpuTextureSlot GetSpellComponentIcon(uint iconId)
|
||||
{
|
||||
if (iconId == 0u) return 0u;
|
||||
if (_componentIcons.TryGetValue(iconId, out uint cached)) return cached;
|
||||
if (!TryDecode(iconId, out DecodedTexture icon)) return 0u;
|
||||
if (iconId == 0u) return GpuTextureSlot.Unassigned;
|
||||
if (_componentIcons.TryGetValue(iconId, out GpuTextureSlot cached)) return cached;
|
||||
if (!TryDecode(iconId, out DecodedTexture icon)) return GpuTextureSlot.Unassigned;
|
||||
byte[] rgba = (byte[])icon.Rgba8.Clone();
|
||||
for (int i = 0; i + 3 < rgba.Length; i += 4)
|
||||
{
|
||||
|
|
@ -365,7 +367,7 @@ public sealed class IconComposer
|
|||
continue;
|
||||
rgba[i] = rgba[i + 1] = rgba[i + 2] = 0;
|
||||
}
|
||||
uint texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true);
|
||||
GpuTextureSlot texture = _cache.UploadRgba8(rgba, icon.Width, icon.Height, nearest: true);
|
||||
_componentIcons[iconId] = texture;
|
||||
return texture;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Where a dragged item came from — the retail <c>InqDropIconInfo</c> flag
|
||||
/// Where a dragged item came from — the retail <c>InqDropIconInfo</c> flag
|
||||
/// distinction (<c>flags & 0xE == 0</c> fresh-from-inventory vs
|
||||
/// <c>flags & 4</c> within-list reorder) expressed as a typed enum. The drop
|
||||
/// handler maps SourceKind + target back to the fresh-vs-reorder decision.
|
||||
/// Decomp anchors: gmToolbarUI 0x004bd162 / 0x004bd1af; InqDropIconInfo 230533.
|
||||
/// </summary>
|
||||
public enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
|
||||
internal enum ItemDragSource { Inventory, ShortcutBar, Equipment, Ground }
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of a drag-in-progress, taken at drag-begin (so a server move arriving
|
||||
/// mid-drag can't mutate it under us). Port of retail's <c>m_dragElement</c> +
|
||||
/// <c>InqDropIconInfo</c> out-params (objId/container/flags, decomp 230533).
|
||||
/// <para><c>SourceContainer</c> is intentionally NOT stored: the handler resolves the
|
||||
/// LIVE container via <c>ClientObjectTable.Get(ObjId).ContainerId</c> at drop — the
|
||||
/// LIVE container via <c>ClientObjectTable.Get(ObjId).ContainerId</c> at drop — the
|
||||
/// same container id retail reads off the dragged element, single source of truth.</para>
|
||||
/// </summary>
|
||||
public sealed record ItemDragPayload(
|
||||
internal sealed record ItemDragPayload(
|
||||
uint ObjId, // dragged weenie guid (retail itemID, +0x5FC)
|
||||
ItemDragSource SourceKind, // what kind of slot it left
|
||||
int SourceSlot, // the source cell's SlotIndex (retail m_lastShortcutNumDragged)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
|
@ -6,14 +6,14 @@ using AcDream.Runtime.Gameplay;
|
|||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Result of offering a primary click to active item-target mode.</summary>
|
||||
public enum ItemPrimaryClickResult
|
||||
internal enum ItemPrimaryClickResult
|
||||
{
|
||||
NotActive,
|
||||
ConsumedSuccess,
|
||||
ConsumedRejected,
|
||||
}
|
||||
|
||||
public readonly record struct PendingBackpackPlacement(
|
||||
internal readonly record struct PendingBackpackPlacement(
|
||||
ulong Token,
|
||||
uint ItemId,
|
||||
uint ContainerId,
|
||||
|
|
@ -25,7 +25,7 @@ public readonly record struct PendingBackpackPlacement(
|
|||
/// target acquisition, and drag-out drops here instead of duplicating
|
||||
/// ItemHolder::UseObject fragments in each panel.
|
||||
/// </summary>
|
||||
public sealed class ItemInteractionController : IDisposable
|
||||
internal sealed class ItemInteractionController : IDisposable
|
||||
{
|
||||
internal const string InventoryRequestBusyMessage =
|
||||
"You can only move or use one item at a time";
|
||||
|
|
@ -1230,7 +1230,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
failures);
|
||||
}
|
||||
|
||||
public readonly record struct AppraisalResponseAcceptance(
|
||||
internal readonly record struct AppraisalResponseAcceptance(
|
||||
bool Accepted,
|
||||
bool FirstResponse);
|
||||
|
||||
|
|
@ -1308,7 +1308,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
{
|
||||
var target = _objects.Get(targetGuid);
|
||||
if (target is null)
|
||||
return false; // retail: GetWeenieObject(target) null → incompatible
|
||||
return false; // retail: GetWeenieObject(target) null → incompatible
|
||||
return ItemInteractionPolicy.IsTargetCompatible(
|
||||
Snapshot(source), Snapshot(target), _playerGuid());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using AcDream.App.Spells;
|
||||
using AcDream.Core.Combat;
|
||||
|
|
@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// appraisal response/subview lifecycle; the imported LayoutDesc owns all
|
||||
/// chrome, geometry, fonts, and scrollbars.
|
||||
/// </summary>
|
||||
public sealed class AppraisalUiController : IRetainedPanelController
|
||||
internal sealed class AppraisalUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x2100006Bu;
|
||||
public const uint RootId = 0x100005F2u;
|
||||
|
|
@ -73,8 +73,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
private readonly CreatureAppraisalRowTemplateFactory? _creatureRowTemplates;
|
||||
private readonly CreatureDisplayNameResolver _creatureNames;
|
||||
private readonly RetailAppraisalNameResolver _itemNames;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<uint, uint> _resolveComponentIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveSpellIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveComponentIcon;
|
||||
private readonly Func<uint, IReadOnlyList<SpellExamineComponent>> _spellComponents;
|
||||
private readonly Func<MagicSchool, uint> _magicSkill;
|
||||
private readonly SpellExamineComponentTemplateFactory? _spellComponentTemplates;
|
||||
|
|
@ -128,8 +128,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates,
|
||||
CreatureDisplayNameResolver? creatureNames,
|
||||
RetailAppraisalNameResolver? itemNames,
|
||||
Func<uint, uint>? resolveSpellIcon,
|
||||
Func<uint, uint>? resolveComponentIcon,
|
||||
Func<uint, GpuTextureSlot>? resolveSpellIcon,
|
||||
Func<uint, GpuTextureSlot>? resolveComponentIcon,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>>? spellComponents,
|
||||
Func<MagicSchool, uint>? magicSkill,
|
||||
SpellExamineComponentTemplateFactory? spellComponentTemplates)
|
||||
|
|
@ -155,8 +155,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
?? new CreatureDisplayNameResolver(
|
||||
new Dictionary<uint, string>());
|
||||
_itemNames = itemNames ?? RetailAppraisalNameResolver.Empty;
|
||||
_resolveSpellIcon = resolveSpellIcon ?? (_ => 0u);
|
||||
_resolveComponentIcon = resolveComponentIcon ?? (_ => 0u);
|
||||
_resolveSpellIcon = resolveSpellIcon ?? (_ => GpuTextureSlot.Unassigned);
|
||||
_resolveComponentIcon = resolveComponentIcon ?? (_ => GpuTextureSlot.Unassigned);
|
||||
_spellComponents = spellComponents ?? (_ => []);
|
||||
_magicSkill = magicSkill ?? (_ => 0u);
|
||||
_spellComponentTemplates = spellComponentTemplates;
|
||||
|
|
@ -283,8 +283,8 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
CreatureAppraisalRowTemplateFactory? creatureRowTemplates = null,
|
||||
CreatureDisplayNameResolver? creatureNames = null,
|
||||
RetailAppraisalNameResolver? itemNames = null,
|
||||
Func<uint, uint>? resolveSpellIcon = null,
|
||||
Func<uint, uint>? resolveComponentIcon = null,
|
||||
Func<uint, GpuTextureSlot>? resolveSpellIcon = null,
|
||||
Func<uint, GpuTextureSlot>? resolveComponentIcon = null,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>>? spellComponents = null,
|
||||
Func<MagicSchool, uint>? magicSkill = null,
|
||||
SpellExamineComponentTemplateFactory? spellComponentTemplates = null)
|
||||
|
|
@ -496,7 +496,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
_characterObjectId = 0;
|
||||
_spellId = 0u;
|
||||
_refreshElapsed = 0;
|
||||
_spellIcon.Texture = 0u;
|
||||
_spellIcon.Texture = GpuTextureSlot.Unassigned;
|
||||
SetSpellText(_spellSchool, string.Empty);
|
||||
SetSpellText(_spellMana, string.Empty);
|
||||
SetSpellText(_spellDuration, string.Empty);
|
||||
|
|
@ -1037,7 +1037,7 @@ public sealed class AppraisalUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public enum AppraisalView
|
||||
internal enum AppraisalView
|
||||
{
|
||||
Item,
|
||||
Creature,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// localized report into <c>m_pMainText</c>; it is not a second character
|
||||
/// sheet and therefore does not repeat name, level, title, or current vitals.
|
||||
/// </summary>
|
||||
public static class CharacterController
|
||||
internal static class CharacterController
|
||||
{
|
||||
public const uint LayoutId = 0x2100006Eu;
|
||||
public const uint RootId = 0x10000183u;
|
||||
|
|
@ -343,7 +343,7 @@ public static class CharacterController
|
|||
/// Event-invalidated report owner for retail's character-information text.
|
||||
/// Stable frames borrow the same shaped line collection.
|
||||
/// </summary>
|
||||
public sealed class CharacterInformationUiController : IRetainedPanelController
|
||||
internal sealed class CharacterInformationUiController : IRetainedPanelController
|
||||
{
|
||||
private readonly Func<CharacterSheet> _data;
|
||||
private readonly CharacterInfoStrings _strings;
|
||||
|
|
@ -402,7 +402,7 @@ public sealed class CharacterInformationUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record CharacterInfoStrings(
|
||||
internal sealed record CharacterInfoStrings(
|
||||
string[] Birth,
|
||||
string[] Played,
|
||||
string DeathsNone,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -17,9 +17,9 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>UpdateAugmentations</c> (0x004b9000), and
|
||||
/// <c>UpdateLoad</c> (0x004b8a20).</para>
|
||||
/// </summary>
|
||||
public sealed class CharacterSheet
|
||||
internal sealed class CharacterSheet
|
||||
{
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
// ── Identity ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Character name (first line of the report).</summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
|
@ -39,8 +39,8 @@ public sealed class CharacterSheet
|
|||
/// <summary>Title string, e.g. "the Adventurer". Null = omit.</summary>
|
||||
public string? Title { get; init; }
|
||||
|
||||
// ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
|
||||
// UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
|
||||
// ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
|
||||
// UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
|
||||
|
||||
/// <summary>Total accrued experience (retail PropertyInt64 1). Header value
|
||||
/// element 0x10000235 (m_pTotalXPText).</summary>
|
||||
|
|
@ -50,7 +50,7 @@ public sealed class CharacterSheet
|
|||
/// 0x10000238 (m_pXPToLevelText).</summary>
|
||||
public long XpToNextLevel { get; init; }
|
||||
|
||||
/// <summary>XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)).
|
||||
/// <summary>XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)).
|
||||
/// Drives the header meter 0x10000236 (m_pXPToLevelMeter).</summary>
|
||||
public float XpFraction { get; init; }
|
||||
|
||||
|
|
@ -58,13 +58,13 @@ public sealed class CharacterSheet
|
|||
/// 0x10000233 (m_pPKStatusText). Null = omit.</summary>
|
||||
public string? PkStatus { get; init; }
|
||||
|
||||
// ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
|
||||
// ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
|
||||
|
||||
/// <summary>Formatted birth date string (retail InqInt(0x62) → strftime).
|
||||
/// <summary>Formatted birth date string (retail InqInt(0x62) → strftime).
|
||||
/// Null = omit the birth line.</summary>
|
||||
public string? BirthDate { get; init; }
|
||||
|
||||
/// <summary>Formatted play-time duration (retail InqInt(0x7d) → QueryDuration).
|
||||
/// <summary>Formatted play-time duration (retail InqInt(0x7d) → QueryDuration).
|
||||
/// Null = omit the age line.</summary>
|
||||
public string? PlayTime { get; init; }
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ public sealed class CharacterSheet
|
|||
/// <summary>Raw retail PropertyInt 0x7D seconds. Null means the quality was absent.</summary>
|
||||
public int? TotalPlayTimeSeconds { get; init; }
|
||||
|
||||
// ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ─────────────────────────────
|
||||
// ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ─────────────────────────────
|
||||
|
||||
public int HealthCurrent { get; init; }
|
||||
public int HealthMax { get; init; }
|
||||
|
|
@ -86,7 +86,7 @@ public sealed class CharacterSheet
|
|||
public int ManaCurrent { get; init; }
|
||||
public int ManaMax { get; init; }
|
||||
|
||||
// ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
|
||||
// ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
|
||||
// InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self.
|
||||
|
||||
public int Strength { get; init; }
|
||||
|
|
@ -96,9 +96,9 @@ public sealed class CharacterSheet
|
|||
public int Focus { get; init; }
|
||||
public int Self { get; init; }
|
||||
|
||||
// ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
|
||||
// ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
|
||||
// Character Information uses 0xB5/0xC0 for Chess/Fishing; skill credits use 0x18.
|
||||
// InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab.
|
||||
// InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab.
|
||||
|
||||
public int UnspentSkillCredits { get; init; }
|
||||
public int SpecializedSkillCredits { get; init; }
|
||||
|
|
@ -111,21 +111,21 @@ public sealed class CharacterSheet
|
|||
|
||||
/// <summary>
|
||||
/// Available (unspent) skill credits shown in the Attributes tab footer State-A.
|
||||
/// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0).
|
||||
/// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0).
|
||||
/// Element 0x10000243 (footer line-1 value in the studio's 3-line layout).
|
||||
/// </summary>
|
||||
public int SkillCredits { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Unassigned (banked) experience points.
|
||||
/// Retail InqInt64(2) — shown in footer line-2 in State-A display.
|
||||
/// Retail InqInt64(2) — shown in footer line-2 in State-A display.
|
||||
/// Element 0x10000245 (footer line-2 value).
|
||||
/// </summary>
|
||||
public long UnassignedXp { get; init; }
|
||||
|
||||
// ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ──
|
||||
// Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent.
|
||||
// Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent.
|
||||
// ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ──
|
||||
// Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent.
|
||||
// Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent.
|
||||
// Cost 0 means the attribute is at max or not trainable. Ordered to match AttrRows:
|
||||
// Strength, Endurance, Coordination, Quickness, Focus, Self, Health, Stamina, Mana.
|
||||
// Source: gmAttributeUI::GetCostToRaise/GetCostToRaise10 (0x0049cb80/0x0049cc70).
|
||||
|
|
@ -150,7 +150,7 @@ public sealed class CharacterSheet
|
|||
/// </summary>
|
||||
public IReadOnlyList<CharacterSkill> Skills { get; init; } = Array.Empty<CharacterSkill>();
|
||||
|
||||
// ── Augmentations (UpdateAugmentations 0x004b9000) ─────────────────────
|
||||
// ── Augmentations (UpdateAugmentations 0x004b9000) ─────────────────────
|
||||
// Retail InqInt(0x162) = AugmentationStat; string-switch 1..0xb.
|
||||
|
||||
/// <summary>Augmentation name from the switch in UpdateAugmentations (0x004b9000),
|
||||
|
|
@ -165,7 +165,7 @@ public sealed class CharacterSheet
|
|||
public IReadOnlyDictionary<uint, int> CharacterInfoProperties { get; init; }
|
||||
= new Dictionary<uint, int>();
|
||||
|
||||
// ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
|
||||
// ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
|
||||
// Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance).
|
||||
|
||||
public int BurdenCurrent { get; init; }
|
||||
|
|
@ -173,7 +173,7 @@ public sealed class CharacterSheet
|
|||
public int EncumbranceAugmentations { get; init; }
|
||||
}
|
||||
|
||||
public enum CharacterSkillAdvancementClass
|
||||
internal enum CharacterSkillAdvancementClass
|
||||
{
|
||||
Inactive = 0,
|
||||
Untrained = 1,
|
||||
|
|
@ -181,7 +181,7 @@ public enum CharacterSkillAdvancementClass
|
|||
Specialized = 3,
|
||||
}
|
||||
|
||||
public sealed record CharacterSkill(
|
||||
internal sealed record CharacterSkill(
|
||||
uint Id,
|
||||
string Name,
|
||||
uint IconDid,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Player;
|
||||
|
|
@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// cited there too (gmAttributeUI::GetCostToRaise 0x0049cb80 family).</para>
|
||||
///
|
||||
/// <para><b>State ownership:</b> optimistic debits go through the owning
|
||||
/// store's eventful APIs — <see cref="ClientObjectTable.UpdateIntProperty"/> /
|
||||
/// store's eventful APIs — <see cref="ClientObjectTable.UpdateIntProperty"/> /
|
||||
/// <see cref="ClientObjectTable.UpdateInt64Property"/> (fires ObjectUpdated)
|
||||
/// when the player object is in the table, else
|
||||
/// <see cref="LocalPlayerState.DebitIntProperty"/> /
|
||||
|
|
@ -26,9 +26,9 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Never write the raw property dictionaries from UI code. The next server
|
||||
/// snapshot remains authoritative over every optimistic value.</para>
|
||||
/// </summary>
|
||||
public sealed class CharacterSheetProvider
|
||||
internal sealed class CharacterSheetProvider
|
||||
{
|
||||
/// <summary>PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp.</summary>
|
||||
/// <summary>PropertyInt64 2 = unassigned (banked) XP — CharacterSheet.UnassignedXp.</summary>
|
||||
private const uint UnassignedXpPropertyId = 2u;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -48,10 +48,10 @@ public sealed class CharacterSheetProvider
|
|||
private readonly Action<uint, ulong>? _sendRaiseSkill;
|
||||
private readonly Action<uint, uint>? _sendTrainSkill;
|
||||
|
||||
/// <summary>Portal SkillTable (0x0E000004) — set by the host once dats load.</summary>
|
||||
/// <summary>Portal SkillTable (0x0E000004) — set by the host once dats load.</summary>
|
||||
public DatReaderWriter.DBObjs.SkillTable? SkillTable { get; set; }
|
||||
|
||||
/// <summary>Portal ExperienceTable (0x0E000018) — set by the host once dats load.</summary>
|
||||
/// <summary>Portal ExperienceTable (0x0E000018) — set by the host once dats load.</summary>
|
||||
public DatReaderWriter.DBObjs.ExperienceTable? ExperienceTable { get; set; }
|
||||
|
||||
public CharacterSheetProvider(
|
||||
|
|
@ -89,7 +89,7 @@ public sealed class CharacterSheetProvider
|
|||
return new ChangeBinding(this, changed);
|
||||
}
|
||||
|
||||
// ── Sheet assembly ─────────────────────────────────────────────────────
|
||||
// ── Sheet assembly ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Best display name: active toon key, else the live object's name, else "Player".</summary>
|
||||
public string CharacterName()
|
||||
|
|
@ -240,8 +240,8 @@ public sealed class CharacterSheetProvider
|
|||
|
||||
/// <summary>
|
||||
/// Load the portal ExperienceTable (0x0E000018), falling back to a
|
||||
/// type scan for older or odd dat collections. Failures are logged —
|
||||
/// never silently swallowed — and leave raise costs unavailable (0).
|
||||
/// type scan for older or odd dat collections. Failures are logged —
|
||||
/// never silently swallowed — and leave raise costs unavailable (0).
|
||||
/// </summary>
|
||||
public static DatReaderWriter.DBObjs.ExperienceTable? LoadExperienceTable(
|
||||
IDatReaderWriter dats, Action<string>? log = null)
|
||||
|
|
@ -275,7 +275,7 @@ public sealed class CharacterSheetProvider
|
|||
}
|
||||
|
||||
/// <summary>XP still needed for the next level + fill fraction of the
|
||||
/// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction).</summary>
|
||||
/// current level band (retail (cur−base)/(cap−base); CharacterSheet.XpFraction).</summary>
|
||||
private (long toNext, float fraction) ComputeLevelXp(int level, long totalXp)
|
||||
{
|
||||
var levels = ExperienceTable?.Levels;
|
||||
|
|
@ -394,7 +394,7 @@ public sealed class CharacterSheetProvider
|
|||
}
|
||||
|
||||
/// <summary>Cost to advance <paramref name="amount"/> ranks along a retail
|
||||
/// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the
|
||||
/// cumulative-XP curve: curve[target] − xpAlreadySpent, clamped at the
|
||||
/// curve end (retail GetCostToRaise/GetCostToRaise10 0x0049cb80/0x0049cc70).</summary>
|
||||
private static long RaiseCostFromXpCurve(uint[]? curve, uint ranks, uint spentXp, int amount)
|
||||
{
|
||||
|
|
@ -427,13 +427,13 @@ public sealed class CharacterSheetProvider
|
|||
private int VitalMax(LocalPlayerState.VitalKind kind) =>
|
||||
_localPlayer.GetMaxApprox(kind) is { } max ? checked((int)Math.Min(int.MaxValue, max)) : 0;
|
||||
|
||||
// ── Raise-request flow ─────────────────────────────────────────────────
|
||||
// ── Raise-request flow ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Send a raise/train action to the server and, when a send delegate
|
||||
/// fired, optimistically apply the local effect so the sheet stays
|
||||
/// current during the round trip. The next server snapshot remains
|
||||
/// authoritative (a rejected raise is corrected by the property echo —
|
||||
/// authoritative (a rejected raise is corrected by the property echo —
|
||||
/// pending/rollback ledger tracked as a follow-up issue).
|
||||
/// </summary>
|
||||
public void HandleRaiseRequest(CharacterStatController.RaiseRequest request)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
|
@ -6,14 +6,14 @@ using AcDream.App.UI;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the Character window's <b>Attributes tab</b> — LayoutDesc 0x2100002E,
|
||||
/// Controller for the Character window's <b>Attributes tab</b> — LayoutDesc 0x2100002E,
|
||||
/// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root
|
||||
/// type 0x1000002A) which in turn chains into the gmStatManagementUI header content.
|
||||
///
|
||||
/// <para>Unlike <see cref="CharacterController"/> (which targets the SEPARATE text-report
|
||||
/// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element),
|
||||
/// this controller binds the <b>real, statically-mounted</b> header + list elements that the
|
||||
/// importer already produces — every id below is confirmed present via
|
||||
/// importer already produces — every id below is confirmed present via
|
||||
/// <see cref="ImportedLayout.FindElement"/>.</para>
|
||||
///
|
||||
/// <para>Ported from <c>gmStatManagementUI::UpdateCharacterInfo</c> (0x004f0770) +
|
||||
|
|
@ -23,7 +23,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>gmAttributeUI::PostInit</c> (0x0049db70) + <c>AttributeInfoRegion</c> / <c>Attribute2ndInfoRegion</c>
|
||||
/// (0x004f1910 / 0x004f19e0). Row icons loaded via sub-element 0x10000129 in the retail
|
||||
/// dat template (each icon is a <c>0x06xxxxxx</c> RenderSurface DataID from SubMap
|
||||
/// 0x25000006 / 0x25000007, spec §2).</para>
|
||||
/// 0x25000006 / 0x25000007, spec §2).</para>
|
||||
///
|
||||
/// <para>Footer State A (nothing selected) bound from
|
||||
/// <c>DisplayDefaultFooter</c> (0x0049cde0): title empty, line-1 value =
|
||||
|
|
@ -39,13 +39,13 @@ namespace AcDream.App.UI.Layout;
|
|||
/// UIStateId.Closed (0x0B). The imported Type-12 tab owns its authored font color and
|
||||
/// propagates the state to its three chrome children through PassToChildren.</para>
|
||||
///
|
||||
/// <para>Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
|
||||
/// <para>Raise buttons: 0x10000246 (×1) + 0x100005EB (×10). State "Normal" = affordable
|
||||
/// (UIStateId.Normal, 0x01), state "Ghosted" = unaffordable or no selection
|
||||
/// (UIStateId.Ghosted, 0x0D). Source: gmAttributeUI::AttributeInfoRegion::Update (0x004f1910).</para>
|
||||
/// </summary>
|
||||
public static class CharacterStatController
|
||||
internal static class CharacterStatController
|
||||
{
|
||||
// ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
|
||||
// ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
|
||||
public const uint NameId = 0x10000231u; // m_pNameText
|
||||
public const uint HeritageId = 0x10000232u; // m_pHeritageText
|
||||
public const uint PkStatusId = 0x10000233u; // m_pPKStatusText
|
||||
|
|
@ -64,16 +64,16 @@ public static class CharacterStatController
|
|||
public const uint ListScrollbarId = 0x1000023Eu; // m_pListBox vertical scrollbar gutter
|
||||
public const uint ListDividerId = 0x1000023Fu; // bottom divider above footer
|
||||
|
||||
// ── Footer STATE-A container id ──────────────────────────────────────────
|
||||
// ── Footer STATE-A container id ──────────────────────────────────────────
|
||||
// 0x10000240 is the "nothing selected" footer group. Its children (0x1000024E label row,
|
||||
// 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
|
||||
// 0x10000242–0x10000245 labels+values) are the correct State-A versions with wider
|
||||
// label widths (195px vs 145px in State B). _byId stores the LAST duplicate, which
|
||||
// is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
|
||||
// is the narrower State-B/C copy — so we walk the tree to 0x10000240 and bind from there.
|
||||
public const uint FooterStateAId = 0x10000240u; // State-A footer container (nothing selected)
|
||||
public const uint FooterStateBId = 0x10000241u; // State-B footer container (row selected)
|
||||
public const uint FooterStateCId = 0x10000247u; // State-C footer container (hide inactive)
|
||||
|
||||
// ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
|
||||
// ── Tab bar element ids (LayoutDesc 0x2100002E root) ────────────────────
|
||||
// These are imported Type-12 UIElement_Text tabs. Their Closed/Open states carry
|
||||
// the caption color and PassToChildren=true; their three retained children carry
|
||||
// the authored left/center/right chrome. Character and Spellbook therefore share
|
||||
|
|
@ -89,7 +89,7 @@ public static class CharacterStatController
|
|||
public const uint SkillsPageId = 0x1000022Cu;
|
||||
public const uint TitlesPageId = 0x10000539u;
|
||||
|
||||
// ── Footer element ids (gmStatManagementUI struct fields) ────────────────
|
||||
// ── Footer element ids (gmStatManagementUI struct fields) ────────────────
|
||||
// Source: acclient.h / DisplayDefaultFooter (0x0049cde0)
|
||||
public const uint FooterTitleId = 0x1000024eu; // GetFooterTitleLabel
|
||||
public const uint FooterLine1Label = 0x10000242u; // GetFooterLineOneLabel
|
||||
|
|
@ -97,26 +97,26 @@ public static class CharacterStatController
|
|||
public const uint FooterLine2Label = 0x10000244u; // GetFooterLineTwoLabel
|
||||
public const uint FooterLine2Value = 0x10000245u; // GetFooterLineTwoValue
|
||||
|
||||
// ── Raise button element ids ──────────────────────────────────────────────
|
||||
// ── Raise button element ids ──────────────────────────────────────────────
|
||||
// Source: gmAttributeUI::PostInit (0x0049db70); CM_Train::Event_TrainAttribute.
|
||||
// Button state "Normal" (UIStateId 0x01) = affordable (green/active);
|
||||
// "Ghosted" (UIStateId 0x0D) = disabled. Hidden when nothing is selected.
|
||||
public const uint RaiseOneId = 0x10000246u; // raise × 1
|
||||
public const uint RaiseTenId = 0x100005EBu; // raise × 10
|
||||
public const uint RaiseOneId = 0x10000246u; // raise × 1
|
||||
public const uint RaiseTenId = 0x100005EBu; // raise × 10
|
||||
|
||||
private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text
|
||||
private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold
|
||||
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// <summary>Row highlight color — semi-translucent gold, matches retail
|
||||
/// UIStateId.Highlight (0x06) sprite 0x06001397 visual intent.</summary>
|
||||
private static readonly Vector4 HighlightBg = new(1f, 0.75f, 0.2f, 0.25f);
|
||||
private static readonly Vector4 BuffedSkillGreen = new(0.55f, 1f, 0.55f, 1f);
|
||||
|
||||
// ── Row layout constants ─────────────────────────────────────────────────
|
||||
// ── Row layout constants ─────────────────────────────────────────────────
|
||||
// RowHeight 22px + IconSize 16px: retail spec (2026-06-26) says icons ~icon-height
|
||||
// and rows tighter. 16px icon fits inside 22px row with 3px vertical padding each side.
|
||||
// The larger row font (0x40000001, MaxCharHeight=18) is clipped to the 22px height which
|
||||
// gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
|
||||
// gives a tight-but-readable line. Retail spec (2026-06-26 ref): "rows tighter, text ≈ icon height".
|
||||
private const float RowHeight = 22f;
|
||||
private const float IconSize = 16f;
|
||||
private const float RowPadX = 4f;
|
||||
|
|
@ -146,7 +146,7 @@ public static class CharacterStatController
|
|||
Skills,
|
||||
}
|
||||
|
||||
public enum RaiseTargetKind
|
||||
internal enum RaiseTargetKind
|
||||
{
|
||||
Attribute,
|
||||
Vital,
|
||||
|
|
@ -154,7 +154,7 @@ public static class CharacterStatController
|
|||
TrainSkill,
|
||||
}
|
||||
|
||||
public readonly record struct RaiseRequest(
|
||||
internal readonly record struct RaiseRequest(
|
||||
RaiseTargetKind Kind,
|
||||
uint StatId,
|
||||
long Cost,
|
||||
|
|
@ -170,7 +170,7 @@ public static class CharacterStatController
|
|||
|
||||
private sealed record SkillRowBinding(UiClickablePanel Panel, CharacterSkill Skill);
|
||||
|
||||
// ── Attribute row descriptors — retail display order per spec §1 ─────────
|
||||
// ── Attribute row descriptors — retail display order per spec §1 ─────────
|
||||
private static readonly (string name, uint iconDid, uint statId)[] AttrRows = new[]
|
||||
{
|
||||
("Strength", 0x060002C8u, 1u),
|
||||
|
|
@ -210,7 +210,7 @@ public static class CharacterStatController
|
|||
Func<CharacterSheet> data,
|
||||
UiDatFont? datFont = null,
|
||||
UiDatFont? rowDatFont = null,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve = null,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve = null,
|
||||
RaiseRequestHandler? onRaiseRequest = null,
|
||||
Action? onClose = null)
|
||||
{
|
||||
|
|
@ -231,35 +231,35 @@ public static class CharacterStatController
|
|||
UiElement? contentPage = FindDirectChildById(layout.Root, AttributesPageId);
|
||||
|
||||
// Name (18px from dat FontDid), Heritage (14px), PkStatus (14px):
|
||||
// Fix C: pass null → Label's null-guard keeps the build-time dat font.
|
||||
// Fix C: pass null → Label's null-guard keeps the build-time dat font.
|
||||
// Controllers still own the text color and the LinesProvider.
|
||||
// Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
|
||||
// Name = WHITE (retail "Horan" is white — confirmed 2026-06-26).
|
||||
Label(layout, contentPage, NameId, null, Vector4.One, () => data().Name);
|
||||
Label(layout, contentPage, HeritageId, null, Body, () => CharacterIdentityText.StatHeaderLine(data()));
|
||||
Label(layout, contentPage, PkStatusId, null, Body, () => data().PkStatus ?? string.Empty);
|
||||
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
// ── Header captions (new — retail labels above/left of each number) ──────
|
||||
// LevelCaption (0x1000023A, 16px from dat): pass null → keep build-time dat font.
|
||||
LabelTwoLine(layout, contentPage, LevelCaptionId, null, Body, "Character", "Level");
|
||||
|
||||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Level number: retail renders this as large gold centered text in the 65×50 element.
|
||||
// Fix C: the dat FontDid for the level element (0x1000023B) is now applied at build
|
||||
// time when the font resolver is provided (studio path). We no longer force rowDatFont
|
||||
// here for the level — the dat's own FontDid drives the font. The Gold color is still
|
||||
// here for the level — the dat's own FontDid drives the font. The Gold color is still
|
||||
// set via LinesProvider. SYNTHESIZED elements (the 9 attribute rows built in
|
||||
// BuildAttributeRows) continue to use datFont directly since they have no dat origin.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
|
||||
// Source: spec §Level area (65,50) + decomp gmStatManagementUI::UpdateCharacterInfo 0x004f0770.
|
||||
// runtime color, dat carries none.
|
||||
Label(layout, contentPage, LevelId, null, Gold, () => data().Level.ToString());
|
||||
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
// TotalXpLabel (16px from dat) + TotalXp (16px from dat): pass null → keep dat font.
|
||||
LabelLeft(layout, contentPage, TotalXpLabelId, null, Body, static () => "Total Experience (XP):");
|
||||
LabelRight(layout, contentPage, TotalXpId, null, Body, () => data().TotalXp.ToString("N0"));
|
||||
|
||||
// XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70).
|
||||
// Fix 5: child elements 0x10000237 (label) and 0x10000238 (value) are now built by
|
||||
// the LayoutImporter as UiText children of the XP meter (non-Type-3 meter children
|
||||
// are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
|
||||
// are explicitly built and registered in byId — see LayoutImporter.BuildWidget).
|
||||
// FindElement now returns them; the controller binds their LinesProvider.
|
||||
// The importer builds them as UiText via DatWidgetFactory.BuildText, applying their
|
||||
// dat-origin HJustify/VJustify/FontDid/FontColor at build time. The controller then
|
||||
|
|
@ -272,21 +272,21 @@ public static class CharacterStatController
|
|||
// Bind the dat-origin XP label (0x10000237) and value (0x10000238).
|
||||
// These are now real UiText children of the meter (built by the importer).
|
||||
// The retail layout places the caption + value ON TOP of the red bar
|
||||
// (ref 2026-06-26: "value … with the red fill bar behind it").
|
||||
// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
|
||||
// (ref 2026-06-26: "value … with the red fill bar behind it").
|
||||
// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1).
|
||||
if (FindTextByDatId(layout, contentPage, XpNextLabelId) is UiText xpLabel)
|
||||
{
|
||||
if (datFont is not null) xpLabel.DatFont = datFont;
|
||||
xpLabel.ClickThrough = true;
|
||||
xpLabel.Centered = false; // left-align (retail: aligns with Total XP label above)
|
||||
xpLabel.RightAligned = false;
|
||||
xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
|
||||
xpLabel.Padding = 0f; // avoid scroll clip — meter bar is ~13px tall
|
||||
|
||||
// Item 1: align the XP-next label's left edge to match the TotalXpLabel's
|
||||
// absolute left edge. The XP-next label is a child of the meter (local coords),
|
||||
// so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
|
||||
// so its Left = TotalXpLabel.Left − meter.Left. This accounts for the meter's
|
||||
// horizontal offset within the panel (the meter starts to the right of the
|
||||
// "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
|
||||
// "Total Experience (XP):" caption row). Source: retail spec §State 1 (the
|
||||
// "XP for next level:" caption left-aligns with "Total Experience (XP):" above).
|
||||
if (FindElementByDatId(layout, contentPage, TotalXpLabelId) is { } totalXpLbl)
|
||||
{
|
||||
|
|
@ -310,7 +310,7 @@ public static class CharacterStatController
|
|||
// The tab visuals are already retained in the imported LayoutDesc. Controllers
|
||||
// bind only click behavior and the active Open/Closed state below.
|
||||
|
||||
// ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
|
||||
// ── Attribute list — 9 rows in list box 0x1000023D ────────────────────
|
||||
// Mutable selected-index box: -1 = nothing selected.
|
||||
|
||||
// Gather EVERY copy of the raise buttons in the tree. The raise button ids
|
||||
|
|
@ -322,7 +322,7 @@ public static class CharacterStatController
|
|||
// At bind time the tree includes all three tab pages (the page-visibility pass
|
||||
// runs AFTER this). Collecting from the full tree is safe: once the page-
|
||||
// visibility pass hides the inactive pages their raise buttons are invisible
|
||||
// regardless of the Visible flag we set here — but the Attributes page's
|
||||
// regardless of the Visible flag we set here — but the Attributes page's
|
||||
// buttons (which are NOT hidden by the page pass) must be explicitly hidden.
|
||||
var allRaise1 = new List<UiButton>();
|
||||
var allRaise10 = new List<UiButton>();
|
||||
|
|
@ -365,7 +365,7 @@ public static class CharacterStatController
|
|||
foreach (var b in allRaise1) b.Visible = false;
|
||||
foreach (var b in allRaise10) b.Visible = false;
|
||||
|
||||
// ── Footer state visibility ───────────────────────────────────────────
|
||||
// ── Footer state visibility ───────────────────────────────────────────
|
||||
// There are THREE footer state groups (A=0x10000240, B=0x10000241, C=0x10000247)
|
||||
// all stacked at the same position within the Attributes page. _byId stores only
|
||||
// the LAST copy of each id; the others live in the VISIBLE Attributes page and must
|
||||
|
|
@ -373,13 +373,13 @@ public static class CharacterStatController
|
|||
//
|
||||
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
|
||||
// All three group elements have DefaultState = StatManagement_Footer_Default
|
||||
// (0x10000011) — the dat does NOT differentiate them by visibility. The parent
|
||||
// (0x10000011) — the dat does NOT differentiate them by visibility. The parent
|
||||
// element (0x1000022F) has a States map {Default, Text, Meter} with PassToChildren=
|
||||
// true, but each child group also registers all three states (IncFlags=None,
|
||||
// Media=0) — meaning the state-propagation produces no media change on any group.
|
||||
// Media=0) — meaning the state-propagation produces no media change on any group.
|
||||
// Retail's gmStatManagementUI uses hardcoded element-id dispatch
|
||||
// (GetChildRecursive(this, 0x10000240) for Default, 0x10000241 for Text, 0x10000247
|
||||
// for Meter) to access the right group's children at runtime — the groups themselves
|
||||
// for Meter) to access the right group's children at runtime — the groups themselves
|
||||
// are never hidden/shown via the dat state mechanism. The controller is the correct
|
||||
// and only place for this visibility management. See retail decomp
|
||||
// gmStatManagementUI::GetFooterTitleLabel @0x004f0170.
|
||||
|
|
@ -389,7 +389,7 @@ public static class CharacterStatController
|
|||
// selected and owns the retail raise buttons; State C stays hidden for now.
|
||||
SetFooterSelected(false);
|
||||
|
||||
// ── Footer State A initial binding ────────────────────────────────────
|
||||
// ── Footer State A initial binding ────────────────────────────────────
|
||||
// Walk to the State-A container directly (rather than _byId which returns the
|
||||
// last duplicate) so we get the wider-label copies (195px) for the unselected state.
|
||||
BindFooterDynamic(layout, datFont, data, activeTab, attrSel, skillSel, contentPage);
|
||||
|
|
@ -428,10 +428,10 @@ public static class CharacterStatController
|
|||
RetailTabBinding.SetClick(titlesTab, null);
|
||||
UpdateTabStates();
|
||||
|
||||
// ── Active-page selection (fixes the dark-overlay) ─────────────────────
|
||||
// ── Active-page selection (fixes the dark-overlay) ─────────────────────
|
||||
// WHY this cannot be done in the importer (dat state-model audit 2026-06-26):
|
||||
// The three tab-page content areas (0x1000022B Attributes, 0x1000022C Skills,
|
||||
// 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
|
||||
// 0x10000539 Titles) all have DefaultState = Undef (0) — the dat carries no
|
||||
// visibility encoding for tabs. Tab visibility is managed at runtime by gmTabUI
|
||||
// via SetVisible(bool) on the page containers. The controller is the correct
|
||||
// and only place for initial tab-page selection.
|
||||
|
|
@ -565,7 +565,7 @@ public static class CharacterStatController
|
|||
ImportedLayout layout,
|
||||
UiElement? contentPage,
|
||||
UiElement? statList,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve)
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve)
|
||||
{
|
||||
if (spriteResolve is null)
|
||||
return null;
|
||||
|
|
@ -624,7 +624,7 @@ public static class CharacterStatController
|
|||
|
||||
private static void ConfigureSkillScrollbar(
|
||||
UiScrollbar bar,
|
||||
Func<uint, (uint handle, int w, int h)> spriteResolve)
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)> spriteResolve)
|
||||
{
|
||||
bar.SpriteResolve = id => { var (h, w, ht) = spriteResolve(id); return (h, w, ht); };
|
||||
bar.TrackSprite = ScrollTrackSprite;
|
||||
|
|
@ -649,12 +649,12 @@ public static class CharacterStatController
|
|||
: SkillContentWidth;
|
||||
}
|
||||
|
||||
// ── 9-row attribute list ─────────────────────────────────────────────────
|
||||
// ── 9-row attribute list ─────────────────────────────────────────────────
|
||||
|
||||
private static List<UiClickablePanel> BuildAttributeRows(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
int[] sel,
|
||||
List<UiButton> allRaise1,
|
||||
|
|
@ -736,7 +736,7 @@ public static class CharacterStatController
|
|||
private static List<UiElement> BuildSkillRows(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
int[] sel,
|
||||
List<UiButton> allRaise1,
|
||||
|
|
@ -792,7 +792,7 @@ public static class CharacterStatController
|
|||
private static UiPanel AddSkillHeader(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
float left,
|
||||
float top,
|
||||
float width,
|
||||
|
|
@ -878,14 +878,14 @@ public static class CharacterStatController
|
|||
: Vector4.One;
|
||||
|
||||
/// <summary>
|
||||
/// Handles a row click: toggle (same row → deselect), else select new row.
|
||||
/// Handles a row click: toggle (same row → deselect), else select new row.
|
||||
/// Updates highlight, footer providers, and raise-button state.
|
||||
/// </summary>
|
||||
private static void HandleRowClick(
|
||||
int clickedIndex,
|
||||
int[] sel,
|
||||
List<UiClickablePanel> rows,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
List<UiButton> allRaise1,
|
||||
List<UiButton> allRaise10)
|
||||
|
|
@ -895,10 +895,10 @@ public static class CharacterStatController
|
|||
|
||||
// Log for live test confirmation (user tests selection in the studio).
|
||||
string rowName = GetRowName(newSel);
|
||||
Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
|
||||
Console.WriteLine($"[CharacterStat] Row click: index={clickedIndex} → selected={newSel} ({rowName})");
|
||||
|
||||
// Update highlight on all rows.
|
||||
// Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
|
||||
// Retail uses sprite 0x06001397 (Button state 6 — the dark horizontal bars)
|
||||
// for the selected row background. When spriteResolve is available, apply the
|
||||
// sprite; otherwise fall back to the translucent gold tint.
|
||||
const uint HighlightSprite = 0x06001397u;
|
||||
|
|
@ -936,7 +936,7 @@ public static class CharacterStatController
|
|||
int clickedIndex,
|
||||
int[] sel,
|
||||
List<SkillRowBinding> rows,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
Func<CharacterSheet> data,
|
||||
List<UiButton> allRaise1,
|
||||
List<UiButton> allRaise10)
|
||||
|
|
@ -956,7 +956,7 @@ public static class CharacterStatController
|
|||
private static void ApplySkillSelectionVisuals(
|
||||
int selectedIndex,
|
||||
IReadOnlyList<SkillRowBinding> rows,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve)
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve)
|
||||
{
|
||||
for (int i = 0; i < rows.Count; i++)
|
||||
{
|
||||
|
|
@ -1227,7 +1227,7 @@ public static class CharacterStatController
|
|||
private static UiClickablePanel AddRow(
|
||||
UiElement list,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, (uint handle, int w, int h)>? spriteResolve,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)>? spriteResolve,
|
||||
float left, float top, float width, float height,
|
||||
uint iconDid,
|
||||
string nameText,
|
||||
|
|
@ -1317,7 +1317,7 @@ public static class CharacterStatController
|
|||
return row;
|
||||
}
|
||||
|
||||
// ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
|
||||
// ── Footer — dynamic (State A + State B via sel[]) ────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Bind all 5 footer elements with providers that close over <paramref name="sel"/>:
|
||||
|
|
@ -1361,7 +1361,7 @@ public static class CharacterStatController
|
|||
// IMPORTANT: The footer state id (0x10000240) appears once per tab-page sub-layout
|
||||
// (Attributes / Skills / Titles). layout._byId stores only the LAST registered copy,
|
||||
// which ends up in the LAST-imported tab page (Titles). The page-visibility pass
|
||||
// hides the Titles page → the bound footer elements would be invisible.
|
||||
// hides the Titles page → the bound footer elements would be invisible.
|
||||
//
|
||||
// Fix: find State A/B inside the explicit Attributes page, not via _byId. The
|
||||
// id dictionary stores the last duplicate and can point at a hidden Skills/Titles
|
||||
|
|
@ -1401,19 +1401,19 @@ public static class CharacterStatController
|
|||
// The dat title element is H=55 (the full footer box). The dat says VJustify=Center, so
|
||||
// without an override the text would center vertically in the 55px box, overlapping
|
||||
// line-1/line-2 below. We set VerticalJustify=Top explicitly so the text renders at the
|
||||
// top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
|
||||
// The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
|
||||
// top of the 55px box (y≈Padding), keeping all three footer lines non-overlapping.
|
||||
// The dat says HJustify=Center (Centered=true from BuildText) — the title is centered.
|
||||
// BackgroundSprite cleared: its full-height sprite would cover line-1/line-2.
|
||||
titleEl.BackgroundSprite = 0;
|
||||
titleEl.VerticalJustify = VJustify.Top; // dat says Center; override to Top (see comment above)
|
||||
titleEl.OneLine = true;
|
||||
}
|
||||
// Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
|
||||
// Title (FooterTitle 0x1000024E, 20px from dat): pass null → keep dat font.
|
||||
// Fix C: the dat has a 20px font for the footer title. Let it drive.
|
||||
if (titleEl is not null)
|
||||
{
|
||||
// DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// DatFont: null → keep the build-time dat font (20px in studio, global fallback in live game).
|
||||
// Centered=true comes from the dat (HJustify=Center) via BuildText — not overridden here.
|
||||
// RightAligned stays false (BuildText default for a Center element).
|
||||
titleEl.ClickThrough = true;
|
||||
titleEl.LinesProvider = () =>
|
||||
|
|
@ -1440,7 +1440,7 @@ public static class CharacterStatController
|
|||
};
|
||||
}
|
||||
|
||||
// Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
|
||||
// Footer lines (all dat-origin with their own font sizes): pass null → keep dat font.
|
||||
var l1L = ByPos(20f, 5f, FooterLine1Label);
|
||||
LabelProvider(l1L, null, Body, () =>
|
||||
{
|
||||
|
|
@ -1475,7 +1475,7 @@ public static class CharacterStatController
|
|||
return cost > 0 ? cost.ToString("N0") : "Infinity!";
|
||||
});
|
||||
|
||||
// Line-2 elements: pass null → keep dat font.
|
||||
// Line-2 elements: pass null → keep dat font.
|
||||
var l2L = ByPos(37f, 5f, FooterLine2Label);
|
||||
LabelProvider(l2L, null, Body, () =>
|
||||
{
|
||||
|
|
@ -1596,7 +1596,7 @@ public static class CharacterStatController
|
|||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
private static void SetCompatibilityAnchorsAllById(
|
||||
UiElement node,
|
||||
|
|
@ -1707,7 +1707,7 @@ public static class CharacterStatController
|
|||
/// <see cref="UiText"/> renders multiple lines oldest-first (top-to-bottom), so
|
||||
/// line 0 = <paramref name="line1"/> (top) and line 1 = <paramref name="line2"/> (bottom).
|
||||
/// This replaces the single-line "Character Level" caption which truncated in the 65px element.
|
||||
/// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).</summary>
|
||||
/// Source: retail spec (2026-06-26-character-window-retail-reference.md §State 1 level caption).</summary>
|
||||
private static void LabelTwoLine(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color,
|
||||
string line1, string line2)
|
||||
=> LabelTwoLine(layout, null, id, datFont, color, line1, line2);
|
||||
|
|
@ -1719,7 +1719,7 @@ public static class CharacterStatController
|
|||
{
|
||||
// Null = keep whatever the importer (dat FontDid resolver) set at build time.
|
||||
if (datFont is not null) t.DatFont = datFont;
|
||||
t.Centered = false; // non-Centered → scroll/multi-line path
|
||||
t.Centered = false; // non-Centered → scroll/multi-line path
|
||||
t.RightAligned = false;
|
||||
t.ClickThrough = true;
|
||||
t.Padding = 1f;
|
||||
|
|
@ -1732,7 +1732,7 @@ public static class CharacterStatController
|
|||
}
|
||||
|
||||
/// <summary>Left-justified label (for captions that should be left-aligned, not centered).
|
||||
/// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
|
||||
/// Padding=0 so a single dat-font line (≈12px) fits cleanly in a small element without
|
||||
/// being clipped by the bottom-pin scroll math (top=Padding, bottom=H-Padding).</summary>
|
||||
private static void LabelLeft(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
=> LabelLeft(layout, null, id, datFont, color, text);
|
||||
|
|
@ -1776,8 +1776,8 @@ public static class CharacterStatController
|
|||
/// <summary>Bind a directly-located <see cref="UiText"/> widget with a provider.
|
||||
/// Used when the widget was found by subtree walk rather than <c>FindElement</c>.
|
||||
/// Sets <c>Padding = 0</c> to prevent the scroll-clip from hiding text in small
|
||||
/// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
|
||||
/// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.</summary>
|
||||
/// (H≈17–18px) footer elements: with the default Padding=4 and a dat font line-height
|
||||
/// of ~12px the bottom-pinned baseY ends up above the top clip boundary → blank.</summary>
|
||||
private static void LabelProvider(UiText? t, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
{
|
||||
if (t is null) return;
|
||||
|
|
@ -1798,7 +1798,7 @@ public static class CharacterStatController
|
|||
/// The standard <see cref="ImportedLayout.FindElement"/> returns only the LAST widget
|
||||
/// registered for a given id; for elements duplicated across tab-page sub-layouts
|
||||
/// (raise buttons, close buttons) we need ALL copies so that visibility changes are
|
||||
/// reflected in every page — not just the last-mounted one.
|
||||
/// reflected in every page — not just the last-mounted one.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -1810,14 +1810,14 @@ public static class CharacterStatController
|
|||
/// We therefore walk the tree recursively and collect every <see cref="UiButton"/> whose
|
||||
/// <c>ActiveState</c> reflects the dat default (before our code sets it), which is not a
|
||||
/// reliable discriminator. Instead, we gather ALL <see cref="UiButton"/> instances from
|
||||
/// the subtree at the known spatial position (bottom of the panel) — but positions can
|
||||
/// the subtree at the known spatial position (bottom of the panel) — but positions can
|
||||
/// overlap across pages.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The correct approach: since <c>_byId</c> stores only one instance per id, we use the
|
||||
/// <see cref="ImportedLayout.FindElement"/> for the canonical id, then do a FULL tree walk
|
||||
/// to find ADDITIONAL <see cref="UiButton"/> instances that have identical Width×Height to
|
||||
/// to find ADDITIONAL <see cref="UiButton"/> instances that have identical Width×Height to
|
||||
/// the known button. This works because the three page copies share the same dat template
|
||||
/// and thus the same geometry. Collected via reference-equality guard to avoid duplicates.
|
||||
/// </para>
|
||||
|
|
@ -1833,7 +1833,7 @@ public static class CharacterStatController
|
|||
_ = layout;
|
||||
|
||||
// Walk the tree and collect ALL UiButton instances matching the canonical geometry.
|
||||
// The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
|
||||
// The canonical copy itself will also be found — that's fine; use a HashSet to dedup.
|
||||
var seen = new HashSet<UiButton>(ReferenceEqualityComparer.Instance);
|
||||
CollectMatchingButtons(node, targetId, seen, result);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
|
@ -10,7 +10,7 @@ using AcDream.UI.Abstractions.Panels.Chat;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream
|
||||
/// Binds the imported chat LayoutDesc (0x21000006) to live behavior — the acdream
|
||||
/// analogue of retail <c>ChatInterface</c> + <c>gmMainChatUI::PostInit @0x4ce130</c>.
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -24,20 +24,20 @@ namespace AcDream.App.UI.Layout;
|
|||
/// and bound in place.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController
|
||||
internal sealed class ChatWindowController : IRetainedWindowStateController, IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000006u;
|
||||
private bool _disposed;
|
||||
|
||||
// Element ids from chat LayoutDesc 0x21000006 (confirmed in Task D/G1).
|
||||
private const uint RootId = 0x1000000Eu;
|
||||
private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it)
|
||||
private const uint ResizeBarId = 0x1000000Fu; // dat top resize bar (800px — dropped; nine-slice grips replace it)
|
||||
private const uint TranscriptPanelId = 0x10000010u;
|
||||
private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
|
||||
private const uint TranscriptId = 0x10000011u; // Type-12 prototype — skipped by factory
|
||||
private const uint TrackId = 0x10000012u;
|
||||
private const uint InputBarId = 0x10000013u;
|
||||
private const uint MenuId = 0x10000014u;
|
||||
private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField
|
||||
private const uint InputId = 0x10000016u; // Type-12 Text + Editable 0x16 → UiField
|
||||
private const uint SendId = 0x10000019u;
|
||||
private const uint MaxMinId = 0x1000046Fu;
|
||||
|
||||
|
|
@ -48,7 +48,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
private const uint MenuItemRow = 0x0600124Eu; // item row bg (template 0x1000001E)
|
||||
private const uint MenuItemSelected = 0x0600124Du; // active channel row
|
||||
|
||||
// ── Public surface ─────────────────────────────────────────────────────
|
||||
// ── Public surface ─────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Root element of the imported layout (the chat window chrome).</summary>
|
||||
public UiElement Root { get; private set; } = null!;
|
||||
|
|
@ -71,7 +71,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
public RetailWindowHandle? WindowHandle { get; private set; }
|
||||
public bool IsMaximized => _maximized;
|
||||
|
||||
// ── Private state ──────────────────────────────────────────────────────
|
||||
// ── Private state ──────────────────────────────────────────────────────
|
||||
|
||||
private ChatChannelKind _activeChannel = ChatChannelKind.Say;
|
||||
|
||||
|
|
@ -86,7 +86,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
private BitmapFont? _cachedTranscriptDebugFont;
|
||||
internal int TranscriptLayoutBuildCount { get; private set; }
|
||||
|
||||
// ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ──
|
||||
// ── Channel knowledge (ported from old UiChannelMenu — gmMainChatUI::InitTalkFocusMenu @0x4cdc50) ──
|
||||
|
||||
private static readonly (string Label, ChatChannelKind? Channel)[] ChannelItems =
|
||||
{
|
||||
|
|
@ -133,7 +133,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
private bool _maximized;
|
||||
private UiButton? _maxMinButton;
|
||||
|
||||
// ── Factory ────────────────────────────────────────────────────────────
|
||||
// ── Factory ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Bind an imported chat layout to live behavior.
|
||||
|
|
@ -156,7 +156,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
/// <param name="datFont">Retail dat font for transcript + input rendering.</param>
|
||||
/// <param name="debugFont">Fallback debug bitmap font (used when
|
||||
/// <paramref name="datFont"/> is null).</param>
|
||||
/// <param name="resolve">Dat RenderSurface id → (GL tex handle, px width, px height).
|
||||
/// <param name="resolve">Dat RenderSurface id → (GL tex handle, px width, px height).
|
||||
/// Forwarded to <see cref="UiScrollbar"/> and <see cref="UiMenu"/>.</param>
|
||||
public static ChatWindowController? Bind(
|
||||
ElementInfo rootInfo,
|
||||
|
|
@ -165,7 +165,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
Func<ICommandBus> busProvider,
|
||||
UiDatFont? datFont,
|
||||
BitmapFont? debugFont,
|
||||
Func<uint, (uint tex, int w, int h)> resolve)
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolve)
|
||||
{
|
||||
// Their parent panels must exist as real widgets in the layout tree.
|
||||
var transcriptPanel = layout.FindElement(TranscriptPanelId);
|
||||
|
|
@ -177,14 +177,14 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
Console.WriteLine(
|
||||
$"[D.2b] ChatWindowController.Bind: missing required elements " +
|
||||
$"(input={input is not null}, " +
|
||||
$"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " +
|
||||
$"panel={transcriptPanel is not null}, bar={inputBar is not null}) — " +
|
||||
$"chat window will not be interactive.");
|
||||
return null;
|
||||
}
|
||||
|
||||
// LayoutDesc 0x21000006 has SEVERAL top-level elements: the gmMainChatUI window
|
||||
// (RootId 0x1000000E) PLUS stray auxiliary elements that are NOT part of the docked
|
||||
// window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the
|
||||
// window — a separate Field+ListBox (0x1000001C/1D, the floaty scrollback), the
|
||||
// talk-focus highlight strip (0x1000001E), and a scroll-button prototype (0x10000526).
|
||||
// LayoutImporter.ImportInfos wraps all top-level elements in a synthetic Type-3 root,
|
||||
// so using layout.Root would render the strays overlapping the real window (the
|
||||
|
|
@ -210,9 +210,9 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
transcriptPanel.Top = 0f;
|
||||
transcriptPanel.Height += 9f; // dat resize-bar height (0x1000000F H=9)
|
||||
|
||||
// ── Transcript ───────────────────────────────────────────────────
|
||||
// ── Transcript ───────────────────────────────────────────────────
|
||||
// The factory now builds the Type-12 transcript element (0x10000011) as a UiText.
|
||||
// Find it in the widget tree and bind the live providers — no remove/add needed.
|
||||
// Find it in the widget tree and bind the live providers — no remove/add needed.
|
||||
c.Transcript = layout.FindElement(TranscriptId) as UiText
|
||||
?? throw new InvalidOperationException("chat transcript 0x10000011 not built as UiText");
|
||||
c.Transcript.DatFont = datFont;
|
||||
|
|
@ -228,7 +228,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Transcript.BackgroundColor = new Vector4(0f, 0f, 0f, 0.35f); // retail translucent transcript
|
||||
c.Transcript.LinesProvider = () => c.GetTranscriptLines(vm);
|
||||
|
||||
// ── Input ────────────────────────────────────────────────────────
|
||||
// ── Input ────────────────────────────────────────────────────────
|
||||
// Editable/selectable/one-line semantics and state sprites came from the
|
||||
// imported property/state bags. The controller supplies runtime services only.
|
||||
c.Input = input;
|
||||
|
|
@ -238,9 +238,9 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Input.SpriteResolve = resolve;
|
||||
c.Input.OnSubmit = text => ChatCommandRouter.Submit(text, vm, busProvider(), c._activeChannel);
|
||||
|
||||
// ── Scrollbar — bind the factory-built Type-11 track element ────────
|
||||
// ── Scrollbar — bind the factory-built Type-11 track element ────────
|
||||
// The factory now builds the Type-11 track element (0x10000012) as a UiScrollbar
|
||||
// directly. Find it, bind it in place — no remove/add needed.
|
||||
// directly. Find it, bind it in place — no remove/add needed.
|
||||
var track = layout.FindElement(TrackId);
|
||||
if (track is UiScrollbar bar)
|
||||
{
|
||||
|
|
@ -253,7 +253,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Scrollbar = bar;
|
||||
}
|
||||
|
||||
// ── Channel menu — bind the factory-built Type-6 UiMenu ──────────
|
||||
// ── Channel menu — bind the factory-built Type-6 UiMenu ──────────
|
||||
if (layout.FindElement(MenuId) is UiMenu menu)
|
||||
{
|
||||
menu.DatFont = datFont; menu.Font = debugFont; menu.SpriteResolve = resolve;
|
||||
|
|
@ -268,7 +268,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
menu.EnabledProvider = p => p is not ChatChannelKind ch || ChannelAvailable(ch);
|
||||
menu.ButtonLabelProvider = () => ChannelButtonLabel(c._activeChannel);
|
||||
// The widget reports the pick; the controller owns Selected. Only a talk-channel
|
||||
// payload updates the active channel + highlight — the null-payload specials are
|
||||
// payload updates the active channel + highlight — the null-payload specials are
|
||||
// deferred no-ops (see the chat re-drive deferred list) and leave selection intact.
|
||||
menu.OnSelect = p =>
|
||||
{
|
||||
|
|
@ -277,18 +277,18 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
c.Menu = menu;
|
||||
}
|
||||
|
||||
// ── Send button — Enter-alternate submit trigger ──────────────────
|
||||
// ── Send button — Enter-alternate submit trigger ──────────────────
|
||||
// Retail's gmMainChatUI wires the Send button to the same ProcessCommand path.
|
||||
if (layout.FindElement(SendId) is UiButton sendEl)
|
||||
{
|
||||
sendEl.OnClick = () => c.Input.Submit();
|
||||
// The Send sprite is a blank gold button — retail draws the caption as text.
|
||||
// The Send sprite is a blank gold button — retail draws the caption as text.
|
||||
sendEl.Label = "Send";
|
||||
sendEl.LabelFont = datFont;
|
||||
sendEl.LabelColor = new Vector4(1f, 0.92f, 0.72f, 1f);
|
||||
}
|
||||
|
||||
// ── Size the channel button to its label + reflow the input field ─
|
||||
// ── Size the channel button to its label + reflow the input field ─
|
||||
// Retail's talk-focus button autosizes to the selected channel name; the input
|
||||
// field then fills the gap from the button's right edge to the Send button. The
|
||||
// dat authors the button at a fixed 46px (too narrow for "Chat" once the LED +
|
||||
|
|
@ -310,12 +310,12 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
ReflowInputRow();
|
||||
}
|
||||
|
||||
// ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
|
||||
// ── Max/min toggle — gmMainChatUI::HandleMaximizeButton ──
|
||||
if (layout.FindElement(MaxMinId) is UiButton maxMinEl)
|
||||
{
|
||||
// The dat puts max/min and the scrollbar up-button at the SAME X (both
|
||||
// right-anchored), so at content width they overlap. Retail shows max/min
|
||||
// just LEFT of the scrollbar column — shift it one button-width left.
|
||||
// just LEFT of the scrollbar column — shift it one button-width left.
|
||||
if (track is not null)
|
||||
maxMinEl.Left = track.Left - maxMinEl.Width;
|
||||
maxMinEl.ResetAnchorCapture();
|
||||
|
|
@ -326,7 +326,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
return c;
|
||||
}
|
||||
|
||||
// ── Max/min implementation ─────────────────────────────────────────────
|
||||
// ── Max/min implementation ─────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Attach the typed outer-frame handle after the controller's imported content
|
||||
|
|
@ -419,7 +419,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Convert the ChatVM's detailed lines to the transcript's
|
||||
|
|
@ -449,7 +449,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
}
|
||||
|
||||
// Word-wrap each message to the transcript's current pixel width (ports retail
|
||||
// GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
|
||||
// GlyphList::Recalculate @0x473800 — break at word boundaries when the line would
|
||||
// exceed wrapWidth). The cache key re-evaluates it after window resize.
|
||||
Func<string, float> measure =
|
||||
datFont is { } df ? s => df.MeasureWidth(s)
|
||||
|
|
@ -486,8 +486,8 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
/// Greedy word-wrap: split <paramref name="text"/> into fragments that each fit in
|
||||
/// <paramref name="maxW"/> pixels (per <paramref name="measure"/>), breaking at spaces.
|
||||
/// A word that is itself wider than the line is broken at CHARACTER boundaries (no
|
||||
/// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL
|
||||
/// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same
|
||||
/// hyphen), packed onto the current line first — so a long unbroken token (e.g. a URL
|
||||
/// or "wwwww…") wraps instead of overflowing, and a "You say," prefix stays on the same
|
||||
/// row as the start of the message. Mirrors retail GlyphList::Recalculate's per-GlyphLine
|
||||
/// emission (which breaks mid-glyph-run when a run exceeds the wrap width).
|
||||
/// </summary>
|
||||
|
|
@ -510,7 +510,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
}
|
||||
if (line.Length > 0 && measure(word) <= maxW)
|
||||
{
|
||||
yield return line.ToString(); // word fits alone → push to a new line
|
||||
yield return line.ToString(); // word fits alone → push to a new line
|
||||
line.Clear();
|
||||
line.Append(word);
|
||||
continue;
|
||||
|
|
@ -532,7 +532,7 @@ public sealed class ChatWindowController : IRetainedWindowStateController, IReta
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Per-<see cref="ChatKind"/> text color — the EXACT retail RGBA values read from a
|
||||
/// Per-<see cref="ChatKind"/> text color — the EXACT retail RGBA values read from a
|
||||
/// live retail client via cdb (the named <c>RGBAColor</c> constants at acclient
|
||||
/// 0x81c4a8+, e.g. <c>colorWhite</c>/<c>colorBrightPurple</c>/<c>colorLightBlue</c>/
|
||||
/// <c>colorGreen</c>, used by <c>ChatInterface::BuildChatColorLookupTable @0x4f31c0</c>).
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>ListenToElementMessage</c> (0x004CC430), and
|
||||
/// <c>RecvNotice_SetCombatMode</c> (0x004CC620).
|
||||
/// </remarks>
|
||||
public sealed class CombatUiController : IRetainedPanelController
|
||||
internal sealed class CombatUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000073u;
|
||||
public const uint BasicPanelId = 0x1000005Cu;
|
||||
|
|
@ -231,7 +231,7 @@ public sealed class CombatUiController : IRetainedPanelController
|
|||
}
|
||||
|
||||
/// <summary>Localized labels assigned by retail <c>gmCombatUI::PostInit</c>.</summary>
|
||||
public sealed record CombatUiLabels(
|
||||
internal sealed record CombatUiLabels(
|
||||
string Speed,
|
||||
string Power,
|
||||
string RepeatAttacks,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>UIElement_ListBox::AddItemFromTemplateList</c> used by
|
||||
/// <c>gmSpellComponentUI::UpdateComponents @ 0x0048A910</c>.
|
||||
/// </summary>
|
||||
public sealed class ComponentBookTemplateFactory
|
||||
internal sealed class ComponentBookTemplateFactory
|
||||
{
|
||||
public const uint LayoutId = 0x21000033u;
|
||||
public const uint CategoryTemplateId = 0x10000466u;
|
||||
|
|
@ -35,7 +35,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
|
||||
private readonly ElementInfo _categoryTemplate;
|
||||
private readonly ElementInfo _componentTemplate;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly Func<uint, (GpuTextureSlot tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
private readonly string[] _categoryNames;
|
||||
|
|
@ -43,7 +43,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
public ComponentBookTemplateFactory(
|
||||
ElementInfo categoryTemplate,
|
||||
ElementInfo componentTemplate,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null,
|
||||
IReadOnlyList<string>? categoryNames = null)
|
||||
|
|
@ -64,7 +64,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
/// </summary>
|
||||
public static ComponentBookTemplateFactory? TryLoad(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
|
|
@ -111,7 +111,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
|
||||
public ComponentRow CreateComponentRow(
|
||||
uint componentId,
|
||||
uint iconTexture,
|
||||
GpuTextureSlot iconTexture,
|
||||
string name,
|
||||
int ownedCount,
|
||||
uint desiredCount)
|
||||
|
|
@ -183,7 +183,7 @@ public sealed class ComponentBookTemplateFactory
|
|||
CaptureFonts(child, resolveFont, fonts);
|
||||
}
|
||||
|
||||
public readonly record struct ComponentRow(
|
||||
internal readonly record struct ComponentRow(
|
||||
UiTemplateListSlot Slot,
|
||||
UiField DesiredField);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -9,7 +9,7 @@ using DatReaderWriter.Types;
|
|||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
public enum CreatureAppraisalValueStyle
|
||||
internal enum CreatureAppraisalValueStyle
|
||||
{
|
||||
Normal,
|
||||
Positive,
|
||||
|
|
@ -17,12 +17,12 @@ public enum CreatureAppraisalValueStyle
|
|||
Incomplete,
|
||||
}
|
||||
|
||||
public readonly record struct CreatureAppraisalRow(
|
||||
internal readonly record struct CreatureAppraisalRow(
|
||||
string Label,
|
||||
string Value,
|
||||
CreatureAppraisalValueStyle Style);
|
||||
|
||||
public enum CreatureAppraisalRowLayer
|
||||
internal enum CreatureAppraisalRowLayer
|
||||
{
|
||||
Combined,
|
||||
Background,
|
||||
|
|
@ -35,7 +35,7 @@ public enum CreatureAppraisalRowLayer
|
|||
/// <c>BasicCreatureExamineUI</c> and the two Update overloads at
|
||||
/// 0x004F1D90/0x004F1E80.
|
||||
/// </summary>
|
||||
public static class CreatureAppraisalRows
|
||||
internal static class CreatureAppraisalRows
|
||||
{
|
||||
private const string Unknown = "???";
|
||||
private const uint DamageRating = 0x133u;
|
||||
|
|
@ -228,20 +228,20 @@ public static class CreatureAppraisalRows
|
|||
/// Instantiates LayoutDesc 0x2100006B's InfoRegion token template
|
||||
/// 0x10000166, matching <c>UIElement_ListBox::AddItemFromTemplateList</c>.
|
||||
/// </summary>
|
||||
public sealed class CreatureAppraisalRowTemplateFactory
|
||||
internal sealed class CreatureAppraisalRowTemplateFactory
|
||||
{
|
||||
public const uint TemplateId = 0x10000166u;
|
||||
public const uint LabelId = 0x1000012Au;
|
||||
public const uint ValueId = 0x1000012Bu;
|
||||
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint Texture, int Width, int Height)> _resolveSprite;
|
||||
private readonly Func<uint, (GpuTextureSlot Texture, int Width, int Height)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public CreatureAppraisalRowTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint Texture, int Width, int Height)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot Texture, int Width, int Height)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
|
|
@ -256,7 +256,7 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
|||
|
||||
public static CreatureAppraisalRowTemplateFactory? TryLoad(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint Texture, int Width, int Height)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot Texture, int Width, int Height)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
|
|
@ -368,7 +368,7 @@ public sealed class CreatureAppraisalRowTemplateFactory
|
|||
/// instance of the same authored template keeps label/value text above it.
|
||||
/// Both lists share one pixel scroll model so their rows cannot drift.
|
||||
/// </summary>
|
||||
public sealed class CreatureAppraisalLayeredList
|
||||
internal sealed class CreatureAppraisalLayeredList
|
||||
{
|
||||
public const float TextInset = 8f;
|
||||
|
||||
|
|
@ -488,7 +488,7 @@ public sealed class CreatureAppraisalLayeredList
|
|||
/// creature enum 0x10000005 through portal EnumMapper 0x2200000E and replaces
|
||||
/// underscores with spaces.
|
||||
/// </summary>
|
||||
public sealed class CreatureDisplayNameResolver
|
||||
internal sealed class CreatureDisplayNameResolver
|
||||
{
|
||||
public const uint MapperDid = 0x2200000Eu;
|
||||
private readonly IReadOnlyDictionary<uint, string> _names;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>compute_str_hash @ 0x00413110</c>. A StringInfo's token selects one
|
||||
/// localized string variant; ordinary UI labels use token zero.
|
||||
/// </remarks>
|
||||
public sealed class DatStringResolver
|
||||
internal sealed class DatStringResolver
|
||||
{
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly Dictionary<uint, StringTable?> _tables = new();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AcDream.App.UI;
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ namespace AcDream.App.UI.Layout;
|
|||
///
|
||||
/// <para>
|
||||
/// The meter's back/front 3-slice sprite ids live on grandchild image elements,
|
||||
/// NOT on the meter element itself (format doc §11). <see cref="BuildMeter"/>
|
||||
/// NOT on the meter element itself (format doc §11). <see cref="BuildMeter"/>
|
||||
/// walks two layers down to extract them: the two Type-3 container children
|
||||
/// ordered by <see cref="ElementInfo.ReadOrder"/> (back behind = lower, front
|
||||
/// on top = higher), then within each container the image children that carry
|
||||
|
|
@ -28,44 +28,44 @@ namespace AcDream.App.UI.Layout;
|
|||
///
|
||||
/// <para>
|
||||
/// The expand-detail overlay present in the front container carries ONLY named
|
||||
/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
|
||||
/// states ("HideDetail"/"ShowDetail") — no "" DirectState entry — so the
|
||||
/// <c>TryGetValue("")</c> filter in <see cref="SliceIds"/> excludes it
|
||||
/// automatically.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class DatWidgetFactory
|
||||
internal static class DatWidgetFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the <see cref="UiElement"/> for <paramref name="info"/>, sets its
|
||||
/// rect (Left/Top/Width/Height) and Anchors, and returns it.
|
||||
/// </summary>
|
||||
/// <param name="info">Resolved, merged element snapshot from the LayoutDesc importer.</param>
|
||||
/// <param name="resolve">RenderSurface id → (GL tex handle, pixel width, pixel height).
|
||||
/// <param name="resolve">RenderSurface id → (GL tex handle, pixel width, pixel height).
|
||||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
/// <param name="datFont">Retail UI font for the meter's "cur/max" number overlay.
|
||||
/// May be null pre-load — the meter falls back to the debug bitmap font.</param>
|
||||
/// <param name="fontResolve">Optional font resolver: FontDid → <see cref="UiDatFont"/>
|
||||
/// May be null pre-load — the meter falls back to the debug bitmap font.</param>
|
||||
/// <param name="fontResolve">Optional font resolver: FontDid → <see cref="UiDatFont"/>
|
||||
/// (or null when the font can't be loaded). When non-null, any element whose
|
||||
/// <see cref="ElementInfo.FontDid"/> is non-zero gets ITS OWN dat font applied instead of
|
||||
/// the shared <paramref name="datFont"/> fallback. Null = original behavior (use
|
||||
/// <paramref name="datFont"/> for every element).</param>
|
||||
/// <returns>The widget for this element. Never null — every type produces a widget.</returns>
|
||||
/// <returns>The widget for this element. Never null — every type produces a widget.</returns>
|
||||
public static UiElement? Create(ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve, UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null,
|
||||
Func<UiStringInfoValue, string?>? stringResolve = null)
|
||||
{
|
||||
// Retail Type 3 = UIElement_Field (reg :126190), but in acdream's CURRENT layouts
|
||||
// (vitals 0x2100006C / chat 0x21000006) Type-3 elements are sprite-bearing chrome +
|
||||
// containers (the 8-piece bevel corners/edges, the transcript/input panels), NOT
|
||||
// editable fields — retail draws those as inert media-bearing Fields, which our
|
||||
// editable fields — retail draws those as inert media-bearing Fields, which our
|
||||
// UiDatElement reproduces pixel-for-pixel (and without the spurious focus/edit
|
||||
// affordance a UiField would add). The one true editable field, the chat input
|
||||
// (0x10000016), resolves to Type 12 and is controller-placed as a UiField. So Type 3
|
||||
// stays on the generic fallback here; register it as UiField only when a window
|
||||
// actually carries a factory-built editable Type-3 field (and UiField grows a
|
||||
// background-media draw + an opt-in editable flag at that point). UiField (the widget)
|
||||
// still ships — it just isn't wired into the factory switch yet.
|
||||
// still ships — it just isn't wired into the factory switch yet.
|
||||
// Resolve this element's own dat font if a resolver is provided and the element
|
||||
// has a FontDid. Falls back to the shared datFont when not set (FontDid==0) or
|
||||
// when the resolver returns null (font missing from dats).
|
||||
|
|
@ -86,11 +86,11 @@ public static class DatWidgetFactory
|
|||
// gmUIElement_*Indicator custom button classes
|
||||
6 => new UiMenu(), // UIElement_Menu (reg :120163)
|
||||
7 => BuildMeter(info, resolve, elementFont), // UIElement_Meter
|
||||
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
|
||||
0xD => new UiViewport(), // UIElement_Viewport — 3-D mini-scene blit leaf
|
||||
11 => BuildScrollbar(info, resolve), // UIElement_Scrollbar (reg :124137)
|
||||
12 => BuildText(info, resolve, elementFont, stringResolve), // UIElement_Text
|
||||
0x13 => new UiDialogRoot(), // ConfirmationDialog
|
||||
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
|
||||
0x10000031u => new UiItemList(resolve), // UIElement_ItemList — toolbar/inventory/paperdoll slots
|
||||
0x10000035u => BuildCheckbox(
|
||||
info, resolve, elementFont, fontResolve, stringResolve), // UIOption_Checkbox
|
||||
_ => new UiDatElement(info, resolve), // generic fallback (incl. Type 3 chrome/containers)
|
||||
|
|
@ -105,7 +105,7 @@ public static class DatWidgetFactory
|
|||
e.Width = info.Width;
|
||||
e.Height = info.Height;
|
||||
|
||||
// Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the
|
||||
// Honor the dat's draw order. ZLevel is the primary layer (higher = further BACK — e.g. the
|
||||
// gmInventoryUI full-window backdrop at ZLevel 100 sits behind the ZLevel-0 panels, #145);
|
||||
// ReadOrder is the within-layer tiebreaker (higher = on top). K=10000 exceeds any window's
|
||||
// element count so ZLevel always dominates. Vitals (all ZLevel 0) keep ZOrder == ReadOrder.
|
||||
|
|
@ -134,7 +134,7 @@ public static class DatWidgetFactory
|
|||
/// </summary>
|
||||
private static UiScrollbar BuildScrollbar(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint tex, int w, int h)> resolve)
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolve)
|
||||
{
|
||||
var bar = new UiScrollbar
|
||||
{
|
||||
|
|
@ -302,39 +302,39 @@ public static class DatWidgetFactory
|
|||
return stateName == "Normal" ? DefaultImage(info) : 0u;
|
||||
}
|
||||
|
||||
// ── Meter ────────────────────────────────────────────────────────────────
|
||||
// ── Meter ────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Builds a <see cref="UiMeter"/> and populates its sprite ids from the meter's
|
||||
/// child/grandchild elements (format doc §11). Two shapes are handled:
|
||||
/// child/grandchild elements (format doc §11). Two shapes are handled:
|
||||
///
|
||||
/// <para>
|
||||
/// <b>3-slice shape</b> (vitals meters — 2 Type-3 containers, each with 3 image grandchildren):
|
||||
/// <b>3-slice shape</b> (vitals meters — 2 Type-3 containers, each with 3 image grandchildren):
|
||||
/// <code>
|
||||
/// meter (Type 7)
|
||||
/// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
|
||||
/// │ ├── left-cap image (DirectState "" → File = back-left sprite)
|
||||
/// │ ├── center image (DirectState "" → File = back-tile sprite)
|
||||
/// │ └── right-cap image (DirectState "" → File = back-right sprite)
|
||||
/// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
|
||||
/// │ ├── left-cap image (→ front-left sprite)
|
||||
/// │ ├── center image (→ front-tile sprite)
|
||||
/// │ ├── right-cap image (→ front-right sprite)
|
||||
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
|
||||
/// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
|
||||
/// ├── back-layer container (Type 3, lower ReadOrder — drawn first / behind)
|
||||
/// │ ├── left-cap image (DirectState "" → File = back-left sprite)
|
||||
/// │ ├── center image (DirectState "" → File = back-tile sprite)
|
||||
/// │ └── right-cap image (DirectState "" → File = back-right sprite)
|
||||
/// ├── front-layer container (Type 3, higher ReadOrder — drawn on top)
|
||||
/// │ ├── left-cap image (→ front-left sprite)
|
||||
/// │ ├── center image (→ front-tile sprite)
|
||||
/// │ ├── right-cap image (→ front-right sprite)
|
||||
/// │ └── expand overlay (named "ShowDetail"/"HideDetail" only — NO DirectState — IGNORED)
|
||||
/// └── text label (Type 0) (IGNORED — Fill/Label providers bound by VitalsController)
|
||||
/// </code>
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <b>Single-image shape</b> (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3
|
||||
/// <b>Single-image shape</b> (toolbar selected-object meters 0x100001A1/0x100001A2 — 1 Type-3
|
||||
/// child, no grandchildren): the back-track sprite is on the meter element's own DirectState;
|
||||
/// the fill sprite is on the single Type-3 child's own DirectState. Both are placed in the
|
||||
/// TILE slot (Back/FrontTile) with left/right caps 0, so <see cref="UiMeter.DrawHBar"/> tiles
|
||||
/// them across the full bar geometry (DrawMode=Normal) and clips the fill to the fraction.
|
||||
/// (retail: gmToolbarUI::HandleSelectionChanged :198635, UIElement_Meter::Initialize :123328)
|
||||
/// <code>
|
||||
/// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E]
|
||||
/// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F]
|
||||
/// meter (Type 7) [DirectState "" → back-track sprite, e.g. 0x0600193E]
|
||||
/// └── fill container (Type 3) [DirectState "" → fill sprite, e.g. 0x0600193F]
|
||||
/// </code>
|
||||
/// </para>
|
||||
///
|
||||
|
|
@ -345,7 +345,7 @@ public static class DatWidgetFactory
|
|||
/// </para>
|
||||
/// </summary>
|
||||
private static UiMeter BuildMeter(ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve, UiDatFont? datFont)
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve, UiDatFont? datFont)
|
||||
{
|
||||
var m = new UiMeter
|
||||
{
|
||||
|
|
@ -383,18 +383,18 @@ public static class DatWidgetFactory
|
|||
// Single-image shape used by the toolbar selected-object meters
|
||||
// (health 0x100001A1, mana 0x100001A2).
|
||||
// - The back-track sprite lives on the meter ELEMENT's own DirectState ("" key of
|
||||
// info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E.
|
||||
// info.StateMedia) — not on any grandchild image. e.g. health back = 0x0600193E.
|
||||
// - The fill sprite lives on the single Type-3 child's own DirectState ("" key of
|
||||
// containers[0].StateMedia). e.g. health fill = 0x0600193F.
|
||||
// The fill child has NO image grandchildren, so SliceIds would return all-zero —
|
||||
// The fill child has NO image grandchildren, so SliceIds would return all-zero —
|
||||
// read the container's StateMedia directly instead.
|
||||
//
|
||||
// These go in the TILE slot (not the left-cap slot): the sprites are DrawMode=Normal,
|
||||
// which retail renders as "tile at native width to fill the full element geometry"
|
||||
// (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter::
|
||||
// (format doc §6; the generic UiDatElement.OnDraw Normal path; UIElement_Meter::
|
||||
// DrawChildren :123574 clips the child's FULL 140px geometry box to the fill fraction).
|
||||
// With the sprite on BackLeft instead, UiMeter.DrawHBar would clamp the cap to the
|
||||
// sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and
|
||||
// sprite's NATIVE width (capL = min(nativeW, 140)) — leaving a right-side gap and
|
||||
// mapping the fill fraction to native width when nativeW < 140. The tile slot makes
|
||||
// midW = full bar width, so the back tiles across all 140px and the front clips to
|
||||
// 140*fraction correctly for any native sprite width (left/right caps unused = 0).
|
||||
|
|
@ -435,7 +435,7 @@ public static class DatWidgetFactory
|
|||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback.");
|
||||
Console.WriteLine($"[D.2b] meter 0x{info.Id:X8}: {containers.Count} Type-3 containers but no recognized 3-slice, direct-fill, or stateful-fill shape — bar may render as solid-color fallback.");
|
||||
}
|
||||
|
||||
return m;
|
||||
|
|
@ -482,11 +482,11 @@ public static class DatWidgetFactory
|
|||
return (left, tile, right);
|
||||
}
|
||||
|
||||
// ── Text ─────────────────────────────────────────────────────────────────
|
||||
// ── Text ─────────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Type-12 UIElement_Text: an editable field or colored-line text view,
|
||||
/// selected from the canonical property bag. The element's
|
||||
/// own Direct/Normal media (if any) becomes the background sprite, drawn under the text —
|
||||
/// own Direct/Normal media (if any) becomes the background sprite, drawn under the text —
|
||||
/// so a Type-12 element that previously rendered via UiDatElement keeps its sprite. Lines
|
||||
/// are bound later by the controller (LinesProvider). An unbound UiText draws nothing
|
||||
/// because <see cref="UiText.BackgroundColor"/> defaults to transparent.
|
||||
|
|
@ -497,15 +497,15 @@ public static class DatWidgetFactory
|
|||
/// that subsequently call <see cref="UiText.Centered"/> / <see cref="UiText.RightAligned"/>
|
||||
/// on dat-origin elements can be simplified. Controllers that <em>explicitly</em> set those
|
||||
/// properties after <see cref="ImportedLayout.FindElement"/> still override the build-time
|
||||
/// defaults — the build-time value is just the starting point, not a lock.
|
||||
/// defaults — the build-time value is just the starting point, not a lock.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
/// <param name="elementFont">The font to seed on the widget. When a font resolver was
|
||||
/// provided and the element's FontDid resolved successfully, this is that element-specific
|
||||
/// font; otherwise it is the shared global fallback. Controllers that call
|
||||
/// <see cref="ImportedLayout.FindElement"/> and set <see cref="UiText.DatFont"/> afterward
|
||||
/// still override this — the build-time value is just the starting point.</param>
|
||||
private static UiElement BuildText(ElementInfo info, Func<uint, (uint, int, int)> resolve,
|
||||
/// still override this — the build-time value is just the starting point.</param>
|
||||
private static UiElement BuildText(ElementInfo info, Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? elementFont = null,
|
||||
Func<UiStringInfoValue, string?>? stringResolve = null)
|
||||
{
|
||||
|
|
@ -547,7 +547,7 @@ public static class DatWidgetFactory
|
|||
|
||||
// Apply horizontal + vertical justification from the dat at build time.
|
||||
// Controllers that call FindElement and set Centered/RightAligned/VerticalJustify
|
||||
// afterward will override these — this is only the dat-driven default.
|
||||
// afterward will override these — this is only the dat-driven default.
|
||||
bool centered = info.HJustify == HJustify.Center;
|
||||
bool rightAligned = info.HJustify == HJustify.Right;
|
||||
var vJustify = info.VJustify switch
|
||||
|
|
@ -580,7 +580,7 @@ public static class DatWidgetFactory
|
|||
|
||||
// Font color from dat property 0x1B (ColorBaseProperty).
|
||||
// When present, seed DefaultColor so controllers that read it don't have to hard-code colors.
|
||||
// Controllers that supply explicit per-line colors via LinesProvider still win — this is only
|
||||
// Controllers that supply explicit per-line colors via LinesProvider still win — this is only
|
||||
// the build-time default.
|
||||
if (info.FontColor.HasValue)
|
||||
t.DefaultColor = info.FontColor.Value;
|
||||
|
|
@ -593,7 +593,7 @@ public static class DatWidgetFactory
|
|||
|
||||
private static UiButton BuildButton(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? elementFont,
|
||||
Func<uint, UiDatFont?>? fontResolve,
|
||||
Func<UiStringInfoValue, string?>? stringResolve)
|
||||
|
|
@ -663,7 +663,7 @@ public static class DatWidgetFactory
|
|||
/// </summary>
|
||||
private static UiButton BuildCheckbox(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? elementFont,
|
||||
Func<uint, UiDatFont?>? fontResolve,
|
||||
Func<UiStringInfoValue, string?>? stringResolve)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using DatReaderWriter;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -9,16 +9,16 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>UIElement_ListBox::AddItemFromTemplateList</c> for
|
||||
/// <c>EffectInfoRegion</c>.
|
||||
/// </summary>
|
||||
public sealed class EffectRowTemplateFactory
|
||||
internal sealed class EffectRowTemplateFactory
|
||||
{
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly Func<uint, (GpuTextureSlot tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public EffectRowTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
|
|
@ -33,7 +33,7 @@ public sealed class EffectRowTemplateFactory
|
|||
|
||||
public static EffectRowTemplateFactory? TryLoad(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
|
|
@ -57,7 +57,7 @@ public sealed class EffectRowTemplateFactory
|
|||
|
||||
public EffectRow Create(
|
||||
uint spellId,
|
||||
uint iconTexture,
|
||||
GpuTextureSlot iconTexture,
|
||||
string name,
|
||||
string remaining)
|
||||
{
|
||||
|
|
@ -90,7 +90,7 @@ public sealed class EffectRowTemplateFactory
|
|||
return new EffectRow(slot, duration, remaining);
|
||||
}
|
||||
|
||||
public sealed class EffectRow
|
||||
internal sealed class EffectRow
|
||||
{
|
||||
private readonly UiText _duration;
|
||||
private string _remaining;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
|
@ -7,7 +7,7 @@ using AcDream.Core.Spells;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>Retail gmEffectsUI positive/negative instance binding.</summary>
|
||||
public sealed class EffectsUiController : IRetainedPanelController
|
||||
internal sealed class EffectsUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x2100001Bu;
|
||||
// gmPanelUI's 0x10000184/185 are panel-slot IDs, not roots in this LayoutDesc.
|
||||
|
|
@ -27,7 +27,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
private readonly Spellbook _spellbook;
|
||||
private readonly bool _positive;
|
||||
private readonly Func<double> _serverTime;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveSpellIcon;
|
||||
private readonly EffectRowTemplateFactory _templates;
|
||||
private readonly string _selectPrompt;
|
||||
private readonly UiItemList _list;
|
||||
|
|
@ -48,7 +48,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
Spellbook spellbook,
|
||||
bool positive,
|
||||
Func<double> serverTime,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
UiItemList list,
|
||||
|
|
@ -81,8 +81,8 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
Spellbook spellbook,
|
||||
bool positive,
|
||||
Func<double> serverTime,
|
||||
Func<uint, (uint Texture, int Width, int Height)> spriteResolve,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<uint, (GpuTextureSlot Texture, int Width, int Height)> spriteResolve,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
EffectRowTemplateFactory templates,
|
||||
string selectPrompt,
|
||||
Action? close = null)
|
||||
|
|
@ -144,7 +144,7 @@ public sealed class EffectsUiController : IRetainedPanelController
|
|||
uint identity = enchantment.Identity;
|
||||
EffectRowTemplateFactory.EffectRow row = _templates.Create(
|
||||
enchantment.SpellId,
|
||||
metadata is null ? 0u : _resolveSpellIcon(enchantment.SpellId),
|
||||
metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(enchantment.SpellId),
|
||||
metadata?.Name ?? $"Spell {enchantment.SpellId}",
|
||||
FormatRemaining(enchantment, _serverTime()));
|
||||
row.Slot.Clicked = () => Select(enchantment.SpellId);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class ElementInfo
|
|||
/// <summary>
|
||||
/// Raw edge-anchor flag values from the dat (<c>LeftEdge</c>, <c>TopEdge</c>,
|
||||
/// <c>RightEdge</c>, <c>BottomEdge</c> fields of <c>ElementDesc</c>).
|
||||
/// Values 0–4. Imported elements preserve these in <see cref="UiLayoutPolicy"/>;
|
||||
/// Values 0–4. Imported elements preserve these in <see cref="UiLayoutPolicy"/>;
|
||||
/// <see cref="ElementReader.ToAnchors"/> is only the compatibility projection
|
||||
/// for older programmatic consumers.
|
||||
/// </summary>
|
||||
|
|
@ -80,7 +80,7 @@ public sealed class ElementInfo
|
|||
|
||||
/// <summary>
|
||||
/// Font dat object id inherited from the base element's <c>Properties[0x1A]</c>
|
||||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>). 0 = none / not inherited.
|
||||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>). 0 = none / not inherited.
|
||||
/// </summary>
|
||||
public uint FontDid;
|
||||
|
||||
|
|
@ -109,7 +109,7 @@ public sealed class ElementInfo
|
|||
public Vector4? FontColor;
|
||||
|
||||
/// <summary>
|
||||
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
|
||||
/// Sprite per state: state name → (RenderSurface file id, DrawMode int).
|
||||
/// The <c>""</c> key represents the unnamed DirectState (<c>ElementDesc.StateDesc</c>).
|
||||
/// Named states use the <c>UIStateId.ToString()</c> value as the key
|
||||
/// (e.g. <c>"HideDetail"</c>, <c>"ShowDetail"</c>).
|
||||
|
|
@ -220,16 +220,16 @@ public sealed class ElementInfo
|
|||
/// No OpenGL, no DatReaderWriter types, no rendering dependencies beyond
|
||||
/// the <see cref="AnchorEdges"/> bit-flag enum from <c>AcDream.App.UI</c>.
|
||||
/// </summary>
|
||||
public static class ElementReader
|
||||
internal static class ElementReader
|
||||
{
|
||||
/// <summary>Compatibility projection from raw retail modes to the legacy
|
||||
/// <see cref="AnchorEdges"/> flags. This projection cannot represent centered
|
||||
/// mode 3 or proportional mode 4 exactly. Imported DAT widgets therefore use
|
||||
/// <see cref="UiLayoutPolicy"/>; call this only for legacy/programmatic paths.</summary>
|
||||
/// <param name="left">LeftEdge dat field value (0–4).</param>
|
||||
/// <param name="top">TopEdge dat field value (0–4).</param>
|
||||
/// <param name="right">RightEdge dat field value (0–4).</param>
|
||||
/// <param name="bottom">BottomEdge dat field value (0–4).</param>
|
||||
/// <param name="left">LeftEdge dat field value (0–4).</param>
|
||||
/// <param name="top">TopEdge dat field value (0–4).</param>
|
||||
/// <param name="right">RightEdge dat field value (0–4).</param>
|
||||
/// <param name="bottom">BottomEdge dat field value (0–4).</param>
|
||||
public static AnchorEdges ToAnchors(uint left, uint top, uint right, uint bottom)
|
||||
{
|
||||
var a = AnchorEdges.None;
|
||||
|
|
@ -284,7 +284,7 @@ public static class ElementReader
|
|||
X = derived.X,
|
||||
Y = derived.Y,
|
||||
// NOTE: 0 is the "not set, inherit from base" sentinel for Width/Height. This
|
||||
// diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is
|
||||
// diverges from the format doc §12 rule 2 ("derived W/H win even if zero") but is
|
||||
// indistinguishable for Plan 1 (all base elements are zero-size Type-12 prototypes).
|
||||
// If a real zero-size derived element ever needs to override a non-zero base in
|
||||
// switch Width/Height to nullable values and use presence-aware merging.
|
||||
|
|
@ -301,11 +301,11 @@ public static class ElementReader
|
|||
// HJustify/VJustify: derived wins when it carries an explicit non-Center value
|
||||
// (the dat property was present and read); otherwise inherit the base prototype's value.
|
||||
// Center is the default (= "not set by this element") so Center-derived never overrides
|
||||
// a non-Center base — matching the FontDid "non-zero wins" convention.
|
||||
// a non-Center base — matching the FontDid "non-zero wins" convention.
|
||||
HJustify = derived.HJustify != HJustify.Center ? derived.HJustify : base_.HJustify,
|
||||
VJustify = derived.VJustify != VJustify.Center ? derived.VJustify : base_.VJustify,
|
||||
// FontColor: derived wins when it has an explicit (non-null) color; otherwise inherit the base.
|
||||
// Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
|
||||
// Null means "dat carried no 0x1B property" — so null-derived does NOT override a non-null base.
|
||||
FontColor = derived.FontColor ?? base_.FontColor,
|
||||
// DefaultStateName: derived wins if set; otherwise inherit the base's default.
|
||||
DefaultStateName = !string.IsNullOrEmpty(derived.DefaultStateName) ? derived.DefaultStateName : base_.DefaultStateName,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Chests, corpses, and other world containers share this bottom-screen strip;
|
||||
/// it is intentionally independent from the owned backpack window.
|
||||
/// </summary>
|
||||
public sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController
|
||||
internal sealed class ExternalContainerController : IItemListDragHandler, IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000008u;
|
||||
public const uint RootId = 0x10000063u;
|
||||
|
|
@ -31,8 +31,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
private readonly SelectionState _selection;
|
||||
private readonly ItemInteractionController _itemInteraction;
|
||||
private readonly StackSplitQuantityState _stackSplitQuantity;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _resolveIcon;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _resolveDragIcon;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> _resolveIcon;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> _resolveDragIcon;
|
||||
private readonly Action<uint> _sendUse;
|
||||
private readonly Action<uint, uint, int> _sendPutItemInContainer;
|
||||
private readonly Action<uint, uint, uint, uint> _sendSplitToContainer;
|
||||
|
|
@ -53,8 +53,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
SelectionState selection,
|
||||
ItemInteractionController itemInteraction,
|
||||
StackSplitQuantityState stackSplitQuantity,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> resolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> resolveDragIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> resolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> resolveDragIcon,
|
||||
Action<uint> sendUse,
|
||||
Action<uint, uint, int> sendPutItemInContainer,
|
||||
Action<uint, uint, uint, uint> sendSplitToContainer,
|
||||
|
|
@ -128,8 +128,8 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
SelectionState selection,
|
||||
ItemInteractionController itemInteraction,
|
||||
StackSplitQuantityState stackSplitQuantity,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> resolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> resolveDragIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> resolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> resolveDragIcon,
|
||||
Action<uint> sendUse,
|
||||
Action<uint, uint, int> sendPutItemInContainer,
|
||||
Action<uint, uint, uint, uint> sendSplitToContainer,
|
||||
|
|
@ -354,9 +354,9 @@ public sealed class ExternalContainerController : IItemListDragHandler, IRetaine
|
|||
private UiItemSlot CreateCell(UiItemList owner, uint guid, ItemDragSource source)
|
||||
{
|
||||
ClientObject? item = _objects.Get(guid);
|
||||
uint icon = item is null ? 0u : _resolveIcon(
|
||||
GpuTextureSlot icon = item is null ? GpuTextureSlot.Unassigned : _resolveIcon(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
uint dragIcon = item is null ? 0u : _resolveDragIcon(
|
||||
GpuTextureSlot dragIcon = item is null ? GpuTextureSlot.Unassigned : _resolveDragIcon(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
var cell = new UiItemSlot
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// sprites, and state names; this controller owns only live state selection and
|
||||
/// authored input actions.
|
||||
/// </summary>
|
||||
public sealed class IndicatorBarController : IRetainedPanelController
|
||||
internal sealed class IndicatorBarController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000071u;
|
||||
|
||||
|
|
@ -270,7 +270,7 @@ public sealed class IndicatorBarController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record IndicatorBarBindings(
|
||||
internal sealed record IndicatorBarBindings(
|
||||
Spellbook Spellbook,
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -10,12 +10,12 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Binds the imported gmInventoryUI tree (LayoutDesc 0x21000023) and populates it from
|
||||
/// <see cref="ClientObjectTable"/>. The acdream analogue of retail
|
||||
/// gmInventoryUI/gmBackpackUI/gm3DItemsUI ::PostInit (named-retail decomp 176236/176596/176728).
|
||||
/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace);
|
||||
/// Container-switching is live (click a side bag → Use 0x0036 → ViewContents 0x0196 full-replace);
|
||||
/// drag-into-bag / wield-drop wire are later sub-phases.
|
||||
/// </summary>
|
||||
public sealed class InventoryController : IItemListDragHandler, IRetainedPanelController
|
||||
internal sealed class InventoryController : IItemListDragHandler, IRetainedPanelController
|
||||
{
|
||||
// Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds).
|
||||
// Element ids — spec §1 (dat dump of 0x21000022 / 0x21000021 + the *::PostInit binds).
|
||||
public const uint ContentsGridId = 0x100001C6u; // gm3DItemsUI m_itemList ("Contents of Backpack")
|
||||
public const uint ContainerListId = 0x100001CAu; // gmBackpackUI m_containerList (side-bag selector)
|
||||
public const uint TopContainerId = 0x100001C9u; // gmBackpackUI m_topContainer (main-pack cell)
|
||||
|
|
@ -30,7 +30,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
private const uint PaperdollWindowId = 0x100001CDu;
|
||||
private const uint BackpackWindowId = 0x100001CEu;
|
||||
|
||||
// 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
|
||||
// 3D-items grid: 192x96 → 6 cols x 3 rows of the 32x32 UIItem cell (template 0x21000037).
|
||||
private const int ContentsColumns = 6;
|
||||
private const float ContentsCellPx = 32f; // gm3DItemsUI grid (192x96 = 6x3 of 32px)
|
||||
private const float BackpackCellPx = 36f; // gmBackpackUI column cells (0x100001C9/CA = 36px)
|
||||
|
|
@ -40,8 +40,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? _dragIconIds;
|
||||
private readonly Func<int?> _strength;
|
||||
private readonly Func<string>? _ownerName;
|
||||
|
||||
|
|
@ -80,8 +80,8 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
ImportedLayout layout,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds,
|
||||
Func<int?> strength,
|
||||
SelectionState selection,
|
||||
Func<string>? ownerName,
|
||||
|
|
@ -169,7 +169,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
_topContainer.ExamineItemRequested = ExamineItem;
|
||||
}
|
||||
|
||||
// Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
|
||||
// Burden meter: vertical 11×58 bar (gmBackpackUI m_burdenMeter, retail direction 4).
|
||||
_burdenMeter = layout.FindElement(BurdenMeterId) as UiMeter;
|
||||
if (_burdenMeter is not null)
|
||||
{
|
||||
|
|
@ -180,7 +180,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
// Captions: drive each host UiText directly with the known string (the caption
|
||||
// elements resolve to UiText). "Contents of Backpack" + "%d%%" are procedural in retail
|
||||
// (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.)
|
||||
// (gm3DItemsUI/gmBackpackUI PostInit/SetLoadLevel); "Burden" is the dat label. (Spec §5.)
|
||||
AttachCaption(layout.FindElement(TitleTextId), () => "Inventory of " + OwnerName(), datFont);
|
||||
AttachCaption(layout.FindElement(BurdenCaptionId), () => "Burden", datFont);
|
||||
AttachCaption(layout.FindElement(ContentsCaptionId), () => "Contents of " + OpenContainerName(), datFont);
|
||||
|
|
@ -228,7 +228,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
ImportedLayout layout,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds,
|
||||
Func<int?> strength,
|
||||
SelectionState selection,
|
||||
UiDatFont? datFont,
|
||||
|
|
@ -244,7 +244,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
ItemInteractionController? itemInteraction = null,
|
||||
Action? onClose = null,
|
||||
StackSplitQuantityState? stackSplitQuantity = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds = null)
|
||||
=> new InventoryController(layout, objects, playerGuid, iconIds, dragIconIds, strength, selection,
|
||||
ownerName, datFont,
|
||||
contentsEmptySprite, sideBagEmptySprite, mainPackEmptySprite,
|
||||
|
|
@ -342,7 +342,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
Populate();
|
||||
}
|
||||
|
||||
/// <summary>True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.</summary>
|
||||
/// <summary>True if the object is in (or wielded by) the player — i.e. a rebuild is warranted.</summary>
|
||||
private bool Concerns(ClientObject o)
|
||||
{
|
||||
uint p = _playerGuid();
|
||||
|
|
@ -425,14 +425,14 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
while (_containerList.GetNumUIItems() < slots) AddEmptyCell(_containerList);
|
||||
}
|
||||
|
||||
// Main-pack cell: the player's own container — clicking it opens/selects the main pack.
|
||||
// Main-pack cell: the player's own container — clicking it opens/selects the main pack.
|
||||
// Retail draws a CONSTANT backpack icon here, NOT the player's body icon: IconData::RenderIcons
|
||||
// (0x0058d1ee) has an IsThePlayer() branch that draws a fixed backpack with m_itemType =
|
||||
// TYPE_CONTAINER. Compose that backpack base over the Container type-underlay (the player
|
||||
// object's own IconId is the character body, which would render wrong here). The backpack
|
||||
// RenderSurface is 0x0600127E, VISUALLY CONFIRMED at the live gate 2026-06-22 (the earlier
|
||||
// 0x060011F4 from a research dat-dump of GetDIDByEnum(0x10000004,7) was a green tile, not the
|
||||
// pack — the index value was misreported). Retires AP-51.
|
||||
// pack — the index value was misreported). Retires AP-51.
|
||||
if (_topContainer is not null)
|
||||
{
|
||||
const uint PlayerPackBaseIcon = 0x0600127Eu; // constant main-pack backpack (visual gate)
|
||||
|
|
@ -442,7 +442,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
p,
|
||||
_iconIds(ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u),
|
||||
dragIconTexture: _dragIconIds?.Invoke(
|
||||
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u) ?? 0u);
|
||||
ItemType.Container, PlayerPackBaseIcon, 0u, 0u, 0u));
|
||||
main.DragAcceptSprite = 0x060011F7u; main.DragRejectSprite = 0x060011F8u;
|
||||
main.Clicked = () => OpenContainer(p);
|
||||
main.DoubleClicked = () => _itemInteraction?.ActivateItem(p);
|
||||
|
|
@ -454,10 +454,10 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
RefreshBurden();
|
||||
}
|
||||
|
||||
/// <summary>The effective open container — the explicit one, or the player (main pack) by default.
|
||||
/// <summary>The effective open container — the explicit one, or the player (main pack) by default.
|
||||
/// Resolved live (not cached at ctor) so a late-arriving player guid is handled. The default
|
||||
/// sentinel is 0; once the main pack is explicitly opened, <c>_openContainer</c> holds the player
|
||||
/// guid instead — both resolve here to the same main-pack container, so the paths are equivalent.</summary>
|
||||
/// guid instead — both resolve here to the same main-pack container, so the paths are equivalent.</summary>
|
||||
private static bool IsBag(ClientObject item) =>
|
||||
item.ContainerTypeHint != 0u
|
||||
|| item.Type.HasFlag(ItemType.Container)
|
||||
|
|
@ -468,17 +468,17 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
/// <summary>The owned destination retail PlaceInBackpack currently uses.</summary>
|
||||
public uint CurrentOpenContainerId => EffectiveOpen();
|
||||
|
||||
/// <summary>Add a populated cell wired to its click role: container cell → open+select,
|
||||
/// item cell → select-only.</summary>
|
||||
/// <summary>Add a populated cell wired to its click role: container cell → open+select,
|
||||
/// item cell → select-only.</summary>
|
||||
private void AddCell(UiItemList? list, uint guid, bool isContainer, bool waiting = false)
|
||||
{
|
||||
if (list is null) return;
|
||||
var item = _objects.Get(guid);
|
||||
uint tex = item is null ? 0u
|
||||
GpuTextureSlot tex = item is null ? GpuTextureSlot.Unassigned
|
||||
: _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
uint dragTex = item is null ? 0u
|
||||
GpuTextureSlot? dragTex = item is null ? null
|
||||
: _dragIconIds?.Invoke(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
|
||||
cell.SetItem(guid, tex, dragIconTexture: dragTex);
|
||||
cell.SetWaitingState(waiting);
|
||||
|
|
@ -520,9 +520,9 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
/// <summary>
|
||||
/// Bind the exact ItemList_DragOver state for this destination. A normal contents-grid
|
||||
/// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9).
|
||||
/// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9).
|
||||
/// An occupied container selector uses ItemSlot_DragOver_DropIn
|
||||
/// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400.
|
||||
/// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400.
|
||||
/// </summary>
|
||||
private void ConfigureDropFeedback(UiItemList list, UiItemSlot cell)
|
||||
{
|
||||
|
|
@ -532,11 +532,11 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
cell.DragRejectSprite = 0x060011F8u;
|
||||
}
|
||||
|
||||
/// <summary>Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
|
||||
/// <summary>Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
|
||||
/// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill =
|
||||
/// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown
|
||||
/// capacity). For a CLOSED side bag the contents aren't indexed until it's opened (ViewContents),
|
||||
/// so the bar reads empty until then — faithful to retail's known-children count.</summary>
|
||||
/// so the bar reads empty until then — faithful to retail's known-children count.</summary>
|
||||
private void SetCapacityBar(UiItemSlot cell, uint containerGuid)
|
||||
{
|
||||
int cap = _objects.Get(containerGuid)?.ItemsCapacity ?? 0;
|
||||
|
|
@ -545,7 +545,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
cell.CapacityFill = Math.Clamp(n / (float)cap, 0f, 1f);
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
|
||||
// ── IItemListDragHandler (B-Drag) — drop an item to move it (optimistic + wire) ──────────────
|
||||
/// <summary>Retail ItemList_BeginDrag selects an unselected item before enabling its waiting
|
||||
/// mesh. Inventory items do not lift-remove (unlike the toolbar): the item stays in its slot
|
||||
/// until the server confirms the eventual drop.</summary>
|
||||
|
|
@ -778,7 +778,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
?? dispatch();
|
||||
|
||||
/// <summary>True only when we KNOW the container is full (capacity known + contents indexed). A
|
||||
/// closed bag (unknown count) returns false → advisory accept; the server is authoritative.</summary>
|
||||
/// closed bag (unknown count) returns false → advisory accept; the server is authoritative.</summary>
|
||||
private bool IsContainerFull(uint container)
|
||||
{
|
||||
int cap = _objects.Get(container)?.ItemsCapacity ?? 0;
|
||||
|
|
@ -815,7 +815,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (guid == 0) return;
|
||||
_selection.Select(guid, SelectionChangeSource.Inventory);
|
||||
uint open = EffectiveOpen();
|
||||
if (guid == open) { ApplyIndicators(); return; } // already open — just move the square
|
||||
if (guid == open) { ApplyIndicators(); return; } // already open — just move the square
|
||||
|
||||
uint p = _playerGuid();
|
||||
_openContainer = guid;
|
||||
|
|
@ -872,7 +872,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (host is null) return;
|
||||
|
||||
// The caption elements (0x100001D7 "Burden", 0x100001C5 "Contents of Backpack",
|
||||
// 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live).
|
||||
// 0x100001D8 "%") resolve to UiText (Type-0 inheriting a text base — confirmed live).
|
||||
// Drive the host UiText DIRECTLY: it is already in the paint tree and renders, whereas
|
||||
// a nested child UiText did not paint. Set it to a static centered single-line label.
|
||||
if (host is UiText t)
|
||||
|
|
@ -913,7 +913,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
}
|
||||
|
||||
/// <summary>Recompute the burden fill + percent. Port of CACQualities::InqLoad
|
||||
/// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden:
|
||||
/// (decomp 0x0058f130) → gmBackpackUI::SetLoadLevel (0x004a6ea0). currentBurden:
|
||||
/// player wire EncumbranceVal (PropertyInt 5) if present, else the carried-Burden sum.</summary>
|
||||
private void RefreshBurden()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -18,7 +18,7 @@ public enum ItemAppraisalFontStyle
|
|||
/// Separator inserted before an appraisal fragment. This is the retained
|
||||
/// equivalent of <c>ItemExamineUI::AddItemInfo</c>'s final argument.
|
||||
/// </summary>
|
||||
public enum ItemAppraisalSeparator
|
||||
internal enum ItemAppraisalSeparator
|
||||
{
|
||||
None,
|
||||
Line,
|
||||
|
|
@ -30,7 +30,7 @@ public enum ItemAppraisalSeparator
|
|||
/// font-color index. Keeping these separate until shaping preserves style
|
||||
/// when a long fragment wraps.
|
||||
/// </summary>
|
||||
public readonly record struct ItemAppraisalFragment(
|
||||
internal readonly record struct ItemAppraisalFragment(
|
||||
string Text,
|
||||
ItemAppraisalSeparator Separator,
|
||||
ItemAppraisalFontStyle Style);
|
||||
|
|
@ -38,7 +38,7 @@ public readonly record struct ItemAppraisalFragment(
|
|||
/// <summary>
|
||||
/// Immutable item appraisal report in retail append order.
|
||||
/// </summary>
|
||||
public sealed class ItemAppraisalReport
|
||||
internal sealed class ItemAppraisalReport
|
||||
{
|
||||
public static ItemAppraisalReport Empty { get; } = new([]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// and its <c>Appraisal_Show*</c> helpers. Static spell prose comes from the
|
||||
/// installed portal.dat spell table, like retail's <c>ClientMagicSystem</c>.
|
||||
/// </summary>
|
||||
public static class ItemAppraisalTextFormatter
|
||||
internal static class ItemAppraisalTextFormatter
|
||||
{
|
||||
private static readonly (uint Requirement, uint Stat, uint Difficulty)[]
|
||||
WieldRequirements =
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// DAT-authored ten-step radial cooldown art owned by the shared retail
|
||||
/// <c>UIElement_UIItem</c> prototype.
|
||||
/// </summary>
|
||||
public readonly record struct ItemCooldownAssets(IReadOnlyList<uint> Sprites)
|
||||
internal readonly record struct ItemCooldownAssets(IReadOnlyList<uint> Sprites)
|
||||
{
|
||||
public const uint CatalogLayoutId = 0x21000037u;
|
||||
public const uint SharedItemPrototypeId = 0x1000033Eu;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// heartbeat before drawing and gives every current and future
|
||||
/// <see cref="UiItemSlot"/> the same display projection.
|
||||
/// </summary>
|
||||
public sealed class ItemCooldownUiController
|
||||
internal sealed class ItemCooldownUiController
|
||||
{
|
||||
private readonly Spellbook _spellbook;
|
||||
private readonly ClientObjectTable _objects;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using DatReaderWriter;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
|
@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// shared UIItem catalog LayoutDesc, = 0x21000037] -> CreateChildElement(catalog, id); the cloned
|
||||
/// prototype's ItemSlot_Empty (state 0x1000001c) media is the empty-cell background.
|
||||
/// </summary>
|
||||
public static class ItemListCellTemplate
|
||||
internal static class ItemListCellTemplate
|
||||
{
|
||||
/// <summary>The shared UIItem cell-template catalog. Hardcoded: retail resolves it via
|
||||
/// GetByEnum(0x10000038,5,0x23) through a master enum-map DAT object (no code literal);
|
||||
|
|
@ -87,14 +87,14 @@ public static class ItemListCellTemplate
|
|||
// child search alone returns nothing for containers. We deliberately do NOT use the
|
||||
// prototype's DirectState child overlay: on the container prototype that child is the
|
||||
// open/selected-container TRIANGLE indicator (0x06005D9C), which retail draws ONLY on the
|
||||
// selected container (a deferred container-selection feature) — never as empty-cell art.
|
||||
// selected container (a deferred container-selection feature) — never as empty-cell art.
|
||||
// (Live visual gate 2026-06-22: frame-first stamped the triangle onto every empty cell.)
|
||||
return FindIconEmpty(catalog, proto, new HashSet<uint>());
|
||||
}
|
||||
|
||||
// ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
|
||||
// ── attribute 0x1000000e: stored as EnumBaseProperty in the dat (Value is the prototype id) ──
|
||||
// Note: the spec anticipated DataIdBaseProperty/ArrayBaseProperty based on the font-DID pattern,
|
||||
// but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
|
||||
// but the live dat uses EnumBaseProperty.Value (uint) — confirmed by runtime reflection.
|
||||
private static uint ReadCellTemplateId(ElementDesc elem)
|
||||
{
|
||||
uint id = ReadIdFromState(elem.StateDesc);
|
||||
|
|
@ -121,7 +121,7 @@ public static class ItemListCellTemplate
|
|||
return 0;
|
||||
}
|
||||
|
||||
// ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ──
|
||||
// ── prototype media: the m_elem_Icon (0x1000033B) ItemSlot_Empty, resolved through inheritance ──
|
||||
// Find the icon child's empty media within `element`'s subtree, following BaseElement edges
|
||||
// within the same catalog (cycle-guarded by `baseSeen`). The 32x32 contents prototype carries
|
||||
// 0x1000033B as a direct child; the 36x36 container prototype reaches it only via an inherited
|
||||
|
|
@ -171,7 +171,7 @@ public static class ItemListCellTemplate
|
|||
return 0;
|
||||
}
|
||||
|
||||
// ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
|
||||
// ── depth-first element search by id (LayoutImporter.FindDesc is private there) ──
|
||||
private static ElementDesc? FindDesc(LayoutDesc ld, uint id)
|
||||
{
|
||||
foreach (var kv in ld.Elements)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.App.Input;
|
||||
using AcDream.App.Input;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>RecvNotice_FinishPowerbar</c> (0x004DA580), and
|
||||
/// <c>ClientCombatSystem::CommenceJump</c> (0x0056AF90).
|
||||
/// </remarks>
|
||||
public sealed class JumpPowerbarController : IRetainedPanelController
|
||||
internal sealed class JumpPowerbarController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000072u;
|
||||
public const uint MeterId = 0x10000034u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
|
@ -12,7 +12,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// The result of importing a retail LayoutDesc: a <see cref="UiElement"/> tree with
|
||||
/// an O(1) lookup table for finding any element by its dat id.
|
||||
/// </summary>
|
||||
public sealed class ImportedLayout
|
||||
internal sealed class ImportedLayout
|
||||
{
|
||||
/// <summary>Root widget of the imported tree.</summary>
|
||||
public UiElement Root { get; }
|
||||
|
|
@ -37,7 +37,7 @@ public sealed class ImportedLayout
|
|||
/// <para>
|
||||
/// <strong>Pure layer</strong> (<see cref="Build"/> / <see cref="BuildFromInfos"/>):
|
||||
/// converts a pre-resolved <see cref="ElementInfo"/> tree into a <see cref="UiElement"/>
|
||||
/// tree via <see cref="DatWidgetFactory"/>. Testable without dats or OpenGL — all tests
|
||||
/// tree via <see cref="DatWidgetFactory"/>. Testable without dats or OpenGL — all tests
|
||||
/// in <c>LayoutImporterTests.cs</c> exercise this layer only.
|
||||
/// </para>
|
||||
///
|
||||
|
|
@ -55,9 +55,9 @@ public sealed class ImportedLayout
|
|||
/// Every other element type recurses its children generically.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class LayoutImporter
|
||||
internal static class LayoutImporter
|
||||
{
|
||||
// ── Pure layer ────────────────────────────────────────────────────────────
|
||||
// ── Pure layer ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Convenience for tests: attach <paramref name="children"/> to
|
||||
|
|
@ -68,7 +68,7 @@ public static class LayoutImporter
|
|||
public static ImportedLayout BuildFromInfos(
|
||||
ElementInfo rootInfo,
|
||||
IEnumerable<ElementInfo> children,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null,
|
||||
Func<UiStringInfoValue, string?>? stringResolve = null)
|
||||
|
|
@ -81,15 +81,15 @@ public static class LayoutImporter
|
|||
/// Pure builder: produce the widget tree from a fully resolved
|
||||
/// <see cref="ElementInfo"/> tree (children already attached).
|
||||
/// </summary>
|
||||
/// <param name="fontResolve">Optional per-element font resolver — FontDid →
|
||||
/// <param name="fontResolve">Optional per-element font resolver — FontDid →
|
||||
/// <see cref="UiDatFont"/> (or null if the font can't be loaded). When supplied,
|
||||
/// elements with a non-zero <see cref="ElementInfo.FontDid"/> get their own dat
|
||||
/// font at build time instead of the shared <paramref name="datFont"/> fallback.
|
||||
/// Null preserves the original single-font behavior for all callers that don't
|
||||
/// pass it — no behavior change for the live game path.</param>
|
||||
/// pass it — no behavior change for the live game path.</param>
|
||||
public static ImportedLayout Build(
|
||||
ElementInfo rootInfo,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null,
|
||||
Func<UiStringInfoValue, string?>? stringResolve = null)
|
||||
|
|
@ -100,7 +100,7 @@ public static class LayoutImporter
|
|||
var root = BuildWidget(rootInfo, resolve, datFont, fontResolve, stringResolve, byId);
|
||||
if (root is null)
|
||||
{
|
||||
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
|
||||
Console.WriteLine($"[D.2b] LayoutImporter: root element 0x{rootInfo.Id:X8} (type {rootInfo.Type}) produced no widget — using empty container fallback.");
|
||||
root = new UiDatElement(rootInfo, resolve);
|
||||
}
|
||||
return new ImportedLayout(root, byId);
|
||||
|
|
@ -108,20 +108,20 @@ public static class LayoutImporter
|
|||
|
||||
private static UiElement? BuildWidget(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve,
|
||||
Func<UiStringInfoValue, string?>? stringResolve,
|
||||
Dictionary<uint, UiElement> byId)
|
||||
{
|
||||
var w = DatWidgetFactory.Create(info, resolve, datFont, fontResolve, stringResolve);
|
||||
if (w is null) return null; // Type-12 style prototype — skip
|
||||
if (w is null) return null; // Type-12 style prototype — skip
|
||||
|
||||
if (info.Id != 0) byId[info.Id] = w;
|
||||
|
||||
// Behavioral widgets that draw their full appearance + reproduce their dat
|
||||
// sub-elements procedurally (Meter's 3-slice, Menu's label/rows, Field/Text caps,
|
||||
// Button labels, Scrollbar arrows) CONSUME their dat children — building those as
|
||||
// Button labels, Scrollbar arrows) CONSUME their dat children — building those as
|
||||
// separate widgets double-draws and lets an invisible child steal pointer/focus
|
||||
// from the behavioral widget (e.g. the channel Menu's label child intercepting the
|
||||
// button click). Only generic containers (UiDatElement, panels) recurse. See
|
||||
|
|
@ -152,7 +152,7 @@ public static class LayoutImporter
|
|||
// double-draw the bar art). All other child types are built normally.
|
||||
//
|
||||
// Safe for vitals: the health/stamina/mana meters have ONLY Type-3 slice children
|
||||
// (no text children). This loop finds nothing for them → no change to vitals.
|
||||
// (no text children). This loop finds nothing for them → no change to vitals.
|
||||
foreach (var child in info.Children)
|
||||
{
|
||||
if (child.Type == 3) continue; // slice containers: already consumed by BuildMeter
|
||||
|
|
@ -171,7 +171,7 @@ public static class LayoutImporter
|
|||
return w;
|
||||
}
|
||||
|
||||
// ── Dat shell ─────────────────────────────────────────────────────────────
|
||||
// ── Dat shell ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Dat shell, ElementInfo half: load the layout + resolve inheritance + build the
|
||||
|
|
@ -187,7 +187,7 @@ public static class LayoutImporter
|
|||
|
||||
// Collect the set of element ids that are referenced as a BaseElement by ANY
|
||||
// element in THIS layout (where BaseLayoutId == layoutId). Such elements are
|
||||
// purely inheritance templates ("prototypes") — retail never instantiates them
|
||||
// purely inheritance templates ("prototypes") — retail never instantiates them
|
||||
// as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc
|
||||
// 0x21000016, which all 18 slot elements inherit from and which has no own media.
|
||||
//
|
||||
|
|
@ -258,7 +258,7 @@ public static class LayoutImporter
|
|||
/// state an element starts in (e.g., <c>Normal</c>, <c>Minimized</c>). It does
|
||||
/// NOT encode visibility of sibling Group containers. The <c>StateDesc</c>'s
|
||||
/// <see cref="DatReaderWriter.Enums.IncorporationFlags"/> contains X/Y/Width/Height/
|
||||
/// ZLevel/PassToChildren — there is no Visible flag.
|
||||
/// ZLevel/PassToChildren — there is no Visible flag.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -276,7 +276,7 @@ public static class LayoutImporter
|
|||
public static ImportedLayout? Import(
|
||||
IDatReaderWriter dats,
|
||||
uint layoutId,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null)
|
||||
{
|
||||
|
|
@ -291,7 +291,7 @@ public static class LayoutImporter
|
|||
IDatReaderWriter dats,
|
||||
uint layoutId,
|
||||
uint rootElementId,
|
||||
Func<uint, (uint, int, int)> resolve,
|
||||
Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
UiDatFont? datFont,
|
||||
Func<uint, UiDatFont?>? fontResolve = null)
|
||||
{
|
||||
|
|
@ -301,7 +301,7 @@ public static class LayoutImporter
|
|||
return Build(rootInfo, resolve, datFont, fontResolve, strings.Resolve);
|
||||
}
|
||||
|
||||
// ── Inheritance resolution ────────────────────────────────────────────────
|
||||
// ── Inheritance resolution ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>True when a pure-container inheritor needs the mounted-base Z-layer
|
||||
/// correction. Child inheritance itself is unconditional and follows retail
|
||||
|
|
@ -353,9 +353,9 @@ public static class LayoutImporter
|
|||
// The mounted slot's layer WITHIN THE FRAME is its OWN ZLevel, not the mounted
|
||||
// sub-window root's. The gm*UI sub-window roots carry ZLevel 1000 (their standalone
|
||||
// top-window layer); ElementReader.Merge's zero-wins-base rule made the slot (own
|
||||
// ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000)
|
||||
// turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's
|
||||
// Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the
|
||||
// ZLevel 0) inherit that 1000, and the #145 ZOrder fold (ReadOrder − ZLevel·10000)
|
||||
// turns 1000 into ZOrder ≈ −10,000,000 — sinking the whole panel BEHIND the frame's
|
||||
// Alphablend backdrop (ZLevel 100 → ≈ −1,000,000). The backdrop then overpaints the
|
||||
// panel's captions/meter/cells (the wash-out bug; the paperdoll root happens to be
|
||||
// ZLevel 0 so it escaped). Restore the slot's own frame-layer so the panel sits in
|
||||
// FRONT of the backdrop. (B-Controller debug 2026-06-21; continuation of #145.)
|
||||
|
|
@ -452,7 +452,7 @@ public static class LayoutImporter
|
|||
if (d.StateDesc is not null)
|
||||
ReadState(d.StateDesc, UiStateInfo.DirectStateId, "", info);
|
||||
|
||||
// Named states (e.g. UIStateId.HideDetail → "HideDetail").
|
||||
// Named states (e.g. UIStateId.HideDetail → "HideDetail").
|
||||
foreach (var s in d.States)
|
||||
ReadState(s.Value, (uint)s.Key, s.Key.ToString(), info);
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ public static class LayoutImporter
|
|||
/// Read the first <see cref="MediaDescImage"/> from <paramref name="sd"/> into
|
||||
/// <c>info.StateMedia[name]</c>, read any <see cref="MediaDescCursor"/> into
|
||||
/// <c>info.StateCursors[name]</c>, and extract the font DID from property 0x1A
|
||||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>) if not yet set.
|
||||
/// (<c>ArrayBaseProperty → DataIdBaseProperty</c>) if not yet set.
|
||||
/// </summary>
|
||||
private static void ReadState(StateDesc sd, uint stateId, string name, ElementInfo info)
|
||||
{
|
||||
|
|
@ -504,7 +504,7 @@ public static class LayoutImporter
|
|||
info.States[stateId] = state;
|
||||
|
||||
// Font DID: Properties[0x1A] is ArrayBaseProperty{ DataIdBaseProperty }.
|
||||
// Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
|
||||
// Format doc §3: "ArrayBaseProperty containing ONE DataIdBaseProperty".
|
||||
if (info.FontDid == 0 && sd.Properties is not null
|
||||
&& sd.Properties.TryGetValue(0x1Au, out var raw)
|
||||
&& raw is ArrayBaseProperty arr && arr.Value.Count > 0
|
||||
|
|
@ -547,15 +547,15 @@ public static class LayoutImporter
|
|||
};
|
||||
}
|
||||
|
||||
// ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A).
|
||||
// ColorBaseProperty (0x1B): ARGB bytes → normalized [0,1] Vector4 (R,G,B,A).
|
||||
// Only read when not already set (first dat state wins; Merge propagates from base).
|
||||
if (info.FontColor is null
|
||||
&& sd.Properties.TryGetValue(0x1Bu, out var cRaw)
|
||||
&& cRaw is ColorBaseProperty cProp)
|
||||
{
|
||||
var c = cProp.Value;
|
||||
// ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4.
|
||||
// Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255).
|
||||
// ColorARGB stores components as bytes (0–255); normalize to [0,1] for Vector4.
|
||||
// Alpha=0 in the dat typically means fully opaque (retail convention: 0 → 255).
|
||||
float a = c.Alpha == 0 ? 1f : c.Alpha / 255f;
|
||||
info.FontColor = new System.Numerics.Vector4(c.Red / 255f, c.Green / 255f, c.Blue / 255f, a);
|
||||
}
|
||||
|
|
@ -638,7 +638,7 @@ public static class LayoutImporter
|
|||
return value;
|
||||
}
|
||||
|
||||
// ── Prototype detection helpers ───────────────────────────────────────────
|
||||
// ── Prototype detection helpers ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Recursively walks <paramref name="d"/> and all its children, adding to
|
||||
|
|
@ -656,7 +656,7 @@ public static class LayoutImporter
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when <paramref name="d"/> carries no own state media — i.e. its
|
||||
/// Returns true when <paramref name="d"/> carries no own state media — i.e. its
|
||||
/// <c>StateDesc</c> (DirectState) and <c>States</c> (named states) yield no
|
||||
/// <see cref="MediaDescImage"/> entries with a non-zero file id.
|
||||
/// Such elements are pure inheritance templates with no rendering content.
|
||||
|
|
@ -669,7 +669,7 @@ public static class LayoutImporter
|
|||
return info.StateMedia.Count == 0;
|
||||
}
|
||||
|
||||
// ── Element tree search ───────────────────────────────────────────────────
|
||||
// ── Element tree search ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Find an <see cref="ElementDesc"/> by id anywhere in the top-level tree of
|
||||
|
|
@ -696,7 +696,7 @@ public static class LayoutImporter
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── Raw-edge layout provenance ────────────────────────────────────────────
|
||||
// ── Raw-edge layout provenance ────────────────────────────────────────────
|
||||
|
||||
private static void SetOriginalParentSize(ElementInfo child, float width, float height)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using AcDream.Core.Net;
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// chrome; this controller owns its five-second text refresh and 120-second
|
||||
/// ping cadence.
|
||||
/// </summary>
|
||||
public sealed class LinkStatusUiController : IRetainedPanelController
|
||||
internal sealed class LinkStatusUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x2100001Du;
|
||||
public const uint RootId = 0x10000167u;
|
||||
|
|
@ -126,7 +126,7 @@ public sealed class LinkStatusUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record LinkStatusStrings(
|
||||
internal sealed record LinkStatusStrings(
|
||||
string Description,
|
||||
string Legend,
|
||||
string DisconnectWarning,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// ghosted until a future game-state owner calls <c>SetMiniGameActive</c>;
|
||||
/// mounting the page now keeps its window lifecycle on the shared retail host.
|
||||
/// </summary>
|
||||
public sealed class MiniGameUiController : IRetainedPanelController
|
||||
internal sealed class MiniGameUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x2100001Eu;
|
||||
public const uint RootId = 0x1000016Au;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Textures;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>gmPaperDollUI::CreateClickMap @ 0x004A4850</c> and
|
||||
/// <c>gmPaperDollUI::GetPaperDollItemUnderMouse @ 0x004A4920</c>.
|
||||
/// </summary>
|
||||
public sealed class PaperdollClickMap
|
||||
internal sealed class PaperdollClickMap
|
||||
{
|
||||
public const uint ClickMapEnum = 0x1000000Cu;
|
||||
public const uint InterfaceEnumCategory = 7u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -10,19 +10,19 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Binds the 24 equip slots mounted under the paperdoll (gmPaperDollUI 0x21000024, nested in the
|
||||
/// inventory frame 0x21000023) to live equipped-item data and makes them drag-drop WIELD targets.
|
||||
/// The acdream analogue of gmPaperDollUI::PostInit + GetLocationInfoFromElementID (named-retail decomp
|
||||
/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
|
||||
/// 175480 / 173620). Slice 1: equip slots only — no 3D doll viewport (that's Slice 2).
|
||||
/// Unwield is handled by InventoryController (dragging an equipped item onto the pack grid).
|
||||
/// </summary>
|
||||
public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController
|
||||
internal sealed class PaperdollController : IItemListDragHandler, IRetainedPanelController
|
||||
{
|
||||
public const uint DollViewportId = 0x100001D5u;
|
||||
public const uint DollDragMaskId = 0x100001D6u;
|
||||
|
||||
// ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
|
||||
// ── Slots-toggle public surface ───────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// The 9 armor-slot element-ids whose Visible state the Slots button (0x100005BE) toggles.
|
||||
/// Doll-view: hidden. Slot-view: shown. Source: gmPaperDollUI::ListenToElementMessage decomp
|
||||
/// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
|
||||
/// 175674-175706 — these are the only 9 ids that element flips. The 12 ordinary non-armor
|
||||
/// lists remain visible in both views; the three Aetheria lists are independently unlock-gated.
|
||||
/// </summary>
|
||||
public static readonly uint[] ArmorSlotElementIds =
|
||||
|
|
@ -36,7 +36,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
/// Default is doll-view (SlotView == false): the 3-D character is visible,
|
||||
/// the 9 armor slots are hidden. Calling Toggle() alternates between views.
|
||||
/// </summary>
|
||||
public sealed class PaperdollViewState
|
||||
internal sealed class PaperdollViewState
|
||||
{
|
||||
public bool SlotView { get; private set; } // false = doll-view (default)
|
||||
public bool DollVisible => !SlotView;
|
||||
|
|
@ -46,8 +46,8 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> _iconIds;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? _dragIconIds;
|
||||
private readonly ItemInteractionController _itemInteraction;
|
||||
private readonly bool _ownsItemInteraction;
|
||||
private readonly SelectionState _selection;
|
||||
|
|
@ -55,7 +55,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
private readonly List<(EquipMask Mask, UiItemList List)> _slots = new();
|
||||
private readonly List<(AetheriaUnlockState Bit, UiItemList List)> _aetheriaSlots = new();
|
||||
|
||||
// ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
|
||||
// ── Slots-toggle state ────────────────────────────────────────────────────────────────────────
|
||||
private readonly PaperdollViewState _viewState = new();
|
||||
private readonly List<UiItemList> _armorSlots = new();
|
||||
private UiElement? _dollViewport; // UiViewport wired in Slice 3; UiElement? keeps this task independent
|
||||
|
|
@ -64,11 +64,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
private PaperdollController(
|
||||
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds, SelectionState selection,
|
||||
ItemInteractionController itemInteraction,
|
||||
uint emptySlotSprite, UiDatFont? datFont,
|
||||
PaperdollClickMap? clickMap,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds,
|
||||
IReadOnlyDictionary<uint, uint>? emptySlotSprites,
|
||||
bool ownsItemInteraction)
|
||||
{
|
||||
|
|
@ -97,10 +97,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
if (list.Cell.ItemId != 0)
|
||||
_itemInteraction?.ActivateItem(list.Cell.ItemId);
|
||||
};
|
||||
// 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot
|
||||
// 3D character (the "figure" — Slice 1/2 correction: NOT a per-slot
|
||||
// silhouette) is the doll viewport, which arrives in Slice 2 with the Slots toggle.
|
||||
// Cell.SpriteResolve + the default accept/reject sprites (ItemSlot_DragOver_Accept ring
|
||||
// 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
|
||||
// 0x060011F9 / reject circle 0x060011F8 — the discrete-slot frames, NOT the inventory grid's
|
||||
// insert-arrow 0x060011F7) are already wired by DatWidgetFactory when it built the UiItemList;
|
||||
// no need to re-set them here.
|
||||
_slots.Add((mask, list));
|
||||
|
|
@ -115,7 +115,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
_objects.Cleared += OnObjectsCleared;
|
||||
_selection.Changed += OnSelectionChanged;
|
||||
|
||||
// ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
|
||||
// ── Slots-toggle wiring ───────────────────────────────────────────────────────────────────
|
||||
foreach (var id in ArmorSlotElementIds)
|
||||
if (layout.FindElement(id) is UiItemList armor) _armorSlots.Add(armor);
|
||||
|
||||
|
|
@ -166,11 +166,11 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
|
||||
public static PaperdollController Bind(
|
||||
ImportedLayout layout, ClientObjectTable objects, Func<uint> playerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds, SelectionState selection,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds, SelectionState selection,
|
||||
ItemInteractionController itemInteraction,
|
||||
uint emptySlotSprite = 0u, UiDatFont? datFont = null,
|
||||
PaperdollClickMap? clickMap = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds = null,
|
||||
IReadOnlyDictionary<uint, uint>? emptySlotSprites = null,
|
||||
bool ownsItemInteraction = false)
|
||||
=> new PaperdollController(
|
||||
|
|
@ -221,7 +221,7 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
Populate();
|
||||
}
|
||||
|
||||
/// <summary>The object belongs to the player (wielded gear or pack contents) — so a change to it may
|
||||
/// <summary>The object belongs to the player (wielded gear or pack contents) — so a change to it may
|
||||
/// add/remove/repaint a doll slot. Player-scoped: an NPC's or vendor's wielded item (which also carries
|
||||
/// CurrentlyEquippedLocation from the wire) must NOT trigger a repaint. A player-equipped item always
|
||||
/// has WielderId==p (login, from CreateObject) or ContainerId==p (live/optimistic wield, set by
|
||||
|
|
@ -251,9 +251,9 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
if ((o.CurrentlyEquippedLocation & mask) != EquipMask.None) { worn = o; break; }
|
||||
|
||||
if (worn is null) { list.Cell.Clear(); continue; }
|
||||
uint tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
|
||||
uint dragTex = _dragIconIds?.Invoke(
|
||||
worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects) ?? 0u;
|
||||
GpuTextureSlot tex = _iconIds(worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
|
||||
GpuTextureSlot? dragTex = _dragIconIds?.Invoke(
|
||||
worn.Type, worn.IconId, worn.IconUnderlayId, worn.IconOverlayId, worn.Effects);
|
||||
list.Cell.SetItem(worn.ObjectId, tex, dragIconTexture: dragTex);
|
||||
}
|
||||
ApplyAetheriaVisibility();
|
||||
|
|
@ -287,10 +287,10 @@ public sealed class PaperdollController : IItemListDragHandler, IRetainedPanelCo
|
|||
return EquipMask.None;
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler ──────────────────────────────────────────────────────────────────────
|
||||
// ── IItemListDragHandler ──────────────────────────────────────────────────────────────────────
|
||||
/// <summary>Selects the wielded item before the waiting mesh appears. The item itself stays put
|
||||
/// until the server confirms, like the inventory grid and unlike the toolbar's remove-on-lift.
|
||||
/// Unwield happens on DROP onto the pack grid — InventoryController.</summary>
|
||||
/// Unwield happens on DROP onto the pack grid — InventoryController.</summary>
|
||||
public void OnDragLift(UiItemList sourceList, UiItemSlot sourceCell, ItemDragPayload payload)
|
||||
{
|
||||
// UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Content;
|
||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Cell creation: <c>UIElement_ItemList::InternalCreateItem @ 0x004E3570</c>.
|
||||
/// Prototype media: live DAT catalog <c>LayoutDesc 0x21000037</c>.
|
||||
/// </remarks>
|
||||
public static class PaperdollSlotBackgrounds
|
||||
internal static class PaperdollSlotBackgrounds
|
||||
{
|
||||
internal readonly record struct Definition(
|
||||
uint Element,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// LayoutDesc <c>0x21000074</c> tree. Like the other gm* controllers, this class only
|
||||
/// finds children by retail id and attaches live providers; it does not recreate DAT chrome.
|
||||
/// </summary>
|
||||
public sealed class RadarController : IRetainedPanelController
|
||||
internal sealed class RadarController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000074u;
|
||||
/// <summary>Production layout property 0x1000002D, recovered directly from the retail DAT.</summary>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net;
|
||||
|
|
@ -15,7 +15,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// AC-specific classification and math decision, while the retained widget remains a
|
||||
/// backend-only renderer.
|
||||
/// </summary>
|
||||
public sealed class RadarSnapshotProvider
|
||||
internal sealed class RadarSnapshotProvider
|
||||
{
|
||||
private static readonly Vector2 ProductionCenter = new(60f, 60f);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Items;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
|
@ -14,7 +14,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>AppraisalSystem::InqCreatureDisplayName @ 0x005B59E0</c> and
|
||||
/// <c>InqHeritageGroupDisplayName @ 0x005B4710</c>.
|
||||
/// </summary>
|
||||
public sealed class RetailAppraisalNameResolver
|
||||
internal sealed class RetailAppraisalNameResolver
|
||||
{
|
||||
private const uint MaterialClientEnum = 0x10000001u;
|
||||
private const uint MaterialSubEnum = 1u;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Property identifiers consumed by retail's <c>Dialog</c> and
|
||||
/// <c>DialogFactory</c> implementations.
|
||||
/// </summary>
|
||||
public static class RetailDialogProperty
|
||||
internal static class RetailDialogProperty
|
||||
{
|
||||
public const uint Priority = 0x8Du;
|
||||
public const uint Type = 0x8Eu;
|
||||
|
|
@ -43,7 +43,7 @@ public enum RetailDialogType : uint
|
|||
/// The raw numeric keys remain visible because type-specific presenters and semantic
|
||||
/// callbacks both extend the same collection in retail.
|
||||
/// </summary>
|
||||
public sealed class RetailDialogData
|
||||
internal sealed class RetailDialogData
|
||||
{
|
||||
private readonly Dictionary<uint, object> _values = new();
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Retained-mode port of retail <c>DialogFactory @ 0x004773C0..0x00478470</c>.
|
||||
/// It owns dialog contexts, independent FIFO queue groups, nonqueued dialogs,
|
||||
/// priority preemption, callback delivery, close notices, and fresh catalog roots.
|
||||
/// </summary>
|
||||
public sealed class RetailDialogFactory : IDisposable
|
||||
internal sealed class RetailDialogFactory : IDisposable
|
||||
{
|
||||
public const uint DefaultQueueKey = 2u;
|
||||
public const uint NonQueuedKey = 1u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -13,7 +13,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// string 0x0DCFFF73 supplies the two labels <c>FPS:</c> and <c>DEG:</c>;
|
||||
/// retail inserts both floating-point values with two decimal places.
|
||||
/// </remarks>
|
||||
public sealed class RetailFpsController
|
||||
internal sealed class RetailFpsController
|
||||
{
|
||||
public const uint LayoutId = 0x2100000Fu;
|
||||
public const uint DisplayElementId = 0x10000047u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// deferred child; this owner applies the same lifecycle to registered retained
|
||||
/// windows without making the toolbar authoritative for non-toolbar panels.
|
||||
/// </summary>
|
||||
public sealed class RetailPanelUiController : IDisposable
|
||||
internal sealed class RetailPanelUiController : IDisposable
|
||||
{
|
||||
public const uint RestorePreviousPropertyId = 0x10000049u;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>How a production retained window obtains its outer chrome.</summary>
|
||||
public enum RetailWindowChrome
|
||||
internal enum RetailWindowChrome
|
||||
{
|
||||
/// <summary>The imported root already is the complete retail outer frame.</summary>
|
||||
Imported,
|
||||
|
|
@ -21,9 +21,9 @@ public enum RetailWindowChrome
|
|||
/// shared nine-slice wrapper, applies exact resize/opacity/visibility policy, mounts
|
||||
/// the outer frame, and returns its registered typed handle.
|
||||
/// </summary>
|
||||
public static class RetailWindowFrame
|
||||
internal static class RetailWindowFrame
|
||||
{
|
||||
public sealed record Options
|
||||
internal sealed record Options
|
||||
{
|
||||
public required string WindowName { get; init; }
|
||||
public RetailWindowChrome Chrome { get; init; } = RetailWindowChrome.NineSlice;
|
||||
|
|
@ -87,7 +87,7 @@ public static class RetailWindowFrame
|
|||
public static RetailWindowHandle Mount(
|
||||
UiRoot root,
|
||||
UiElement content,
|
||||
Func<uint, (uint handle, int w, int h)> resolveChrome,
|
||||
Func<uint, (GpuTextureSlot handle, int w, int h)> resolveChrome,
|
||||
Options options)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(root);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -7,7 +7,7 @@ using AcDream.Core.Selection;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
|
||||
/// Controller for the action bar's selected-object strip (ids 0x1000019E–0x100001A4).
|
||||
/// Analogue of retail <c>gmToolbarUI::HandleSelectionChanged</c>
|
||||
/// (<c>docs/research/named-retail/acclient_2013_pseudo_c.txt:198635</c>) +
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> (<c>:196213</c>) +
|
||||
|
|
@ -18,29 +18,29 @@ namespace AcDream.App.UI.Layout;
|
|||
/// guid is provided it sets the name, flashes the selection overlay briefly, and sends
|
||||
/// either <c>QueryHealth (0x01BF)</c> for health-bearing targets or
|
||||
/// <c>QueryItemMana (0x0263)</c> for owned non-stack items. The Health meter
|
||||
/// becomes visible only when the server actually reports health for the selected guid —
|
||||
/// becomes visible only when the server actually reports health for the selected guid —
|
||||
/// either an <c>UpdateHealth (0x01C0)</c> arrives (retail
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> → <c>SetVisible(1)</c>) or the value is already
|
||||
/// <c>RecvNotice_UpdateObjectHealth</c> → <c>SetVisible(1)</c>) or the value is already
|
||||
/// cached. So a friendly NPC you have not assessed shows name-only (no bar), and a
|
||||
/// monster's bar appears after damage / a successful assess — matching retail.
|
||||
/// monster's bar appears after damage / a successful assess — matching retail.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// <strong>Retail element roles</strong> (PostInit, <c>:198119</c>): <c>m_pSelObjectField</c>
|
||||
/// is the container <c>0x1000019E</c> whose <c>SetState(0x1000000b/0c)</c> drives a
|
||||
/// 0.25s <c>Pause→Normal</c> flash that cascades to the overlay child's green frame.
|
||||
/// 0.25s <c>Pause→Normal</c> flash that cascades to the overlay child's green frame.
|
||||
/// acdream has no state-cascade / transition-animation system, so this controller drives
|
||||
/// the overlay element <c>0x100001A0</c> directly and reverts it after the same
|
||||
/// <see cref="FlashSeconds"/> to reproduce the brief flash. The name element
|
||||
/// <c>0x1000019F</c> is bumped to the top of the strip's z-order so it draws OVER the
|
||||
/// overlay frame and the health bar (retail draws the name over the bar — see the
|
||||
/// overlay frame and the health bar (retail draws the name over the bar — see the
|
||||
/// "Drudge Slinker" reference shot).
|
||||
/// </para>
|
||||
///
|
||||
/// </summary>
|
||||
public sealed class SelectedObjectController : IRetainedPanelController
|
||||
internal sealed class SelectedObjectController : IRetainedPanelController
|
||||
{
|
||||
// ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
|
||||
// ── Element ids (toolbar LayoutDesc 0x21000016) ─────────────────────────
|
||||
/// <summary>Selected-object container / field element id (retail m_pSelObjectField).</summary>
|
||||
public const uint ContainerId = 0x1000019E;
|
||||
/// <summary>Selected-object name element id (retail m_pSelObjectName, UIElement_Text).</summary>
|
||||
|
|
@ -56,24 +56,24 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
/// <summary>Horizontal stack quantity slider (retail m_pStackSizeSlider).</summary>
|
||||
public const uint StackSizeSliderId = 0x100001A4;
|
||||
|
||||
/// <summary>Selection-overlay flash duration — retail's container ObjectSelected state is a
|
||||
/// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E).</summary>
|
||||
/// <summary>Selection-overlay flash duration — retail's container ObjectSelected state is a
|
||||
/// Pause(0.25s)→Normal transition (toolbar dump, element 0x1000019E).</summary>
|
||||
private const double FlashSeconds = 0.25;
|
||||
|
||||
/// <summary>Z-order for the name so it draws OVER the overlay frame + health bar.
|
||||
/// The strip's other children sit at ReadOrder 1–4; this floats the name to the top.</summary>
|
||||
/// The strip's other children sit at ReadOrder 1–4; this floats the name to the top.</summary>
|
||||
private const int NameZOrderOnTop = 1_000_000;
|
||||
|
||||
/// <summary>Z-order for the selection-flash overlay — above the health meter (so the green
|
||||
/// <summary>Z-order for the selection-flash overlay — above the health meter (so the green
|
||||
/// flash isn't hidden by the bar) but below the name (so the name stays readable).</summary>
|
||||
private const int OverlayZOrder = NameZOrderOnTop - 1;
|
||||
|
||||
/// <summary>Height (px) of the black name band at the top of the 31px bar sprite. The name
|
||||
/// label is constrained to this band (top-aligned) so the health bar shows below it —
|
||||
/// label is constrained to this band (top-aligned) so the health bar shows below it —
|
||||
/// retail "name on the black, bar below". The bar sprite's colored region starts ~y14.</summary>
|
||||
private const float NameBandHeight = 15f;
|
||||
|
||||
// ── Found elements (any may be null for partial/test layouts) ───────────
|
||||
// ── Found elements (any may be null for partial/test layouts) ───────────
|
||||
private readonly UiElement? _name;
|
||||
private readonly UiDatElement? _overlay;
|
||||
private readonly UiMeter? _healthMeter;
|
||||
|
|
@ -81,7 +81,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
private readonly UiField? _stackSizeEntry;
|
||||
private readonly UiScrollbar? _stackSizeSlider;
|
||||
|
||||
// ── Captured delegates ───────────────────────────────────────────────────
|
||||
// ── Captured delegates ───────────────────────────────────────────────────
|
||||
private readonly Func<uint, bool> _isHealthTarget;
|
||||
private readonly Func<uint, bool> _isOwnedByPlayer;
|
||||
private readonly Func<uint, string?> _resolveName;
|
||||
|
|
@ -97,7 +97,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
private readonly Action<Action<uint, float, bool>> _unsubscribeItemManaChanged;
|
||||
private readonly Action<Action<ClientObject>> _unsubscribeObjectUpdated;
|
||||
|
||||
// ── Live state (read by closures on the per-frame draw path) ────────────
|
||||
// ── Live state (read by closures on the per-frame draw path) ────────────
|
||||
private uint? _current;
|
||||
private string? _currentName;
|
||||
private double _flashRemaining; // > 0 while the selection overlay is flashing
|
||||
|
|
@ -143,7 +143,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_unsubscribeItemManaChanged = unsubscribeItemManaChanged;
|
||||
_unsubscribeObjectUpdated = unsubscribeObjectUpdated;
|
||||
|
||||
// Find elements — silently skip absent ones (partial/test layouts).
|
||||
// Find elements — silently skip absent ones (partial/test layouts).
|
||||
_name = layout.FindElement(NameId);
|
||||
_overlay = layout.FindElement(OverlayId) as UiDatElement;
|
||||
_healthMeter = layout.FindElement(HealthMeterId) as UiMeter;
|
||||
|
|
@ -152,7 +152,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_stackSizeSlider = layout.FindElement(StackSizeSliderId) as UiScrollbar;
|
||||
|
||||
// The selection-flash overlay must draw OVER the health meter (which spans the whole
|
||||
// strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e.
|
||||
// strip) — otherwise the meter hides the green flash whenever a bar is visible (i.e.
|
||||
// for players/monsters). Float it just below the name so the name stays readable.
|
||||
if (_overlay is not null) _overlay.ZOrder = OverlayZOrder;
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
//
|
||||
// The bar sprite (0x0600193E/F, 146x31) carries a ~14px BLACK name band across its
|
||||
// TOP with the colored bar in the lower portion (confirmed from the dat). Retail
|
||||
// draws the object name in that black band with the health bar BELOW it — so the
|
||||
// draws the object name in that black band with the health bar BELOW it — so the
|
||||
// label is TOP-aligned by constraining its height to the band, not centered over the
|
||||
// whole 31px strip (which overlapped the bar's middle).
|
||||
if (_name is not null)
|
||||
|
|
@ -240,7 +240,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
||||
/// <param name="selection">The single Core selected-object owner.</param>
|
||||
/// <param name="subscribeHealthChanged">Called once with <see cref="OnHealthChanged"/>
|
||||
/// (typical host: <c>h => Combat.HealthChanged += h</c>) — drives meter visibility.</param>
|
||||
/// (typical host: <c>h => Combat.HealthChanged += h</c>) — drives meter visibility.</param>
|
||||
/// <param name="isHealthTarget">Returns true for guids that may show a health meter
|
||||
/// (proxy for retail's <c>IsPlayer() || pet_owner || ObjectIsAttackable()</c>).</param>
|
||||
/// <param name="name">Returns retail's NAME_APPROPRIATE display name for a guid.</param>
|
||||
|
|
@ -296,8 +296,8 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
_sendQueryItemMana(0);
|
||||
}
|
||||
|
||||
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
|
||||
// + SetVisible(0) on the meters). ──────────────────────────────────────
|
||||
// ── 1. Clear first (retail: SetText("") + m_pSelObjectField->SetState(0)
|
||||
// + SetVisible(0) on the meters). ──────────────────────────────────────
|
||||
if (selectionChanged)
|
||||
{
|
||||
if (_healthMeter is not null) _healthMeter.Visible = false;
|
||||
|
|
@ -319,15 +319,15 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
|
||||
uint g = guid.Value;
|
||||
|
||||
// ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
|
||||
// ── 2. Name (displayed via the UiText child's LinesProvider reading _currentName). ──
|
||||
uint stackSize = _stackSize(g);
|
||||
string? objectName = _resolveName(g);
|
||||
_currentName = stackSize > 1u && !string.IsNullOrEmpty(objectName)
|
||||
? $"{stackSize} {objectName}"
|
||||
: objectName;
|
||||
|
||||
// ── 3. Selection overlay: brief flash (retail container ObjectSelected
|
||||
// = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ──────────────
|
||||
// ── 3. Selection overlay: brief flash (retail container ObjectSelected
|
||||
// = Pause(0.25s)→Normal). "StackedItemSelected" for stacks. ──────────────
|
||||
SetOverlayState(stackSize > 1u
|
||||
? RetailUiStateIds.StackedItemSelected
|
||||
: RetailUiStateIds.ObjectSelected);
|
||||
|
|
@ -344,9 +344,9 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
if (_stackSizeSlider is not null) _stackSizeSlider.Visible = true;
|
||||
}
|
||||
|
||||
// ── 4. Health: query, and show the meter only if real health is already known.
|
||||
// ── 4. Health: query, and show the meter only if real health is already known.
|
||||
// Otherwise the meter appears when OnHealthChanged fires for this guid
|
||||
// (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
|
||||
// (retail RecvNotice_UpdateObjectHealth :196213). ──────────────────────────
|
||||
if (stackSize <= 1u && _isHealthTarget(g))
|
||||
{
|
||||
if (selectionChanged)
|
||||
|
|
@ -378,7 +378,7 @@ public sealed class SelectedObjectController : IRetainedPanelController
|
|||
if (_flashRemaining <= 0) return;
|
||||
_flashRemaining -= deltaSeconds;
|
||||
if (_flashRemaining <= 0)
|
||||
SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank
|
||||
SetOverlayState(UiStateInfo.DirectStateId); // flash done → overlay back to blank
|
||||
}
|
||||
|
||||
private void SetOverlayState(uint state)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -9,19 +9,19 @@ namespace AcDream.App.UI.Layout;
|
|||
/// the component icon as the template root's own <c>UIRegion</c> image while
|
||||
/// retaining child <c>0x10000330</c> as the missing-component overlay.
|
||||
/// </summary>
|
||||
public sealed class SpellExamineComponentTemplateFactory
|
||||
internal sealed class SpellExamineComponentTemplateFactory
|
||||
{
|
||||
public const uint TemplateId = 0x1000032Eu;
|
||||
public const uint MissingOverlayId = 0x10000330u;
|
||||
|
||||
private readonly ElementInfo _template;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolveSprite;
|
||||
private readonly Func<uint, (GpuTextureSlot tex, int w, int h)> _resolveSprite;
|
||||
private readonly UiDatFont? _defaultFont;
|
||||
private readonly IReadOnlyDictionary<uint, UiDatFont?> _fonts;
|
||||
|
||||
public SpellExamineComponentTemplateFactory(
|
||||
ElementInfo template,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
IReadOnlyDictionary<uint, UiDatFont?>? fonts = null)
|
||||
{
|
||||
|
|
@ -33,7 +33,7 @@ public sealed class SpellExamineComponentTemplateFactory
|
|||
|
||||
public static SpellExamineComponentTemplateFactory? TryLoad(
|
||||
IDatReaderWriter dats,
|
||||
Func<uint, (uint tex, int w, int h)> resolveSprite,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolveSprite,
|
||||
UiDatFont? defaultFont,
|
||||
Func<uint, UiDatFont?>? resolveFont)
|
||||
{
|
||||
|
|
@ -53,7 +53,7 @@ public sealed class SpellExamineComponentTemplateFactory
|
|||
fonts);
|
||||
}
|
||||
|
||||
public UiElement Create(uint iconTexture, bool owned)
|
||||
public UiElement Create(GpuTextureSlot iconTexture, bool owned)
|
||||
{
|
||||
ImportedLayout content = LayoutImporter.Build(
|
||||
_template,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// list points at this prototype through ItemList attribute <c>0x1000000E</c>;
|
||||
/// retail clones it for every learned spell.
|
||||
/// </summary>
|
||||
public readonly record struct SpellbookRowStyle(
|
||||
internal readonly record struct SpellbookRowStyle(
|
||||
float Width,
|
||||
float Height,
|
||||
float IconLeft,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
|
@ -10,7 +10,7 @@ using AcDream.Content;
|
|||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
public enum SpellbookWindowPage { Spells, Components }
|
||||
internal enum SpellbookWindowPage { Spells, Components }
|
||||
|
||||
/// <summary>
|
||||
/// Binds retail's combined spellbook/component-book LayoutDesc 0x21000034.
|
||||
|
|
@ -19,7 +19,7 @@ public enum SpellbookWindowPage { Spells, Components }
|
|||
/// two authored tabs are stateful UIElement_Text controls, matching
|
||||
/// <c>gmSpellbookUI::PostInit @ 0x0048B2B0</c> and the resolved retail layout.
|
||||
/// </summary>
|
||||
public sealed class SpellbookWindowController : IRetainedPanelController
|
||||
internal sealed class SpellbookWindowController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000034u;
|
||||
public const uint RootId = 0x100002A8u;
|
||||
|
|
@ -50,8 +50,8 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
|||
private readonly Func<uint> _playerGuid;
|
||||
private readonly IReadOnlyDictionary<uint, SpellComponentDescriptor> _components;
|
||||
private readonly SelectionState _selection;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<uint, uint> _resolveComponentIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveSpellIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveComponentIcon;
|
||||
private readonly Func<uint, int> _spellLevel;
|
||||
private readonly Action<uint> _selectObject;
|
||||
private readonly Action<uint> _addFavorite;
|
||||
|
|
@ -86,8 +86,8 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
|||
Func<uint> playerGuid,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||
SelectionState selection,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<uint, uint> resolveComponentIcon,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
Func<uint, GpuTextureSlot> resolveComponentIcon,
|
||||
Func<uint, int> spellLevel,
|
||||
Action<uint> selectObject,
|
||||
Action<uint> addFavorite,
|
||||
|
|
@ -170,8 +170,8 @@ public sealed class SpellbookWindowController : IRetainedPanelController
|
|||
Func<uint> playerGuid,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
|
||||
SelectionState selection,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<uint, uint> resolveComponentIcon,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
Func<uint, GpuTextureSlot> resolveComponentIcon,
|
||||
Func<uint, int> spellLevel,
|
||||
Action<uint> selectObject,
|
||||
Action<uint> addFavorite,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.App.Spells;
|
||||
|
|
@ -16,7 +16,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// LayoutDesc 0x21000073. Favorites remain server-persisted; casting emits one
|
||||
/// request through <see cref="RuntimeSpellCastState"/>.
|
||||
/// </summary>
|
||||
public sealed class SpellcastingUiController : IRetainedPanelController
|
||||
internal sealed class SpellcastingUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint PageId = 0x10000061u;
|
||||
public const uint SpellNameId = 0x1000048Bu;
|
||||
|
|
@ -42,8 +42,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
private readonly SelectionState _selection;
|
||||
private readonly ClientObjectTable _objects;
|
||||
private readonly Func<uint> _playerGuid;
|
||||
private readonly Func<uint, uint> _resolveSpellIcon;
|
||||
private readonly Func<ClientObject, uint> _resolveItemDragIcon;
|
||||
private readonly Func<uint, GpuTextureSlot> _resolveSpellIcon;
|
||||
private readonly Func<ClientObject, GpuTextureSlot> _resolveItemDragIcon;
|
||||
private readonly Action<uint> _useItem;
|
||||
private readonly Action<uint>? _examineSpell;
|
||||
private readonly Action<int, int, uint>? _addFavorite;
|
||||
|
|
@ -73,8 +73,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
RuntimeSpellCastState casting,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<ClientObject, uint> resolveItemDragIcon,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
Func<ClientObject, GpuTextureSlot> resolveItemDragIcon,
|
||||
Action<uint> useItem,
|
||||
Action<uint>? examineSpell,
|
||||
SelectionState selection,
|
||||
|
|
@ -162,8 +162,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
RuntimeSpellCastState casting,
|
||||
ClientObjectTable objects,
|
||||
Func<uint> playerGuid,
|
||||
Func<uint, uint> resolveSpellIcon,
|
||||
Func<ClientObject, uint> resolveItemDragIcon,
|
||||
Func<uint, GpuTextureSlot> resolveSpellIcon,
|
||||
Func<ClientObject, GpuTextureSlot> resolveItemDragIcon,
|
||||
Action<uint> useItem,
|
||||
SelectionState selection,
|
||||
Action<int, int, uint>? addFavorite,
|
||||
|
|
@ -351,7 +351,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
var slot = new UiCatalogSlot
|
||||
{
|
||||
EntryId = id,
|
||||
CatalogIconTexture = metadata is null ? 0u : _resolveSpellIcon(id),
|
||||
CatalogIconTexture = metadata is null ? GpuTextureSlot.Unassigned : _resolveSpellIcon(id),
|
||||
Label = metadata?.Name ?? $"Spell {id}",
|
||||
SpriteResolve = list.SpriteResolve,
|
||||
CatalogDragPayload = new SpellFavoriteDragPayload(tab, position, id),
|
||||
|
|
@ -530,9 +530,9 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
_endowmentHost.Visible = endowment is not null;
|
||||
_endowmentSlot.EntryId = _endowmentItemId;
|
||||
_endowmentSlot.CatalogIconTexture = _endowmentSpellId == 0u
|
||||
? 0u : _resolveSpellIcon(_endowmentSpellId);
|
||||
? GpuTextureSlot.Unassigned : _resolveSpellIcon(_endowmentSpellId);
|
||||
_endowmentSlot.CatalogOverlayTexture = endowment is null
|
||||
? 0u : _resolveItemDragIcon(endowment);
|
||||
? GpuTextureSlot.Unassigned : _resolveItemDragIcon(endowment);
|
||||
string spellName = _spellbook.TryGetMetadata(_endowmentSpellId, out SpellMetadata metadata)
|
||||
? metadata.Name : $"Spell {_endowmentSpellId}";
|
||||
_endowmentSlot.Label = endowment is null
|
||||
|
|
@ -603,13 +603,13 @@ public sealed class SpellcastingUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId);
|
||||
internal sealed record SpellFavoriteDragPayload(int SourceTab, int SourcePosition, uint SpellId);
|
||||
|
||||
/// <summary>
|
||||
/// A learned-spell shortcut carried from the spellbook. Unlike a favorite drag,
|
||||
/// lifting this payload never removes anything from its source collection.
|
||||
/// </summary>
|
||||
public sealed record SpellbookShortcutDragPayload(uint SpellId);
|
||||
internal sealed record SpellbookShortcutDragPayload(uint SpellId);
|
||||
|
||||
internal static class FavoriteListExtensions
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
|
@ -8,7 +8,7 @@ using AcDream.Core.Selection;
|
|||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
|
||||
/// Binds the imported gmToolbarUI window (LayoutDesc 0x21000016) to live data —
|
||||
/// the gm*UI::PostInit analogue. Finds the 18 shortcut slots (UiItemList) by id,
|
||||
/// populates them from the persisted PlayerDescription shortcuts
|
||||
/// (UpdateFromPlayerDesc), re-binds deferred slots when an item's CreateObject
|
||||
|
|
@ -24,7 +24,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// <c>CreateObject</c> resolves a formerly-unknown guid.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController
|
||||
internal sealed class ToolbarController : IItemListDragHandler, IRetainedPanelController
|
||||
{
|
||||
// Slot element ids in slot-index order (toolbar LayoutDesc 0x21000016, pre-dump).
|
||||
// Row 1 = slots 0-8 (0x100001A7..0x100001AF), Row 2 = slots 9-17 (0x100006B7..0x100006BF).
|
||||
|
|
@ -39,7 +39,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
// SelectedObjectController owns the health/mana meters and both stack controls,
|
||||
// including retail's initial-hidden state and selection-driven visibility.
|
||||
|
||||
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
|
||||
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
|
||||
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
||||
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669)
|
||||
// SetVisible's exactly one element depending on the incoming mode.
|
||||
|
|
@ -64,9 +64,9 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private readonly ClientObjectTable _repo;
|
||||
private readonly CombatState? _combatState;
|
||||
private readonly ShortcutStore _store;
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, uint>? _dragIconIds;
|
||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> _iconIds; // (itemType, icon, underlay, overlay, effects) → GL tex
|
||||
private readonly Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? _dragIconIds;
|
||||
private readonly Action<uint> _useItem; // guid → fire UseObject
|
||||
private readonly Action<ShortcutEntry>? _sendAddShortcut;
|
||||
private readonly Action<uint>? _sendRemoveShortcut; // (index)
|
||||
private readonly ItemInteractionController? _itemInteraction;
|
||||
|
|
@ -83,8 +83,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||
// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621) re-stamps ghosting.
|
||||
// Occupancy branch (decomp 229481):
|
||||
// occupied → regular 0x10000042 / ghosted 0x10000043
|
||||
// empty → background digit 0x1000005e (stance-independent)
|
||||
// occupied → regular 0x10000042 / ghosted 0x10000043
|
||||
// empty → background digit 0x1000005e (stance-independent)
|
||||
private uint[]? _regularDigits;
|
||||
private uint[]? _ghostedDigits;
|
||||
private uint[]? _emptyDigits;
|
||||
|
|
@ -94,7 +94,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
ImportedLayout layout,
|
||||
ClientObjectTable repo,
|
||||
ShortcutStore shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds,
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState,
|
||||
uint[]? regularDigits,
|
||||
|
|
@ -110,7 +110,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds = null)
|
||||
{
|
||||
_repo = repo;
|
||||
_combatState = combatState;
|
||||
|
|
@ -193,7 +193,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_examineButton.OnClick = () => _itemInteraction?.ExamineSelectedOrEnterMode(_selectedObjectId());
|
||||
|
||||
// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669):
|
||||
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
|
||||
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
|
||||
// always spawns in peace mode; retail has not yet called SetVisible when PostInit runs.
|
||||
SetCombatMode(CombatMode.NonCombat);
|
||||
|
||||
|
|
@ -204,7 +204,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_selection.Changed += OnSelectionChanged;
|
||||
|
||||
// D.5.4: the table now holds ALL objects (creatures, NPCs, etc.), so filter
|
||||
// to our 18 shortcut guids — else every creature spawn in a busy zone
|
||||
// to our 18 shortcut guids — else every creature spawn in a busy zone
|
||||
// needlessly re-populates the bar (gmToolbarUI::SetDelayedShortcutNum pattern).
|
||||
repo.ObjectAdded += OnRepositoryObjectChanged;
|
||||
repo.ObjectUpdated += OnRepositoryObjectChanged;
|
||||
|
|
@ -310,22 +310,22 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// if the shortcut list is refreshed outside the repo-event path.
|
||||
/// </summary>
|
||||
/// <param name="layout">Imported toolbar layout (LayoutDesc 0x21000016).</param>
|
||||
/// <param name="repo">Live item repository — must stay alive for the controller's lifetime.</param>
|
||||
/// <param name="repo">Live item repository — must stay alive for the controller's lifetime.</param>
|
||||
/// <param name="shortcuts">
|
||||
/// Runtime-owned retail shortcut manager. The toolbar borrows this exact
|
||||
/// mutable owner; it never reconstructs another slot map.
|
||||
/// </param>
|
||||
/// <param name="iconIds">Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle.</param>
|
||||
/// <param name="iconIds">Resolves (itemType, iconId, underlayId, overlayId, effects) → GL texture handle.</param>
|
||||
/// <param name="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
||||
/// <param name="combatState">
|
||||
/// Optional live combat state — when provided, the toolbar subscribes to
|
||||
/// Optional live combat state — when provided, the toolbar subscribes to
|
||||
/// <see cref="CombatState.CombatModeChanged"/> and updates the four mutually-exclusive
|
||||
/// combat-mode indicator elements accordingly.
|
||||
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
|
||||
/// </param>
|
||||
/// <param name="regularDigits">
|
||||
/// Regular digit DID array (property 0x10000042 from LayoutDesc 0x21000037 element
|
||||
/// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id.
|
||||
/// 0x1000034A under composite 0x10000346). Index i → slot label digit (i+1) RenderSurface id.
|
||||
/// Null if the dat lookup failed (no digits drawn). Retail reference:
|
||||
/// UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||
/// </param>
|
||||
|
|
@ -333,14 +333,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// <param name="emptyDigits">
|
||||
/// Empty-slot background digit DID array (property 0x1000005e, stance-independent).
|
||||
/// Used when a slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||||
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state).
|
||||
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty state).
|
||||
/// Null if the dat lookup failed (empty slots draw no digit, which is safe).
|
||||
/// </param>
|
||||
public static ToolbarController Bind(
|
||||
ImportedLayout layout,
|
||||
ClientObjectTable repo,
|
||||
ShortcutStore shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> iconIds,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> iconIds,
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState = null,
|
||||
uint[]? regularDigits = null,
|
||||
|
|
@ -356,7 +356,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null,
|
||||
Func<ItemType, uint, uint, uint, uint, uint>? dragIconIds = null)
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot>? dragIconIds = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
regularDigits, ghostedDigits, emptyDigits, itemInteraction,
|
||||
|
|
@ -427,15 +427,15 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
if (list is null) continue;
|
||||
var item = _repo.Get(guid);
|
||||
if (item is null) continue; // deferred: ObjectAdded re-calls Populate
|
||||
uint tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
uint dragTex = _dragIconIds?.Invoke(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects) ?? 0u;
|
||||
GpuTextureSlot tex = _iconIds(item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
GpuTextureSlot? dragTex = _dragIconIds?.Invoke(
|
||||
item.Type, item.IconId, item.IconUnderlayId, item.IconOverlayId, item.Effects);
|
||||
list.Cell.SetItem(guid, tex, entry, dragTex);
|
||||
}
|
||||
|
||||
// Re-stamp slot number labels after any item change.
|
||||
// Digit SPRITE SOURCE depends on occupancy (decomp UIElement_UIItem::SetShortcutNum:229481):
|
||||
// occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e.
|
||||
// occupied → regular 0x10000042 / ghosted 0x10000043; empty → background 0x1000005e.
|
||||
// The digit is ALWAYS shown on top-row slots (SetVisible(1) at decomp 229511).
|
||||
RestampShortcutNumbers();
|
||||
}
|
||||
|
|
@ -450,7 +450,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// </summary>
|
||||
public void SetCombatMode(CombatMode mode)
|
||||
{
|
||||
// Index → mode mapping matches CombatIndicatorIds declaration order:
|
||||
// Index → mode mapping matches CombatIndicatorIds declaration order:
|
||||
// 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
||||
bool[] show =
|
||||
{
|
||||
|
|
@ -475,14 +475,14 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
|
||||
/// <summary>
|
||||
/// Push digit-array references and shortcut-number state into every slot cell.
|
||||
/// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown
|
||||
/// Top row (indices 0–8): SetShortcutNum(i, _shortcutsGhosted) — numbers 1–9 always shown
|
||||
/// (the digit is ALWAYS visible, SetVisible(1) at decomp 229511; only the sprite
|
||||
/// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481).
|
||||
/// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there.
|
||||
/// SOURCE differs by occupancy — see UIElement_UIItem::SetShortcutNum decomp 229481).
|
||||
/// Bottom row (indices 9–17): ClearShortcutNum() — retail shows no numbers there.
|
||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465);
|
||||
/// gmToolbarUI::RecvNotice_SetCombatMode (196610-196621).
|
||||
/// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043;
|
||||
/// empty → background 0x1000005e (decomp 229481/229493).
|
||||
/// Occupancy → source: occupied → regular 0x10000042 / ghosted 0x10000043;
|
||||
/// empty → background 0x1000005e (decomp 229481/229493).
|
||||
/// </summary>
|
||||
private void RestampShortcutNumbers()
|
||||
{
|
||||
|
|
@ -494,7 +494,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
cell.GhostedDigits = _ghostedDigits;
|
||||
cell.EmptyDigits = _emptyDigits;
|
||||
if (i < 9)
|
||||
cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown
|
||||
cell.SetShortcutNum(i, _shortcutsGhosted); // top row: slot labels 1–9 always shown
|
||||
else
|
||||
cell.ClearShortcutNum(); // bottom row: no slot labels
|
||||
}
|
||||
|
|
@ -503,7 +503,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
/// <summary>
|
||||
/// Wire the <see cref="UiItemSlot.Clicked"/> callback on a slot cell so that
|
||||
/// clicking a bound item fires <see cref="_useItem"/> with the slot's current guid.
|
||||
/// Mirrors retail's <c>gmToolbarUI</c> click → <c>UseShortcut</c> dispatch.
|
||||
/// Mirrors retail's <c>gmToolbarUI</c> click → <c>UseShortcut</c> dispatch.
|
||||
/// </summary>
|
||||
private void WireClick(UiItemList list)
|
||||
{
|
||||
|
|
@ -662,12 +662,12 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
return true;
|
||||
}
|
||||
|
||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||
// ── IItemListDragHandler (B.2 live handler) ──────────────────────────────
|
||||
// Retail: gmToolbarUI is the m_dragHandler for every shortcut slot list.
|
||||
// Retail model (remove-on-lift / place-on-drop / no-restore):
|
||||
// lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately)
|
||||
// drop → AddShortcut (0x019C) + optional swap of evicted item into source
|
||||
// off-bar release → the lift's removal stands (no restore)
|
||||
// lift → RemoveShortcut (0x019D) + store.Remove (slot empties immediately)
|
||||
// drop → AddShortcut (0x019C) + optional swap of evicted item into source
|
||||
// off-bar release → the lift's removal stands (no restore)
|
||||
// Retail ref: gmToolbarUI::HandleDropRelease acclient_2013_pseudo_c.txt:197971
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -678,7 +678,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
if (payload.ObjId != 0 && _selectedObjectId() != payload.ObjId)
|
||||
_selectItem?.Invoke(payload.ObjId);
|
||||
|
||||
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
|
||||
// Retail RecvNotice_ItemListBeginDrag → RemoveShortcut (0x004bd930/0x004bd450): the lifted
|
||||
// shortcut leaves the bar (+ wire) the instant the drag starts; it's re-placed only on a
|
||||
// drop onto a slot. Off-bar release leaves it removed.
|
||||
_sendRemoveShortcut?.Invoke((uint)payload.SourceSlot);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// dispatcher owns chords/scopes; this class only maps semantic actions to the
|
||||
/// exact <c>gmToolbarUI::ListenToGlobalMessage @ 0x004BE4E0</c> slot intent.
|
||||
/// </summary>
|
||||
public sealed class ToolbarInputController
|
||||
internal sealed class ToolbarInputController
|
||||
{
|
||||
private readonly ToolbarController _toolbar;
|
||||
private readonly SelectionState _selection;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
@ -10,28 +10,28 @@ namespace AcDream.App.UI.Layout;
|
|||
/// faithful because retail's base element render is exactly "stamp the media per draw-mode".
|
||||
///
|
||||
/// <para>
|
||||
/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the
|
||||
/// For Plan 1, all observed draw modes produce the same alpha-blended tiled quad — the
|
||||
/// sprite shader already alpha-blends, so no per-mode branch is needed here. The named
|
||||
/// constants document the real enum for Plan 2.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// DrawModeType (DatReaderWriter.Enums), stored as int in <see cref="ElementInfo"/> to
|
||||
/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6:
|
||||
/// keep this dat-free. See docs/research/2026-06-15-layoutdesc-format.md §6:
|
||||
/// <c>Undefined=0, Normal=1, Overlay=2, Alphablend=3</c>. There is no Stretch mode.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// Tiling uses UV-repeat on BOTH axes (<c>Width/tw</c>, <c>Height/th</c>) so vertical
|
||||
/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too.
|
||||
/// chrome edges (e.g. a 5×10 sprite drawn over a 5×48 rect) tile vertically too.
|
||||
/// <see cref="AcDream.App.Rendering.TextureCache.UploadRgba8"/> sets
|
||||
/// <c>GL_REPEAT</c> on both S and T, so vertical tiling is always active.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiDatElement : UiElement, IUiDatStateful
|
||||
internal sealed class UiDatElement : UiElement, IUiDatStateful
|
||||
{
|
||||
// DrawModeType enum values from DatReaderWriter.Enums.
|
||||
// See docs/research/2026-06-15-layoutdesc-format.md §6.
|
||||
// See docs/research/2026-06-15-layoutdesc-format.md §6.
|
||||
#pragma warning disable IDE0051 // private constants kept for documentation / Plan 2
|
||||
private const int DrawUndefined = 0;
|
||||
private const int DrawNormal = 1;
|
||||
|
|
@ -40,7 +40,7 @@ public sealed class UiDatElement : UiElement, IUiDatStateful
|
|||
#pragma warning restore IDE0051
|
||||
|
||||
private readonly ElementInfo _info;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly Func<uint, (GpuTextureSlot tex, int w, int h)> _resolve;
|
||||
|
||||
/// <summary>The dat element id from <see cref="ElementInfo.Id"/>. Exposed so controllers
|
||||
/// can identify which logical element a UiDatElement represents when walking subtrees
|
||||
|
|
@ -102,9 +102,9 @@ public sealed class UiDatElement : UiElement, IUiDatStateful
|
|||
}
|
||||
|
||||
/// <param name="info">Merged <see cref="ElementInfo"/> for this element.</param>
|
||||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
public UiDatElement(ElementInfo info, Func<uint, (uint tex, int w, int h)> resolve)
|
||||
public UiDatElement(ElementInfo info, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve)
|
||||
{
|
||||
_info = info;
|
||||
_resolve = resolve;
|
||||
|
|
@ -172,13 +172,13 @@ public sealed class UiDatElement : UiElement, IUiDatStateful
|
|||
/// The image remains this element's own media, so authored descendants retain
|
||||
/// their normal foreground relationship.
|
||||
/// </summary>
|
||||
public uint? RuntimeImageTexture { get; set; }
|
||||
public GpuTextureSlot? RuntimeImageTexture { get; set; }
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (MediaVisible && RuntimeImageTexture is uint runtimeTexture)
|
||||
if (MediaVisible && RuntimeImageTexture is { } runtimeTexture)
|
||||
{
|
||||
if (runtimeTexture != 0u)
|
||||
if (runtimeTexture.IsAssigned)
|
||||
{
|
||||
ctx.DrawSprite(
|
||||
runtimeTexture,
|
||||
|
|
@ -200,9 +200,9 @@ public sealed class UiDatElement : UiElement, IUiDatStateful
|
|||
if (MediaVisible && file != 0)
|
||||
{
|
||||
var (tex, tw, th) = _resolve(file);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
if (tex.IsAssigned && tw != 0 && th != 0)
|
||||
{
|
||||
// Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI
|
||||
// Normal → TILE at native size on both axes (UV-repeat; GL_REPEAT-wrapped UI
|
||||
// texture), matching ImgTex::TileCSI. Overlay/Alphablend use the same blit (the
|
||||
// sprite shader already alpha-blends). No Stretch mode exists in DrawModeType.
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
namespace AcDream.App.UI.Layout;
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// One runtime listbox entry instantiated from an authored LayoutDesc template.
|
||||
/// Retail <c>UIElement_ListBox::AddItemFromTemplateList</c> creates exactly this
|
||||
/// ownership shape: the list owns the entry while the template owns its visuals.
|
||||
/// </summary>
|
||||
public sealed class UiTemplateListSlot : UiItemSlot
|
||||
internal sealed class UiTemplateListSlot : UiItemSlot
|
||||
{
|
||||
private readonly IUiDatStateful? _statefulRoot;
|
||||
private readonly uint _normalState;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Retained port of retail <c>gmVitaeUI</c>. It presents the active vitae
|
||||
/// modifier and the authoritative experience pool needed to regain one percent.
|
||||
/// </summary>
|
||||
public sealed class VitaeUiController : IRetainedPanelController
|
||||
internal sealed class VitaeUiController : IRetainedPanelController
|
||||
{
|
||||
public const uint LayoutId = 0x21000020u;
|
||||
public const uint RootId = 0x100001C1u;
|
||||
|
|
@ -151,7 +151,7 @@ public sealed class VitaeUiController : IRetainedPanelController
|
|||
}
|
||||
}
|
||||
|
||||
public sealed record VitaeStrings(
|
||||
internal sealed record VitaeStrings(
|
||||
string FullStrength,
|
||||
string LostPrefix,
|
||||
string LostSuffix,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.UI.Layout;
|
|||
/// Per-window controller for the vitals layout (LayoutDesc 0x2100006C).
|
||||
/// Mirrors retail <c>gmVitalsUI::PostInit</c>: grab the three meter elements
|
||||
/// by their dat element ids and bind live data providers (fill fraction + cur/max
|
||||
/// text) to each. This is the ONLY per-window code in the whole importer — pure
|
||||
/// text) to each. This is the ONLY per-window code in the whole importer — pure
|
||||
/// data wiring, not graphics.
|
||||
///
|
||||
/// <para>The slice sprites + dat font on each <see cref="UiMeter"/> are already
|
||||
|
|
@ -16,10 +16,10 @@ namespace AcDream.App.UI.Layout;
|
|||
/// only binds the dynamic vitals data. Do not touch meter rendering fields here.</para>
|
||||
///
|
||||
/// <para>Element ids confirmed from
|
||||
/// <c>docs/research/2026-06-15-layoutdesc-format.md §11</c>
|
||||
/// <c>docs/research/2026-06-15-layoutdesc-format.md §11</c>
|
||||
/// (vitals window 0x2100006C dump).</para>
|
||||
/// </summary>
|
||||
public static class VitalsController
|
||||
internal static class VitalsController
|
||||
{
|
||||
/// <summary>Dat element id for the Health meter (0x100000E6).</summary>
|
||||
public const uint Health = 0x100000E6;
|
||||
|
|
@ -34,7 +34,7 @@ public static class VitalsController
|
|||
/// <summary>
|
||||
/// Bind live vitals data providers to the Health, Stamina, and Mana meter
|
||||
/// elements found in <paramref name="layout"/>. Any meter whose id is absent
|
||||
/// from the layout is silently skipped — partial layouts (e.g. test fakes)
|
||||
/// from the layout is silently skipped — partial layouts (e.g. test fakes)
|
||||
/// do not cause errors.
|
||||
/// </summary>
|
||||
/// <param name="layout">Imported vitals layout tree.</param>
|
||||
|
|
@ -58,7 +58,7 @@ public static class VitalsController
|
|||
BindMeter(layout, Mana, ManaText, manaPct, manaText);
|
||||
}
|
||||
|
||||
/// <summary>White cur/max numbers — matches the former <c>UiMeter.LabelColor</c> default.</summary>
|
||||
/// <summary>White cur/max numbers — matches the former <c>UiMeter.LabelColor</c> default.</summary>
|
||||
private static readonly Vector4 NumberColor = new(1f, 1f, 1f, 1f);
|
||||
|
||||
private static void BindMeter(
|
||||
|
|
@ -66,7 +66,7 @@ public static class VitalsController
|
|||
Func<float> pct,
|
||||
Func<string> text)
|
||||
{
|
||||
// Silently skip if the id is absent — missing meters are not an error (partial layouts).
|
||||
// Silently skip if the id is absent — missing meters are not an error (partial layouts).
|
||||
if (layout.FindElement(id) is not UiMeter m) return;
|
||||
|
||||
m.Fill = () => pct();
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.Ui;
|
||||
using AcDream.Content;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
public readonly record struct VividTargetInfo(
|
||||
internal readonly record struct VividTargetInfo(
|
||||
Vector3 SelectionSphereCenter,
|
||||
float SelectionSphereRadius,
|
||||
uint ItemType,
|
||||
uint ObjectDescriptionFlags);
|
||||
|
||||
public sealed record VividTargetRuntimeBindings(
|
||||
internal sealed record VividTargetRuntimeBindings(
|
||||
SelectionState Selection,
|
||||
Func<uint> PlayerGuid,
|
||||
Func<bool> Enabled,
|
||||
|
|
@ -25,7 +25,7 @@ public sealed record VividTargetRuntimeBindings(
|
|||
/// this presentation from the live object's selection sphere, independently of
|
||||
/// whether the world renderer drew or occluded the object.
|
||||
/// </summary>
|
||||
public sealed class VividTargetIndicatorController
|
||||
internal sealed class VividTargetIndicatorController
|
||||
{
|
||||
private const uint ClientEnumCategory = 0x10000009u;
|
||||
private const uint FirstSourceImageEnum = 1u;
|
||||
|
|
@ -85,7 +85,7 @@ public sealed class VividTargetIndicatorController
|
|||
return null;
|
||||
|
||||
var resolved = assets.ResolveSprite(did);
|
||||
if (resolved.Texture == 0u || resolved.Width <= 0 || resolved.Height <= 0)
|
||||
if (!resolved.Texture.IsAssigned || resolved.Width <= 0 || resolved.Height <= 0)
|
||||
return null;
|
||||
sources[i] = new VividTargetSource(
|
||||
resolved.Texture, resolved.Width, resolved.Height);
|
||||
|
|
@ -387,6 +387,6 @@ internal readonly record struct VividTargetProjection(
|
|||
float AngleDegrees);
|
||||
|
||||
internal readonly record struct VividTargetSource(
|
||||
uint Texture,
|
||||
GpuTextureSlot Texture,
|
||||
float Width,
|
||||
float Height);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
using System.Reflection;
|
||||
|
|
@ -10,16 +10,16 @@ namespace AcDream.App.UI;
|
|||
/// Parses our KSML-style panel markup (mirrors retail's ElementDesc fields)
|
||||
/// into a live <see cref="UiElement"/> subtree. <c>{Binding}</c> attribute
|
||||
/// values resolve against a supplied object by property name (reflection).
|
||||
/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7.
|
||||
/// This is the format the future LayoutDesc importer will emit. See D.2b spec §7.
|
||||
/// </summary>
|
||||
public static class MarkupDocument
|
||||
internal static class MarkupDocument
|
||||
{
|
||||
/// <param name="xml">Raw XML markup for a single panel.</param>
|
||||
/// <param name="binding">Object whose public properties are bound to <c>{PropName}</c> attributes.</param>
|
||||
/// <param name="resolve">Surface id → (GL handle, width, height) for chrome sprites.</param>
|
||||
/// <param name="resolve">Surface id → (GL handle, width, height) for chrome sprites.</param>
|
||||
/// <param name="style">Optional controls.ini stylesheet for the title color.</param>
|
||||
public static UiNineSlicePanel Build(
|
||||
string xml, object binding, Func<uint, (uint, int, int)> resolve,
|
||||
string xml, object binding, Func<uint, (GpuTextureSlot, int, int)> resolve,
|
||||
ControlsIni? style = null)
|
||||
{
|
||||
var root = XDocument.Parse(xml).Root ?? throw new FormatException("empty markup");
|
||||
|
|
@ -86,7 +86,7 @@ public static class MarkupDocument
|
|||
CultureInfo.InvariantCulture, out var v) ? v : 0f;
|
||||
|
||||
/// <summary>
|
||||
/// Parses <c>#AARRGGBB</c> → RGBA <see cref="Vector4"/> (alpha first, matching
|
||||
/// Parses <c>#AARRGGBB</c> → RGBA <see cref="Vector4"/> (alpha first, matching
|
||||
/// controls.ini convention). Falls back to opaque white on bad input.
|
||||
/// </summary>
|
||||
private static Vector4 Color(string? hex)
|
||||
|
|
|
|||
|
|
@ -1,66 +1,66 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retail window-chrome RenderSurface DataIds, CONFIRMED via the D.2b Step-0
|
||||
/// prove-out (2026-06-14). These are RenderSurface objects (0x06xxxxxx) decoded
|
||||
/// DIRECTLY (<see cref="Rendering.TextureCache.GetOrUploadRenderSurface"/>), NOT
|
||||
/// through the Surface→SurfaceTexture chain.
|
||||
/// through the Surface→SurfaceTexture chain.
|
||||
///
|
||||
/// <para>
|
||||
/// The universal floating-window bevel is an <b>8-piece border</b> (4 corners
|
||||
/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single
|
||||
/// 5×5 + 4 edges) drawn around a tiled center fill — it is NOT a single
|
||||
/// 9-slice texture. Decoded sizes are in the comments (from the prove-out).
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
/// The edge/corner → position mapping below is a reasonable guess pending the
|
||||
/// The edge/corner → position mapping below is a reasonable guess pending the
|
||||
/// LayoutDesc 0x21000040 parse (sub-project 3) and is confirmed visually in the
|
||||
/// first vitals-panel render. If a corner's bevel highlight looks wrong, swap
|
||||
/// the four corner constants; if top/bottom or left/right look inverted, swap
|
||||
/// those edge pairs.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class RetailChromeSprites
|
||||
internal static class RetailChromeSprites
|
||||
{
|
||||
/// <summary>Tiled interior fill — the shared panel background (48×48).</summary>
|
||||
/// <summary>Tiled interior fill — the shared panel background (48×48).</summary>
|
||||
public const uint CenterFill = 0x06004CC2;
|
||||
|
||||
/// <summary>Horizontal top edge (10×5, tiled across the top span).</summary>
|
||||
/// <summary>Horizontal top edge (10×5, tiled across the top span).</summary>
|
||||
public const uint TopEdge = 0x060074BF;
|
||||
/// <summary>Horizontal bottom edge (10×5).</summary>
|
||||
/// <summary>Horizontal bottom edge (10×5).</summary>
|
||||
public const uint BottomEdge = 0x060074C1;
|
||||
/// <summary>Vertical left edge (5×10).</summary>
|
||||
/// <summary>Vertical left edge (5×10).</summary>
|
||||
public const uint LeftEdge = 0x060074C0;
|
||||
/// <summary>Vertical right edge (5×10).</summary>
|
||||
/// <summary>Vertical right edge (5×10).</summary>
|
||||
public const uint RightEdge = 0x060074C2;
|
||||
|
||||
/// <summary>Top-left corner (5×5).</summary>
|
||||
/// <summary>Top-left corner (5×5).</summary>
|
||||
public const uint CornerTL = 0x060074C3;
|
||||
/// <summary>Top-right corner (5×5).</summary>
|
||||
/// <summary>Top-right corner (5×5).</summary>
|
||||
public const uint CornerTR = 0x060074C4;
|
||||
/// <summary>Bottom-left corner (5×5).</summary>
|
||||
/// <summary>Bottom-left corner (5×5).</summary>
|
||||
public const uint CornerBL = 0x060074C5;
|
||||
/// <summary>Bottom-right corner (5×5).</summary>
|
||||
/// <summary>Bottom-right corner (5×5).</summary>
|
||||
public const uint CornerBR = 0x060074C6;
|
||||
|
||||
/// <summary>Border thickness in pixels = the corner/edge sprite size (5px).</summary>
|
||||
public const int Border = 5;
|
||||
|
||||
// ── Resize-grip overlay ──────────────────────────────────────────────
|
||||
// ── Resize-grip overlay ──────────────────────────────────────────────
|
||||
// A second 8-piece layer drawn ON TOP of the bevel above: the gold ridged
|
||||
// accents + square corner studs that frame a resizable retail window. From
|
||||
// the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each
|
||||
// corner is the same 5×5 stud (0x06006129); the edges are gold double-line
|
||||
// the vitals LayoutDesc 0x2100006C (elements 0x1000063B–0x10000642): each
|
||||
// corner is the same 5×5 stud (0x06006129); the edges are gold double-line
|
||||
// strips tiled along each side. These have transparent gaps, so the bevel
|
||||
// shows through — both layers are needed.
|
||||
/// <summary>Corner grip stud, all four corners (5×5).</summary>
|
||||
// shows through — both layers are needed.
|
||||
/// <summary>Corner grip stud, all four corners (5×5).</summary>
|
||||
public const uint GripCorner = 0x06006129;
|
||||
/// <summary>Top edge grip (10×5, tiled across).</summary>
|
||||
/// <summary>Top edge grip (10×5, tiled across).</summary>
|
||||
public const uint GripTop = 0x0600612A;
|
||||
/// <summary>Left edge grip (5×10, tiled down).</summary>
|
||||
/// <summary>Left edge grip (5×10, tiled down).</summary>
|
||||
public const uint GripLeft = 0x0600612B;
|
||||
/// <summary>Bottom edge grip (10×5).</summary>
|
||||
/// <summary>Bottom edge grip (10×5).</summary>
|
||||
public const uint GripBottom = 0x0600612C;
|
||||
/// <summary>Right edge grip (5×10).</summary>
|
||||
/// <summary>Right edge grip (5×10).</summary>
|
||||
public const uint GripRight = 0x0600612D;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using AcDream.App.UI.Layout;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI;
|
|||
/// <c>ClientUISystem::UsageConfirmation_* @ 0x00566420..0x005669EA</c> and
|
||||
/// completion mirrors <c>ClientUISystem::UsageCallback @ 0x00565B20</c>.
|
||||
/// </summary>
|
||||
public sealed class RetailItemConfirmationController : IDisposable
|
||||
internal sealed class RetailItemConfirmationController : IDisposable
|
||||
{
|
||||
internal const string PlayerKillerMessage =
|
||||
"Using this altar will make you a player killer, able to attack or be attacked by other player killers. Are you sure you want to do this?";
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>gmPanelUI</c> panel ids carried by DAT property <c>0x10000029</c>.
|
||||
/// The toolbar exposes only a subset of these ids; Helpful/Harmful effects use
|
||||
/// the authored <c>gmUIElement_EffectsIndicator</c> buttons instead.
|
||||
/// </summary>
|
||||
public static class RetailPanelCatalog
|
||||
internal static class RetailPanelCatalog
|
||||
{
|
||||
public const uint CharacterInformation = 3u;
|
||||
public const uint PositiveEffects = 4u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Globalization;
|
||||
using System.Globalization;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI;
|
|||
/// Already-trained skill XP raises deliberately bypass this controller, matching
|
||||
/// retail's separate <c>gmSkillUI::RaiseSelection @ 0x0049C8C0</c> path.
|
||||
/// </summary>
|
||||
public sealed class RetailSkillTrainingConfirmationController
|
||||
internal sealed class RetailSkillTrainingConfirmationController
|
||||
{
|
||||
public const string MessageFormat =
|
||||
"Are you sure you want to spend {0} credits to train {1}?";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.App.Plugins;
|
||||
using AcDream.App.Combat;
|
||||
using AcDream.App.Input;
|
||||
|
|
@ -24,41 +24,41 @@ using Silk.NET.Input;
|
|||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
public sealed record RetailUiAssets(
|
||||
internal sealed record RetailUiAssets(
|
||||
IDatReaderWriter Dats,
|
||||
object DatLock,
|
||||
Func<uint, (uint Texture, int Width, int Height)> ResolveSprite,
|
||||
Func<uint, (GpuTextureSlot Texture, int Width, int Height)> ResolveSprite,
|
||||
Func<uint, UiDatFont?> ResolveFont,
|
||||
UiDatFont? DefaultFont,
|
||||
BitmapFont? DebugFont,
|
||||
ControlsIni Controls,
|
||||
IconComposer Icons);
|
||||
|
||||
public sealed record VitalsRuntimeBindings(VitalsVM ViewModel);
|
||||
internal sealed record VitalsRuntimeBindings(VitalsVM ViewModel);
|
||||
|
||||
public sealed record ChatRuntimeBindings(ChatVM ViewModel, Func<ICommandBus> CommandBus);
|
||||
internal sealed record ChatRuntimeBindings(ChatVM ViewModel, Func<ICommandBus> CommandBus);
|
||||
|
||||
public sealed record RadarRuntimeBindings(
|
||||
internal sealed record RadarRuntimeBindings(
|
||||
Func<UiRadarSnapshot> Snapshot,
|
||||
SelectionState Selection,
|
||||
Action<bool> SetUiLocked);
|
||||
|
||||
public sealed record CombatRuntimeBindings(
|
||||
internal sealed record CombatRuntimeBindings(
|
||||
CombatState State,
|
||||
RuntimeCombatAttackState Attacks,
|
||||
Func<GameplaySettings> Gameplay,
|
||||
Action<GameplaySettings> SetGameplay);
|
||||
|
||||
public sealed record MagicRuntimeBindings(
|
||||
internal sealed record MagicRuntimeBindings(
|
||||
Spellbook Spellbook,
|
||||
RuntimeSpellCastState Casting,
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
IReadOnlyDictionary<uint, SpellComponentDescriptor> Components,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<uint, uint> ResolveSpellIcon,
|
||||
Func<uint, uint> ResolveComponentIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveDragIcon,
|
||||
Func<uint, GpuTextureSlot> ResolveSpellIcon,
|
||||
Func<uint, GpuTextureSlot> ResolveComponentIcon,
|
||||
SelectionState Selection,
|
||||
Func<uint, int> SpellLevel,
|
||||
Func<uint, IReadOnlyList<SpellExamineComponent>> SpellComponents,
|
||||
|
|
@ -72,14 +72,14 @@ public sealed record MagicRuntimeBindings(
|
|||
Action<uint, uint> SetDesiredComponent,
|
||||
Func<double> ServerTime);
|
||||
|
||||
public sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
||||
internal sealed record JumpPowerbarRuntimeBindings(Func<JumpChargeSnapshot> Snapshot);
|
||||
|
||||
public sealed record FpsRuntimeBindings(
|
||||
internal sealed record FpsRuntimeBindings(
|
||||
Func<double> FramesPerSecond,
|
||||
Func<double> DegradeMultiplier,
|
||||
Func<bool> IsVisible);
|
||||
|
||||
public sealed record IndicatorRuntimeBindings(
|
||||
internal sealed record IndicatorRuntimeBindings(
|
||||
Spellbook Spellbook,
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
|
|
@ -89,11 +89,11 @@ public sealed record IndicatorRuntimeBindings(
|
|||
Action RequestLinkStatusPing,
|
||||
Action EndCharacterSession);
|
||||
|
||||
public sealed record ToolbarRuntimeBindings(
|
||||
internal sealed record ToolbarRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
ShortcutStore Shortcuts,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveDragIcon,
|
||||
Action<uint> UseItem,
|
||||
CombatState Combat,
|
||||
ItemManaState ItemMana,
|
||||
|
|
@ -114,13 +114,13 @@ public sealed record ToolbarRuntimeBindings(
|
|||
Func<uint> PlayerGuid,
|
||||
Action<uint, uint, int>? SendPutItemInContainer);
|
||||
|
||||
public sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
||||
internal sealed record CharacterRuntimeBindings(CharacterSheetProvider Provider);
|
||||
|
||||
public sealed record InventoryRuntimeBindings(
|
||||
internal sealed record InventoryRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveDragIcon,
|
||||
Func<int?> Strength,
|
||||
Action<uint>? SendUse,
|
||||
Action<uint, uint, int>? SendPutItemInContainer,
|
||||
|
|
@ -129,11 +129,11 @@ public sealed record InventoryRuntimeBindings(
|
|||
ItemInteractionController ItemInteraction,
|
||||
SelectionState Selection);
|
||||
|
||||
public sealed record ExternalContainerRuntimeBindings(
|
||||
internal sealed record ExternalContainerRuntimeBindings(
|
||||
ExternalContainerState State,
|
||||
ClientObjectTable Objects,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, uint> ResolveDragIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveIcon,
|
||||
Func<ItemType, uint, uint, uint, uint, GpuTextureSlot> ResolveDragIcon,
|
||||
ItemInteractionController ItemInteraction,
|
||||
SelectionState Selection,
|
||||
Action<uint> SendUse,
|
||||
|
|
@ -141,12 +141,12 @@ public sealed record ExternalContainerRuntimeBindings(
|
|||
Action<uint, uint, uint, uint> SendStackableSplitToContainer,
|
||||
Func<uint, bool> IsWithinUseRange);
|
||||
|
||||
public sealed record RetailUiPersistenceBindings(
|
||||
internal sealed record RetailUiPersistenceBindings(
|
||||
SettingsStore Store,
|
||||
Func<string> CharacterKey,
|
||||
Func<(int Width, int Height)> ScreenSize);
|
||||
|
||||
public sealed record RetailUiProbeBindings(
|
||||
internal sealed record RetailUiProbeBindings(
|
||||
bool Enabled,
|
||||
string? ScriptPath,
|
||||
bool DumpOnStart,
|
||||
|
|
@ -155,19 +155,19 @@ public sealed record RetailUiProbeBindings(
|
|||
Func<InputAction, bool, bool> SetInputHeld,
|
||||
Testing.IRetailUiAutomationRuntime? Runtime = null);
|
||||
|
||||
public sealed record RetailUiCursorBindings(
|
||||
internal sealed record RetailUiCursorBindings(
|
||||
CursorFeedbackController Feedback,
|
||||
RetailCursorManager Manager);
|
||||
|
||||
public sealed record ConfirmationRuntimeBindings(
|
||||
internal sealed record ConfirmationRuntimeBindings(
|
||||
Action<uint, uint, bool> SendResponse);
|
||||
|
||||
public sealed record AppraisalRuntimeBindings(
|
||||
internal sealed record AppraisalRuntimeBindings(
|
||||
Func<string> PlayerName,
|
||||
Action<uint, string> SendSetInscription,
|
||||
Action<string> DisplaySystemMessage);
|
||||
|
||||
public sealed record RetailUiRuntimeBindings(
|
||||
internal sealed record RetailUiRuntimeBindings(
|
||||
UiHost Host,
|
||||
RetailUiAssets Assets,
|
||||
VitalsRuntimeBindings Vitals,
|
||||
|
|
@ -196,7 +196,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
/// state/action delegates and GL/DAT resolvers; this runtime imports layouts, binds
|
||||
/// controllers, mounts windows, owns persistence, and presents one tick/draw/dispose seam.
|
||||
/// </summary>
|
||||
public sealed class RetailUiRuntime : IDisposable
|
||||
internal sealed class RetailUiRuntime : IDisposable
|
||||
{
|
||||
private readonly RetailUiRuntimeBindings _bindings;
|
||||
private StackSplitQuantityState StackSplitQuantity => _bindings.StackSplitQuantity;
|
||||
|
|
@ -566,7 +566,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
ImportedLayout? layout = Import(0x2100006Cu);
|
||||
if (layout is null)
|
||||
{
|
||||
Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable.");
|
||||
Console.WriteLine("[D.2b] vitals: LayoutDesc 0x2100006C not found — vitals unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -590,7 +590,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
MinWidth = 40f,
|
||||
ContentClickThrough = false,
|
||||
});
|
||||
Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C).");
|
||||
Console.WriteLine("[D.2b] retail UI active — vitals window from LayoutDesc importer (0x2100006C).");
|
||||
}
|
||||
|
||||
private void MountRadar()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -7,7 +7,7 @@ namespace AcDream.App.UI;
|
|||
/// mutations flow through this handle so lifecycle observers see the same state
|
||||
/// regardless of whether a change came from input, restore, or controller logic.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowHandle
|
||||
internal sealed class RetailWindowHandle
|
||||
{
|
||||
private readonly RetailWindowManager _owner;
|
||||
private bool _notifiedVisible;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.UI.Abstractions.Panels.Settings;
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI;
|
|||
/// <c>default</c> character key so startup layout cannot overwrite a real
|
||||
/// character's state.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowLayoutPersistence : IDisposable
|
||||
internal sealed class RetailWindowLayoutPersistence : IDisposable
|
||||
{
|
||||
private readonly RetailWindowManager _manager;
|
||||
private readonly SettingsStore _store;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
|
|
@ -9,7 +9,7 @@ namespace AcDream.App.UI;
|
|||
/// and controller teardown. <see cref="UiRoot"/> remains responsible for raw
|
||||
/// input ownership and reports completed interactions to this manager.
|
||||
/// </summary>
|
||||
public sealed class RetailWindowManager : IDisposable
|
||||
internal sealed class RetailWindowManager : IDisposable
|
||||
{
|
||||
private readonly UiRoot _root;
|
||||
private readonly Dictionary<string, RetailWindowHandle> _byName =
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -8,7 +8,7 @@ namespace AcDream.App.UI;
|
|||
/// inventory+paperdoll). Teardown runs in reverse construction order and is
|
||||
/// idempotent.
|
||||
/// </summary>
|
||||
public sealed class RetainedPanelControllerGroup : IRetainedPanelController
|
||||
internal sealed class RetainedPanelControllerGroup : IRetainedPanelController
|
||||
{
|
||||
private readonly IRetainedPanelController[] _controllers;
|
||||
private bool _disposed;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI.Testing;
|
|||
/// Snapshot row for the live retained-mode retail UI tree. Coordinates are
|
||||
/// absolute root/screen pixels.
|
||||
/// </summary>
|
||||
public sealed record RetailUiProbeElement(
|
||||
internal sealed record RetailUiProbeElement(
|
||||
int Index,
|
||||
int Depth,
|
||||
string Path,
|
||||
|
|
@ -32,14 +32,14 @@ public sealed record RetailUiProbeElement(
|
|||
public int CenterY => (int)MathF.Round(Y + Height * 0.5f);
|
||||
}
|
||||
|
||||
public sealed record RetailUiProbeAssertion(bool Success, string Message);
|
||||
internal sealed record RetailUiProbeAssertion(bool Success, string Message);
|
||||
|
||||
/// <summary>
|
||||
/// Test/diagnostic harness for the retail-style UI. It drives <see cref="UiRoot"/>
|
||||
/// mouse events, never panel controllers directly, so automated checks exercise
|
||||
/// the same click, double-click, and drag-drop path as a player.
|
||||
/// </summary>
|
||||
public sealed class RetailUiAutomationProbe
|
||||
internal sealed class RetailUiAutomationProbe
|
||||
{
|
||||
private readonly UiRoot _root;
|
||||
private readonly ClientObjectTable _objects;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
|
|
@ -15,7 +15,7 @@ public enum RetailUiAutomationCheckpointStatus
|
|||
Cancelled,
|
||||
}
|
||||
|
||||
public interface IRetailUiAutomationCheckpoint
|
||||
internal interface IRetailUiAutomationCheckpoint
|
||||
{
|
||||
int Sequence { get; }
|
||||
string Name { get; }
|
||||
|
|
@ -28,7 +28,7 @@ public interface IRetailUiAutomationCheckpoint
|
|||
/// diagnostics. Implementations run on the same update/render thread as the
|
||||
/// script; they must not mutate gameplay state or call GL from another thread.
|
||||
/// </summary>
|
||||
public interface IRetailUiAutomationRuntime
|
||||
internal interface IRetailUiAutomationRuntime
|
||||
{
|
||||
bool IsWorldReady { get; }
|
||||
bool IsWorldViewportVisible { get; }
|
||||
|
|
@ -48,7 +48,7 @@ public interface IRetailUiAutomationRuntime
|
|||
/// execute on render ticks. Pointer commands use the same <see cref="UiRoot"/>
|
||||
/// path as physical mouse input; semantic input uses the production dispatcher.
|
||||
/// </summary>
|
||||
public sealed class RetailUiAutomationScriptRunner : IDisposable
|
||||
internal sealed class RetailUiAutomationScriptRunner : IDisposable
|
||||
{
|
||||
private readonly RetailUiAutomationProbe _probe;
|
||||
private readonly Action<string> _log;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI.Layout;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Generic dat-widget button — the production replacement for any dat element of
|
||||
/// Generic dat-widget button — the production replacement for any dat element of
|
||||
/// Type 1 (UIElement_Button, registered via RegisterElementClass(1, UIElement_Button::Create)
|
||||
/// @ acclient_2013_pseudo_c.txt:125828).
|
||||
///
|
||||
|
|
@ -21,7 +21,7 @@ namespace AcDream.App.UI;
|
|||
/// <para>
|
||||
/// State selection: picks <see cref="ElementInfo.DefaultStateName"/> if set, then
|
||||
/// "Normal" if the element has a Normal state sprite, then falls back to the unnamed
|
||||
/// DirectState ("" key) — identical to <see cref="UiDatElement"/>.
|
||||
/// DirectState ("" key) — identical to <see cref="UiDatElement"/>.
|
||||
/// </para>
|
||||
///
|
||||
/// <para>
|
||||
|
|
@ -30,12 +30,12 @@ namespace AcDream.App.UI;
|
|||
/// earlier dev-scaffold widget with no dat sprites.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
||||
internal sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
||||
{
|
||||
private readonly ElementInfo _info;
|
||||
private readonly ElementInfo _mediaInfo;
|
||||
private readonly FaceSegment[] _faceSegments;
|
||||
private readonly Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly Func<uint, (GpuTextureSlot tex, int w, int h)> _resolve;
|
||||
private readonly HashSet<uint> _availableStates = new();
|
||||
private bool _pressed;
|
||||
private bool _pointerOver;
|
||||
|
|
@ -103,7 +103,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
public LabelAlignment LabelAlign { get; set; } = LabelAlignment.Center;
|
||||
|
||||
/// <summary>Label horizontal alignment options.</summary>
|
||||
public enum LabelAlignment { Center, Left }
|
||||
internal enum LabelAlignment { Center, Left }
|
||||
|
||||
public bool ToggleBehavior { get; }
|
||||
public bool RolloverEnabled { get; }
|
||||
|
|
@ -123,7 +123,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized).
|
||||
/// Active state name, runtime-settable (e.g. Max/Min toggling Normal ↔ Minimized).
|
||||
/// Matches <see cref="UiDatElement.ActiveState"/>.
|
||||
/// </summary>
|
||||
public string ActiveState { get; set; } = "";
|
||||
|
|
@ -198,11 +198,11 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
}
|
||||
|
||||
/// <param name="info">Merged <see cref="ElementInfo"/> for this element.</param>
|
||||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||||
/// <param name="resolve">Dat file-id → (GL texture handle, native px width, native px height).
|
||||
/// Returns (0,0,0) when the texture is not yet uploaded.</param>
|
||||
public UiButton(
|
||||
ElementInfo info,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
ElementInfo? mediaInfo = null,
|
||||
IReadOnlyList<ElementInfo>? faceSegments = null)
|
||||
{
|
||||
|
|
@ -212,7 +212,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
? []
|
||||
: faceSegments.Select(static segment => new FaceSegment(segment)).ToArray();
|
||||
_resolve = resolve;
|
||||
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
||||
ClickThrough = false; // buttons are interactive — opt OUT of click-through
|
||||
|
||||
// Visual transitions can select only states with an actual button face.
|
||||
// Retail layouts commonly declare an empty Normal_pressed descriptor while
|
||||
|
|
@ -254,7 +254,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
/// procedurally, so the importer must not build the button's children as widgets.</summary>
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
/// <summary>A button is interactive — it must receive its Click even inside a whole-window-Draggable
|
||||
/// <summary>A button is interactive — it must receive its Click even inside a whole-window-Draggable
|
||||
/// frame (e.g. the paperdoll "Slots" toggle in the inventory window), so it opts out of the
|
||||
/// IA-12 whole-window-drag that would otherwise swallow the press.</summary>
|
||||
public override bool HandlesClick => true;
|
||||
|
|
@ -282,9 +282,9 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
if (file != 0)
|
||||
{
|
||||
var (tex, tw, th) = _resolve(file);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
if (tex.IsAssigned && tw != 0 && th != 0)
|
||||
{
|
||||
// Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped
|
||||
// Tiled draw — same call shape as UiDatElement.OnDraw (UV-repeat; GL_REPEAT-wrapped
|
||||
// UI texture). Matches ImgTex::TileCSI; no Stretch mode exists.
|
||||
float faceWidth = FaceWidth > 0f ? FaceWidth : Width;
|
||||
float faceHeight = FaceHeight > 0f ? FaceHeight : Height;
|
||||
|
|
@ -312,7 +312,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
if (dragSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = _resolve(dragSprite);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
|
@ -322,7 +322,7 @@ public sealed class UiButton : UiElement, IUiGlobalTimeListener, IUiDatStateful
|
|||
if (file == 0 || rect.Width <= 0 || rect.Height <= 0)
|
||||
return;
|
||||
var (texture, textureWidth, textureHeight) = _resolve(file);
|
||||
if (texture == 0 || textureWidth == 0 || textureHeight == 0)
|
||||
if (!texture.IsAssigned || textureWidth == 0 || textureHeight == 0)
|
||||
return;
|
||||
|
||||
// Same tiled/cropped media path as UiDatElement. Segment geometry is
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Inputs to retail <c>UIElement_Button::UpdateState_</c>.</summary>
|
||||
public readonly record struct UiButtonVisualInput(
|
||||
|
|
@ -12,7 +12,7 @@ public readonly record struct UiButtonVisualInput(
|
|||
/// GL-free retail button visual-state policy. State ids are the numeric
|
||||
/// <c>UIStateId</c> values from the DAT.
|
||||
/// </summary>
|
||||
public static class UiButtonStateMachine
|
||||
internal static class UiButtonStateMachine
|
||||
{
|
||||
public const uint Normal = 1u;
|
||||
public const uint NormalRollover = 2u;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -9,12 +9,12 @@ namespace AcDream.App.UI;
|
|||
/// It reuses the retail UIItemList geometry without pretending a spell id is an
|
||||
/// object GUID; catalog drags stay distinct from physical item drag/drop.
|
||||
/// </summary>
|
||||
public sealed class UiCatalogSlot : UiItemSlot
|
||||
internal sealed class UiCatalogSlot : UiItemSlot
|
||||
{
|
||||
public uint EntryId { get; set; }
|
||||
public uint CatalogIconTexture { get; set; }
|
||||
public GpuTextureSlot CatalogIconTexture { get; set; } = GpuTextureSlot.Unassigned;
|
||||
/// <summary>Optional second 32x32 layer (retail spell endowment item icon).</summary>
|
||||
public uint CatalogOverlayTexture { get; set; }
|
||||
public GpuTextureSlot CatalogOverlayTexture { get; set; } = GpuTextureSlot.Unassigned;
|
||||
public string Label { get; set; } = string.Empty;
|
||||
public string? Detail { get; set; }
|
||||
public bool ShowLabel { get; init; }
|
||||
|
|
@ -43,8 +43,8 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
|
||||
public override bool IsDragSource => CatalogDragPayload is not null;
|
||||
public override object? GetDragPayload() => CatalogDragPayload;
|
||||
public override (uint tex, int w, int h)? GetDragGhost() =>
|
||||
CatalogDragPayload is not null && CatalogIconTexture != 0u
|
||||
public override (GpuTextureSlot tex, int w, int h)? GetDragGhost() =>
|
||||
CatalogDragPayload is not null && CatalogIconTexture.IsAssigned
|
||||
? (CatalogIconTexture, 32, 32)
|
||||
: null;
|
||||
|
||||
|
|
@ -92,7 +92,7 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
if (BackgroundSprite != 0u && SpriteResolve is not null)
|
||||
{
|
||||
var (texture, _, _) = SpriteResolve(BackgroundSprite);
|
||||
if (texture != 0u)
|
||||
if (texture.IsAssigned)
|
||||
ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
@ -101,16 +101,16 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
|
||||
float iconWidth = IconWidth > 0f ? IconWidth : ShowLabel ? MathF.Min(32f, Height) : Width;
|
||||
float iconHeight = IconHeight > 0f ? IconHeight : ShowLabel ? MathF.Min(32f, Height) : Height;
|
||||
if (CatalogIconTexture != 0)
|
||||
if (CatalogIconTexture.IsAssigned)
|
||||
ctx.DrawSprite(CatalogIconTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
else if (SpriteResolve is not null && EmptySprite != 0)
|
||||
{
|
||||
var (texture, _, _) = SpriteResolve(EmptySprite);
|
||||
if (texture != 0)
|
||||
if (texture.IsAssigned)
|
||||
ctx.DrawSprite(texture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
if (CatalogOverlayTexture != 0)
|
||||
if (CatalogOverlayTexture.IsAssigned)
|
||||
ctx.DrawSprite(CatalogOverlayTexture, IconLeft, IconTop, iconWidth, iconHeight, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
|
||||
// Spell favorites are UIElement_UIItems too. Retail's
|
||||
|
|
@ -147,7 +147,7 @@ public sealed class UiCatalogSlot : UiItemSlot
|
|||
{
|
||||
if (!Selected || SpriteResolve is null || SelectedSprite == 0u) return;
|
||||
var (texture, _, _) = SpriteResolve(SelectedSprite);
|
||||
if (texture != 0u)
|
||||
if (texture.IsAssigned)
|
||||
ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and
|
||||
/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the
|
||||
/// A toolbar-frame variant that snaps its height between two stops — collapsed (row 2 hidden) and
|
||||
/// expanded (row 2 shown) — and toggles a set of "second-row" elements to match. Resized via the
|
||||
/// bottom edge (the mount sets <see cref="UiElement.ResizableEdges"/> = Bottom); each tick it resolves
|
||||
/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a
|
||||
/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17.
|
||||
/// the dragged height to the nearer stop so the frame always rests collapsed or expanded — never a
|
||||
/// half-row. Toolkit UX (keystone.dll has no decomp; the dat stacks both rows always) — see IA-17.
|
||||
/// </summary>
|
||||
public sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController
|
||||
internal sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateController
|
||||
{
|
||||
public UiCollapsibleFrame(Func<uint, (uint, int, int)> resolve) : base(resolve) { }
|
||||
public UiCollapsibleFrame(Func<uint, (GpuTextureSlot, int, int)> resolve) : base(resolve) { }
|
||||
|
||||
public float CollapsedHeight { get; set; }
|
||||
public float ExpandedHeight { get; set; }
|
||||
|
|
@ -25,13 +25,13 @@ public sealed class UiCollapsibleFrame : UiNineSlicePanel, IRetainedWindowStateC
|
|||
protected override void OnTick(double deltaSeconds)
|
||||
{
|
||||
base.OnTick(deltaSeconds);
|
||||
if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap
|
||||
if (ExpandedHeight <= CollapsedHeight) return; // not configured yet — no snap
|
||||
bool expanded = IsExpanded;
|
||||
Height = expanded ? ExpandedHeight : CollapsedHeight; // snap the dragged height to a stop
|
||||
for (int i = 0; i < SecondRow.Count; i++) SecondRow[i].Visible = expanded;
|
||||
}
|
||||
|
||||
/// <summary>Test hook — OnTick is protected. Drives one snap+visibility reconcile.</summary>
|
||||
/// <summary>Test hook — OnTick is protected. Drives one snap+visibility reconcile.</summary>
|
||||
internal void TickForTest(double dt) => OnTick(dt);
|
||||
|
||||
public RetainedWindowState CaptureWindowState()
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
|
@ -17,7 +17,7 @@ namespace AcDream.App.UI;
|
|||
/// as two textured quads exactly the way the retail client does.
|
||||
///
|
||||
/// <para>
|
||||
/// Retail render model — <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// Retail render model — <c>SurfaceWindow::DrawCharacter</c>
|
||||
/// (acclient 0x00442bd0, Font::GetCharDesc + the two SurfaceWindow blits): for
|
||||
/// each glyph it copies the BACKGROUND atlas sub-rect first, tinted with the
|
||||
/// outline color (black), then the FOREGROUND atlas sub-rect, tinted with the
|
||||
|
|
@ -30,27 +30,27 @@ namespace AcDream.App.UI;
|
|||
///
|
||||
/// <para>
|
||||
/// Atlas format: the foreground atlas (0x06005EE5 for Font 0x40000000) is
|
||||
/// PFID_A8 — alpha-only. Our <c>SurfaceDecoder</c> expands A8 to RGBA as
|
||||
/// PFID_A8 — alpha-only. Our <c>SurfaceDecoder</c> expands A8 to RGBA as
|
||||
/// (255,255,255, alpha). The UI sprite shader path (ui_text.frag,
|
||||
/// <c>uUseTexture==2</c>) MULTIPLIES the sampled texel by the per-vertex tint
|
||||
/// (<c>texture(uTex,vUv) * vColor</c>), so tinting a white+alpha glyph by a
|
||||
/// color gives that color with the glyph's alpha — black for the outline pass,
|
||||
/// color gives that color with the glyph's alpha — black for the outline pass,
|
||||
/// text color for the fill pass. No shader change was needed.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiDatFont
|
||||
internal sealed class UiDatFont
|
||||
{
|
||||
/// <summary>Retail UI font id (Latin-1, 16x16 max, with outline atlas).</summary>
|
||||
public const uint DefaultFontId = 0x40000000u;
|
||||
|
||||
/// <summary>Foreground (glyph pixels) GL texture handle + atlas pixel size.</summary>
|
||||
public uint ForegroundTexture { get; }
|
||||
/// <summary>Foreground (glyph pixels) texture-table slot + atlas pixel size.</summary>
|
||||
public GpuTextureSlot ForegroundTexture { get; }
|
||||
public int ForegroundWidth { get; }
|
||||
public int ForegroundHeight { get; }
|
||||
|
||||
/// <summary>Background (outline/shadow) GL texture handle + atlas pixel size.
|
||||
/// 0 when the font has no background atlas (then the outline pass is skipped).</summary>
|
||||
public uint BackgroundTexture { get; }
|
||||
/// <summary>Background (outline/shadow) texture-table slot + atlas pixel size.
|
||||
/// Unassigned when the font has no background atlas (then the outline pass is skipped).</summary>
|
||||
public GpuTextureSlot BackgroundTexture { get; }
|
||||
public int BackgroundWidth { get; }
|
||||
public int BackgroundHeight { get; }
|
||||
|
||||
|
|
@ -63,8 +63,8 @@ public sealed class UiDatFont
|
|||
private readonly Dictionary<char, FontCharDesc> _glyphs;
|
||||
|
||||
internal UiDatFont(
|
||||
uint fgTex, int fgW, int fgH,
|
||||
uint bgTex, int bgW, int bgH,
|
||||
GpuTextureSlot fgTex, int fgW, int fgH,
|
||||
GpuTextureSlot bgTex, int bgW, int bgH,
|
||||
float lineHeight, float baselineOffset,
|
||||
Dictionary<char, FontCharDesc> glyphs)
|
||||
{
|
||||
|
|
@ -78,7 +78,7 @@ public sealed class UiDatFont
|
|||
/// <summary>True if this font carries a separate outline/shadow atlas
|
||||
/// (retail's <c>m_pBackgroundSurface</c>). When false the outline pass is
|
||||
/// skipped and only the foreground (fill) glyphs are drawn.</summary>
|
||||
public bool HasBackground => BackgroundTexture != 0;
|
||||
public bool HasBackground => BackgroundTexture.IsAssigned;
|
||||
|
||||
/// <summary>Look up a glyph descriptor for a character. Returns false for
|
||||
/// characters not present in the font's table (callers skip them).</summary>
|
||||
|
|
@ -88,7 +88,7 @@ public sealed class UiDatFont
|
|||
/// Load Font <paramref name="fontId"/> from the dat collection and upload
|
||||
/// both atlases through the texture cache (the same direct-RenderSurface
|
||||
/// path the D.2b chrome sprites use). Returns null if the Font DBObj is
|
||||
/// missing — callers fall back to the debug bitmap font.
|
||||
/// missing — callers fall back to the debug bitmap font.
|
||||
/// </summary>
|
||||
public static UiDatFont? Load(IDatReaderWriter dats, TextureCache cache, uint fontId = DefaultFontId)
|
||||
{
|
||||
|
|
@ -104,9 +104,9 @@ public sealed class UiDatFont
|
|||
|
||||
// Point-sample the glyph atlases (nearest) so small UI text stays pixel-crisp;
|
||||
// bilinear softens the dat font noticeably (the chat menu/button text "blur").
|
||||
uint fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true);
|
||||
GpuTextureSlot fgTex = cache.GetOrUploadRenderSurface(font.ForegroundSurfaceDataId, out int fgW, out int fgH, nearest: true);
|
||||
|
||||
uint bgTex = 0; int bgW = 0, bgH = 0;
|
||||
GpuTextureSlot bgTex = GpuTextureSlot.Unassigned; int bgW = 0, bgH = 0;
|
||||
if (font.BackgroundSurfaceDataId != 0)
|
||||
bgTex = cache.GetOrUploadRenderSurface(font.BackgroundSurfaceDataId, out bgW, out bgH, nearest: true);
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ public sealed class UiDatFont
|
|||
/// <summary>
|
||||
/// Pure pen-advance summation seam: total width of <paramref name="text"/>
|
||||
/// given a <paramref name="lookup"/> that maps each char to its descriptor
|
||||
/// (null = not in the font → contributes nothing). Lets the advance math be
|
||||
/// (null = not in the font → contributes nothing). Lets the advance math be
|
||||
/// unit-tested with synthetic glyphs, with no GL or dat dependency.
|
||||
/// </summary>
|
||||
public static float MeasureWidth(string? text, Func<char, FontCharDesc?> lookup)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.UI;
|
|||
/// Full-device modal root for retail dialog element classes. The authored child with id
|
||||
/// <c>0x3D</c> supplies the visible popup art; this root only owns modality and input capture.
|
||||
/// </summary>
|
||||
public sealed class UiDialogRoot : UiPanel
|
||||
internal sealed class UiDialogRoot : UiPanel
|
||||
{
|
||||
public Action? Cancel { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Which parent edges a child keeps a fixed margin to on resize.
|
||||
/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches.</summary>
|
||||
/// Left+Right ⇒ width stretches; Top+Bottom ⇒ height stretches.</summary>
|
||||
[System.Flags]
|
||||
public enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 }
|
||||
internal enum AnchorEdges { None = 0, Left = 1, Top = 2, Right = 4, Bottom = 8 }
|
||||
|
||||
/// <summary>Retail dat cursor media attached to a UI state.</summary>
|
||||
public readonly record struct UiCursorMedia(uint File, int HotspotX, int HotspotY)
|
||||
|
|
@ -21,7 +21,7 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot
|
|||
/// Design notes:
|
||||
/// - Retail AC delegates widget semantics to the external
|
||||
/// <c>keystone.dll</c> library (see
|
||||
/// <c>docs/research/retail-ui/02-class-hierarchy.md</c> — there is no
|
||||
/// <c>docs/research/retail-ui/02-class-hierarchy.md</c> — there is no
|
||||
/// widget hierarchy inside <c>acclient.exe</c> itself). We implement
|
||||
/// our own retained-mode toolkit here, matching the <i>behavior</i>
|
||||
/// described in the decompile without trying to byte-match Keystone's
|
||||
|
|
@ -35,13 +35,13 @@ public readonly record struct UiCursorMedia(uint File, int HotspotX, int Hotspot
|
|||
/// - Coordinates are in <b>screen pixels</b>, origin top-left.
|
||||
/// <see cref="Bounds"/> is in the parent's local coordinate space.
|
||||
/// </summary>
|
||||
public abstract class UiElement
|
||||
internal abstract class UiElement
|
||||
{
|
||||
// ── Identity ─────────────────────────────────────────────────────────
|
||||
// ── Identity ─────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// Unique 32-bit event ID. Retail uses the range <c>0x10000000+</c>
|
||||
/// for custom app events (see
|
||||
/// <c>docs/research/retail-ui/04-input-events.md §3</c>). Assigned
|
||||
/// <c>docs/research/retail-ui/04-input-events.md §3</c>). Assigned
|
||||
/// by <see cref="UiRoot"/> when the element is added to the tree.
|
||||
/// </summary>
|
||||
public uint EventId { get; internal set; }
|
||||
|
|
@ -96,7 +96,7 @@ public abstract class UiElement
|
|||
return default;
|
||||
}
|
||||
|
||||
// ── Geometry ────────────────────────────────────────────────────────
|
||||
// ── Geometry ────────────────────────────────────────────────────────
|
||||
/// <summary>X in the parent's local pixel space.</summary>
|
||||
public float Left { get; set; }
|
||||
public float Top { get; set; }
|
||||
|
|
@ -119,7 +119,7 @@ public abstract class UiElement
|
|||
}
|
||||
}
|
||||
|
||||
// ── State flags ─────────────────────────────────────────────────────
|
||||
// ── State flags ─────────────────────────────────────────────────────
|
||||
private bool _visible = true;
|
||||
public bool Visible
|
||||
{
|
||||
|
|
@ -152,7 +152,7 @@ public abstract class UiElement
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// If true, <see cref="HitTest"/> skips this element — the event
|
||||
/// If true, <see cref="HitTest"/> skips this element — the event
|
||||
/// passes through to whatever is behind. Used by decoration widgets
|
||||
/// (portrait frames, ornamental dividers).
|
||||
/// </summary>
|
||||
|
|
@ -212,13 +212,13 @@ public abstract class UiElement
|
|||
|
||||
/// <summary>If true, a left-drag starting on this element is delivered to the
|
||||
/// element (e.g. text selection) instead of moving/resizing an ancestor window.
|
||||
/// Edge resize on a resizable ancestor still wins — only the interior move /
|
||||
/// Edge resize on a resizable ancestor still wins — only the interior move /
|
||||
/// drag-drop candidacy is suppressed in favour of the element's own handling.</summary>
|
||||
public bool CapturesPointerDrag { get; set; }
|
||||
|
||||
/// <summary>If true, a left-press-and-move on this element starts a DRAG-DROP
|
||||
/// (<see cref="UiRoot"/> promotes to BeginDrag) rather than moving a Draggable
|
||||
/// ancestor window — so an item cell inside the toolbar frame drags the item, not
|
||||
/// ancestor window — so an item cell inside the toolbar frame drags the item, not
|
||||
/// the window. Distinct from <see cref="CapturesPointerDrag"/> (a self-driven
|
||||
/// interior drag like text selection, which does NOT promote to BeginDrag). Default
|
||||
/// false; overridden by drag sources (e.g. an occupied <see cref="UiItemSlot"/>).</summary>
|
||||
|
|
@ -227,7 +227,7 @@ public abstract class UiElement
|
|||
/// <summary>If true, a left-press on this element is handled BY the element (it receives the Click
|
||||
/// on release) instead of being captured as a whole-window move on a Draggable ancestor. Set by
|
||||
/// interactive leaf widgets (e.g. <see cref="UiButton"/>) so they stay clickable inside a
|
||||
/// whole-window-Draggable frame like the inventory window — where, without this, the IA-12
|
||||
/// whole-window-Draggable frame like the inventory window — where, without this, the IA-12
|
||||
/// whole-window-drag swallows the press and the Click is never emitted. Distinct from
|
||||
/// <see cref="IsDragSource"/> (starts a drag-drop) and <see cref="CapturesPointerDrag"/> (a
|
||||
/// self-driven interior drag such as text selection). Default false.</summary>
|
||||
|
|
@ -260,7 +260,7 @@ public abstract class UiElement
|
|||
ResizeEdges.Left | ResizeEdges.Right | ResizeEdges.Top | ResizeEdges.Bottom;
|
||||
|
||||
/// <summary>Edges this element anchors to in its parent. Default Left|Top
|
||||
/// (pinned top-left, fixed size — no reflow). Left|Right stretches width.</summary>
|
||||
/// (pinned top-left, fixed size — no reflow). Left|Right stretches width.</summary>
|
||||
private AnchorEdges _anchors = AnchorEdges.Left | AnchorEdges.Top;
|
||||
|
||||
/// <summary>Edges this programmatic element anchors to in its parent. Assigning
|
||||
|
|
@ -284,7 +284,7 @@ public abstract class UiElement
|
|||
/// </summary>
|
||||
public UiLayoutPolicy? LayoutPolicy { get; set; }
|
||||
|
||||
// ── Tree structure ──────────────────────────────────────────────────
|
||||
// ── Tree structure ──────────────────────────────────────────────────
|
||||
public UiElement? Parent { get; private set; }
|
||||
|
||||
private readonly List<UiElement> _children = new();
|
||||
|
|
@ -355,7 +355,7 @@ public abstract class UiElement
|
|||
/// <summary>
|
||||
/// True if this widget draws its full appearance itself and REPRODUCES its dat
|
||||
/// sub-elements procedurally (3-slice caps, button labels, scroll arrows, popup
|
||||
/// rows…) — so the <see cref="AcDream.App.UI.Layout.LayoutImporter"/> must NOT build
|
||||
/// rows…) — so the <see cref="AcDream.App.UI.Layout.LayoutImporter"/> must NOT build
|
||||
/// those dat child elements as separate widgets (they would double-draw and, worse,
|
||||
/// steal pointer/focus from the behavioral widget). All registered behavioral widgets
|
||||
/// (Meter/Menu/Button/Scrollbar/Text/Field) return <c>true</c>; the generic container
|
||||
|
|
@ -365,7 +365,7 @@ public abstract class UiElement
|
|||
/// </summary>
|
||||
public virtual bool ConsumesDatChildren => false;
|
||||
|
||||
// ── Virtual overrides ───────────────────────────────────────────────
|
||||
// ── Virtual overrides ───────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Draw THIS element (not its children). Children are composited by
|
||||
|
|
@ -384,7 +384,7 @@ public abstract class UiElement
|
|||
|
||||
/// <summary>
|
||||
/// Draw content that must sit ON TOP of the ENTIRE UI, regardless of this
|
||||
/// element's position in the tree — open menus, dropdowns, tooltips. Called in
|
||||
/// element's position in the tree — open menus, dropdowns, tooltips. Called in
|
||||
/// a SECOND traversal after the whole tree's <see cref="OnDraw"/> pass, with the
|
||||
/// same accumulated transform/alpha this element had during its normal draw.
|
||||
/// Retail spawns popups as ROOT elements (UIElement_Menu::MakePopup) for exactly
|
||||
|
|
@ -425,9 +425,9 @@ public abstract class UiElement
|
|||
public virtual object? GetDragPayload() => null;
|
||||
|
||||
/// <summary>The texture <see cref="UiRoot"/> paints at the cursor while this element
|
||||
/// is the drag source: (GL handle, width, height). Null = no ghost. Keeps
|
||||
/// is the drag source: (texture-table slot, width, height). Null = no ghost. Keeps
|
||||
/// <see cref="UiRoot"/> item-agnostic. Retail analog: m_dragIcon (decomp 229738).</summary>
|
||||
public virtual (uint tex, int w, int h)? GetDragGhost() => null;
|
||||
public virtual (GpuTextureSlot tex, int w, int h)? GetDragGhost() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Notifies the source widget when the root starts or finishes carrying its drag payload.
|
||||
|
|
@ -444,7 +444,7 @@ public abstract class UiElement
|
|||
/// </summary>
|
||||
public virtual string? GetTooltipText() => null;
|
||||
|
||||
// ── Framework entry points (internal, called by UiRoot) ─────────────
|
||||
// ── Framework entry points (internal, called by UiRoot) ─────────────
|
||||
|
||||
internal void DrawSelfAndChildren(UiRenderContext ctx)
|
||||
{
|
||||
|
|
@ -551,7 +551,7 @@ public abstract class UiElement
|
|||
return null;
|
||||
|
||||
// Children first, in reverse Z-order (topmost first). ClickThrough means
|
||||
// THIS element is transparent to the pointer — but its children are NOT.
|
||||
// THIS element is transparent to the pointer — but its children are NOT.
|
||||
// A ClickThrough container (e.g. a UiDatElement panel that hosts the chat
|
||||
// input / transcript) must still let the pointer reach its behavioral
|
||||
// children, so the ClickThrough check happens AFTER the child walk, gating
|
||||
|
|
@ -571,7 +571,7 @@ public abstract class UiElement
|
|||
return OnHitTest(localX, localY) ? this : null;
|
||||
}
|
||||
|
||||
// ── Anchor layout ────────────────────────────────────────────────────
|
||||
// ── Anchor layout ────────────────────────────────────────────────────
|
||||
|
||||
private bool _anchorCaptured;
|
||||
private float _amL, _amT, _amR, _amB, _aw0, _ah0;
|
||||
|
|
@ -659,7 +659,7 @@ public abstract class UiElement
|
|||
}
|
||||
|
||||
/// <summary>Walk up to the owning <see cref="UiRoot"/> (the top of the tree), or null
|
||||
/// if this element is not attached. Lets a widget reach focus/capture services — e.g.
|
||||
/// if this element is not attached. Lets a widget reach focus/capture services — e.g.
|
||||
/// a chat input blurring itself (exiting write mode) after submit.</summary>
|
||||
internal UiRoot? FindRoot()
|
||||
{
|
||||
|
|
@ -668,8 +668,8 @@ public abstract class UiElement
|
|||
return e as UiRoot;
|
||||
}
|
||||
|
||||
/// <summary>Compute an anchored child rect. Left&Right ⇒ stretch width
|
||||
/// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise
|
||||
/// <summary>Compute an anchored child rect. Left&Right ⇒ stretch width
|
||||
/// (keep both margins); Right only ⇒ pin to right at fixed width; otherwise
|
||||
/// pin left at fixed width. Same logic vertically.</summary>
|
||||
public static (float x, float y, float w, float h) ComputeAnchoredRect(
|
||||
AnchorEdges a, float mL, float mT, float mR, float mB,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -10,16 +10,16 @@ namespace AcDream.App.UI;
|
|||
/// Layout from decompiled <c>chunk_004A0000.c</c> paperdoll handler
|
||||
/// <c>FUN_004A5FA0</c>:
|
||||
/// <code>
|
||||
/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source)
|
||||
/// int source_id; // param_2[0] — e.g. 0x100001d6 (drag source)
|
||||
/// void* target_widget; // param_2[1]
|
||||
/// int event_type; // param_2[2] — see UiEventType
|
||||
/// int event_type; // param_2[2] — see UiEventType
|
||||
/// int data0; // param_2[3]
|
||||
/// int data1; // param_2[4] — typically x in local coords
|
||||
/// int data2; // param_2[5] — typically y
|
||||
/// int data1; // param_2[4] — typically x in local coords
|
||||
/// int data2; // param_2[5] — typically y
|
||||
/// int data3; // param_2[6]
|
||||
/// </code>
|
||||
/// </summary>
|
||||
public readonly record struct UiEvent(
|
||||
internal readonly record struct UiEvent(
|
||||
uint SourceId,
|
||||
UiElement? Target,
|
||||
int Type, // see <see cref="UiEventType"/>
|
||||
|
|
@ -33,22 +33,22 @@ public readonly record struct UiEvent(
|
|||
/// Retail AC UI event-type constants. Each value matches the decompiled
|
||||
/// switch-case in widgets' OnEvent handlers (e.g. 0x01 click, 0x15 drag
|
||||
/// begin, 0x3E drop released). Win32 WM_* numbers are reused for raw
|
||||
/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches
|
||||
/// button/key/mouse events (0x200 = WM_MOUSEMOVE etc.) — this matches
|
||||
/// retail where internal event codes collide deliberately with WM_*.
|
||||
///
|
||||
/// Evidence from decompile:
|
||||
/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270
|
||||
/// - 0x05/0x06 hover — chunk_00460000.c ~6253
|
||||
/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the
|
||||
/// - 0x01 click — chunk_00470000.c ~11140, chunk_004C0000.c ~9270
|
||||
/// - 0x05/0x06 hover — chunk_00460000.c ~6253
|
||||
/// - 0x07 tooltip — chunk_00460000.c ~6253 (the UI manager polls the
|
||||
/// hovered element's tooltip deadline before global time)
|
||||
/// - 0x0A scroll — chunk_00470000.c ~11210
|
||||
/// - 0x0E right-click— chunk_004A0000.c ~2674
|
||||
/// - 0x15 drag begin — chunk_004A0000.c ~2707
|
||||
/// - 0x1C drag-over — chunk_004A0000.c ~2723
|
||||
/// - 0x21 drag-enter — chunk_004A0000.c ~2714
|
||||
/// - 0x3E drop-released — chunk_004A0000.c ~2754
|
||||
/// - 0x0A scroll — chunk_00470000.c ~11210
|
||||
/// - 0x0E right-click— chunk_004A0000.c ~2674
|
||||
/// - 0x15 drag begin — chunk_004A0000.c ~2707
|
||||
/// - 0x1C drag-over — chunk_004A0000.c ~2723
|
||||
/// - 0x21 drag-enter — chunk_004A0000.c ~2714
|
||||
/// - 0x3E drop-released — chunk_004A0000.c ~2754
|
||||
/// </summary>
|
||||
public static class UiEventType
|
||||
internal static class UiEventType
|
||||
{
|
||||
public const int Click = 0x01;
|
||||
public const int HoverEnter = 0x05;
|
||||
|
|
@ -82,7 +82,7 @@ public static class UiEventType
|
|||
/// <summary>
|
||||
/// Mouse button enum matching retail's 1/2/3 encoding.
|
||||
/// </summary>
|
||||
public enum UiMouseButton
|
||||
internal enum UiMouseButton
|
||||
{
|
||||
Left = 1,
|
||||
Right = 2,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
|
|
@ -11,16 +11,16 @@ namespace AcDream.App.UI;
|
|||
/// as stubs for future item-window use.
|
||||
///
|
||||
/// <para>
|
||||
/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the
|
||||
/// Caret is a glyph index; the caret pixel-X is Σ glyph advances (UiDatFont) to the
|
||||
/// caret. Supports mouse + Shift-arrow SELECTION, clipboard cut/copy/paste, and
|
||||
/// held-key auto-repeat (hold Backspace deletes continuously). Submit (Enter / Send)
|
||||
/// fires <see cref="OnSubmit"/>, clears, and pushes history (100-entry cap,
|
||||
/// sentinel 0xFFFFFFFF — port of <c>ChatInterface::ProcessCommand @0x4f5100</c>).
|
||||
/// sentinel 0xFFFFFFFF — port of <c>ChatInterface::ProcessCommand @0x4f5100</c>).
|
||||
/// </para>
|
||||
///
|
||||
/// Decomp: UIElement_Text MoveCursor @0x468d00, FindPixelsFromPos @0x472b40.
|
||||
/// </summary>
|
||||
public sealed class UiField : UiElement
|
||||
internal sealed class UiField : UiElement
|
||||
{
|
||||
private readonly record struct WrappedLine(int Start, int Length, string Text);
|
||||
|
||||
|
|
@ -71,9 +71,9 @@ public sealed class UiField : UiElement
|
|||
/// Wired by the host from <see cref="UiHost.Keyboard"/>.</summary>
|
||||
public Silk.NET.Input.IKeyboard? Keyboard { get; set; }
|
||||
|
||||
/// <summary>Dat sprite resolver (id → GL texture + size) for the focused-field
|
||||
/// <summary>Dat sprite resolver (id → GL texture + size) for the focused-field
|
||||
/// background. Null = fall back to the flat <see cref="BackgroundColor"/> rect.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
/// <summary>Unfocused/default state sprite imported from the DAT.</summary>
|
||||
public uint BackgroundSprite { get; set; }
|
||||
/// <summary>Gold "lit" field background drawn when focused (retail Normal_focussed
|
||||
|
|
@ -120,7 +120,7 @@ public sealed class UiField : UiElement
|
|||
/// are reproduced procedurally, so the importer must not build them as widgets.</summary>
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
// ── Editing primitives ──────────────────────────────────────────────
|
||||
// ── Editing primitives ──────────────────────────────────────────────
|
||||
|
||||
public void InsertChar(char c)
|
||||
{
|
||||
|
|
@ -169,7 +169,7 @@ public sealed class UiField : UiElement
|
|||
|
||||
private void MoveCaret(int delta, bool shift) => MoveCaretTo(_caret + delta, shift);
|
||||
|
||||
// ── Selection ────────────────────────────────────────────────────────
|
||||
// ── Selection ────────────────────────────────────────────────────────
|
||||
|
||||
private (int lo, int hi) SelSpan()
|
||||
{
|
||||
|
|
@ -268,7 +268,7 @@ public sealed class UiField : UiElement
|
|||
_historyIndex = -1;
|
||||
}
|
||||
|
||||
// ── Submit + history ─────────────────────────────────────────────────
|
||||
// ── Submit + history ─────────────────────────────────────────────────
|
||||
|
||||
public void Submit()
|
||||
{
|
||||
|
|
@ -315,9 +315,9 @@ public sealed class UiField : UiElement
|
|||
_selAnchor = null;
|
||||
}
|
||||
|
||||
// ── Geometry ─────────────────────────────────────────────────────────
|
||||
// ── Geometry ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Pixel-X of the caret (Σ glyph advances to <paramref name="i"/>).</summary>
|
||||
/// <summary>Pixel-X of the caret (Σ glyph advances to <paramref name="i"/>).</summary>
|
||||
private float MeasureTo(int i)
|
||||
{
|
||||
if (i <= 0) return 0f;
|
||||
|
|
@ -328,7 +328,7 @@ public sealed class UiField : UiElement
|
|||
|
||||
public float CaretPixelX() => MeasureTo(_caret);
|
||||
|
||||
/// <summary>Map a local X (click) to the nearest caret index — retail
|
||||
/// <summary>Map a local X (click) to the nearest caret index — retail
|
||||
/// FindPixelsFromPos inverse. Accounts for the horizontal scroll offset.</summary>
|
||||
private int HitCharX(float localX)
|
||||
{
|
||||
|
|
@ -344,25 +344,25 @@ public sealed class UiField : UiElement
|
|||
return best;
|
||||
}
|
||||
|
||||
// ── Draw ─────────────────────────────────────────────────────────────
|
||||
// ── Draw ─────────────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// Focused = "write mode": draw the gold lit field sprite (retail Normal_focussed).
|
||||
// Unfocused: draw the imported default state, then the flat translucent fallback.
|
||||
// Both go through the sprite bucket
|
||||
// (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top.
|
||||
// (DrawFill / DrawSprite) so the text — also sprite-bucket — draws on top.
|
||||
bool lit = _focused && SpriteResolve is not null && FocusFieldSprite != 0;
|
||||
if (lit)
|
||||
{
|
||||
var (tex, tw, th) = SpriteResolve!(FocusFieldSprite);
|
||||
if (tex != 0 && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
if (tex.IsAssigned && tw > 0) ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
else lit = false;
|
||||
}
|
||||
if (!lit && SpriteResolve is not null && BackgroundSprite != 0)
|
||||
{
|
||||
var (tex, tw, th) = SpriteResolve(BackgroundSprite);
|
||||
if (tex != 0 && tw > 0 && th > 0)
|
||||
if (tex.IsAssigned && tw > 0 && th > 0)
|
||||
{
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0f, 0f,
|
||||
Width / tw, Height / th, Vector4.One);
|
||||
|
|
@ -382,7 +382,7 @@ public sealed class UiField : UiElement
|
|||
float visibleW = MathF.Max(1f, Width - 2f * Padding);
|
||||
|
||||
// Horizontal scroll: keep the caret inside the field; clamp so we never scroll past
|
||||
// the text. Then draw only the glyph window that lands inside the field — a single-
|
||||
// the text. Then draw only the glyph window that lands inside the field — a single-
|
||||
// line text box clips + scrolls (retail UIElement_Text) rather than overflowing the
|
||||
// field (which previously spilled the text out into the 3D world).
|
||||
float caretX = MeasureTo(_caret);
|
||||
|
|
@ -417,7 +417,7 @@ public sealed class UiField : UiElement
|
|||
|
||||
if (_focused)
|
||||
{
|
||||
// Caret on TOP of the text → submitted after the text in the same bucket.
|
||||
// Caret on TOP of the text → submitted after the text in the same bucket.
|
||||
float cx = Padding + alignX + (caretX - _scrollX);
|
||||
if (cx >= Padding - 1f && cx <= Width - Padding + 1f)
|
||||
ctx.DrawFill(cx, ty, 1f, lh, TextColor);
|
||||
|
|
@ -620,7 +620,7 @@ public sealed class UiField : UiElement
|
|||
return line.Start + best;
|
||||
}
|
||||
|
||||
// ── Auto-repeat ──────────────────────────────────────────────────────
|
||||
// ── Auto-repeat ──────────────────────────────────────────────────────
|
||||
|
||||
protected override void OnTick(double deltaSeconds)
|
||||
{
|
||||
|
|
@ -649,7 +649,7 @@ public sealed class UiField : UiElement
|
|||
&& (Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftLeft)
|
||||
|| Keyboard.IsKeyPressed(Silk.NET.Input.Key.ShiftRight));
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────
|
||||
// ── Events ───────────────────────────────────────────────────────────
|
||||
|
||||
public override bool OnEvent(in UiEvent e)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using Silk.NET.Input;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -14,7 +14,7 @@ namespace AcDream.App.UI;
|
|||
///
|
||||
/// Usage (from <c>GameWindow.OnLoad</c>):
|
||||
/// <code>
|
||||
/// _uiHost = new UiHost(_gl, shadersDir, _debugFont);
|
||||
/// _uiHost = new UiHost(_gpuDevice, () => _gpuFrameLifetime.Current, _debugFont);
|
||||
/// _uiHost.Root.WorldMouseFallThrough += (btn, x, y, f) => HandleWorldClick(btn, x, y);
|
||||
/// _uiHost.Root.WorldKeyFallThrough += (vk, lp) => HandleHotkey(vk);
|
||||
///
|
||||
|
|
@ -35,7 +35,7 @@ namespace AcDream.App.UI;
|
|||
/// (Keystone root, widget tree). We fuse them into a single host class
|
||||
/// because we're not linking to Keystone.
|
||||
/// </summary>
|
||||
public sealed class UiHost : System.IDisposable
|
||||
internal sealed class UiHost : System.IDisposable
|
||||
{
|
||||
public UiRoot Root { get; } = new();
|
||||
public RetailWindowManager WindowManager => Root.WindowManager;
|
||||
|
|
@ -44,12 +44,13 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
/// <summary>The last wired keyboard. Exposed so widgets that need clipboard
|
||||
/// access (<see cref="IKeyboard.ClipboardText"/>) or modifier-key state
|
||||
/// (<see cref="IKeyboard.IsKeyPressed"/>) — e.g. <see cref="UiText"/>'s
|
||||
/// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins.</summary>
|
||||
/// (<see cref="IKeyboard.IsKeyPressed"/>) — e.g. <see cref="UiText"/>'s
|
||||
/// Ctrl+C copy — can reach the device. One-keyboard desktop: last wins.</summary>
|
||||
public IKeyboard? Keyboard { get; private set; }
|
||||
|
||||
private long _startTicks = System.Environment.TickCount64;
|
||||
private readonly HostQuiescenceGate _quiescence;
|
||||
private readonly Func<IGpuFrame> _currentFrame;
|
||||
private readonly List<IRetainedUiInputBinding> _inputBindings = new();
|
||||
private ResourceShutdownTransaction? _inputShutdown;
|
||||
private ResourceShutdownTransaction? _shutdown;
|
||||
|
|
@ -58,23 +59,24 @@ public sealed class UiHost : System.IDisposable
|
|||
|
||||
internal bool IsDisposalComplete => _disposed;
|
||||
|
||||
public UiHost(GL gl, string shaderDir, BitmapFont? defaultFont = null)
|
||||
: this(gl, shaderDir, defaultFont, new HostQuiescenceGate())
|
||||
public UiHost(IGpuDevice device, Func<IGpuFrame> currentFrame, BitmapFont? defaultFont = null)
|
||||
: this(device, currentFrame, defaultFont, new HostQuiescenceGate())
|
||||
{
|
||||
}
|
||||
|
||||
internal UiHost(
|
||||
GL gl,
|
||||
string shaderDir,
|
||||
IGpuDevice device,
|
||||
Func<IGpuFrame> currentFrame,
|
||||
BitmapFont? defaultFont,
|
||||
HostQuiescenceGate quiescence)
|
||||
{
|
||||
_quiescence = quiescence ?? throw new ArgumentNullException(nameof(quiescence));
|
||||
TextRenderer = new TextRenderer(gl, shaderDir);
|
||||
_currentFrame = currentFrame ?? throw new ArgumentNullException(nameof(currentFrame));
|
||||
TextRenderer = new TextRenderer(device);
|
||||
DefaultFont = defaultFont;
|
||||
}
|
||||
|
||||
// ── Per-frame ──────────────────────────────────────────────────────
|
||||
// ── Per-frame ──────────────────────────────────────────────────────
|
||||
|
||||
public void Tick(double deltaSeconds)
|
||||
{
|
||||
|
|
@ -90,10 +92,10 @@ public sealed class UiHost : System.IDisposable
|
|||
var ctx = new UiRenderContext(TextRenderer, screenSize, DefaultFont);
|
||||
TextRenderer.Begin(screenSize);
|
||||
Root.Draw(ctx);
|
||||
TextRenderer.Flush(DefaultFont);
|
||||
TextRenderer.Flush(DefaultFont, _currentFrame());
|
||||
}
|
||||
|
||||
// ── Input wiring helpers ───────────────────────────────────────────
|
||||
// ── Input wiring helpers ───────────────────────────────────────────
|
||||
|
||||
public void WireMouse(IMouse mouse)
|
||||
{
|
||||
|
|
@ -183,7 +185,7 @@ public sealed class UiHost : System.IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
// ── Window manager forwarders (delegate to UiRoot) ─────────────────
|
||||
// ── Window manager forwarders (delegate to UiRoot) ─────────────────
|
||||
|
||||
/// <summary>Register a top-level window for Show/Hide/Toggle. See <see cref="UiRoot.RegisterWindow"/>.</summary>
|
||||
public RetailWindowHandle RegisterWindow(
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>Cell order for a multi-column <see cref="UiItemList"/>.</summary>
|
||||
public enum UiItemListFlow
|
||||
internal enum UiItemListFlow
|
||||
{
|
||||
/// <summary>Retail listbox bit 0 set: index advances across columns, then down rows.</summary>
|
||||
RowMajor,
|
||||
|
|
@ -19,7 +19,7 @@ public enum UiItemListFlow
|
|||
/// LayoutImporter must NOT build dat children. The toolbar uses single-cell
|
||||
/// instances (one slot); the inventory phase will grow this to an N-cell grid.
|
||||
/// </summary>
|
||||
public sealed class UiItemList : UiElement
|
||||
internal sealed class UiItemList : UiElement
|
||||
{
|
||||
private readonly List<UiItemSlot> _cells = new();
|
||||
private int _layoutDeferralDepth;
|
||||
|
|
@ -29,7 +29,7 @@ public sealed class UiItemList : UiElement
|
|||
public UiScrollable Scroll { get; }
|
||||
|
||||
public UiItemList(
|
||||
Func<uint, (uint tex, int w, int h)>? spriteResolve = null,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)>? spriteResolve = null,
|
||||
UiScrollable? scroll = null)
|
||||
{
|
||||
Scroll = scroll ?? new UiScrollable();
|
||||
|
|
@ -40,7 +40,7 @@ public sealed class UiItemList : UiElement
|
|||
|
||||
public override bool ConsumesDatChildren => true;
|
||||
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
private IReadOnlyList<uint>? _cooldownSprites;
|
||||
public IReadOnlyList<uint>? CooldownSprites
|
||||
|
|
@ -165,7 +165,7 @@ public sealed class UiItemList : UiElement
|
|||
cell.CooldownStepProvider ??= _cooldownStepProvider;
|
||||
if (_cellEmptySprite != 0) cell.EmptySprite = _cellEmptySprite;
|
||||
// The list lays cells out procedurally (grid offset + scroll clip), so cells must be
|
||||
// EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor
|
||||
// EXEMPT from the parent anchor pass — UiElement.DrawSelfAndChildren runs ApplyAnchor
|
||||
// on every child after OnDraw, which would otherwise reset the scroll offset to each
|
||||
// cell's captured base position (the escaping-grid bug). Anchors=None makes LayoutCells
|
||||
// the sole authority over cell rects.
|
||||
|
|
@ -197,7 +197,7 @@ public sealed class UiItemList : UiElement
|
|||
/// </summary>
|
||||
public UiItemListFlow Flow { get; set; } = UiItemListFlow.RowMajor;
|
||||
|
||||
/// <summary>Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes
|
||||
/// <summary>Fixed cell width in grid mode. 0 = "fill the list" — the single cell sizes
|
||||
/// to the whole list (the toolbar single-slot legacy). Set >0 (with CellHeight) for a grid.</summary>
|
||||
public float CellWidth { get; set; }
|
||||
/// <summary>Fixed cell height in grid mode (pairs with CellWidth).</summary>
|
||||
|
|
|
|||
|
|
@ -1,16 +1,24 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Accept/reject overlay state while a drag hovers an item slot cell. Top-level
|
||||
/// (not nested in <see cref="UiItemSlot"/>, which is internal) because
|
||||
/// <c>CursorFeedbackSnapshot</c> is a public test-visible type that carries this
|
||||
/// value.
|
||||
/// </summary>
|
||||
public enum DragAcceptState { None, Accept, Reject }
|
||||
|
||||
/// <summary>
|
||||
/// One item-in-a-slot cell (port of retail UIElement_UIItem, class 0x10000032).
|
||||
/// A behavioral LEAF: it draws the empty-slot sprite when unbound, else a
|
||||
/// pre-composited icon texture (set by the controller). Holds the bound weenie
|
||||
/// guid (retail UIElement_UIItem::itemID, +0x5FC).
|
||||
/// </summary>
|
||||
public class UiItemSlot : UiElement
|
||||
internal class UiItemSlot : UiElement
|
||||
{
|
||||
public UiItemSlot() { ClickThrough = false; }
|
||||
|
||||
|
|
@ -19,15 +27,15 @@ public class UiItemSlot : UiElement
|
|||
/// <summary>Bound weenie guid (0 = empty). Retail UIElement_UIItem::itemID.</summary>
|
||||
public uint ItemId { get; private set; }
|
||||
|
||||
/// <summary>Pre-composited icon GL texture for the bound item (0 = none).</summary>
|
||||
public uint IconTexture { get; private set; }
|
||||
/// <summary>Pre-composited icon texture-table slot for the bound item (Unassigned = none).</summary>
|
||||
public GpuTextureSlot IconTexture { get; private set; } = GpuTextureSlot.Unassigned;
|
||||
|
||||
/// <summary>
|
||||
/// Underlay-free cursor graphic for the bound item (retail <c>IconData::m_pDragIcon</c>).
|
||||
/// Falls back to <see cref="IconTexture"/> only for callers that have not supplied the
|
||||
/// dedicated composite.
|
||||
/// </summary>
|
||||
public uint DragIconTexture { get; private set; }
|
||||
public GpuTextureSlot DragIconTexture { get; private set; } = GpuTextureSlot.Unassigned;
|
||||
|
||||
/// <summary>
|
||||
/// Lossless shortcut record when this cell belongs to the toolbar. Physical item
|
||||
|
|
@ -36,7 +44,7 @@ public class UiItemSlot : UiElement
|
|||
public ShortcutEntry? Shortcut { get; private set; }
|
||||
|
||||
/// <summary>This cell's own index within its panel (0..17 toolbar; container slot
|
||||
/// for inventory). Distinct from <see cref="ShortcutNum"/> (the 1–9 label, -1 on the
|
||||
/// for inventory). Distinct from <see cref="ShortcutNum"/> (the 1–9 label, -1 on the
|
||||
/// bottom row). Set by the controller; used as the drag payload's SourceSlot and to
|
||||
/// identify the drop TARGET slot.</summary>
|
||||
public int SlotIndex { get; set; } = -1;
|
||||
|
|
@ -54,24 +62,24 @@ public class UiItemSlot : UiElement
|
|||
|
||||
/// <summary>True when this cell is the OPEN container (its contents fill the grid). Draws the
|
||||
/// open-container triangle. Port of UIElement_ItemList::UpdateOpenContainerIndicator
|
||||
/// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId.</summary>
|
||||
/// (0x004e3070) → SetOpenContainerState (0x004e1200): shown when item.itemID == openContainerId.</summary>
|
||||
public bool IsOpenContainer { get; set; }
|
||||
/// <summary>Open-container triangle sprite (element 0x10000450 on the container prototype
|
||||
/// 0x1000033F). Configurable; guard id != 0 before resolving.</summary>
|
||||
public uint OpenContainerSprite { get; set; } = 0x06005D9Cu;
|
||||
|
||||
/// <summary>True when this cell is the SELECTED item. Draws the green/yellow selection square.
|
||||
/// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState
|
||||
/// Port of UIElement_ItemList::ItemList_SetSelectedItem (0x004e2fe0) → SetSelectedState
|
||||
/// (0x004e1240): shown when item.itemID == selectedItemId. Uniform across item + container cells.</summary>
|
||||
public bool Selected { get; set; }
|
||||
/// <summary>Selected-item square sprite (element 0x10000342 on the 32×32 item prototype
|
||||
/// <summary>Selected-item square sprite (element 0x10000342 on the 32×32 item prototype
|
||||
/// 0x10000341; pixel-confirmed green/yellow frame). Drawn as a procedural overlay so it renders
|
||||
/// on the 36×36 container cell too (whose prototype lacks the square child).</summary>
|
||||
/// on the 36×36 container cell too (whose prototype lacks the square child).</summary>
|
||||
public uint SelectedSprite { get; set; } = 0x06004D21u;
|
||||
|
||||
/// <summary>Container fullness [0..1], or -1 = hidden (the cell is not a container, or its
|
||||
/// itemsCapacity is unknown/0). Port of UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a
|
||||
/// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when
|
||||
/// per-cell vertical UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only when
|
||||
/// isContainer && itemsCapacity > 0, fill = numContainedItems / itemsCapacity (meter attr 0x69).</summary>
|
||||
public float CapacityFill { get; set; } = -1f;
|
||||
/// <summary>Capacity-bar track sprite (meter 0x10000347 DirectState).</summary>
|
||||
|
|
@ -79,10 +87,8 @@ public class UiItemSlot : UiElement
|
|||
/// <summary>Capacity-bar fill sprite (meter 0x10000347 front child 0x00000002 DirectState).</summary>
|
||||
public uint CapacityFrontSprite { get; set; } = 0x06004D23u;
|
||||
|
||||
/// <summary>Accept/reject overlay state while a drag hovers this cell.</summary>
|
||||
public enum DragAcceptState { None, Accept, Reject }
|
||||
private DragAcceptState _dragAccept = DragAcceptState.None;
|
||||
/// <summary>Current overlay state — internal so unit tests can assert it (InternalsVisibleTo).</summary>
|
||||
/// <summary>Current overlay state — internal so unit tests can assert it (InternalsVisibleTo).</summary>
|
||||
internal DragAcceptState DragAcceptVisual => _dragAccept;
|
||||
|
||||
/// <summary>Empty-slot sprite. Default = the generic toolbar empty-slot border
|
||||
|
|
@ -102,7 +108,7 @@ public class UiItemSlot : UiElement
|
|||
private bool _primaryPressConsumed;
|
||||
|
||||
/// <summary>RenderSurface id -> (GL texture, w, h). Set by the factory/controller.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// DAT-authored 10%..100% radial cooldown overlays from element ids
|
||||
|
|
@ -118,21 +124,21 @@ public class UiItemSlot : UiElement
|
|||
|
||||
public void SetItem(
|
||||
uint itemId,
|
||||
uint iconTexture,
|
||||
GpuTextureSlot iconTexture,
|
||||
ShortcutEntry? shortcut = null,
|
||||
uint dragIconTexture = 0)
|
||||
GpuTextureSlot? dragIconTexture = null)
|
||||
{
|
||||
ItemId = itemId;
|
||||
IconTexture = iconTexture;
|
||||
DragIconTexture = dragIconTexture;
|
||||
DragIconTexture = dragIconTexture ?? GpuTextureSlot.Unassigned;
|
||||
Shortcut = shortcut;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
ItemId = 0;
|
||||
IconTexture = 0;
|
||||
DragIconTexture = 0;
|
||||
IconTexture = GpuTextureSlot.Unassigned;
|
||||
DragIconTexture = GpuTextureSlot.Unassigned;
|
||||
Shortcut = null;
|
||||
_waiting = false;
|
||||
_primaryPressConsumed = false;
|
||||
|
|
@ -145,13 +151,13 @@ public class UiItemSlot : UiElement
|
|||
: null;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override (uint tex, int w, int h)? GetDragGhost()
|
||||
public override (GpuTextureSlot tex, int w, int h)? GetDragGhost()
|
||||
{
|
||||
if (ItemId == 0) return null;
|
||||
// RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its
|
||||
// containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16).
|
||||
if (DragIconTexture != 0) return (DragIconTexture, 32, 32);
|
||||
return IconTexture != 0 ? (IconTexture, (int)Width, (int)Height) : null;
|
||||
// RenderIcons creates m_pDragIcon on a fixed 0x20 × 0x20 surface, even when its
|
||||
// containing bag cell is 36 × 36. The cursor hotspot is correspondingly (16,16).
|
||||
if (DragIconTexture.IsAssigned) return (DragIconTexture, 32, 32);
|
||||
return IconTexture.IsAssigned ? (IconTexture, (int)Width, (int)Height) : null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
|
@ -171,7 +177,7 @@ public class UiItemSlot : UiElement
|
|||
internal void SetWaitingState(bool waiting)
|
||||
=> _waiting = waiting && ItemId != 0;
|
||||
|
||||
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
|
||||
/// <summary>An OCCUPIED slot is a drag source — a press-and-move picks up the item
|
||||
/// rather than moving the toolbar window. An EMPTY slot is NOT a drag source, so a
|
||||
/// press-and-move there falls through to the IA-12 whole-window-drag, keeping the bar
|
||||
/// movable by its empty cells / chrome. Drives <see cref="UiRoot"/>'s mousedown
|
||||
|
|
@ -187,23 +193,23 @@ public class UiItemSlot : UiElement
|
|||
return null;
|
||||
}
|
||||
|
||||
// ── Shortcut number (slot label) ─────────────────────────────────────────
|
||||
// ── Shortcut number (slot label) ─────────────────────────────────────────
|
||||
// Port of UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||
// Retail draws the digit on the cell's ShortcutNum sub-element, picking the
|
||||
// digit image from a DID-array property: 0x10000042 (regular) / 0x10000043 (ghosted),
|
||||
// indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface
|
||||
// indexed by slot position. Each digit is a 32×32 PFID_A8R8G8B8 RenderSurface
|
||||
// with the digit baked into the top-left corner (rest alpha=0), drawn Alphablend.
|
||||
|
||||
/// <summary>Slot position in the shortcut bar (0-indexed). -1 = no number (retail
|
||||
/// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1.</summary>
|
||||
/// SetVisible(0) when edi < 0). Top row: 0..8 → digits 1..9. Bottom row: -1.</summary>
|
||||
public int ShortcutNum { get; private set; } = -1;
|
||||
|
||||
/// <summary>True when retail marks the physical-item shortcut as ghosted.</summary>
|
||||
public bool ShortcutGhosted { get; private set; }
|
||||
|
||||
/// <summary>Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
||||
/// <summary>Regular digit DID array. Index i → digit (i+1) sprite RenderSurface id.
|
||||
/// Injected by the controller after reading LayoutDesc 0x21000037.
|
||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
||||
/// Retail ref: UIElement_UIItem::SetShortcutNum (decomp 229481) — occupied slot picks
|
||||
/// property 0x10000042 when the shortcut is not ghosted.</summary>
|
||||
public uint[]? RegularDigits { get; set; }
|
||||
|
||||
|
|
@ -213,7 +219,7 @@ public class UiItemSlot : UiElement
|
|||
|
||||
/// <summary>Empty-slot digit DID array (property 0x1000005e, stance-independent).
|
||||
/// Used when the slot is EMPTY (ItemId == 0). Retail ref: UIElement_UIItem::SetShortcutNum
|
||||
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).</summary>
|
||||
/// (decomp 229481) — else branch when m_elem_Icon->m_state == 0x1000001c (empty).</summary>
|
||||
public uint[]? EmptyDigits { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -241,8 +247,8 @@ public class UiItemSlot : UiElement
|
|||
/// <summary>
|
||||
/// Returns the digit DID array that OnDraw will use, following the retail occupancy
|
||||
/// branch in UIElement_UIItem::SetShortcutNum (decomp 229481):
|
||||
/// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42)
|
||||
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
|
||||
/// occupied (ItemId != 0) → ShortcutGhosted ? GhostedDigits : RegularDigits (0x10000043/42)
|
||||
/// empty (ItemId == 0) → EmptyDigits (0x1000005e, stance-independent)
|
||||
/// Exposed as an internal method so unit tests can assert array selection without
|
||||
/// needing a real render context.
|
||||
/// </summary>
|
||||
|
|
@ -253,7 +259,7 @@ public class UiItemSlot : UiElement
|
|||
: EmptyDigits;
|
||||
}
|
||||
|
||||
// ── Events / draw ─────────────────────────────────────────────────────────
|
||||
// ── Events / draw ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Invoked by <see cref="OnEvent"/> when a completed left click lands
|
||||
/// on a bound slot. Selection already occurred on MouseDown through the owning
|
||||
|
|
@ -269,7 +275,7 @@ public class UiItemSlot : UiElement
|
|||
{
|
||||
switch (e.Type)
|
||||
{
|
||||
// Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a
|
||||
// Use fires on CLICK (mouse-up over the same cell), not MouseDown — so a
|
||||
// drag (press + >3px move) does NOT also use the item. UiRoot suppresses the
|
||||
// post-drag Click (UiRoot.cs FinishDrag returns before the Click emit).
|
||||
case UiEventType.MouseDown:
|
||||
|
|
@ -296,13 +302,13 @@ public class UiItemSlot : UiElement
|
|||
return true;
|
||||
|
||||
case UiEventType.DragBegin:
|
||||
// Notify the source list's handler so it can lift (remove + wire) — retail
|
||||
// RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first.
|
||||
// Notify the source list's handler so it can lift (remove + wire) — retail
|
||||
// RecvNotice_ItemListBeginDrag → RemoveShortcut. UiRoot snapshotted the ghost first.
|
||||
if (FindList() is { DragHandler: { } lh } liftList && e.Payload is ItemDragPayload lp)
|
||||
lh.OnDragLift(liftList, this, lp);
|
||||
return true;
|
||||
|
||||
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
||||
case UiEventType.DragEnter: // pointer entered me mid-drag → ask the list's handler
|
||||
_dragAccept = FindList() is { DragHandler: { } h } list
|
||||
&& e.Payload is ItemDragPayload p
|
||||
? h.OnDragOver(list, this, p) switch
|
||||
|
|
@ -314,7 +320,7 @@ public class UiItemSlot : UiElement
|
|||
: DragAcceptState.Reject;
|
||||
return true;
|
||||
|
||||
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
|
||||
case UiEventType.DragOver: // UiRoot fires this on LEAVE → neutral
|
||||
_dragAccept = DragAcceptState.None;
|
||||
return true;
|
||||
|
||||
|
|
@ -331,43 +337,43 @@ public class UiItemSlot : UiElement
|
|||
{
|
||||
// Draw the icon (filled slot) or the empty-slot border. Both paths fall through
|
||||
// to the digit draw below; the slot label always shows on top-row slots.
|
||||
if (ItemId != 0 && IconTexture != 0)
|
||||
if (ItemId != 0 && IconTexture.IsAssigned)
|
||||
{
|
||||
ctx.DrawSprite(IconTexture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
else if (SpriteResolve is not null && EmptySprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(EmptySprite);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Digit overlay: UIElement_UIItem::SetShortcutNum (acclient_2013_pseudo_c.txt:229465).
|
||||
// Occupancy branch (decomp 229481):
|
||||
// occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43
|
||||
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
|
||||
// occupied (ItemId != 0) → regular/ghosted digit set 0x10000042/43
|
||||
// empty (ItemId == 0) → background digit set 0x1000005e, stance-independent
|
||||
// Each digit image is corner-baked (glyph in top-left, rest alpha=0); drawn
|
||||
// full-cell Alphablend so the transparent region is invisible.
|
||||
DrawShortcutOverlay(ctx);
|
||||
|
||||
// Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical
|
||||
// UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells
|
||||
// Container capacity bar — UIElement_UIItem::UpdateCapacityDisplay (0x004e16e0): a vertical
|
||||
// UIElement_Meter (element 0x10000347, 5×30 at x=26,y=1) shown only for container cells
|
||||
// (CapacityFill >= 0). Track drawn full; fill clipped to the fraction from the BOTTOM (the
|
||||
// "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first.
|
||||
// "how full" direction). Procedural — UiItemSlot is a behavioral leaf. Guard id != 0 first.
|
||||
if (CapacityFill >= 0f && SpriteResolve is not null)
|
||||
{
|
||||
const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1)
|
||||
const float by = 1f, bw = 5f, bh = 30f; // element 0x10000347 size (dat 5×30 at y=1)
|
||||
float bx = Width - bw; // flush to the cell's right edge (visual gate: the dat X=26 sat ~5px off the edge)
|
||||
if (CapacityBackSprite != 0)
|
||||
{
|
||||
var (bt, _, _) = SpriteResolve(CapacityBackSprite);
|
||||
if (bt != 0) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
if (bt.IsAssigned) ctx.DrawSprite(bt, bx, by, bw, bh, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
float f = Math.Clamp(CapacityFill, 0f, 1f);
|
||||
if (f > 0f && CapacityFrontSprite != 0)
|
||||
{
|
||||
var (ft, _, _) = SpriteResolve(CapacityFrontSprite);
|
||||
if (ft != 0)
|
||||
if (ft.IsAssigned)
|
||||
{
|
||||
// Bottom-up fill: draw the bottom f-fraction of the bar, sampling the matching
|
||||
// bottom slice of the front sprite (UV v from 1-f to 1).
|
||||
|
|
@ -377,7 +383,7 @@ public class UiItemSlot : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||||
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
|
||||
// sets it for physical inventory/equipment cells while leaving their icon in place.
|
||||
// Draw it before the persistent selected/open indicators: retail keeps selection visible
|
||||
|
|
@ -385,7 +391,7 @@ public class UiItemSlot : UiElement
|
|||
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(WaitingSprite);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
@ -396,18 +402,18 @@ public class UiItemSlot : UiElement
|
|||
if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(OpenContainerSprite);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
if (Selected && SpriteResolve is not null && SelectedSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(SelectedSprite);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
|
||||
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
|
||||
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
|
||||
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
|
||||
if (_dragAccept != DragAcceptState.None && SpriteResolve is not null)
|
||||
{
|
||||
|
|
@ -415,7 +421,7 @@ public class UiItemSlot : UiElement
|
|||
if (id != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(id);
|
||||
if (tex != 0)
|
||||
if (tex.IsAssigned)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
|
@ -428,7 +434,7 @@ public class UiItemSlot : UiElement
|
|||
if (cooldownSprite != 0u && SpriteResolve is not null)
|
||||
{
|
||||
var (texture, _, _) = SpriteResolve(cooldownSprite);
|
||||
if (texture != 0u)
|
||||
if (texture.IsAssigned)
|
||||
ctx.DrawSprite(
|
||||
texture,
|
||||
0f,
|
||||
|
|
@ -460,7 +466,7 @@ public class UiItemSlot : UiElement
|
|||
return;
|
||||
|
||||
var (texture, _, _) = SpriteResolve(did);
|
||||
if (texture != 0)
|
||||
if (texture.IsAssigned)
|
||||
ctx.DrawSprite(texture, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>An inclusive integer pixel box, matching retail <c>Box2D</c>.</summary>
|
||||
public readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1)
|
||||
internal readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1)
|
||||
{
|
||||
public int Width => X1 - X0 + 1;
|
||||
public int Height => Y1 - Y0 + 1;
|
||||
|
|
@ -16,7 +16,7 @@ public readonly record struct UiPixelRect(int X0, int Y0, int X1, int Y1)
|
|||
/// retain all four independent modes; programmatic widgets continue to use
|
||||
/// <see cref="AnchorEdges"/>.
|
||||
/// </summary>
|
||||
public sealed class UiLayoutPolicy
|
||||
internal sealed class UiLayoutPolicy
|
||||
{
|
||||
public uint LeftMode { get; }
|
||||
public uint TopMode { get; }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
|
||||
|
|
@ -13,10 +13,10 @@ namespace AcDream.App.UI;
|
|||
/// knowledge are populated by the controller, not baked into this widget. Built
|
||||
/// by <see cref="AcDream.App.UI.Layout.DatWidgetFactory"/> for Type-6 elements.
|
||||
/// </summary>
|
||||
public sealed class UiMenu : UiElement
|
||||
internal sealed class UiMenu : UiElement
|
||||
{
|
||||
/// <summary>One menu row: its label + an opaque payload the controller maps back.</summary>
|
||||
public readonly record struct MenuItem(string Label, object? Payload);
|
||||
internal readonly record struct MenuItem(string Label, object? Payload);
|
||||
|
||||
/// <summary>The rows, populated by the controller. Laid out column-major:
|
||||
/// rows 0..RowsPerColumn-1 in column 0, then the next group in column 1, etc.</summary>
|
||||
|
|
@ -28,10 +28,10 @@ public sealed class UiMenu : UiElement
|
|||
/// <summary>Fired with the picked item's payload when a row is chosen.</summary>
|
||||
public Action<object?>? OnSelect { get; set; }
|
||||
|
||||
/// <summary>Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled.</summary>
|
||||
/// <summary>Per-payload enabled gate (disabled rows render greyed + are inert). Null ⇒ all enabled.</summary>
|
||||
public Func<object?, bool>? EnabledProvider { get; set; }
|
||||
|
||||
/// <summary>Button-face caption (the active target). Null ⇒ blank face.</summary>
|
||||
/// <summary>Button-face caption (the active target). Null ⇒ blank face.</summary>
|
||||
public Func<string>? ButtonLabelProvider { get; set; }
|
||||
|
||||
public int RowsPerColumn { get; set; } = 7; // items per column (dat item template)
|
||||
|
|
@ -43,29 +43,29 @@ public sealed class UiMenu : UiElement
|
|||
// square; the label starts just past it (box width + small gap) so text aligns with
|
||||
// the box instead of overlapping it.
|
||||
private const float TextIndent = 19f;
|
||||
// The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its
|
||||
// left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't
|
||||
// The button face sprite (0x06004D65/66) bakes a status LED (red→green) into its
|
||||
// left socket (~x4–20 of the 46px button); the caption starts past it so it doesn't
|
||||
// render over the LED.
|
||||
private const float ButtonTextIndent = 20f;
|
||||
|
||||
public UiDatFont? DatFont { get; set; }
|
||||
public AcDream.App.Rendering.BitmapFont? Font { get; set; }
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
// Button face sprites (dat menu element 0x10000014).
|
||||
public uint NormalSprite { get; set; }
|
||||
public uint PressedSprite { get; set; }
|
||||
// Popup chrome sprites (dat menu popup template, layout 0x21000006).
|
||||
public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles)
|
||||
public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17)
|
||||
public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row
|
||||
public uint PopupBgSprite { get; set; } // 0x0600124C — panel fill (191×2 tiles)
|
||||
public uint ItemNormalSprite { get; set; } // 0x0600124E — a row background (191×17)
|
||||
public uint ItemHighlightSprite { get; set; } // 0x0600124D — the active channel's row
|
||||
|
||||
public Vector4 TextColor { get; set; } = new(1f, 0.92f, 0.72f, 1f);
|
||||
/// <summary>Available item text — retail white #FFFFFF (gmMainChatUI talk-focus
|
||||
/// <summary>Available item text — retail white #FFFFFF (gmMainChatUI talk-focus
|
||||
/// enabled state). Confirmed via decomp: enabled items render white.</summary>
|
||||
public Vector4 TextColorAvailable { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
/// <summary>Disabled/unavailable item text — retail GREYS these (UIElement state 0xd
|
||||
/// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that
|
||||
/// <summary>Disabled/unavailable item text — retail GREYS these (UIElement state 0xd
|
||||
/// disabled StateDesc colour). NOT the salmon colorPink (0x81c528) we had before — that
|
||||
/// belongs to the chat-MESSAGE palette and was misapplied. Exact float lives in the dat
|
||||
/// StateDesc (not a code symbol); ~0.5 neutral grey here pending a live cdb dump.</summary>
|
||||
public Vector4 TextColorGhosted { get; set; } = new(0.5f, 0.5f, 0.5f, 1f);
|
||||
|
|
@ -92,7 +92,7 @@ public sealed class UiMenu : UiElement
|
|||
if (resolve is not null)
|
||||
{
|
||||
var (tex, tw, _) = resolve(_open ? PressedSprite : NormalSprite);
|
||||
if (tex != 0 && tw > 0) DrawButtonFace(ctx, tex, tw);
|
||||
if (tex.IsAssigned && tw > 0) DrawButtonFace(ctx, tex, tw);
|
||||
}
|
||||
DrawLabel(ctx, ButtonLabelProvider?.Invoke() ?? "", ButtonTextIndent, (Height - LineH()) * 0.5f, TextColor);
|
||||
}
|
||||
|
|
@ -102,7 +102,7 @@ public sealed class UiMenu : UiElement
|
|||
// point. Slicing keeps the LED + arrow undistorted when the button widens to its label.
|
||||
private const float FaceCapL = 20f, FaceCapR = 12f;
|
||||
|
||||
private void DrawButtonFace(UiRenderContext ctx, uint tex, float tw)
|
||||
private void DrawButtonFace(UiRenderContext ctx, GpuTextureSlot tex, float tw)
|
||||
{
|
||||
float uL = FaceCapL / tw, uR = (tw - FaceCapR) / tw;
|
||||
float midDest = Width - FaceCapL - FaceCapR;
|
||||
|
|
@ -112,7 +112,7 @@ public sealed class UiMenu : UiElement
|
|||
ctx.DrawSprite(tex, Width - FaceCapR, 0f, FaceCapR, Height, uR, 0f, 1f, 1f, Vector4.One); // arrow cap
|
||||
}
|
||||
|
||||
/// <summary>The button width that fits "LED cap + channel label + arrow cap" — retail
|
||||
/// <summary>The button width that fits "LED cap + channel label + arrow cap" — retail
|
||||
/// sizes the talk-focus button to its selected label. The controller widens the button
|
||||
/// to this and reflows the input field to start after it.</summary>
|
||||
public float NaturalButtonWidth()
|
||||
|
|
@ -123,7 +123,7 @@ public sealed class UiMenu : UiElement
|
|||
}
|
||||
|
||||
/// <summary>The open popup draws in the OVERLAY pass so it sits on top of the whole
|
||||
/// UI — otherwise the translucent chat panel (drawn after this element in the main
|
||||
/// UI — otherwise the translucent chat panel (drawn after this element in the main
|
||||
/// pass) greys out the part of the popup that overlaps it.</summary>
|
||||
protected override void OnDrawOverlay(UiRenderContext ctx)
|
||||
{
|
||||
|
|
@ -133,7 +133,7 @@ public sealed class UiMenu : UiElement
|
|||
// Column-major popup opening UPWARD from the button, wrapped in the universal
|
||||
// 8-piece window bevel (retail UIElement_Menu::MakePopup spawns the popup as a
|
||||
// bevelled floating window). Force OPAQUE (a menu reads solid even though the
|
||||
// chat window is translucent). Draw bevel → panel fill → row sprites → labels,
|
||||
// chat window is translucent). Draw bevel → panel fill → row sprites → labels,
|
||||
// all through the sprite bucket in submission order so labels land on top.
|
||||
ctx.PushAlphaAbsolute(1f);
|
||||
try
|
||||
|
|
@ -170,7 +170,7 @@ public sealed class UiMenu : UiElement
|
|||
/// <paramref name="w"/>,<paramref name="h"/>). Reuses the same geometry +
|
||||
/// <see cref="RetailChromeSprites"/> ids as <see cref="UiNineSlicePanel"/>; no resize
|
||||
/// grips (a menu popup is not resizable).</summary>
|
||||
private void DrawBevel(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
private void DrawBevel(UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
float x, float y, float w, float h)
|
||||
{
|
||||
var r = UiNineSlicePanel.ComputeFrameRects(w, h, Border);
|
||||
|
|
@ -188,13 +188,13 @@ public sealed class UiMenu : UiElement
|
|||
|
||||
private float LineH() => DatFont?.LineHeight ?? Font?.LineHeight ?? 14f;
|
||||
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (id == 0) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
// Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1).
|
||||
if (!tex.IsAssigned || tw == 0 || th == 0) return;
|
||||
// Tile at native size (the panel fill is 191×2; rows are 191×17 = 1:1).
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
@ -230,7 +230,7 @@ public sealed class UiMenu : UiElement
|
|||
// The widget REPORTS the pick; the controller owns Selected (it sets
|
||||
// Selected only for payloads it acts on). This mirrors retail
|
||||
// UIElement_Menu::NewSelection delegating to the owner rather than
|
||||
// self-selecting — so a deferred/no-op item (e.g. the Squelch /
|
||||
// self-selecting — so a deferred/no-op item (e.g. the Squelch /
|
||||
// Tell-to-Selected specials, null payload) leaves the current
|
||||
// selection + highlight unchanged when the controller ignores it.
|
||||
OnSelect?.Invoke(Items[idx].Payload);
|
||||
|
|
|
|||
|
|
@ -1,20 +1,20 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// A horizontal vital bar (retail HP/Stamina/Mana style): a background rect, a
|
||||
/// partial-width solid fill, and an optional centered "current/max" numeric
|
||||
/// overlay. <see cref="Fill"/> returns 0..1 (null = no data → empty bar);
|
||||
/// overlay. <see cref="Fill"/> returns 0..1 (null = no data → empty bar);
|
||||
/// <see cref="Label"/> returns the overlay text (null = no number).
|
||||
///
|
||||
/// <para>
|
||||
/// Solid-color fill + debug font for Spec 1. The retail gradient bar sprite
|
||||
/// (glassy center highlight) and the retail dat font are a later polish pass —
|
||||
/// (glassy center highlight) and the retail dat font are a later polish pass —
|
||||
/// retail's vitals are bars exactly like this, just sprited.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiMeter : UiElement, IUiDatStateful
|
||||
internal sealed class UiMeter : UiElement, IUiDatStateful
|
||||
{
|
||||
private readonly Dictionary<uint, uint> _stateFillSprites = new();
|
||||
|
||||
|
|
@ -37,13 +37,13 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
|
||||
/// <summary>Resolver from a RenderSurface DataId to (GL handle, w, h). When set
|
||||
/// with the 9-slice ids below, the bar draws the retail sprites instead of solid color.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
// Retail vital bars are a horizontal 3-slice: a fixed-width bevelled left-cap,
|
||||
// a TILED gradient middle (the "fill-tile" repeats at native width — it does not
|
||||
// a TILED gradient middle (the "fill-tile" repeats at native width — it does not
|
||||
// stretch), and a fixed-width right-cap. The "back" slice is the empty track
|
||||
// (drawn full width); the "front" slice is the coloured fill (drawn full-geometry
|
||||
// but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's
|
||||
// but CLIPPED to the fill fraction — its own right-cap shows at 100%, the back's
|
||||
// shows through when partial). Ids come from the stacked vitals LayoutDesc
|
||||
// (0x2100006C) via the dump-vitals-layout CLI; 0 = none.
|
||||
/// <summary>Empty-track left-cap RenderSurface id.</summary>
|
||||
|
|
@ -110,8 +110,8 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
}
|
||||
|
||||
/// <summary>Clamp <paramref name="pct"/> to [0,1] and return the vertical fill rect
|
||||
/// (local px). <paramref name="fromBottom"/> true → the fill occupies the bottom
|
||||
/// <c>h*pct</c> px (retail direction 4); false → the top (direction 2).</summary>
|
||||
/// (local px). <paramref name="fromBottom"/> true → the fill occupies the bottom
|
||||
/// <c>h*pct</c> px (retail direction 4); false → the top (direction 2).</summary>
|
||||
public static (float x, float y, float w, float h) ComputeVFillRect(
|
||||
float pct, float w, float h, bool fromBottom)
|
||||
{
|
||||
|
|
@ -142,7 +142,7 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
// empty track, drawn full width; the FRONT 3-slice is the coloured fill,
|
||||
// drawn at FULL width too but horizontally CLIPPED to the fill fraction.
|
||||
// The front carries its own right-cap (shown at 100%); clipping below 100%
|
||||
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
||||
// removes it and reveals the back track's right-cap — retail's scissor-fill.
|
||||
DrawHBar(ctx, resolve, BackLeft, BackTile, BackRight, Width);
|
||||
if (pct is not null && p > 0f)
|
||||
DrawHBar(ctx, resolve, FrontLeft, FrontTile, FrontRight, Width * p);
|
||||
|
|
@ -191,7 +191,7 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
/// A 0 id skips that slice.
|
||||
/// </summary>
|
||||
private void DrawHBar(
|
||||
UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint leftId, uint midId, uint rightId, float clipW)
|
||||
{
|
||||
if (clipW <= 0f) return;
|
||||
|
|
@ -201,18 +201,18 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
// testing `tex != 0` would draw a 1px magenta cap. The single-image meter (toolbar
|
||||
// selected-object bar) has no left/right caps (ids 0); the 3-slice vitals meter sets
|
||||
// all six ids. Guard on the id, not the resolved handle.
|
||||
var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (0u, 0, 0);
|
||||
var (mt, mw, _) = midId != 0 ? resolve(midId) : (0u, 0, 0);
|
||||
var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (0u, 0, 0);
|
||||
var (lt, lw, _) = leftId != 0 ? resolve(leftId) : (GpuTextureSlot.Unassigned, 0, 0);
|
||||
var (mt, mw, _) = midId != 0 ? resolve(midId) : (GpuTextureSlot.Unassigned, 0, 0);
|
||||
var (rt, rw, _) = rightId != 0 ? resolve(rightId) : (GpuTextureSlot.Unassigned, 0, 0);
|
||||
|
||||
float capL = lt != 0 ? MathF.Min(lw, w) : 0f;
|
||||
float capR = rt != 0 ? MathF.Min(rw, w - capL) : 0f;
|
||||
float capL = lt.IsAssigned ? MathF.Min(lw, w) : 0f;
|
||||
float capR = rt.IsAssigned ? MathF.Min(rw, w - capL) : 0f;
|
||||
float midW = w - capL - capR;
|
||||
|
||||
// Each slice's texture repeats every NATIVE-width px (UV-repeat; the UI
|
||||
// texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their
|
||||
// own native width → a single 1:1 copy. The wide middle spans many native
|
||||
// widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather
|
||||
// texture is GL_REPEAT-wrapped — TextureCache.UploadRgba8). Caps span their
|
||||
// own native width → a single 1:1 copy. The wide middle spans many native
|
||||
// widths → it TILES, matching retail's "fill-tile" + ImgTex::TileCSI rather
|
||||
// than stretching one copy. (Same UV-repeat the chrome border already uses.)
|
||||
DrawPiece(ctx, lt, 0f, capL, lw, h, clipW);
|
||||
DrawPiece(ctx, mt, capL, midW, mw, h, clipW);
|
||||
|
|
@ -224,12 +224,12 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
/// bottom (<paramref name="fromBottom"/>) or top, UV-cropped to that fraction of the
|
||||
/// sprite so the fill reveals the matching part of the art (retail
|
||||
/// UIElement_Meter::DrawChildren Box2D clip, direction 2/4). A 0 id is a no-op.</summary>
|
||||
private void DrawVBar(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
private void DrawVBar(UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint tileId, float visibleH, bool fromBottom, bool isFill)
|
||||
{
|
||||
if (tileId == 0 || visibleH <= 0f) return;
|
||||
var (tex, _, _) = resolve(tileId);
|
||||
if (tex == 0) return;
|
||||
if (!tex.IsAssigned) return;
|
||||
float w = Width, h = Height;
|
||||
if (visibleH > h) visibleH = h;
|
||||
float frac = h > 0f ? visibleH / h : 0f;
|
||||
|
|
@ -243,17 +243,17 @@ public sealed class UiMeter : UiElement, IUiDatStateful
|
|||
|
||||
/// <summary>Draw a slice over local [<paramref name="pieceX"/>,
|
||||
/// pieceX+<paramref name="pieceW"/>], with the texture repeating every
|
||||
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
|
||||
/// <paramref name="nativeW"/> px (UV-repeat — the UI texture is GL_REPEAT-wrapped).
|
||||
/// Clipped so nothing past <paramref name="clipW"/> shows. For a cap (span == native)
|
||||
/// this is one 1:1 copy; for the wide middle it tiles; a partial last copy is
|
||||
/// UV-cropped.</summary>
|
||||
private static void DrawPiece(
|
||||
UiRenderContext ctx, uint tex, float pieceX, float pieceW, float nativeW, float h, float clipW)
|
||||
UiRenderContext ctx, GpuTextureSlot tex, float pieceX, float pieceW, float nativeW, float h, float clipW)
|
||||
{
|
||||
if (tex == 0 || pieceW <= 0f || nativeW <= 0f) return;
|
||||
if (!tex.IsAssigned || pieceW <= 0f || nativeW <= 0f) return;
|
||||
float visibleW = MathF.Min(pieceW, clipW - pieceX);
|
||||
if (visibleW <= 0f) return;
|
||||
float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy
|
||||
float u1 = visibleW / nativeW; // >1 ⇒ texture repeats (tiles); ≤1 ⇒ a partial copy
|
||||
ctx.DrawSprite(tex, pieceX, 0f, visibleW, h, 0f, 0f, u1, 1f, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -10,17 +10,17 @@ namespace AcDream.App.UI;
|
|||
/// the widget is testable without GL. In production:
|
||||
/// <c>id => { var t = cache.GetOrUploadRenderSurface(id, out var w, out var h); return (t, w, h); }</c>.
|
||||
/// </summary>
|
||||
public class UiNineSlicePanel : UiPanel
|
||||
internal class UiNineSlicePanel : UiPanel
|
||||
{
|
||||
/// <summary>A placed chrome piece: destination rect in local pixel space.</summary>
|
||||
public readonly record struct Rect(float X, float Y, float W, float H);
|
||||
internal readonly record struct Rect(float X, float Y, float W, float H);
|
||||
|
||||
/// <summary>The nine destination rects for an 8-piece border + center.</summary>
|
||||
public readonly record struct FrameRects(
|
||||
internal readonly record struct FrameRects(
|
||||
Rect Center, Rect Top, Rect Bottom, Rect Left, Rect Right,
|
||||
Rect TL, Rect TR, Rect BL, Rect BR);
|
||||
|
||||
private readonly System.Func<uint, (uint tex, int w, int h)> _resolve;
|
||||
private readonly System.Func<uint, (GpuTextureSlot tex, int w, int h)> _resolve;
|
||||
|
||||
/// <summary>
|
||||
/// Whether this wrapper paints the shared center surface. Some retail child
|
||||
|
|
@ -29,7 +29,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
/// </summary>
|
||||
public bool DrawCenterFill { get; set; } = true;
|
||||
|
||||
public UiNineSlicePanel(System.Func<uint, (uint, int, int)> resolve)
|
||||
public UiNineSlicePanel(System.Func<uint, (GpuTextureSlot, int, int)> resolve)
|
||||
{
|
||||
_resolve = resolve;
|
||||
BackgroundColor = Vector4.Zero; // suppress the base flat-rect fill
|
||||
|
|
@ -46,7 +46,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
/// <summary>
|
||||
/// Destination rects (local px) for a frame of (<paramref name="w"/>,
|
||||
/// <paramref name="h"/>) with border thickness <paramref name="b"/>:
|
||||
/// b×b corners, top/bottom edges spanning the interior width at height b,
|
||||
/// b×b corners, top/bottom edges spanning the interior width at height b,
|
||||
/// left/right edges spanning the interior height at width b, center fills
|
||||
/// the interior.
|
||||
/// </summary>
|
||||
|
|
@ -68,7 +68,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
// Center fill is the window BACKGROUND — it must sit UNDER the content, so it
|
||||
// Center fill is the window BACKGROUND — it must sit UNDER the content, so it
|
||||
// draws here (before children). The bevel border + grip is the OUTERMOST layer
|
||||
// and draws in OnDrawAfterChildren (over the content's edges) so content can
|
||||
// never poke through the frame (e.g. the toolbar's 2px bottom-right cap overhang).
|
||||
|
|
@ -91,7 +91,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
DrawStretched(ctx, RetailChromeSprites.CornerBR, r.BR);
|
||||
|
||||
// Resize-grip overlay (gold ridged edges + square corner studs) on top of the
|
||||
// bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642).
|
||||
// bevel — the second border layer the vitals LayoutDesc carries (0x1000063B–0x10000642).
|
||||
DrawTiled(ctx, RetailChromeSprites.GripTop, r.Top);
|
||||
DrawTiled(ctx, RetailChromeSprites.GripBottom, r.Bottom);
|
||||
DrawTiled(ctx, RetailChromeSprites.GripLeft, r.Left);
|
||||
|
|
@ -106,7 +106,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
{
|
||||
if (d.W <= 0 || d.H <= 0) return;
|
||||
var (tex, tw, th) = _resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
if (!tex.IsAssigned || tw == 0 || th == 0) return;
|
||||
ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, d.W / tw, d.H / th, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
@ -114,7 +114,7 @@ public class UiNineSlicePanel : UiPanel
|
|||
{
|
||||
if (d.W <= 0 || d.H <= 0) return;
|
||||
var (tex, _, _) = _resolve(id);
|
||||
if (tex == 0) return;
|
||||
if (!tex.IsAssigned) return;
|
||||
ctx.DrawSprite(tex, d.X, d.Y, d.W, d.H, 0, 0, 1, 1, Vector4.One);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -14,7 +14,7 @@ namespace AcDream.App.UI;
|
|||
/// <c>AcFont</c>/<c>UiSpriteBatch</c> consumes those directly, we draw a
|
||||
/// simple translucent rectangle so panels are visible during development.
|
||||
/// </summary>
|
||||
public class UiPanel : UiElement
|
||||
internal class UiPanel : UiElement
|
||||
{
|
||||
/// <summary>Background fill color. Set <see cref="Vector4.Zero"/> to skip.</summary>
|
||||
public Vector4 BackgroundColor { get; set; } = new(0f, 0f, 0f, 0.55f);
|
||||
|
|
@ -32,14 +32,14 @@ public class UiPanel : UiElement
|
|||
|
||||
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
|
||||
/// Required when <see cref="BackgroundSprite"/> is non-zero.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
protected override void OnDraw(UiRenderContext ctx)
|
||||
{
|
||||
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
|
||||
{
|
||||
var (tex, tw, th) = sr(BackgroundSprite);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
if (tex.IsAssigned && tw != 0 && th != 0)
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
else if (BackgroundColor.W > 0f)
|
||||
|
|
@ -64,7 +64,7 @@ public class UiPanel : UiElement
|
|||
/// <c>FUN_0040b8f0</c> then drawn by the widget's draw method through
|
||||
/// <c>FUN_00698330</c>.
|
||||
/// </summary>
|
||||
public class UiLabel : UiElement
|
||||
internal class UiLabel : UiElement
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
|
|
@ -81,10 +81,10 @@ public class UiLabel : UiElement
|
|||
/// a <c>StateDesc</c> per <c>UIStateId</c> (normal / hot / pressed /
|
||||
/// disabled) from the panel layout.
|
||||
/// Note: the dat-widget button (Type 1 / UIElement_Button) is <see cref="AcDream.App.UI.UiButton"/>
|
||||
/// in <c>UiButton.cs</c> — that is the production widget used by D.2b panels.
|
||||
/// in <c>UiButton.cs</c> — that is the production widget used by D.2b panels.
|
||||
/// This class is the earlier dev-scaffold button (plain rect + text; no dat sprites).
|
||||
/// </summary>
|
||||
public class UiSimpleButton : UiPanel
|
||||
internal class UiSimpleButton : UiPanel
|
||||
{
|
||||
public string Text { get; set; } = string.Empty;
|
||||
public Vector4 TextColor { get; set; } = new(1f, 1f, 1f, 1f);
|
||||
|
|
@ -120,7 +120,7 @@ public class UiSimpleButton : UiPanel
|
|||
|
||||
/// <summary>
|
||||
/// A <see cref="UiPanel"/> that fires an <see cref="OnClick"/> callback when the user
|
||||
/// left-clicks it. Used for the attribute-list rows in the Character window — each row
|
||||
/// left-clicks it. Used for the attribute-list rows in the Character window — each row
|
||||
/// is a transparent container that needs to respond to pointer hits while its children
|
||||
/// (icon, name, value) are ClickThrough decorations.
|
||||
///
|
||||
|
|
@ -136,7 +136,7 @@ public class UiSimpleButton : UiPanel
|
|||
/// with NO left/right end-caps. Bar height is <see cref="SelectionBarHeight"/> pixels
|
||||
/// (default 3px).</para>
|
||||
/// </summary>
|
||||
public class UiClickablePanel : UiPanel
|
||||
internal class UiClickablePanel : UiPanel
|
||||
{
|
||||
/// <summary>Called when the user releases the left mouse button over this panel.</summary>
|
||||
public Action? OnClick { get; set; }
|
||||
|
|
@ -144,7 +144,7 @@ public class UiClickablePanel : UiPanel
|
|||
/// <summary>When true and <see cref="UiPanel.BackgroundSprite"/> is non-zero, draws
|
||||
/// the sprite as a thin horizontal bar at the top AND bottom edges of the panel,
|
||||
/// NOT as a full-height stretched fill. Matches retail's selected-row highlight
|
||||
/// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill).
|
||||
/// (sprite 0x06001397 — 300×32 px — shown as bars, not a block fill).
|
||||
/// Default false (preserves legacy full-stretch behavior).</summary>
|
||||
public bool UseSelectionBars { get; set; }
|
||||
|
||||
|
|
@ -154,7 +154,7 @@ public class UiClickablePanel : UiPanel
|
|||
|
||||
public UiClickablePanel()
|
||||
{
|
||||
// Rows must receive pointer events — override the UiPanel default (ClickThrough=false,
|
||||
// Rows must receive pointer events — override the UiPanel default (ClickThrough=false,
|
||||
// which is the UiElement base default). Explicit for clarity.
|
||||
ClickThrough = false;
|
||||
}
|
||||
|
|
@ -180,18 +180,18 @@ public class UiClickablePanel : UiPanel
|
|||
if (UseSelectionBars && BackgroundSprite != 0 && SpriteResolve is { } sr)
|
||||
{
|
||||
// Draw the selection highlight as a thin bar at the TOP and BOTTOM of the row.
|
||||
// The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at
|
||||
// The sprite (0x06001397) is 300×32 px — we draw it as horizontal strips at
|
||||
// native height (SelectionBarHeight), stretched to full panel width (UV tile
|
||||
// horizontally). No left/right end-caps: u0=0, u1=Width/nativeW (UV repeat).
|
||||
var (tex, tw, th) = sr(BackgroundSprite);
|
||||
if (tex != 0 && tw > 0 && th > 0)
|
||||
if (tex.IsAssigned && tw > 0 && th > 0)
|
||||
{
|
||||
float barH = SelectionBarHeight;
|
||||
float uTile = tw > 0 ? Width / tw : 1f;
|
||||
// Top bar: shows the top barH px of the sprite (v = 0 → barH/th).
|
||||
// Top bar: shows the top barH px of the sprite (v = 0 → barH/th).
|
||||
float vBot = th > 0 ? barH / th : 1f;
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, barH, 0f, 0f, uTile, vBot, Vector4.One);
|
||||
// Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1).
|
||||
// Bottom bar: shows the bottom barH px of the sprite (v = 1−barH/th → 1).
|
||||
float vTop2 = th > 0 ? 1f - barH / th : 0f;
|
||||
ctx.DrawSprite(tex, 0f, Height - barH, Width, barH, 0f, vTop2, uTile, 1f, Vector4.One);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.Core.Ui;
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI;
|
|||
/// <paramref name="PixelY"/> are local pixels in the 120x120 radar disc. World-to-player
|
||||
/// projection stays in the Core retail port; this record is the backend-facing draw seam.
|
||||
/// </summary>
|
||||
public readonly record struct UiRadarBlip(
|
||||
internal readonly record struct UiRadarBlip(
|
||||
uint ObjectId,
|
||||
string Name,
|
||||
float PixelX,
|
||||
|
|
@ -25,7 +25,7 @@ public readonly record struct UiRadarBlip(
|
|||
/// CoordinatesOnRadar is disabled). <paramref name="BlankBlips"/> mirrors
|
||||
/// <c>ClientUISystem::m_bRadarBlank</c>: static chrome and the player marker remain visible.
|
||||
/// </summary>
|
||||
public sealed record UiRadarSnapshot(
|
||||
internal sealed record UiRadarSnapshot(
|
||||
float PlayerHeadingDegrees,
|
||||
IReadOnlyList<UiRadarBlip> Blips,
|
||||
string? CoordinatesText,
|
||||
|
|
@ -50,7 +50,7 @@ public sealed record UiRadarSnapshot(
|
|||
/// <item><c>gmRadarUI::UseTime</c> 0x004D98A0 (25 ms refresh cadence).</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class UiRadar : UiElement
|
||||
internal sealed class UiRadar : UiElement
|
||||
{
|
||||
public const uint RetailClassId = 0x10000010u;
|
||||
public const float RetailRefreshSeconds = RetailRadar.UpdateIntervalSeconds;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System.Numerics;
|
||||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -33,19 +33,19 @@ internal readonly record struct UiClipRect(float Left, float Top, float Right, f
|
|||
/// when iterating the UI tree. Our version is explicit so it plugs
|
||||
/// cleanly into Silk.NET.
|
||||
/// </summary>
|
||||
public sealed class UiRenderContext
|
||||
internal sealed class UiRenderContext
|
||||
{
|
||||
public TextRenderer TextRenderer { get; }
|
||||
public BitmapFont? DefaultFont { get; set; }
|
||||
public Vector2 ScreenSize { get; }
|
||||
|
||||
// Transform stack — simple 2D translate (no rotation/scale for UI).
|
||||
// Transform stack — simple 2D translate (no rotation/scale for UI).
|
||||
private readonly System.Collections.Generic.List<Vector2> _stack = new();
|
||||
private Vector2 _current;
|
||||
private readonly System.Collections.Generic.List<UiClipRect?> _clipStack = new();
|
||||
private UiClipRect? _clip;
|
||||
|
||||
// Alpha (opacity) stack — a window pushes its Opacity so its background/sprite
|
||||
// Alpha (opacity) stack — a window pushes its Opacity so its background/sprite
|
||||
// draws fade (retail's translucent-chat effect). Text draws bypass this (they go
|
||||
// straight to TextRenderer), so text stays sharp over a translucent background.
|
||||
private readonly System.Collections.Generic.List<float> _alphaStack = new();
|
||||
|
|
@ -57,7 +57,7 @@ public sealed class UiRenderContext
|
|||
/// <summary>Multiply <paramref name="a"/> into the running opacity. Pair with <see cref="PopAlpha"/>.</summary>
|
||||
public void PushAlpha(float a) { _alphaStack.Add(_alpha); _alpha *= a; }
|
||||
|
||||
/// <summary>Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays
|
||||
/// <summary>Push an ABSOLUTE opacity (replaces, not multiplies) — for popups/overlays
|
||||
/// that must stay opaque even inside a translucent window. Pair with <see cref="PopAlpha"/>.</summary>
|
||||
public void PushAlphaAbsolute(float a) { _alphaStack.Add(_alpha); _alpha = a; }
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ public sealed class UiRenderContext
|
|||
public void BeginOverlayLayer() => TextRenderer.OverlayMode = true;
|
||||
public void EndOverlayLayer() => TextRenderer.OverlayMode = false;
|
||||
|
||||
// ── Pass-through draw helpers (add current translate) ──────────────
|
||||
// ── Pass-through draw helpers (add current translate) ──────────────
|
||||
|
||||
public void DrawRect(float x, float y, float w, float h, Vector4 color)
|
||||
{
|
||||
|
|
@ -129,7 +129,7 @@ public sealed class UiRenderContext
|
|||
|
||||
/// <summary>Solid-colour fill drawn in the SPRITE bucket (painter order with text), for
|
||||
/// a panel BACKGROUND that text draws on top of. <see cref="DrawRect"/> composites after
|
||||
/// all sprites and would cover the text — use this for backgrounds, that for foreground
|
||||
/// all sprites and would cover the text — use this for backgrounds, that for foreground
|
||||
/// fills (carets, vital bars).</summary>
|
||||
public void DrawFill(float x, float y, float w, float h, Vector4 color)
|
||||
{
|
||||
|
|
@ -149,7 +149,7 @@ public sealed class UiRenderContext
|
|||
DrawRect(x + w - t, y + t, t, h - 2f * t, color);
|
||||
}
|
||||
|
||||
public void DrawSprite(uint texture, float x, float y, float w, float h,
|
||||
public void DrawSprite(GpuTextureSlot texture, float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 tint)
|
||||
{
|
||||
x += _current.X;
|
||||
|
|
@ -158,7 +158,7 @@ public sealed class UiRenderContext
|
|||
}
|
||||
|
||||
private void DrawSpriteAbsolute(
|
||||
uint texture, float x, float y, float w, float h,
|
||||
GpuTextureSlot texture, float x, float y, float w, float h,
|
||||
float u0, float v0, float u1, float v1, Vector4 tint, bool applyAlpha)
|
||||
{
|
||||
if (_clip is { } clip
|
||||
|
|
@ -213,7 +213,7 @@ public sealed class UiRenderContext
|
|||
///
|
||||
/// <para><paramref name="outline"/> gates the black outline pass. Retail decides
|
||||
/// this PER text element: <c>UIElement_Text::DrawSelf</c> (acclient 0x00467aa0)
|
||||
/// runs the outline pass only when <c>m_bitField & 0x10</c> is set — i.e. the
|
||||
/// runs the outline pass only when <c>m_bitField & 0x10</c> is set — i.e. the
|
||||
/// element called <c>SetOutline(true)</c> (LayoutDesc property 0xd). The DEFAULT
|
||||
/// is OFF (one fill-only pass): the talk-focus menu items set no outline, so an
|
||||
/// always-on outline shows as a grey halo over the solid menu panel. Pass
|
||||
|
|
@ -232,10 +232,10 @@ public sealed class UiRenderContext
|
|||
|
||||
// Snap the LINE baseline to a whole pixel ONCE. Retail's
|
||||
// SurfaceWindow::DrawCharacter (acclient 0x00442bd0) takes an int32 pen Y
|
||||
// (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every
|
||||
// (arg3) and adds the glyph's integer m_VerticalOffsetBefore (a schar) — every
|
||||
// glyph on a line shares one integer baseline. If we instead round EACH glyph's
|
||||
// Y independently and the caller passes a fractional line Y (e.g. a channel-menu
|
||||
// item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round
|
||||
// item centered in a 17px row over a 16px font → y = 0.5), adjacent letters round
|
||||
// to different rows and the line looks crooked ("letters dip down"). The vitals
|
||||
// digits never showed it because their bar baseline lands on an integer; chat text
|
||||
// does. Snapping the baseline once, then adding the integer offset, keeps the whole
|
||||
|
|
@ -251,7 +251,7 @@ public sealed class UiRenderContext
|
|||
|
||||
// Horizontal: snap each glyph's dest X to a whole pixel (the pen keeps its
|
||||
// true fractional advance). Vertical: integer baseline + integer per-glyph
|
||||
// offset — never an independent per-glyph round (see baseY note above).
|
||||
// offset — never an independent per-glyph round (see baseY note above).
|
||||
float gx = System.MathF.Round(pen + g.HorizontalOffsetBefore);
|
||||
float gy = baseY + g.VerticalOffsetBefore;
|
||||
float gw = g.Width;
|
||||
|
|
@ -259,10 +259,10 @@ public sealed class UiRenderContext
|
|||
|
||||
if (gw > 0f && gh > 0f)
|
||||
{
|
||||
// Background (outline) atlas pass, tinted black — drawn behind. Gated by
|
||||
// Background (outline) atlas pass, tinted black — drawn behind. Gated by
|
||||
// `outline` (retail's per-element m_bitField & 0x10); off by default so UI
|
||||
// text is crisp fill-only and free of the grey halo over solid panels.
|
||||
if (outline && font.BackgroundTexture != 0)
|
||||
if (outline && font.BackgroundTexture.IsAssigned)
|
||||
{
|
||||
var (bu0, bv0, bu1, bv1) = AtlasUv(
|
||||
g.OffsetX, g.OffsetY, g.Width, g.Height,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -22,7 +22,7 @@ public enum ResizeEdges { None = 0, Left = 1, Right = 2, Top = 4, Bottom = 8 }
|
|||
/// or <see cref="WorldKeyFallThrough"/> event fires so the game world
|
||||
/// (camera, player controller) still receives input.
|
||||
/// </summary>
|
||||
public sealed class UiRoot : UiElement
|
||||
internal sealed class UiRoot : UiElement
|
||||
{
|
||||
public UiRoot()
|
||||
{
|
||||
|
|
@ -32,7 +32,7 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Single owner for named retained-window lifecycle and raise policy.</summary>
|
||||
public RetailWindowManager WindowManager { get; }
|
||||
|
||||
// ── Device-level state ───────────────────────────────────────────────
|
||||
// ── Device-level state ───────────────────────────────────────────────
|
||||
public int MouseX { get; private set; }
|
||||
public int MouseY { get; private set; }
|
||||
public bool LeftButtonDown { get; private set; }
|
||||
|
|
@ -42,7 +42,7 @@ public sealed class UiRoot : UiElement
|
|||
/// <summary>Widget currently receiving keyboard events.</summary>
|
||||
public UiElement? KeyboardFocus { get; private set; }
|
||||
|
||||
/// <summary>The edit control activated by Tab/Enter when nothing is focused — retail's
|
||||
/// <summary>The edit control activated by Tab/Enter when nothing is focused — retail's
|
||||
/// chat input "write mode" toggle. Set by the host once the chat window is built.</summary>
|
||||
public UiElement? DefaultTextInput { get; set; }
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ public sealed class UiRoot : UiElement
|
|||
/// True when the pointer is over a widget OR a widget holds mouse capture.
|
||||
/// The host ORs this into the InputDispatcher's WantCaptureMouse gate so game
|
||||
/// actions (movement, world-pick) are suppressed while the user interacts with
|
||||
/// a retail window — mirrors ImGui's WantCaptureMouse.
|
||||
/// a retail window — mirrors ImGui's WantCaptureMouse.
|
||||
/// </summary>
|
||||
public bool WantsMouse => Captured is not null || HitTestTopDown(MouseX, MouseY).element is not null;
|
||||
|
||||
|
|
@ -111,9 +111,9 @@ public sealed class UiRoot : UiElement
|
|||
&& !target.HandlesClick;
|
||||
}
|
||||
}
|
||||
private (uint tex, int w, int h)? _dragGhost;
|
||||
private (GpuTextureSlot tex, int w, int h)? _dragGhost;
|
||||
/// <summary>Snapshotted drag-ghost (tex,w,h), exposed for tests. See BeginDrag.</summary>
|
||||
internal (uint tex, int w, int h)? DragGhostForTest => _dragGhost;
|
||||
internal (GpuTextureSlot tex, int w, int h)? DragGhostForTest => _dragGhost;
|
||||
private UiElement? _lastDragHoverTarget;
|
||||
private int _pressX, _pressY;
|
||||
private bool _dragCandidate;
|
||||
|
|
@ -272,7 +272,7 @@ public sealed class UiRoot : UiElement
|
|||
return false;
|
||||
}
|
||||
|
||||
// ── Per-frame pumping ────────────────────────────────────────────────
|
||||
// ── Per-frame pumping ────────────────────────────────────────────────
|
||||
|
||||
public void Tick(double dt, long nowMs)
|
||||
{
|
||||
|
|
@ -303,7 +303,7 @@ public sealed class UiRoot : UiElement
|
|||
|
||||
public void Draw(UiRenderContext ctx)
|
||||
{
|
||||
// Render children (panels) sorted by z-order — modal last so it
|
||||
// Render children (panels) sorted by z-order — modal last so it
|
||||
// sits on top.
|
||||
DrawSelfAndChildren(ctx);
|
||||
// Second pass: open popups/menus draw ON TOP of the whole tree (so e.g. the
|
||||
|
|
@ -324,12 +324,12 @@ public sealed class UiRoot : UiElement
|
|||
/// never intercepts hit-tests.</summary>
|
||||
private void DrawDragGhost(UiRenderContext ctx)
|
||||
{
|
||||
if (_dragGhost is not { } g || g.tex == 0) return;
|
||||
if (_dragGhost is not { } g || !g.tex.IsAssigned) return;
|
||||
ctx.DrawSprite(g.tex, MouseX - g.w / 2f, MouseY - g.h / 2f, g.w, g.h,
|
||||
0f, 0f, 1f, 1f, new Vector4(1f, 1f, 1f, GhostAlpha));
|
||||
}
|
||||
|
||||
// ── Input entry points (called from GameWindow's Silk.NET handlers) ──
|
||||
// ── Input entry points (called from GameWindow's Silk.NET handlers) ──
|
||||
|
||||
public void OnMouseMove(int x, int y)
|
||||
{
|
||||
|
|
@ -426,7 +426,7 @@ public sealed class UiRoot : UiElement
|
|||
if (target is null)
|
||||
{
|
||||
// Clicking the 3D world exits write mode (no submit) and returns control to
|
||||
// the character — retail blurs the chat input on an outside click.
|
||||
// the character — retail blurs the chat input on an outside click.
|
||||
if (btn == UiMouseButton.Left) SetKeyboardFocus(null);
|
||||
WorldMouseFallThrough?.Invoke(btn, x, y, flags);
|
||||
return;
|
||||
|
|
@ -473,7 +473,7 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
else if (target.CapturesPointerDrag || target.HandlesClick)
|
||||
{
|
||||
// The pressed widget owns its pointer interaction — either an interior drag (e.g. text
|
||||
// The pressed widget owns its pointer interaction — either an interior drag (e.g. text
|
||||
// selection, CapturesPointerDrag) or a click it must receive (e.g. a UiButton,
|
||||
// HandlesClick). Either way do NOT move the ancestor window. The already-dispatched
|
||||
// MouseDown + SetCapture(target) let the target handle it; on release OnMouseUp emits
|
||||
|
|
@ -513,7 +513,7 @@ public sealed class UiRoot : UiElement
|
|||
// Deliver TARGET-LOCAL coords (consistent with MouseMove/MouseUp, which use
|
||||
// target.ScreenPosition). HitTestTopDown's lx/ly are relative to the TOP-LEVEL
|
||||
// child, so for a nested target (e.g. the chat view inset inside its window)
|
||||
// they'd be offset by the child's position — which mis-anchored drag-select.
|
||||
// they'd be offset by the child's position — which mis-anchored drag-select.
|
||||
var sp = target.ScreenPosition;
|
||||
var e = new UiEvent(target.EventId, target, rawType,
|
||||
Data0: (int)flags, Data1: (int)(x - sp.X), Data2: (int)(y - sp.Y));
|
||||
|
|
@ -615,7 +615,7 @@ public sealed class UiRoot : UiElement
|
|||
return;
|
||||
}
|
||||
|
||||
// No capture — give the world a chance.
|
||||
// No capture — give the world a chance.
|
||||
WorldMouseFallThrough?.Invoke(btn, x, y, flags);
|
||||
}
|
||||
|
||||
|
|
@ -687,7 +687,7 @@ public sealed class UiRoot : UiElement
|
|||
BubbleEvent(KeyboardFocus, in e);
|
||||
}
|
||||
|
||||
// ── Focus + capture ─────────────────────────────────────────────────
|
||||
// ── Focus + capture ─────────────────────────────────────────────────
|
||||
|
||||
public void SetKeyboardFocus(UiElement? e)
|
||||
{
|
||||
|
|
@ -726,12 +726,12 @@ public sealed class UiRoot : UiElement
|
|||
PointerCaptureChanged?.Invoke(previous, null);
|
||||
}
|
||||
|
||||
// ── Window manager (named top-level windows: Show / Hide / Toggle) ───
|
||||
// ── Window manager (named top-level windows: Show / Hide / Toggle) ───
|
||||
|
||||
// Registry state lives in RetailWindowManager; methods below are compatibility forwarders.
|
||||
|
||||
/// <summary>Register a top-level window under a name for Show/Hide/Toggle.
|
||||
/// Does NOT add it to the tree — the caller mounts via AddChild and controls
|
||||
/// Does NOT add it to the tree — the caller mounts via AddChild and controls
|
||||
/// initial Visible. Idempotent registration returns the existing typed handle;
|
||||
/// replacement performs full lifecycle teardown of the prior registration.</summary>
|
||||
public RetailWindowHandle RegisterWindow(
|
||||
|
|
@ -788,7 +788,7 @@ public sealed class UiRoot : UiElement
|
|||
WindowResized?.Invoke(handle.Name, window);
|
||||
}
|
||||
|
||||
// ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ────────
|
||||
// ── Drag-drop (retail event chain 0x15 → 0x21 → 0x1C → 0x3E) ────────
|
||||
|
||||
private void BeginDrag(UiElement source)
|
||||
{
|
||||
|
|
@ -796,7 +796,7 @@ public sealed class UiRoot : UiElement
|
|||
if (payload is null) { _dragCandidate = false; return; }
|
||||
DragSource = source;
|
||||
DragPayload = payload;
|
||||
_dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell
|
||||
_dragGhost = source.GetDragGhost(); // snapshot NOW — the DragBegin handler may empty the source cell
|
||||
var e = new UiEvent(source.EventId, source, UiEventType.DragBegin, Payload: payload);
|
||||
source.OnEvent(in e);
|
||||
// Retail UIElement_ItemList::ItemList_BeginDrag @ 0x004E32D0 selects an
|
||||
|
|
@ -843,7 +843,7 @@ public sealed class UiRoot : UiElement
|
|||
var (t, lx, ly) = HitTestTopDown(x, y);
|
||||
if (t is not null)
|
||||
{
|
||||
// Dropped on a real element — deliver DropReleased; the hit cell's handler places.
|
||||
// Dropped on a real element — deliver DropReleased; the hit cell's handler places.
|
||||
// A non-item target's OnEvent ignores it, so an off-bar drop leaves the lift's removal.
|
||||
var e = new UiEvent(source!.EventId, t, UiEventType.DropReleased,
|
||||
Data1: (int)lx, Data2: (int)ly, Payload: payload);
|
||||
|
|
@ -859,7 +859,7 @@ public sealed class UiRoot : UiElement
|
|||
_lastDragHoverTarget = null;
|
||||
}
|
||||
|
||||
// ── Hover / tooltip ─────────────────────────────────────────────────
|
||||
// ── Hover / tooltip ─────────────────────────────────────────────────
|
||||
|
||||
private void UpdateHover(int x, int y)
|
||||
{
|
||||
|
|
@ -892,7 +892,7 @@ public sealed class UiRoot : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
// ── Helpers ─────────────────────────────────────────────────────────
|
||||
|
||||
public void FireEvent(int type, UiElement target, object? payload = null)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
|
|
@ -11,7 +11,7 @@ namespace AcDream.App.UI;
|
|||
/// Decomp anchors: SetScrollableXY @0x4740c0, UpdateScrollbarSize_ @0x4741a0,
|
||||
/// UpdateScrollbarPosition_ @0x473f20, UIElement_Text::InqScrollDelta @0x4689b0.
|
||||
/// </summary>
|
||||
public sealed class UiScrollable
|
||||
internal sealed class UiScrollable
|
||||
{
|
||||
/// <summary>Total wrapped content height in px (m_iScrollableHeight).</summary>
|
||||
public int ContentHeight { get; set; }
|
||||
|
|
@ -30,7 +30,7 @@ public sealed class UiScrollable
|
|||
/// <summary>True when content exceeds the view (a scrollbar is warranted).</summary>
|
||||
public bool HasOverflow => ContentHeight > ViewHeight;
|
||||
|
||||
/// <summary>True when the offset is at (or past) the bottom — used for bottom-pin.</summary>
|
||||
/// <summary>True when the offset is at (or past) the bottom — used for bottom-pin.</summary>
|
||||
public bool AtEnd => _scrollY >= MaxScroll;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -64,7 +64,7 @@ public sealed class UiScrollable
|
|||
/// <summary>Position ratio = scroll/(content-view) in [0,1] (UpdateScrollbarPosition_).</summary>
|
||||
public float PositionRatio => MaxScroll <= 0 ? 0f : (float)_scrollY / MaxScroll;
|
||||
|
||||
/// <summary>Inverse of PositionRatio — used when the user drags the thumb.</summary>
|
||||
/// <summary>Inverse of PositionRatio — used when the user drags the thumb.</summary>
|
||||
public void SetPositionRatio(float ratio)
|
||||
=> SetScrollY((int)MathF.Round(Math.Clamp(ratio, 0f, 1f) * MaxScroll));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
|
|
@ -10,7 +10,7 @@ namespace AcDream.App.UI;
|
|||
/// pixel scroll model as chat text and item grids, and clips whole rows because
|
||||
/// the UI renderer does not have a scissor stack yet.
|
||||
/// </summary>
|
||||
public sealed class UiScrollablePanel : UiPanel
|
||||
internal sealed class UiScrollablePanel : UiPanel
|
||||
{
|
||||
private readonly Dictionary<UiElement, float> _baseTops = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Numerics;
|
||||
|
||||
namespace AcDream.App.UI;
|
||||
|
|
@ -6,20 +6,20 @@ namespace AcDream.App.UI;
|
|||
/// <summary>
|
||||
/// Generic scrollbar. Ports retail <c>UIElement_Scrollbar</c>
|
||||
/// (RegisterElementClass(0xb) @ acclient_2013_pseudo_c.txt:124137);
|
||||
/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line.
|
||||
/// thumb size = trackLen * ThumbRatio (min 8px); step ±1 line.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Dat element ids (chat LayoutDesc 0x21000006): track 0x10000012 (X=474 Y=6 W=16 H=68),
|
||||
/// thumb 0x1000048C. The track is instanced from base layout 0x2100003E which contains
|
||||
/// the full scrollbar widget with distinct up/down button children:
|
||||
/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69.
|
||||
/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C.
|
||||
/// Up button element 0x10000071 — Y=0, 16×16, Normal sprite 0x06004C69.
|
||||
/// Down button element 0x10000072 — Y=32, 16×16, Normal sprite 0x06004C6C.
|
||||
/// Track body sprite: 0x06004C5F (48px tall in the base template; stretched to H=68 in chat).
|
||||
/// Thumb is a 3-slice: top cap 0x06004C60, middle 0x06004C63, bottom cap 0x06004C66.
|
||||
/// The widget reproduces referenced button children procedurally and uses their
|
||||
/// authored extent for drawing and hit comparison (16px in this base layout).
|
||||
/// </remarks>
|
||||
public sealed class UiScrollbar : UiElement
|
||||
internal sealed class UiScrollbar : UiElement
|
||||
{
|
||||
public override bool ReceivesHoverMouseMove => true;
|
||||
|
||||
|
|
@ -64,8 +64,8 @@ public sealed class UiScrollbar : UiElement
|
|||
public void SetScalarPosition(float position)
|
||||
=> ScalarPosition = Math.Clamp(position, 0f, 1f);
|
||||
|
||||
/// <summary>RenderSurface id → (GL tex, w, h). 0 id = skip.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
/// <summary>RenderSurface id → (GL tex, w, h). 0 id = skip.</summary>
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
/// <summary>Track background sprite id (0x06004C5F from layout 0x2100003E element 0x10000455).</summary>
|
||||
public uint TrackSprite { get; set; }
|
||||
|
|
@ -212,8 +212,8 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
if (Model is not { } m) return;
|
||||
|
||||
// Track background — TILED vertically (retail DrawMode=Normal). The native track
|
||||
// sprite (~16×32) repeats to fill the element height instead of stretch-distorting.
|
||||
// Track background — TILED vertically (retail DrawMode=Normal). The native track
|
||||
// sprite (~16×32) repeats to fill the element height instead of stretch-distorting.
|
||||
DrawTiled(ctx, resolve, TrackSprite, 0f, 0f, Width, Height);
|
||||
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Height);
|
||||
|
|
@ -224,7 +224,7 @@ public sealed class UiScrollbar : UiElement
|
|||
DrawSprite(ctx, resolve, ActiveEndSprite,
|
||||
0f, Height - incrementExtent, Width, incrementExtent);
|
||||
|
||||
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
||||
// Thumb — only when content overflows the view. Retail 3-slice: top cap +
|
||||
// tiled middle + bottom cap (base layout 0x2100003E thumb sub-elements
|
||||
// 0x10000364/65/66). Falls back to a single tiled middle if the caps are unset
|
||||
// or the thumb is too short to hold both caps.
|
||||
|
|
@ -248,7 +248,7 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
private void DrawHorizontalModel(
|
||||
UiRenderContext ctx,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
UiScrollable model)
|
||||
{
|
||||
float decrementExtent = AxisExtent(DecrementButtonExtent, Width);
|
||||
|
|
@ -275,23 +275,23 @@ public sealed class UiScrollbar : UiElement
|
|||
}
|
||||
|
||||
/// <summary>Draw a sprite stretched 1:1 to the dest rect.</summary>
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
private void DrawSprite(UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, _, _) = resolve(id);
|
||||
if (tex == 0) return;
|
||||
if (!tex.IsAssigned) return;
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
/// <summary>Draw a sprite TILED to fill the dest rect (UV-repeat at native size on
|
||||
/// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1.</summary>
|
||||
private void DrawTiled(UiRenderContext ctx, Func<uint, (uint tex, int w, int h)> resolve,
|
||||
/// both axes — the UI texture is GL_REPEAT-wrapped). A native-width axis gives 1:1.</summary>
|
||||
private void DrawTiled(UiRenderContext ctx, Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint id, float x, float y, float w, float h)
|
||||
{
|
||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
if (!tex.IsAssigned || tw == 0 || th == 0) return;
|
||||
ctx.DrawSprite(tex, x, y, w, h, 0f, 0f, w / tw, h / th, Vector4.One);
|
||||
}
|
||||
|
||||
|
|
@ -301,7 +301,7 @@ public sealed class UiScrollbar : UiElement
|
|||
/// </summary>
|
||||
private void DrawTiledClipped(
|
||||
UiRenderContext ctx,
|
||||
Func<uint, (uint tex, int w, int h)> resolve,
|
||||
Func<uint, (GpuTextureSlot tex, int w, int h)> resolve,
|
||||
uint id,
|
||||
float rangeLeft,
|
||||
float x,
|
||||
|
|
@ -310,7 +310,7 @@ public sealed class UiScrollbar : UiElement
|
|||
{
|
||||
if (id == 0 || w <= 0f || h <= 0f) return;
|
||||
var (tex, tw, th) = resolve(id);
|
||||
if (tex == 0 || tw == 0 || th == 0) return;
|
||||
if (!tex.IsAssigned || tw == 0 || th == 0) return;
|
||||
float u0 = (x - rangeLeft) / tw;
|
||||
float u1 = u0 + w / tw;
|
||||
ctx.DrawSprite(tex, x, 0f, w, h, u0, 0f, u1, h / th, Vector4.One);
|
||||
|
|
@ -390,13 +390,13 @@ public sealed class UiScrollbar : UiElement
|
|||
|
||||
if (ly >= ty && ly <= ty + th)
|
||||
{
|
||||
// Clicked inside the thumb — begin drag with offset from thumb top.
|
||||
// Clicked inside the thumb — begin drag with offset from thumb top.
|
||||
_draggingThumb = true;
|
||||
_dragOffsetY = ly - ty;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Clicked above or below thumb — page scroll (HandleButtonClick page case).
|
||||
// Clicked above or below thumb — page scroll (HandleButtonClick page case).
|
||||
m.ScrollByPage(ly < ty ? -1 : 1);
|
||||
}
|
||||
return true;
|
||||
|
|
@ -520,7 +520,7 @@ public sealed class UiScrollbar : UiElement
|
|||
return false;
|
||||
}
|
||||
|
||||
private float ScalarThumbWidth(Func<uint, (uint tex, int w, int h)>? resolve)
|
||||
private float ScalarThumbWidth(Func<uint, (GpuTextureSlot tex, int w, int h)>? resolve)
|
||||
{
|
||||
if (resolve is not null && ThumbSprite != 0)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
namespace AcDream.App.UI;
|
||||
namespace AcDream.App.UI;
|
||||
|
||||
/// <summary>
|
||||
/// Shared DAT digit overlays used by UIItem shortcut cells. Both gmToolbarUI and
|
||||
|
|
@ -6,7 +6,7 @@ namespace AcDream.App.UI;
|
|||
/// share these number arrays. The underlying ItemSlot_Empty surface remains a
|
||||
/// per-ItemList asset selected by that list's cell prototype.
|
||||
/// </summary>
|
||||
public sealed record UiShortcutDigitGraphics(
|
||||
internal sealed record UiShortcutDigitGraphics(
|
||||
uint[]? RegularDigits,
|
||||
uint[]? GhostedDigits,
|
||||
uint[]? EmptyDigits);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Text;
|
||||
|
|
@ -20,7 +20,7 @@ namespace AcDream.App.UI;
|
|||
/// Display-only text remains click-through and cannot steal focus or window drag.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public sealed class UiText : UiElement, IUiDatStateful
|
||||
internal sealed class UiText : UiElement, IUiDatStateful
|
||||
{
|
||||
/// <summary>Optional base-element click notice used by authored text tabs.</summary>
|
||||
public Action? OnClick { get; set; }
|
||||
|
|
@ -30,11 +30,11 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
public uint ElementId { get; set; }
|
||||
|
||||
/// <summary>One display line: pre-formatted text + its colour.</summary>
|
||||
public readonly record struct Line(string Text, Vector4 Color);
|
||||
internal readonly record struct Line(string Text, Vector4 Color);
|
||||
|
||||
/// <summary>A caret position: a line index into the cached line list plus a
|
||||
/// character index (0..line.Text.Length, i.e. a caret slot between glyphs).</summary>
|
||||
public readonly record struct Pos(int Line, int Col);
|
||||
internal readonly record struct Pos(int Line, int Col);
|
||||
|
||||
/// <summary>Provider of the lines to show, oldest-first. Polled each frame.</summary>
|
||||
public Func<IReadOnlyList<Line>> LinesProvider { get; set; } = static () => Array.Empty<Line>();
|
||||
|
|
@ -58,7 +58,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// otherwise white (<see cref="Vector4.One"/>).
|
||||
///
|
||||
/// <para>Controllers that supply a per-line color via <see cref="LinesProvider"/>
|
||||
/// (e.g. <c>new UiText.Line(text, explicitColor)</c>) are unaffected — they always
|
||||
/// (e.g. <c>new UiText.Line(text, explicitColor)</c>) are unaffected — they always
|
||||
/// win over this default. This property is only a convenience starting point for
|
||||
/// controllers that want to read the dat color rather than hard-code it.</para>
|
||||
/// </summary>
|
||||
|
|
@ -84,7 +84,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
|
||||
/// <summary>Resolves a dat RenderSurface id to (GL tex handle, pixel width, pixel height).
|
||||
/// Required when <see cref="BackgroundSprite"/> is non-zero.</summary>
|
||||
public Func<uint, (uint tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
public Func<uint, (GpuTextureSlot tex, int w, int h)>? SpriteResolve { get; set; }
|
||||
|
||||
/// <summary>Highlight colour painted behind a selected character span.</summary>
|
||||
public Vector4 SelectionColor { get; set; } = new(0.25f, 0.45f, 0.85f, 0.5f);
|
||||
|
|
@ -138,7 +138,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// <summary>Static right-aligned single-line mode: draws the FIRST line right-justified
|
||||
/// within the element rect, vertically centered, with NO scroll/selection machinery.
|
||||
/// Used for value labels in attribute/skill rows where the number must hug the right edge.
|
||||
/// Mutually exclusive with <see cref="Centered"/> — if both are true, Centered takes
|
||||
/// Mutually exclusive with <see cref="Centered"/> — if both are true, Centered takes
|
||||
/// precedence. Pair with <c>ClickThrough = true</c> for non-interactive labels.</summary>
|
||||
public bool RightAligned { get; set; }
|
||||
|
||||
|
|
@ -146,20 +146,20 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// Vertical position of the text within the element rect in single-line mode
|
||||
/// (<see cref="Centered"/> or <see cref="RightAligned"/>).
|
||||
/// <list type="bullet">
|
||||
/// <item><description><b>Center</b> (default) — vertically centered, matching the original
|
||||
/// <item><description><b>Center</b> (default) — vertically centered, matching the original
|
||||
/// behavior of the centered/right-aligned paths.</description></item>
|
||||
/// <item><description><b>Top</b> — text is placed at <c>y = Padding</c> (top of the content
|
||||
/// <item><description><b>Top</b> — text is placed at <c>y = Padding</c> (top of the content
|
||||
/// area), so the text sits at the top of the element rather than centering in it.
|
||||
/// Used for footer title elements whose dat box is the full footer height (55 px) but
|
||||
/// the text should render near the top.</description></item>
|
||||
/// <item><description><b>Bottom</b> — text is placed at <c>y = Height - lineHeight - Padding</c>.</description></item>
|
||||
/// <item><description><b>Bottom</b> — text is placed at <c>y = Height - lineHeight - Padding</c>.</description></item>
|
||||
/// </list>
|
||||
/// Only meaningful when <see cref="Centered"/> or <see cref="RightAligned"/> is true.
|
||||
/// Has no effect on the scrollable multi-line path.
|
||||
/// </summary>
|
||||
public VJustify VerticalJustify { get; set; } = VJustify.Center;
|
||||
|
||||
/// <summary>The scroll model — also read by the linked UiScrollbar.</summary>
|
||||
/// <summary>The scroll model — also read by the linked UiScrollbar.</summary>
|
||||
public UiScrollable Scroll { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -179,7 +179,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
|
||||
private const float WheelLines = 1f; // lines advanced per wheel notch (retail = 1 line per notch)
|
||||
|
||||
// ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ──
|
||||
// ── Cached layout from the last OnDraw, so OnEvent hit-tests the SAME geometry ──
|
||||
private IReadOnlyList<Line> _lastLines = Array.Empty<Line>();
|
||||
private BitmapFont? _lastFont;
|
||||
private UiDatFont? _lastDatFont;
|
||||
|
|
@ -193,7 +193,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
private bool _drawTextAfterChildren;
|
||||
private bool _honorDatVerticalJustification;
|
||||
|
||||
// ── Selection state ──────────────────────────────────────────────────
|
||||
// ── Selection state ──────────────────────────────────────────────────
|
||||
private Pos? _selAnchor; // where the drag started
|
||||
private Pos? _selCaret; // where the drag currently is
|
||||
private bool _selecting;
|
||||
|
|
@ -325,7 +325,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
|
||||
/// <summary>
|
||||
/// Clamp a scroll offset to [0, max] where max = content-height - view-height
|
||||
/// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests.
|
||||
/// (never negative — when everything fits, scroll is pinned to 0). Exposed for tests.
|
||||
/// </summary>
|
||||
public static float ClampScroll(float scroll, float contentHeight, float viewHeight)
|
||||
{
|
||||
|
|
@ -340,14 +340,14 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
if (BackgroundSprite != 0 && SpriteResolve is { } sr)
|
||||
{
|
||||
var (tex, tw, th) = sr(BackgroundSprite);
|
||||
if (tex != 0 && tw != 0 && th != 0)
|
||||
if (tex.IsAssigned && tw != 0 && th != 0)
|
||||
ctx.DrawSprite(tex, 0, 0, Width, Height, 0, 0, Width / tw, Height / th, Vector4.One);
|
||||
}
|
||||
|
||||
// Background must draw UNDER the transcript text. DrawStringDat emits into the
|
||||
// sprite bucket which flushes BEFORE rects, so a DrawRect background would wash
|
||||
// over the text. DrawFill routes the background through the sprite bucket too,
|
||||
// submitted first → text on top.
|
||||
// submitted first → text on top.
|
||||
ctx.DrawFill(0, 0, Width, Height, BackgroundColor);
|
||||
|
||||
if (!_drawTextAfterChildren)
|
||||
|
|
@ -520,7 +520,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
hx = lineX + bitmapFont!.MeasureWidth(text.Substring(0, c0));
|
||||
hw = bitmapFont.MeasureWidth(text.Substring(c0, c1 - c0));
|
||||
}
|
||||
// Highlight sits BEHIND the line's text → sprite bucket, submitted
|
||||
// Highlight sits BEHIND the line's text → sprite bucket, submitted
|
||||
// before this line's DrawStringDat.
|
||||
ctx.DrawFill(hx, y, hw, lh, SelectionColor);
|
||||
}
|
||||
|
|
@ -572,7 +572,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
if (!Selectable && !WheelScrollEnabled) return false;
|
||||
// Silk wheel +Y = scroll up = reveal older = toward the TOP = decrease ScrollY.
|
||||
// ScrollByLines sign: +down/newer, -up/older.
|
||||
// e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines.
|
||||
// e.Data0 > 0 → wheel up → want older → ScrollByLines with negative lines.
|
||||
Scroll.ScrollByLines((int)(-e.Data0 * WheelLines));
|
||||
return true;
|
||||
}
|
||||
|
|
@ -616,7 +616,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
|| Keyboard.IsKeyPressed(Silk.NET.Input.Key.ControlRight));
|
||||
if (ctrl && key == Silk.NET.Input.Key.C)
|
||||
{
|
||||
// Only touch the clipboard when there's a selection — an empty
|
||||
// Only touch the clipboard when there's a selection — an empty
|
||||
// copy must NOT clobber what the user previously copied.
|
||||
if (Keyboard is not null)
|
||||
{
|
||||
|
|
@ -636,7 +636,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
return false;
|
||||
}
|
||||
|
||||
// ── Selection helpers ────────────────────────────────────────────────
|
||||
// ── Selection helpers ────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Select the entire cached transcript (Ctrl+A).</summary>
|
||||
private void SelectAll()
|
||||
|
|
@ -670,7 +670,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
return SelectedText(_lastLines, start, end);
|
||||
}
|
||||
|
||||
// ── Pure, testable logic (no GL / no font texture) ───────────────────
|
||||
// ── Pure, testable logic (no GL / no font texture) ───────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Compute the Y offset (local space) for a single line in the Centered/RightAligned
|
||||
|
|
@ -731,7 +731,7 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// <paramref name="end"/> (inclusive of start.Col, exclusive of end.Col) from
|
||||
/// <paramref name="lines"/>. Multi-line selections are joined with "\n":
|
||||
/// the first line from start.Col to its end, whole middle lines, and the last
|
||||
/// line up to end.Col. Pure — unit-testable without GL.
|
||||
/// line up to end.Col. Pure — unit-testable without GL.
|
||||
/// </summary>
|
||||
public static string SelectedText(IReadOnlyList<Line> lines, Pos start, Pos end)
|
||||
{
|
||||
|
|
@ -870,8 +870,8 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
/// The caret column for a horizontal position <paramref name="x"/> (already
|
||||
/// adjusted for the left padding, so x=0 is the start of the text). Walks the
|
||||
/// string accumulating each glyph's advance and snaps the caret to whichever
|
||||
/// side of the glyph midpoint <paramref name="x"/> falls on — natural
|
||||
/// Windows-like caret placement. Pure — unit-testable with a synthetic advance.
|
||||
/// side of the glyph midpoint <paramref name="x"/> falls on — natural
|
||||
/// Windows-like caret placement. Pure — unit-testable with a synthetic advance.
|
||||
/// </summary>
|
||||
/// <param name="text">The line text.</param>
|
||||
/// <param name="advanceOf">Per-character advance (pixels) lookup.</param>
|
||||
|
|
@ -888,6 +888,6 @@ public sealed class UiText : UiElement, IUiDatStateful
|
|||
if (x < mid) return i; // caret sits before this glyph
|
||||
cursor += adv;
|
||||
}
|
||||
return text.Length; // past the last glyph → end caret
|
||||
return text.Length; // past the last glyph → end caret
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue