feat(ui): Map/House panel — slices 2+3, panel shell + Map tab
Mounts host 0x2100006E slot 0x1000018C (RetailPanelCatalog.MapHouse = 16) as a two-tab UiTabPanel (Map default, House second) through the OP3/FA3 recipe (LayoutImporter.Build -> Bind -> ActivateTabBehavior). Toolbar button 0x1000019A un-ghosts (added to both RetailPanelCatalog.Mounted and .Toolbar). Combined into one commit because MapHousePanelController.Bind depends on both MapPageController and HousePageController existing — splitting them would mean landing dead code first. Map tab (gmMapUI, MapPageController): - Calendar formatter matching gmMapUI::Update's "Date: %s\nTime: %s" shape, reusing WorldTimeService.CurrentCalendar (new Func<DerethDateTime.Calendar> dependency threaded through InteractionRetainedUiDependencies/GameWindow — a stable long-lived service, not routed through the deferred-binding machinery Radar's per-session state needs). MonthName enum values already match retail display text; HourName's "AndHalf" suffix is rewritten to "-and-Half". - Coordinate math + marker placement reuse RadarCoordinates/ LandDefs.GidToLcoord verbatim (both already byte-exact ports of CPlayerSystem::InqPlayerCoords/LandDefs::gid_to_lcoord) — no re-port. PlaceMarkerOnMap's centering math (m_x0 + x - w/2) ported from gmMapUI::PlaceMarkerOnMap @0x004a18b0. Indoor gating clears the coordinate text and hides the player marker, matching gmMapUI::Update's else branch. - 53-town s_rgLocations table ported verbatim into MapLocations.cs. Markers built once at bind time via the panel's own RowTemplateResolver against m_pMap's authored hotspot-template attrs (0x47/0x48), with literal-string tooltips through AuthoredTooltipText/Enabled (RetailTooltipPresenter) — closes divergence-register row TS-85's last item, gmMapUI::AddMapNote @0x004A1C51. - Structural finding: m_pMap (0x100001EC) is itself authored as a Type-1 BUTTON (the GM click-to-teleport hook at gmMapUI::ListenToElementMessage), and the player/house icons (0x100001ED/EE) are its own NESTED children, not siblings — UiButton.ConsumesDatChildren swallows them from the normally-built tree. Both are re-resolved standalone through the same template resolver the town hotspots use and reattached under m_pMap. House tab (gmHouseUI, HousePageController): mounts the ListBox (0x100001E6) with its authored row template, wired to an empty Lines() source by default — genuinely empty until Slice 4's wire lands, matching retail's own PostInit (no Update call, no static content). 21 new tests (7 MapHousePanelControllerTests, 14 MapPageControllerTests): tab table pairing, close button, town-hotspot count/tooltips, calendar formatter golden values (Frostfell 27/119 P.Y., every HourName incl. AndHalf), player/house marker placement and indoor-gating reproduced against the real fixture via already-tested RadarCoordinates (no re-derivation). Fixture map_house_2100006E_1000018C.json captured via the shared RetailLayoutFixtureGenerator (other 34 fixtures deliberately NOT regenerated — out of scope for this batch, would touch unrelated panels' schema drift). Full solution builds clean; App suite 5391/0 failed/71 skipped (non-live; one earlier flaky streaming failure unrelated to this change, confirmed pre-existing on the branch before these commits). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
04841b6807
commit
6b0fa4ff0d
15 changed files with 5865 additions and 3 deletions
|
|
@ -289,6 +289,20 @@ public sealed record SocialRuntimeBindings(
|
|||
// Trailing/optional per the established compatibility convention.
|
||||
AcDream.Runtime.Gameplay.IRuntimeTradeView? Trade = null);
|
||||
|
||||
/// <summary>
|
||||
/// Batch C (overnight hover/UI round, 2026-08-17): bindings for the
|
||||
/// two-tab Map/House panel. <see cref="HousePosition"/> defaults to
|
||||
/// "no house" and <see cref="HouseLines"/> to empty when the caller doesn't
|
||||
/// wire the House wire groundwork — the panel still mounts and the Map tab
|
||||
/// still works standalone.
|
||||
/// </summary>
|
||||
public sealed record MapHouseRuntimeBindings(
|
||||
Func<AcDream.Core.World.DerethDateTime.Calendar> CurrentCalendar,
|
||||
Func<uint> PlayerCellId,
|
||||
Func<CreateObject.ServerPosition?>? HousePosition = null,
|
||||
Func<IReadOnlyList<string>>? HouseLines = null,
|
||||
Action? HouseShown = null);
|
||||
|
||||
public sealed record InventoryRuntimeBindings(
|
||||
ClientObjectTable Objects,
|
||||
Func<uint> PlayerGuid,
|
||||
|
|
@ -451,6 +465,7 @@ public sealed record RetailUiRuntimeBindings(
|
|||
AppraisalRuntimeBindings Appraisal,
|
||||
OptionsRuntimeBindings Options,
|
||||
SocialRuntimeBindings Social,
|
||||
MapHouseRuntimeBindings MapHouse,
|
||||
StackSplitQuantityState StackSplitQuantity,
|
||||
BufferedUiRegistry? Plugins,
|
||||
RetailUiPersistenceBindings? Persistence,
|
||||
|
|
@ -542,6 +557,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
MountDialogFactory();
|
||||
MountTooltipPresenter();
|
||||
MountSocialPanel();
|
||||
MountMapHousePanel();
|
||||
MountCharacter();
|
||||
MountPlugins();
|
||||
MountInventory();
|
||||
|
|
@ -650,6 +666,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
public VendorUiController? VendorController { get; private set; }
|
||||
public OptionsPanelController? OptionsPanelController { get; private set; }
|
||||
public SocialPanelController? SocialPanelController { get; private set; }
|
||||
public MapHousePanelController? MapHousePanelController { get; private set; }
|
||||
internal CharacterManagementUiController? CharacterManagementController =>
|
||||
_characterManagementMount?.Controller;
|
||||
internal CharacterCreationUiController? CharacterCreationController =>
|
||||
|
|
@ -822,6 +839,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
SelectedObjectController?.Tick(deltaSeconds);
|
||||
ExternalContainerController?.Tick();
|
||||
SocialPanelController?.Tick();
|
||||
MapHousePanelController?.Tick(deltaSeconds);
|
||||
_itemCooldownController?.Tick();
|
||||
_characterManagementMount?.Tick();
|
||||
CharacterManagementController?.Tick();
|
||||
|
|
@ -3256,6 +3274,117 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
Console.WriteLine("[UI] retail social panel from LayoutDesc importer (0x2100006E slot 0x1000018F).");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Batch C (overnight hover/UI round, 2026-08-17): the two-tab Map/House
|
||||
/// panel — host <c>0x2100006E</c> slot <c>0x1000018C</c>,
|
||||
/// <see cref="RetailPanelCatalog.MapHouse"/> id 16. Same import/Build/Bind
|
||||
/// recipe as <see cref="MountSocialPanel"/>. The House tab's ListBox rows
|
||||
/// resolve through its own authored template directly via
|
||||
/// <see cref="UiTemplateListBox.AddItemFromTemplateList"/> (a FIXED
|
||||
/// single template, unlike Friends/Squelch/Fellowship's live-roster
|
||||
/// row families) — the <see cref="Layout.RowTemplateResolver"/> below
|
||||
/// serves only the Map tab's per-town hotspot template.
|
||||
/// </summary>
|
||||
private void MountMapHousePanel()
|
||||
{
|
||||
ElementInfo? rootInfo;
|
||||
ImportedLayout? layout;
|
||||
var strings = new DatStringResolver(_bindings.Assets.Dats);
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
rootInfo = LayoutImporter.ImportInfos(
|
||||
_bindings.Assets.Dats,
|
||||
Layout.MapHousePanelController.HostLayoutId,
|
||||
Layout.MapHousePanelController.SlotElementId);
|
||||
layout = rootInfo is null
|
||||
? null
|
||||
: LayoutImporter.Build(
|
||||
rootInfo,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont,
|
||||
strings.Resolve);
|
||||
}
|
||||
if (rootInfo is null || layout is null)
|
||||
{
|
||||
Console.WriteLine("[UI] Map/House panel: LayoutDesc 0x2100006E slot 0x1000018C not found.");
|
||||
return;
|
||||
}
|
||||
|
||||
// The town-hotspot template (m_pMap's own 0x47/0x48 attrs) is
|
||||
// resolved once and cached — same "resolve the ElementInfo once,
|
||||
// Build a fresh UiElement per call" shape RowTemplateResolver uses
|
||||
// for the social panel's row families.
|
||||
var hotspotTemplate = new Layout.RowTemplateResolver(
|
||||
(templateLayoutId, templateElementId) => LayoutImporter.ImportInfos(
|
||||
_bindings.Assets.Dats, templateLayoutId, templateElementId),
|
||||
info => LayoutImporter.Build(
|
||||
info,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont).Root);
|
||||
UiElement? ResolveHotspotTemplate(uint templateLayoutId, uint templateElementId)
|
||||
{
|
||||
lock (_bindings.Assets.DatLock)
|
||||
return hotspotTemplate.Resolve(templateLayoutId, templateElementId);
|
||||
}
|
||||
|
||||
MapHouseRuntimeBindings mh = _bindings.MapHouse;
|
||||
var callbacks = new Layout.MapHousePanelController.Callbacks(
|
||||
Toggle: () => ToggleWindow(WindowNames.MapHouse),
|
||||
Map: new Layout.MapPageController.Bindings(
|
||||
CurrentCalendar: mh.CurrentCalendar,
|
||||
PlayerCellId: mh.PlayerCellId,
|
||||
HousePosition: mh.HousePosition ?? (static () => null),
|
||||
TemplateResolver: ResolveHotspotTemplate),
|
||||
House: new Layout.HousePageController.Bindings(
|
||||
Lines: mh.HouseLines ?? (static () => Array.Empty<string>()),
|
||||
OnShown: mh.HouseShown));
|
||||
|
||||
Layout.MapHousePanelController? controller;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
controller = Layout.MapHousePanelController.Bind(rootInfo, layout, callbacks);
|
||||
if (controller is null)
|
||||
{
|
||||
Console.WriteLine("[UI] Map/House panel: required root did not build as UiTabPanel.");
|
||||
return;
|
||||
}
|
||||
|
||||
controller.ActivateTabs();
|
||||
MapHousePanelController = controller;
|
||||
|
||||
RetailWindowHandle handle = RetailWindowFrame.Mount(
|
||||
Host.Root,
|
||||
controller.Root,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
new RetailWindowFrame.Options
|
||||
{
|
||||
WindowName = WindowNames.MapHouse,
|
||||
Chrome = RetailWindowChrome.NineSlice,
|
||||
Left = 240f,
|
||||
Top = 160f,
|
||||
Visible = false,
|
||||
ResizeX = false,
|
||||
ResizeY = false,
|
||||
ConstrainDragToParent = true,
|
||||
ConstrainResizeToParent = true,
|
||||
ContentAnchors = AnchorEdges.Left | AnchorEdges.Top
|
||||
| AnchorEdges.Right | AnchorEdges.Bottom,
|
||||
ContentClickThrough = false,
|
||||
DrawChromeCenter = !AuthorsFullPanelCenter(rootInfo),
|
||||
Controller = controller,
|
||||
});
|
||||
_panelUi.RegisterMainPanel(
|
||||
RetailPanelCatalog.MapHouse,
|
||||
WindowNames.MapHouse,
|
||||
handle,
|
||||
rootInfo.TryGetEffectiveBool(
|
||||
RetailPanelUiController.RestorePreviousPropertyId,
|
||||
out bool restorePrevious)
|
||||
&& restorePrevious);
|
||||
Console.WriteLine("[UI] retail Map/House panel from LayoutDesc importer (0x2100006E slot 0x1000018C).");
|
||||
}
|
||||
|
||||
private void MountDialogFactory()
|
||||
{
|
||||
if (DialogFactory is not null)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue