From 310dbd59b864a54bec401bb7b938d014322a5ebf Mon Sep 17 00:00:00 2001 From: Alexandru Macocian Date: Thu, 5 Mar 2026 14:03:31 +0100 Subject: [PATCH] Move to fully managed frame --- Daybreak.API/Services/PartyService.cs | 115 ++++++++------------------ 1 file changed, 33 insertions(+), 82 deletions(-) diff --git a/Daybreak.API/Services/PartyService.cs b/Daybreak.API/Services/PartyService.cs index 8f68844b..6cef8ec0 100644 --- a/Daybreak.API/Services/PartyService.cs +++ b/Daybreak.API/Services/PartyService.cs @@ -343,8 +343,9 @@ public sealed class PartyService : IHostedService /// /// Intercepts TemplatesSummary_PopulateSkillData when a team build is cached. - /// Creates an independent floating frame with skill template previews for - /// each build beyond the first, positioned relative to the template dialog. + /// Blocks the game's own rendering and creates our own floating frame + /// containing ALL builds (0 through N-1), positioned near the template dialog. + /// Returns true to suppress the game's PopulateSkillData entirely. /// private unsafe bool PopulateSkillData(WrappedPointer frameData, WrappedPointer skillTemplateData) { @@ -370,20 +371,17 @@ public sealed class PartyService : IHostedService return false; } - // If we already have a floating preview for this template, don't recreate + // If we already have a floating preview for this template, suppress the game's draw if (this.floatingPreviewFrame != 0) { - return false; + return true; } scopedLogger.LogDebug( - "Creating floating preview for {count} extra builds", - teamBuild.Builds.Count - 1); + "Creating floating preview for all {count} builds", + teamBuild.Builds.Count); - // Create a container frame to hold the extra build previews. - // CreateContainerFrame installs the ContainerFrameHandler which handles - // layout (kSetLayout) and measure (kMeasureContent) messages, and allocates - // a ContainerContext to track child count, height, and spacing. + // Create a container frame for ALL builds (including build 0). fixed (char* label = "DaybreakTeamPreview") { var floatingFrame = GWCA.GW.FrameMgr.CreateContainerFrame( @@ -397,17 +395,11 @@ public sealed class PartyService : IHostedService this.floatingPreviewFrame = (nint)floatingFrame; - scopedLogger.LogDebug( - "Floating frame created: FrameId={frameId}, IsCreated={isCreated}, IsVisible={isVisible}, IsHidden={isHidden}, State=0x{state:X}", - floatingFrame->FrameId, floatingFrame->IsCreated, floatingFrame->IsVisible, floatingFrame->IsHidden, floatingFrame->FrameState); - this.isPopulatingExtraBuilds = true; try { - // PopulateSkillTemplatePreview now handles the full lifecycle: - // creates a child frame with SkillFrameHandler, allocates context, - // creates sub-children (skill icons, labels), then populates with template data. - for (var i = 1; i < teamBuild.Builds.Count; i++) + // Create and populate a child frame for EVERY build in the team + for (var i = 0; i < teamBuild.Builds.Count; i++) { var template = new SkillTemplate(); PopulateSkillTemplate(teamBuild.Builds[i], &template); @@ -417,8 +409,6 @@ public sealed class PartyService : IHostedService scopedLogger.LogWarning("Failed to create and populate preview for build {index}", i); continue; } - - scopedLogger.LogDebug("Created and populated preview for build {index}", i); } } finally @@ -426,33 +416,14 @@ public sealed class PartyService : IHostedService this.isPopulatingExtraBuilds = false; } - // Trigger the container's built-in layout handler to compute - // child sizes and stack them vertically before positioning. - var layoutResult = GWCA.GW.FrameMgr.LayoutContainer(floatingFrame); + // Compute desired height for all builds var desiredH = GWCA.GW.FrameMgr.GetContainerDesiredHeight(floatingFrame); + scopedLogger.LogDebug("Container created with {count} builds, desiredHeight={h}", + teamBuild.Builds.Count, desiredH); - scopedLogger.LogDebug( - "Post-layout: LayoutResult={layoutResult}, DesiredHeight={h}", - layoutResult, desiredH); - - // Dump frame position data after layout - var pos = &floatingFrame->Position; - scopedLogger.LogDebug( - "Floating frame position: Flags=0x{flags:X}, " + - "Pos=[L={left}, B={bottom}, R={right}, T={top}], " + - "Content=[L={cl}, B={cb}, R={cr}, T={ct}], " + - "Screen=[L={sl}, B={sb}, R={sr}, T={st}], " + - "State=0x{state:X}, IsVisible={vis}", - pos->Flags, pos->Left, pos->Bottom, pos->Right, pos->Top, - pos->ContentLeft, pos->ContentBottom, pos->ContentRight, pos->ContentTop, - pos->ScreenLeft, pos->ScreenBottom, pos->ScreenRight, pos->ScreenTop, - floatingFrame->FrameState, floatingFrame->IsVisible); - - // Position relative to the original template summary frame. - // PopulateSkillData fires during message 0x55 processing, BEFORE - // the layout pass runs. The original frame's screen coordinates are - // all zeros at this point. Defer positioning to the next game tick - // when ProcessLayoutQueue will have computed valid screen coords. + // Defer positioning until the game's layout pass has computed + // screen coordinates for the original summary frame. We use that + // frame's position as an anchor even though we're replacing its content. var origFrameId = frameData.Pointer->FrameId; var floatingFramePtr = floatingFrame; var height = desiredH; @@ -462,31 +433,15 @@ public sealed class PartyService : IHostedService }, CancellationToken.None, forceEnqueue: true); } - // Let the original proceed for build 0 on the existing frame - return false; - } - - private static unsafe void SetFallbackPosition(Frame* frame, float desiredH) - { - var fpos = (FramePositionData*)((byte*)frame + 0xD8); - fpos->Flags = 0; - fpos->Left = 100f; - fpos->Bottom = 100f; - fpos->Right = 500f; - fpos->Top = 100f + desiredH; - fpos->ScreenLeft = 100f; - fpos->ScreenBottom = 100f; - fpos->ScreenRight = 500f; - fpos->ScreenTop = 100f + desiredH; - GWCA.GW.UI.TriggerFrameRedraw(frame); - GWCA.GW.FrameMgr.LayoutContainer(frame); + // Return true to suppress the game's own PopulateSkillData for build 0. + // We're drawing everything ourselves. + return true; } /// /// Deferred positioning callback. Waits for the original summary frame /// to have valid screen coordinates after the layout pass, then positions - /// the floating preview below it. Retries on subsequent game ticks if - /// the layout hasn't settled yet. + /// the floating preview at the same location (replacing it visually). /// private unsafe void PositionFloatingPreview(uint origFrameId, Frame* floatingFrame, float desiredH, int retriesRemaining) { @@ -495,7 +450,6 @@ public sealed class PartyService : IHostedService if (origFrame is null) { scopedLogger.LogWarning("Original frame {fid} no longer valid", origFrameId); - SetFallbackPosition(floatingFrame, desiredH); return; } @@ -508,11 +462,9 @@ public sealed class PartyService : IHostedService if (retriesRemaining <= 0) { scopedLogger.LogWarning("Original frame still has zero dimensions after all retries"); - SetFallbackPosition(floatingFrame, desiredH); return; } - // Layout hasn't settled yet — try again next tick _ = this.gameThreadService.QueueOnGameThread(() => { this.PositionFloatingPreview(origFrameId, floatingFrame, desiredH, retriesRemaining - 1); @@ -520,29 +472,28 @@ public sealed class PartyService : IHostedService return; } - scopedLogger.LogDebug( - "Positioning below origFrame={fid}, Screen=[L={sl}, B={sb}, R={sr}, T={st}]", - origFrameId, - origPos->ScreenLeft, origPos->ScreenBottom, origPos->ScreenRight, origPos->ScreenTop); - - // Position our container directly below the original summary frame. + // Position our container at the same location as the original summary frame, + // extending downward to fit all builds. var fpos = (FramePositionData*)((byte*)floatingFrame + 0xD8); fpos->Flags = 0; fpos->Left = origPos->ScreenLeft; - fpos->Bottom = origPos->ScreenBottom - desiredH; - fpos->Right = origPos->ScreenLeft + origWidth; - fpos->Top = origPos->ScreenBottom; + fpos->Top = origPos->ScreenTop; + fpos->Right = origPos->ScreenRight; + fpos->Bottom = origPos->ScreenTop - desiredH; fpos->ScreenLeft = origPos->ScreenLeft; - fpos->ScreenBottom = origPos->ScreenBottom - desiredH; - fpos->ScreenRight = origPos->ScreenLeft + origWidth; - fpos->ScreenTop = origPos->ScreenBottom; + fpos->ScreenTop = origPos->ScreenTop; + fpos->ScreenRight = origPos->ScreenRight; + fpos->ScreenBottom = origPos->ScreenTop - desiredH; GWCA.GW.UI.TriggerFrameRedraw(floatingFrame); - // Re-layout children with the actual container width + // Hide the original frame since we're replacing it + GWCA.GW.UI.SetFrameVisible(origFrame, false); + + // Layout children within the now-sized container GWCA.GW.FrameMgr.LayoutContainer(floatingFrame); scopedLogger.LogDebug( - "Positioned: Screen=[L={sl}, B={sb}, R={sr}, T={st}]", + "Positioned at Screen=[L={sl}, B={sb}, R={sr}, T={st}], origFrame hidden", fpos->ScreenLeft, fpos->ScreenBottom, fpos->ScreenRight, fpos->ScreenTop); }