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

@ -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