fix(client): restore retail interaction parity
All checks were successful
CI / linux-portable (push) Successful in 3m27s
CI / windows-gate (push) Successful in 6m42s
CI / release (push) Successful in 2m12s

Harden keyboard and camera routing, inventory and vendor interactions, chat/emotes, relog portal flow, and paperdoll rendering. Add retail research, connected gate coverage, and release-gate validation.
This commit is contained in:
Erik 2026-08-26 20:45:11 +02:00
parent 0c699240e0
commit f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions

View file

@ -473,6 +473,7 @@ public sealed unsafe partial class WbDrawDispatcher
}
float opacity = PackedPartOpacity(
entity.ServerGuid,
entity.LocalEntityId,
(uint)setupPartIndex);
if (opacity < 1f)
@ -527,6 +528,7 @@ public sealed unsafe partial class WbDrawDispatcher
// one-part assumption and kept the Bind Stone's four
// hook-hidden shard parts visible.
float opacity = PackedPartOpacity(
entity.ServerGuid,
entity.LocalEntityId,
(uint)partIndex);
if (opacity < 1f)
@ -585,20 +587,24 @@ public sealed unsafe partial class WbDrawDispatcher
anyVao != 0 && alphaQueueCollecting;
private float PackedPartOpacity(
uint serverGuid,
uint localEntityId,
uint setupPartIndex)
{
float opacity = EntityOpacity(serverGuid);
if (opacity <= 0f)
return 0f;
if (!_translucencyFades.TryGetCurrentValue(
localEntityId,
setupPartIndex,
out float translucency))
{
return 1f;
return opacity;
}
return translucency >= 1f
? 0f
: 1f - translucency;
: opacity * (1f - translucency);
}
private bool ClassifyPackedBatches(

View file

@ -176,7 +176,8 @@ public sealed unsafe partial class WbDrawDispatcher
RetailAlphaQueue? alphaQueue = null,
long? alphaScratchBudgetBytes = null,
TerrainAtlas.RetailDetailTextureBinding buildingDetail = default,
Func<bool>? buildingDetailEnabled = null)
Func<bool>? buildingDetailEnabled = null,
Func<uint, float>? hierarchicalTranslucency = null)
{
_device = device ?? throw new ArgumentNullException(nameof(device));
_frames = frames ?? throw new ArgumentNullException(nameof(frames));
@ -192,6 +193,7 @@ public sealed unsafe partial class WbDrawDispatcher
_selectionSink = selectionSink;
_selectionLighting = selectionSink as IRetailSelectionLightingSource;
_alphaQueue = alphaQueue;
_hierarchicalTranslucency = hierarchicalTranslucency;
_alphaSource = new AlphaDrawSource(this);
_buildingDetail = buildingDetail;
_buildingDetailEnabled = buildingDetailEnabled ?? DisableDetailTextures;

View file

@ -91,6 +91,7 @@ public sealed partial class WbDrawDispatcher : IDisposable
private readonly IRetailSelectionRenderSink? _selectionSink;
private readonly IRetailSelectionLightingSource? _selectionLighting;
private readonly RetailAlphaQueue? _alphaQueue;
private readonly Func<uint, float>? _hierarchicalTranslucency;
private readonly AlphaDrawSource _alphaSource;
private readonly RetainedScratchCapacityPolicy _alphaScratchPolicy;
private int _scratchPeakUnits;
@ -1784,11 +1785,12 @@ public sealed partial class WbDrawDispatcher : IDisposable
// sets draw_state|=1 and skips the whole part outright — not a
// blend to nothing. TranslucencyFadeManager.AdvanceAll guarantees
// t=1 commits the bitwise-exact value so this check is safe.
float opacityMultiplier = 1.0f;
float opacityMultiplier = EntityOpacity(entity.ServerGuid);
if (opacityMultiplier <= 0f) continue;
if (_translucencyFades.TryGetCurrentValue(entity.Id, (uint)setupPartIndex, out float translucencyValue))
{
if (translucencyValue >= 1.0f) continue; // skip this part's draw entirely
opacityMultiplier = 1f - translucencyValue; // CMaterial::SetTranslucencySimple 0x005396f0
opacityMultiplier *= 1f - translucencyValue; // CMaterial::SetTranslucencySimple 0x005396f0
}
if (!ClassifyBatches(partData, model, entity, meshRef, paletteIdentity, restPose, opacityMultiplier, collector, entityHasCutoutSubset))
@ -1818,12 +1820,14 @@ public sealed partial class WbDrawDispatcher : IDisposable
// entity — the Bind Stone's idle cycle hides its four authored
// shard parts (3-6) with TransparentPartHook start=end=1.0
// every loop, and they stayed visible.
float opacityMultiplier = 1.0f;
float opacityMultiplier = EntityOpacity(entity.ServerGuid);
bool fullyInvisible = false;
if (opacityMultiplier <= 0f)
fullyInvisible = true;
if (_translucencyFades.TryGetCurrentValue(entity.Id, (uint)partIdx, out float translucencyValue))
{
if (translucencyValue >= 1.0f) fullyInvisible = true;
else opacityMultiplier = 1f - translucencyValue;
else opacityMultiplier *= 1f - translucencyValue;
}
if (!fullyInvisible)
@ -1901,6 +1905,32 @@ public sealed partial class WbDrawDispatcher : IDisposable
observeCurrentPath: true);
}
/// <summary>
/// Readiness barrier for a private creature viewport. Unlike the world
/// reveal queue this has no retained scan state: the caller owns a tiny,
/// exact entity list and retries it each frame. A completed result means
/// both mesh render data and every palette/original-texture composite can
/// be classified without clearing the private target to an empty frame.
/// </summary>
internal bool PreparePrivateEntityResources(
IReadOnlyList<WorldEntity> entities)
{
ArgumentNullException.ThrowIfNull(entities);
bool complete = true;
for (int i = 0; i < entities.Count; i++)
{
if (PrepareCompositeEntity(entities[i]) != CompositeWarmupResult.Complete)
complete = false;
}
return complete;
}
private float EntityOpacity(uint serverGuid)
{
float translucency = _hierarchicalTranslucency?.Invoke(serverGuid) ?? 0f;
return 1f - Math.Clamp(translucency, 0f, 1f);
}
/// <summary>
/// Whether there is a mesh source to draw from. The encoder arm has no
/// vertex array of its own — the pipeline owns one shaped by