fix(client): restore retail interaction parity
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:
parent
0c699240e0
commit
f6fe0f2a4f
151 changed files with 10162 additions and 1211 deletions
|
|
@ -85,6 +85,28 @@ internal sealed class CameraFrameController : ICameraFramePhase
|
|||
retail.AdjustPitch(+adjustment * 0.02f);
|
||||
if (input.Lower)
|
||||
retail.AdjustPitch(-adjustment * 0.02f);
|
||||
if (input.RotateLeft)
|
||||
retail.YawOffset += adjustment * 0.02f;
|
||||
if (input.RotateRight)
|
||||
retail.YawOffset -= adjustment * 0.02f;
|
||||
}
|
||||
else
|
||||
{
|
||||
ChaseCameraAdjustmentInput input = _input.CaptureChaseAdjustment();
|
||||
float adjustment = CameraDiagnostics.CameraAdjustmentSpeed
|
||||
* timing.SimulationDeltaSecondsSingle;
|
||||
if (input.ZoomIn)
|
||||
legacy.AdjustDistance(-adjustment);
|
||||
if (input.ZoomOut)
|
||||
legacy.AdjustDistance(+adjustment);
|
||||
if (input.Raise)
|
||||
legacy.AdjustPitch(+adjustment * 0.02f);
|
||||
if (input.Lower)
|
||||
legacy.AdjustPitch(-adjustment * 0.02f);
|
||||
if (input.RotateLeft)
|
||||
legacy.YawOffset += adjustment * 0.02f;
|
||||
if (input.RotateRight)
|
||||
legacy.YawOffset -= adjustment * 0.02f;
|
||||
}
|
||||
|
||||
if (!_localFrame.TryGetPresentationAfterNetwork(out var playerFrame))
|
||||
|
|
|
|||
|
|
@ -10,6 +10,21 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class ChaseCamera : ICamera
|
||||
{
|
||||
private const float RetailDefaultBack = 2.5f;
|
||||
private const float RetailDefaultUp = 0.75f;
|
||||
private bool _lookingDown;
|
||||
private bool _mapMode;
|
||||
private bool _inHead;
|
||||
private bool _savedInHead;
|
||||
private float _savedDistance;
|
||||
private float _savedPitch;
|
||||
private float _savedYawOffset;
|
||||
private Vector3? _targetDirectionLocal;
|
||||
private Vector3? _savedTargetDirectionLocal;
|
||||
|
||||
public bool IsLookingDown => _lookingDown;
|
||||
public bool IsMapMode => _mapMode;
|
||||
public bool IsInHead => _inHead;
|
||||
public Vector3 Position { get; private set; }
|
||||
public float Aspect { get; set; } = 16f / 9f;
|
||||
// #389: smartbox law at the 16:9 default aspect — see RetailFieldOfView.
|
||||
|
|
@ -108,10 +123,35 @@ public sealed class ChaseCamera : ICamera
|
|||
float horizontalDist = Distance * MathF.Cos(Pitch);
|
||||
float verticalDist = Distance * MathF.Sin(Pitch);
|
||||
|
||||
Position = new Vector3(
|
||||
playerPosition.X - forwardX * horizontalDist,
|
||||
playerPosition.Y - forwardY * horizontalDist,
|
||||
_trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne)
|
||||
if (_inHead)
|
||||
{
|
||||
Vector3 forward = new(MathF.Cos(playerYaw), MathF.Sin(playerYaw), 0f);
|
||||
Position = new Vector3(
|
||||
playerPosition.X,
|
||||
playerPosition.Y,
|
||||
_trackedZ + EyeHeight) + forward * 0.18f;
|
||||
_lookAt = Position + forward;
|
||||
}
|
||||
else if (_targetDirectionLocal is { } localDirection)
|
||||
{
|
||||
Vector3 pivot = new(playerPosition.X, playerPosition.Y, _trackedZ + EyeHeight);
|
||||
var directedPose = RetailChaseCamera.ComputeTargetDirectionPose(
|
||||
pivot,
|
||||
new Vector3(MathF.Cos(playerYaw), MathF.Sin(playerYaw), 0f),
|
||||
Distance,
|
||||
Pitch,
|
||||
localDirection);
|
||||
Position = directedPose.eye;
|
||||
Vector3 direction = directedPose.forward;
|
||||
_lookAt = Position + direction;
|
||||
}
|
||||
else
|
||||
{
|
||||
Position = new Vector3(
|
||||
playerPosition.X - forwardX * horizontalDist,
|
||||
playerPosition.Y - forwardY * horizontalDist,
|
||||
_trackedZ + EyeHeight + verticalDist); // ← uses tracked Z (pinned to ground while airborne)
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -119,6 +159,8 @@ public sealed class ChaseCamera : ICamera
|
|||
/// </summary>
|
||||
public void AdjustPitch(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
|
||||
}
|
||||
|
||||
|
|
@ -127,6 +169,101 @@ public sealed class ChaseCamera : ICamera
|
|||
/// </summary>
|
||||
public void AdjustDistance(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax);
|
||||
}
|
||||
|
||||
public void SetRetailDefaultView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
EyeHeight = 1.5f;
|
||||
SetViewerOffset(RetailDefaultBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void SetRetailFirstPersonView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = true;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
Distance = 0.18f;
|
||||
Pitch = 0f;
|
||||
}
|
||||
|
||||
public void ToggleRetailLookDownView()
|
||||
{
|
||||
if (_lookingDown)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(2f, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void ToggleRetailMapModeView()
|
||||
{
|
||||
if (_mapMode)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
if (!_lookingDown)
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = true;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(450f, RetailDefaultUp);
|
||||
}
|
||||
|
||||
private void SaveLookDownView()
|
||||
{
|
||||
_savedDistance = Distance;
|
||||
_savedPitch = Pitch;
|
||||
_savedYawOffset = YawOffset;
|
||||
_savedTargetDirectionLocal = _targetDirectionLocal;
|
||||
_savedInHead = _inHead;
|
||||
}
|
||||
|
||||
private void RestoreLookDownView()
|
||||
{
|
||||
Distance = _savedDistance;
|
||||
Pitch = _savedPitch;
|
||||
YawOffset = _savedYawOffset;
|
||||
_targetDirectionLocal = _savedTargetDirectionLocal;
|
||||
_inHead = _savedInHead;
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
}
|
||||
|
||||
private void ExitLookDownForAdjustment()
|
||||
{
|
||||
if (_lookingDown)
|
||||
RestoreLookDownView();
|
||||
}
|
||||
|
||||
private void ExitInHeadForAdjustment()
|
||||
{
|
||||
if (!_inHead)
|
||||
return;
|
||||
_inHead = false;
|
||||
Distance = DistanceMin;
|
||||
}
|
||||
|
||||
private void SetViewerOffset(float back, float up)
|
||||
{
|
||||
Distance = MathF.Sqrt(back * back + up * up);
|
||||
Pitch = MathF.Atan2(up, back);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using AcDream.Core.Plugins;
|
||||
using AcDream.App.Composition;
|
||||
using AcDream.App.Input;
|
||||
using AcDream.App.Physics;
|
||||
using AcDream.App.Rendering.Gpu;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
|
|
@ -17,6 +18,7 @@ using DatReaderWriter;
|
|||
using Silk.NET.Input;
|
||||
using Silk.NET.Maths;
|
||||
using Silk.NET.Windowing;
|
||||
using AcDream.UI.Abstractions.Input;
|
||||
|
||||
namespace AcDream.App.Rendering;
|
||||
|
||||
|
|
@ -599,14 +601,18 @@ public sealed class GameWindow :
|
|||
// startup — no other call to RetailDefaults() / AcdreamCurrentDefaults()
|
||||
// should land in the GameWindow construction path.
|
||||
private readonly AcDream.UI.Abstractions.Input.KeyBindings _keyBindings;
|
||||
private bool _keyBindingsPersisted;
|
||||
private readonly GraphicalHostPlatformServices _platformServices;
|
||||
private readonly ApplicationPathSet _applicationPaths;
|
||||
|
||||
private static AcDream.UI.Abstractions.Input.KeyBindings LoadStartupKeyBindings(
|
||||
string path)
|
||||
{
|
||||
var bindings = AcDream.UI.Abstractions.Input.KeyBindings.LoadOrDefault(path);
|
||||
Console.WriteLine($"keybinds: loaded {bindings.All.Count} bindings from {path}");
|
||||
var bindings = AcDream.App.Input.RetailKeymapProfileStore.LoadActiveOrJson(
|
||||
path, out string profileName);
|
||||
Console.WriteLine(
|
||||
$"keybinds: loaded {bindings.All.Count} bindings; active retail profile "
|
||||
+ $"'{profileName}', JSON mirror {path}");
|
||||
return bindings;
|
||||
}
|
||||
|
||||
|
|
@ -1522,7 +1528,8 @@ public sealed class GameWindow :
|
|||
hostInputCamera.GpuFrameLifetime,
|
||||
() => WorldTime.CurrentCalendar,
|
||||
settingsDevTools.RenderPacks,
|
||||
_renderPackDiagnostics.CaptureDiagnostics),
|
||||
_renderPackDiagnostics.CaptureDiagnostics,
|
||||
_applicationPaths.ScreenshotsDirectory),
|
||||
_retailUiLease,
|
||||
this).Compose(
|
||||
platformResult,
|
||||
|
|
@ -1569,6 +1576,7 @@ public sealed class GameWindow :
|
|||
_cellVisibility,
|
||||
_liveWorldOrigin,
|
||||
_localPlayerIdentity,
|
||||
_chaseCameraInput,
|
||||
_pointerPosition,
|
||||
_playerApproachCompletions,
|
||||
_renderResourceLifetime,
|
||||
|
|
@ -1821,6 +1829,7 @@ public sealed class GameWindow :
|
|||
|
||||
if (!_lifetime.HasShutdownRoots)
|
||||
{
|
||||
PersistKeyBindingsAtShutdown();
|
||||
// Campaign LA slice LA1: capture BEFORE the shutdown roots run —
|
||||
// by the time teardown completes, IsInWorld is always false
|
||||
// regardless of whether a real session was ever connected.
|
||||
|
|
@ -1861,6 +1870,33 @@ public sealed class GameWindow :
|
|||
ReportExited(report);
|
||||
}
|
||||
|
||||
private void PersistKeyBindingsAtShutdown()
|
||||
{
|
||||
// Construction-only tests and failed starts never create the input
|
||||
// dispatcher. They must not materialize a profile in the real user's
|
||||
// Documents folder merely because the half-built window is disposed.
|
||||
if (_keyBindingsPersisted || _inputDispatcher is null) return;
|
||||
_keyBindingsPersisted = true;
|
||||
try
|
||||
{
|
||||
KeyBindings current = _inputDispatcher.Bindings;
|
||||
var profiles = new AcDream.App.Input.RetailKeymapProfileStore(
|
||||
_applicationPaths.KeyBindingsFile);
|
||||
RetailKeymapSaveResult saved = profiles.SaveActive(current);
|
||||
if (saved.Status != RetailKeymapSaveStatus.Saved)
|
||||
{
|
||||
Console.WriteLine(
|
||||
$"keymap: shutdown save failed ({saved.Status}): {saved.Error}");
|
||||
return;
|
||||
}
|
||||
current.SaveToFile(_applicationPaths.KeyBindingsFile);
|
||||
}
|
||||
catch (Exception failure)
|
||||
{
|
||||
Console.WriteLine($"keymap: shutdown persistence failed: {failure.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes the ONE terminal "exited" status event for this session
|
||||
/// (fix #406). A resource-shutdown transaction can converge cleanly
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ internal interface IPaperdollDollRenderer
|
|||
{
|
||||
void SetDoll(WorldEntity? doll);
|
||||
|
||||
void Prepare();
|
||||
|
||||
uint Render(int width, int height);
|
||||
}
|
||||
|
||||
|
|
@ -73,9 +75,6 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
|
||||
public void Render()
|
||||
{
|
||||
if (!_view.TryGetVisibleSize(out int width, out int height))
|
||||
return;
|
||||
|
||||
if (_dirty)
|
||||
{
|
||||
if (_factory.TryBuild(out WorldEntity? doll))
|
||||
|
|
@ -101,6 +100,11 @@ internal sealed class PaperdollFramePresenter : IPrivateEntityViewportFrame
|
|||
}
|
||||
}
|
||||
|
||||
_renderer.Prepare();
|
||||
|
||||
if (!_view.TryGetVisibleSize(out int width, out int height))
|
||||
return;
|
||||
|
||||
_view.SetTextureHandle(_renderer.Render(width, height));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ public sealed class PaperdollViewportRenderer :
|
|||
|
||||
public void SetDoll(WorldEntity? doll) => _renderer.SetEntity(doll);
|
||||
|
||||
public void Prepare() => _renderer.Prepare();
|
||||
|
||||
public uint Render(int width, int height) =>
|
||||
_renderer.Render(width, height);
|
||||
|
||||
|
|
|
|||
|
|
@ -94,6 +94,11 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
/// feature does not exist for them, not just "unused".</summary>
|
||||
private readonly EntitySlot? _backdropSlot;
|
||||
|
||||
// One stable sampled texture-table slot is part of the retained viewport's
|
||||
// presentation contract. Rotating the slot with the Vulkan flight index
|
||||
// made the UI sample a freshly-created/cleared sibling after world reveal.
|
||||
// The frame submission order already protects this target's write -> sample
|
||||
// transition; keep its identity stable until resize or disposal.
|
||||
private IGpuRenderTarget? _target;
|
||||
private IGpuSampler? _sampler;
|
||||
private GpuTextureSlot _slot = GpuTextureSlot.Unassigned;
|
||||
|
|
@ -170,6 +175,29 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
|
||||
public void SetEntity(WorldEntity? entity) => _mainSlot.Set(entity);
|
||||
|
||||
/// <summary>
|
||||
/// Advances the private entity's mesh and texture-composite readiness
|
||||
/// without allocating or clearing a render target. Paperdoll uses this
|
||||
/// while its tab is hidden so first-open work is already resident.
|
||||
/// </summary>
|
||||
public bool Prepare()
|
||||
{
|
||||
if (!_mainSlot.PrepareForDraw()
|
||||
|| !(_backdropSlot?.PrepareForDraw() ?? true))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
WorldEntity? entity = _mainSlot.Entity;
|
||||
if (entity is null || entity.MeshRefs.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
IReadOnlyList<WorldEntity> entities = BuildDrawEntities(
|
||||
_backdropSlot?.Entity,
|
||||
entity);
|
||||
return _dispatcher.PreparePrivateEntityResources(entities);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets or clears the environment backdrop entity drawn BEHIND the main
|
||||
/// entity — GF-7/GF-14's fix, retail's <c>gmCG3DView::m_pbgObject</c>. Only
|
||||
|
|
@ -219,6 +247,16 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
if (entity is null || entity.MeshRefs.Count == 0 || width <= 0 || height <= 0)
|
||||
return 0u;
|
||||
|
||||
IReadOnlyList<WorldEntity> drawEntities = BuildDrawEntities(
|
||||
_backdropSlot?.Entity,
|
||||
entity);
|
||||
if (!_dispatcher.PreparePrivateEntityResources(drawEntities))
|
||||
{
|
||||
return _hasRenderedScene && _slot.IsAssigned
|
||||
? UiTextureTableHandle.FromSlot(_slot)
|
||||
: 0u;
|
||||
}
|
||||
|
||||
EnsureRenderTarget(width, height);
|
||||
if (_target is null)
|
||||
return 0u;
|
||||
|
|
@ -254,7 +292,6 @@ internal sealed class PrivateEntityViewportRenderer :
|
|||
|
||||
UploadCreatureLight();
|
||||
|
||||
IReadOnlyList<WorldEntity> drawEntities = BuildDrawEntities(_backdropSlot?.Entity, entity);
|
||||
var entries =
|
||||
new (uint, Vector3, Vector3, IReadOnlyList<WorldEntity>,
|
||||
IReadOnlyDictionary<uint, WorldEntity>?)[]
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ namespace AcDream.App.Rendering;
|
|||
/// </summary>
|
||||
public sealed class RetailChaseCamera : ICamera
|
||||
{
|
||||
private const float RetailDefaultBack = 2.5f;
|
||||
private const float RetailDefaultUp = 0.75f;
|
||||
private const float RetailLookDownBack = 2f;
|
||||
private const float RetailMapBack = 450f;
|
||||
private const float RetailFirstPersonForward = 0.18f;
|
||||
|
||||
// ICamera surface.
|
||||
public Vector3 Position { get; private set; }
|
||||
|
||||
|
|
@ -75,6 +81,20 @@ public sealed class RetailChaseCamera : ICamera
|
|||
/// <summary>Height of look-at anchor above the player's feet (m). Retail default 1.5.</summary>
|
||||
public float PivotHeight { get; set; } = 1.5f;
|
||||
|
||||
private bool _lookingDown;
|
||||
private bool _mapMode;
|
||||
private bool _inHead;
|
||||
private bool _savedInHead;
|
||||
private float _savedDistance;
|
||||
private float _savedPitch;
|
||||
private float _savedYawOffset;
|
||||
private Vector3? _targetDirectionLocal;
|
||||
private Vector3? _savedTargetDirectionLocal;
|
||||
|
||||
public bool IsLookingDown => _lookingDown;
|
||||
public bool IsMapMode => _mapMode;
|
||||
public bool IsInHead => _inHead;
|
||||
|
||||
/// <summary>
|
||||
/// Optional spring-arm collision probe. When set (and
|
||||
/// <see cref="CameraDiagnostics.CollideCamera"/> is true), the damped eye
|
||||
|
|
@ -172,8 +192,13 @@ public sealed class RetailChaseCamera : ICamera
|
|||
// target supplies the frame heading. Without this local rotation, enabling
|
||||
// Keep in View snaps the camera behind the target and disables RMB orbit.
|
||||
float viewerYawOffset = trackedHeading.HasValue ? YawOffset : 0f;
|
||||
(Vector3 targetEye, Vector3 targetForward) = ComputeDesiredPose(
|
||||
pivotWorld, heading, Distance, Pitch, viewerYawOffset);
|
||||
(Vector3 targetEye, Vector3 targetForward) = _inHead
|
||||
? ComputeInHeadPose(pivotWorld, heading)
|
||||
: _targetDirectionLocal is { } localDirection
|
||||
? ComputeTargetDirectionPose(
|
||||
pivotWorld, heading, Distance, Pitch, localDirection)
|
||||
: ComputeDesiredPose(
|
||||
pivotWorld, heading, Distance, Pitch, viewerYawOffset);
|
||||
|
||||
// 5. Stateful sought position (#180). Retail CameraManager::UpdateCamera
|
||||
// (0x00456660) interpolates FROM THE CURRENT SWEPT VIEWER toward the
|
||||
|
|
@ -279,16 +304,120 @@ public sealed class RetailChaseCamera : ICamera
|
|||
/// <see cref="DistanceMin"/>..<see cref="DistanceMax"/>. Mirrors
|
||||
/// legacy <c>ChaseCamera.AdjustDistance</c>.
|
||||
/// </summary>
|
||||
public void AdjustDistance(float delta) =>
|
||||
public void AdjustDistance(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Distance = Math.Clamp(Distance + delta, DistanceMin, DistanceMax);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adjust the camera pitch by a delta (radians), clamped to
|
||||
/// <see cref="PitchMin"/>..<see cref="PitchMax"/>. Mirrors legacy
|
||||
/// <c>ChaseCamera.AdjustPitch</c>.
|
||||
/// </summary>
|
||||
public void AdjustPitch(float delta) =>
|
||||
public void AdjustPitch(float delta)
|
||||
{
|
||||
ExitLookDownForAdjustment();
|
||||
ExitInHeadForAdjustment();
|
||||
Pitch = Math.Clamp(Pitch + delta, PitchMin, PitchMax);
|
||||
}
|
||||
|
||||
public void SetRetailDefaultView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
PivotHeight = 1.5f;
|
||||
SetViewerOffset(RetailDefaultBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void SetRetailFirstPersonView()
|
||||
{
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
_inHead = true;
|
||||
_targetDirectionLocal = null;
|
||||
YawOffset = 0f;
|
||||
Distance = RetailFirstPersonForward;
|
||||
Pitch = 0f;
|
||||
// Do not spend a transition frame inside the head/neck. Retail's
|
||||
// SetInHead installs the new viewer offset as one camera preset.
|
||||
_initialised = false;
|
||||
}
|
||||
|
||||
public void ToggleRetailLookDownView()
|
||||
{
|
||||
if (_lookingDown)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = false;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(RetailLookDownBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
public void ToggleRetailMapModeView()
|
||||
{
|
||||
if (_mapMode)
|
||||
{
|
||||
RestoreLookDownView();
|
||||
return;
|
||||
}
|
||||
if (!_lookingDown)
|
||||
SaveLookDownView();
|
||||
_lookingDown = true;
|
||||
_mapMode = true;
|
||||
_inHead = false;
|
||||
_targetDirectionLocal = new Vector3(0f, 0.5f, -1.8f);
|
||||
SetViewerOffset(RetailMapBack, RetailDefaultUp);
|
||||
}
|
||||
|
||||
private void SaveLookDownView()
|
||||
{
|
||||
_savedDistance = Distance;
|
||||
_savedPitch = Pitch;
|
||||
_savedYawOffset = YawOffset;
|
||||
_savedTargetDirectionLocal = _targetDirectionLocal;
|
||||
_savedInHead = _inHead;
|
||||
}
|
||||
|
||||
private void RestoreLookDownView()
|
||||
{
|
||||
Distance = _savedDistance;
|
||||
Pitch = _savedPitch;
|
||||
YawOffset = _savedYawOffset;
|
||||
_targetDirectionLocal = _savedTargetDirectionLocal;
|
||||
_inHead = _savedInHead;
|
||||
_lookingDown = false;
|
||||
_mapMode = false;
|
||||
}
|
||||
|
||||
private void ExitLookDownForAdjustment()
|
||||
{
|
||||
if (_lookingDown)
|
||||
RestoreLookDownView();
|
||||
}
|
||||
|
||||
private void ExitInHeadForAdjustment()
|
||||
{
|
||||
if (!_inHead)
|
||||
return;
|
||||
_inHead = false;
|
||||
Distance = DistanceMin;
|
||||
}
|
||||
|
||||
private void SetViewerOffset(float back, float up)
|
||||
{
|
||||
Distance = MathF.Sqrt(back * back + up * up);
|
||||
Pitch = MathF.Atan2(up, back);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Public entry point for the mouse-input low-pass filter. Calls
|
||||
|
|
@ -436,6 +565,47 @@ public sealed class RetailChaseCamera : ICamera
|
|||
return (eye, forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Retail <c>CameraSet::SetInHead @ 0x00458CE0</c>: the target direction
|
||||
/// is local +Y and the viewer offset is local +Y * 0.18. It is not a
|
||||
/// negative chase boom looking back toward the player's neck.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward) ComputeInHeadPose(
|
||||
Vector3 pivotWorld,
|
||||
Vector3 heading)
|
||||
{
|
||||
Vector3 forward = Vector3.Normalize(heading);
|
||||
return (pivotWorld + forward * RetailFirstPersonForward, forward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Transform both retail <c>viewer_offset</c> and <c>target_direction</c>
|
||||
/// through the target frame. LookDown/MapMode do not merely point a
|
||||
/// horizontally-positioned camera toward the ground: the downward target
|
||||
/// direction pitches the frame whose -Y/+Z offset places the viewer. For
|
||||
/// MapMode's (0,-450,0.75) offset this puts the viewer high above and only
|
||||
/// slightly behind the character, matching CameraSet::SetMapMode.
|
||||
/// </summary>
|
||||
internal static (Vector3 eye, Vector3 forward) ComputeTargetDirectionPose(
|
||||
Vector3 pivotWorld,
|
||||
Vector3 heading,
|
||||
float distance,
|
||||
float pitch,
|
||||
Vector3 targetDirectionLocal)
|
||||
{
|
||||
var (frameForward, frameRight, frameUp) = BuildBasis(heading);
|
||||
Vector3 targetForward = Vector3.Normalize(
|
||||
frameForward * targetDirectionLocal.Y
|
||||
- frameRight * targetDirectionLocal.X
|
||||
+ frameUp * targetDirectionLocal.Z);
|
||||
var (_, _, targetUp) = BuildBasis(targetForward);
|
||||
|
||||
float back = distance * MathF.Cos(pitch);
|
||||
float up = distance * MathF.Sin(pitch);
|
||||
Vector3 eye = pivotWorld - targetForward * back + targetUp * up;
|
||||
return (eye, targetForward);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build an orthonormal basis with <c>forward = heading</c>. World
|
||||
/// up is <c>(0, 0, 1)</c>; if <c>heading</c> is near-parallel to it
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue