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 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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue