fix(streaming): preserve portal destination ownership

Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
This commit is contained in:
Erik 2026-07-25 08:35:12 +02:00
parent 2c848d4167
commit 823936ec31
57 changed files with 2551 additions and 324 deletions

View file

@ -53,6 +53,7 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
private readonly IPaperdollDollRenderer _renderer;
private readonly IPaperdollFrameView _view;
private readonly IPaperdollDollFactory _factory;
private WorldEntity? _doll;
private bool _dirty = true;
public PaperdollFramePresenter(
@ -78,20 +79,111 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
{
if (_factory.TryBuild(out WorldEntity? doll))
{
_renderer.SetDoll(doll);
// Same-generation CreateObject refreshes can repeat the exact
// player ObjDesc at a portal boundary. Retail redresses its
// private inventory object in place; releasing and reacquiring
// an identical synthetic owner briefly blanks the viewport and
// churns its texture composites.
if (!HasEquivalentAppearance(_doll, doll))
{
_renderer.SetDoll(doll);
_doll = doll;
}
_dirty = false;
}
else
{
// Retail's private viewport remains present while the live
// player projection is still materializing. Keep the dirty
// edge armed and clear any doll from the prior session.
_renderer.SetDoll(null);
// gmPaperDollUI::RedressCreature @ 0x004A3BC0 leaves its
// private m_pInventoryObject intact when the SmartBox player
// is temporarily unavailable. Keep the successful doll and
// retry this dirty redress on the next visible frame.
}
}
_view.SetTextureHandle(_renderer.Render(width, height));
}
/// <summary>
/// Clears the private object only at the owning character-session
/// boundary, matching gmPaperDollUI's private-object lifetime.
/// </summary>
public void ResetSession()
{
_renderer.SetDoll(null);
_doll = null;
_dirty = true;
}
private static bool HasEquivalentAppearance(
WorldEntity? current,
WorldEntity? candidate)
{
if (current is null || candidate is null)
return ReferenceEquals(current, candidate);
if (current.SourceGfxObjOrSetupId != candidate.SourceGfxObjOrSetupId
|| current.Scale != candidate.Scale
|| current.HiddenPartsMask != candidate.HiddenPartsMask
|| current.MeshRefs.Count != candidate.MeshRefs.Count
|| current.PartOverrides.Count != candidate.PartOverrides.Count)
{
return false;
}
for (int i = 0; i < current.MeshRefs.Count; i++)
{
MeshRef left = current.MeshRefs[i];
MeshRef right = candidate.MeshRefs[i];
if (left.GfxObjId != right.GfxObjId
|| left.PartTransform != right.PartTransform
|| !DictionaryEquals(
left.SurfaceOverrides,
right.SurfaceOverrides))
{
return false;
}
}
for (int i = 0; i < current.PartOverrides.Count; i++)
{
if (current.PartOverrides[i] != candidate.PartOverrides[i])
return false;
}
PaletteOverride? leftPalette = current.PaletteOverride;
PaletteOverride? rightPalette = candidate.PaletteOverride;
if (leftPalette is null || rightPalette is null)
return leftPalette is null && rightPalette is null;
if (leftPalette.BasePaletteId != rightPalette.BasePaletteId
|| leftPalette.SubPalettes.Count != rightPalette.SubPalettes.Count)
{
return false;
}
for (int i = 0; i < leftPalette.SubPalettes.Count; i++)
{
if (leftPalette.SubPalettes[i] != rightPalette.SubPalettes[i])
return false;
}
return true;
}
private static bool DictionaryEquals(
IReadOnlyDictionary<uint, uint>? left,
IReadOnlyDictionary<uint, uint>? right)
{
if (left is null || right is null)
return left is null && right is null;
if (left.Count != right.Count)
return false;
foreach ((uint key, uint value) in left)
{
if (!right.TryGetValue(key, out uint rightValue)
|| rightValue != value)
{
return false;
}
}
return true;
}
}
/// <summary>Retained-UI visibility and texture publication for the doll view.</summary>