fix: complete retail parity stability pass
This commit is contained in:
parent
d3df4cb20a
commit
f7aa8e0eb7
131 changed files with 7765 additions and 1190 deletions
|
|
@ -109,4 +109,36 @@ public class InteriorEntityPartitionTests
|
|||
Assert.Empty(result.Dynamics);
|
||||
Assert.Empty(result.OutdoorStatic);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrustumRejectsWholeLandblockBeforeWalkingItsEntities()
|
||||
{
|
||||
const uint culledLandblock = 0xA8B4FFFFu;
|
||||
const uint cameraLandblock = 0xA9B4FFFFu;
|
||||
var culled = Ent(10, serverGuid: 0x80000010u, parentCell: OutdoorCell);
|
||||
var camera = Ent(11, serverGuid: 0x80000011u, parentCell: OutdoorCell);
|
||||
var entries = new[]
|
||||
{
|
||||
(culledLandblock,
|
||||
new Vector3(10f),
|
||||
new Vector3(11f),
|
||||
(IReadOnlyList<WorldEntity>)new[] { culled },
|
||||
(IReadOnlyDictionary<uint, WorldEntity>?)null),
|
||||
(cameraLandblock,
|
||||
new Vector3(10f),
|
||||
new Vector3(11f),
|
||||
(IReadOnlyList<WorldEntity>)new[] { camera },
|
||||
(IReadOnlyDictionary<uint, WorldEntity>?)null),
|
||||
};
|
||||
|
||||
InteriorEntityPartition.Result result =
|
||||
InteriorEntityPartition.Partition(
|
||||
new HashSet<uint>(),
|
||||
entries,
|
||||
FrustumPlanes.FromViewProjection(Matrix4x4.Identity),
|
||||
neverCullLandblockId: cameraLandblock);
|
||||
|
||||
Assert.DoesNotContain(culled, result.Dynamics);
|
||||
Assert.Equal(camera, Assert.Single(result.Dynamics));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -381,6 +381,50 @@ public sealed class LiveEntityAnimationPresenterTests
|
|||
Assert.Equal(0.5f, fixture.State.CurrFrame);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyCycle_WrapsAndInterpolatesThroughSharedRetailPlayback()
|
||||
{
|
||||
var fixture = Build(partCount: 1);
|
||||
fixture.State.Sequencer = null;
|
||||
fixture.State.LowFrame = 0;
|
||||
fixture.State.HighFrame = 2;
|
||||
fixture.State.Framerate = 2f;
|
||||
fixture.State.CurrFrame = 2.5f;
|
||||
var animation = new Animation();
|
||||
foreach (float x in new[] { 0f, 10f, 20f })
|
||||
{
|
||||
var frame = new AnimationFrame(1);
|
||||
frame.Frames.Add(new Frame
|
||||
{
|
||||
Origin = new Vector3(x, 0f, 0f),
|
||||
Orientation = Quaternion.Identity,
|
||||
});
|
||||
animation.PartFrames.Add(frame);
|
||||
}
|
||||
fixture.State.Animation = animation;
|
||||
var schedule = new LiveEntityAnimationSchedule(
|
||||
SequenceFrames: null,
|
||||
LegacyAdvanceSeconds: 0.5f,
|
||||
ComposeParts: true,
|
||||
fixture.Record,
|
||||
fixture.Entity,
|
||||
fixture.State,
|
||||
fixture.Record.ObjectClockEpoch,
|
||||
fixture.Record.ProjectionMutationVersion,
|
||||
fixture.State.PresentationRevision);
|
||||
var presenter = Presenter(fixture.Live, new EntityEffectPoseRegistry(), new Context());
|
||||
|
||||
presenter.Present(new Dictionary<RuntimeEntityKey, LiveEntityAnimationSchedule>
|
||||
{
|
||||
[fixture.Record.ProjectionKey!.Value] = schedule,
|
||||
});
|
||||
|
||||
// Legacy arithmetic: 2.5 + (0.5 * 2) = 3.5, wrapped over the
|
||||
// inclusive 0..2 span to 0.5, then interpolated halfway 0 -> 10.
|
||||
Assert.Equal(0.5f, fixture.State.CurrFrame);
|
||||
Assert.Equal(new Vector3(5f, 0f, 0f), fixture.Entity.MeshRefs[0].PartTransform.Translation);
|
||||
}
|
||||
|
||||
private static LiveEntityAnimationPresenter Presenter(
|
||||
LiveEntityRuntime live,
|
||||
EntityEffectPoseRegistry poses,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,39 @@
|
|||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using AcDream.App.Rendering.Sky;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Sky;
|
||||
|
||||
public sealed class RainAnimationClockTests
|
||||
{
|
||||
[Fact]
|
||||
public void AnimationPhaseUsesMonotonicStopwatchTicks()
|
||||
{
|
||||
const long start = 1234;
|
||||
|
||||
Assert.Equal(
|
||||
1f,
|
||||
SkyRenderer.ElapsedAnimationSeconds(start, start + Stopwatch.Frequency));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RendererDoesNotReadTheAdjustableSystemClock()
|
||||
{
|
||||
string source = File.ReadAllText(Path.Combine(
|
||||
RepositoryRoot(), "src", "AcDream.App", "Rendering", "Sky", "SkyRenderer.cs"));
|
||||
|
||||
Assert.Contains("Stopwatch.GetTimestamp()", source, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("DateTime.UtcNow", source, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static string RepositoryRoot()
|
||||
{
|
||||
var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "AcDream.slnx")))
|
||||
directory = directory.Parent;
|
||||
return directory?.FullName
|
||||
?? throw new InvalidOperationException("Could not locate repository root.");
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Vfx;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.Types;
|
||||
|
|
@ -22,20 +23,59 @@ public sealed class SkyPesFrameControllerTests
|
|||
{
|
||||
private const uint AuroraSetup = 0x02000714u;
|
||||
private const uint AuroraScript = 0x330007DBu;
|
||||
private const uint LightningEmitter = 0x320002C2u;
|
||||
|
||||
private sealed class Harness
|
||||
{
|
||||
private sealed class RecordingHookSink : IAnimationHookSink
|
||||
{
|
||||
public List<(uint EntityId, Vector3 Position, AnimationHook Hook)> Calls { get; } = [];
|
||||
|
||||
public void OnHook(
|
||||
uint entityId,
|
||||
Vector3 entityWorldPosition,
|
||||
AnimationHook hook) =>
|
||||
Calls.Add((entityId, entityWorldPosition, hook));
|
||||
}
|
||||
|
||||
public readonly List<uint> ResolvedScriptIds = [];
|
||||
public readonly List<string> Diagnostics = [];
|
||||
public readonly List<(uint EntityId, Vector3 Position, AnimationHook Hook)> HookCalls;
|
||||
public readonly PhysicsScriptRunner Runner;
|
||||
public readonly SkyPesFrameController Controller;
|
||||
public readonly ParticleSystem Particles;
|
||||
|
||||
public Harness()
|
||||
public Harness(AnimationHook? hook = null, double hookTime = 0.0)
|
||||
{
|
||||
var registry = new EmitterDescRegistry();
|
||||
var system = new ParticleSystem(registry, new Random(42));
|
||||
registry.Register(new EmitterDesc
|
||||
{
|
||||
DatId = LightningEmitter,
|
||||
Type = ParticleType.Still,
|
||||
Flags = EmitterFlags.Billboard,
|
||||
EmitterKind = ParticleEmitterKind.BirthratePerSec,
|
||||
MaxParticles = 2,
|
||||
InitialParticles = 1,
|
||||
LifetimeMin = 0.01f,
|
||||
LifetimeMax = 0.01f,
|
||||
Lifespan = 0.01f,
|
||||
StartSize = 1f,
|
||||
EndSize = 1f,
|
||||
StartAlpha = 1f,
|
||||
EndAlpha = 1f,
|
||||
Birthrate = 1000f,
|
||||
});
|
||||
Particles = new ParticleSystem(registry, new Random(42));
|
||||
var poses = new EntityEffectPoseRegistry();
|
||||
var sink = new ParticleHookSink(system, poses);
|
||||
var sink = new ParticleHookSink(Particles, poses)
|
||||
{
|
||||
DiagnosticSink = Diagnostics.Add,
|
||||
};
|
||||
var recording = new RecordingHookSink();
|
||||
HookCalls = recording.Calls;
|
||||
var router = new AnimationHookRouter();
|
||||
router.Register(sink);
|
||||
router.Register(recording);
|
||||
Runner = new PhysicsScriptRunner(
|
||||
id =>
|
||||
{
|
||||
|
|
@ -43,12 +83,12 @@ public sealed class SkyPesFrameControllerTests
|
|||
var script = new DatPhysicsScript();
|
||||
script.ScriptData.Add(new PhysicsScriptData
|
||||
{
|
||||
StartTime = 0.0,
|
||||
Hook = new SoundHook(),
|
||||
StartTime = hookTime,
|
||||
Hook = hook ?? new SoundHook(),
|
||||
});
|
||||
return script;
|
||||
},
|
||||
sink);
|
||||
router);
|
||||
Controller = new SkyPesFrameController(
|
||||
Runner,
|
||||
sink,
|
||||
|
|
@ -169,4 +209,52 @@ public sealed class SkyPesFrameControllerTests
|
|||
h.Controller.Update(0.1f, null, Vector3.Zero);
|
||||
Assert.Equal(0, h.Runner.ActiveScriptCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LightningPartZeroUsesTheLiveCarrierPoseAndStopsOnDayGroupFlip()
|
||||
{
|
||||
var create = new CreateParticleHook
|
||||
{
|
||||
EmitterInfoId = LightningEmitter,
|
||||
EmitterId = 1u,
|
||||
PartIndex = 0u,
|
||||
Offset = new Frame(),
|
||||
};
|
||||
var h = new Harness(create);
|
||||
|
||||
h.Controller.Update(0.1f, Group(Carrier()), new Vector3(4f, 5f, 6f));
|
||||
h.Runner.Tick(0.0);
|
||||
|
||||
Assert.Equal(1, h.Particles.ActiveEmitterCount);
|
||||
Assert.Empty(h.Diagnostics);
|
||||
|
||||
h.Controller.Update(0.1f, null, Vector3.Zero);
|
||||
h.Runner.Tick(1.0);
|
||||
|
||||
Assert.Equal(0, h.Runner.ActiveScriptCount);
|
||||
Assert.Empty(h.Diagnostics);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistentWeatherCarrier_RefreshesSoundAnchorToCurrentCamera()
|
||||
{
|
||||
var sound = new SoundTweakedHook
|
||||
{
|
||||
SoundId = 0x0A00038Bu,
|
||||
Volume = 0.1f,
|
||||
Priority = 1f,
|
||||
};
|
||||
var h = new Harness(sound, hookTime: 1.0);
|
||||
var initialCamera = new Vector3(10f, 20f, 30f);
|
||||
var currentCamera = new Vector3(410f, 520f, 630f);
|
||||
|
||||
h.Controller.Update(0.3f, Group(Carrier()), initialCamera);
|
||||
h.Controller.Update(0.4f, Group(Carrier()), currentCamera);
|
||||
h.Runner.Tick(1.0);
|
||||
|
||||
var call = Assert.Single(h.HookCalls);
|
||||
Assert.Same(sound, call.Hook);
|
||||
Assert.Equal(currentCamera, call.Position);
|
||||
Assert.Single(h.ResolvedScriptIds);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,11 +30,25 @@ using AcDream.App.Tests.Rendering.Gpu;
|
|||
using AcDream.Content;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Xunit;
|
||||
using CullMode = DatReaderWriter.Enums.CullMode;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering.Wb;
|
||||
|
||||
public class EnvCellRendererTests
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(CullMode.Landblock)]
|
||||
[InlineData(CullMode.None)]
|
||||
[InlineData(CullMode.Clockwise)]
|
||||
[InlineData(CullMode.CounterClockwise)]
|
||||
public void CellShellCullPolicy_UsesRetailConstructedMeshClockwiseCull(
|
||||
CullMode sourceSidesType)
|
||||
{
|
||||
Assert.Equal(
|
||||
CullMode.Clockwise,
|
||||
EnvCellRenderer.ResolveRetailCellShellCullMode(sourceSidesType));
|
||||
}
|
||||
|
||||
private sealed class NullPreparedAssetSource : IPreparedAssetSource
|
||||
{
|
||||
public PreparedAssetSourceStats Stats => default;
|
||||
|
|
|
|||
|
|
@ -300,6 +300,56 @@ public sealed class WorldRenderFrameBuilderTests
|
|||
Assert.Contains(hiddenLight, lighting.PointSnapshot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PersistentAtDay_UsesNoonLandscapeLightingWithoutChangingTheSkyClock()
|
||||
{
|
||||
SkyStateProvider sky = SkyStateProvider.Default();
|
||||
var clock = new WorldTimeService(sky);
|
||||
clock.PinnedDayFraction = 0f;
|
||||
var lighting = new LightManager();
|
||||
bool persistentDaylight = true;
|
||||
var environment = new RuntimeWorldFrameEnvironmentPreparation(
|
||||
RuntimeOptions.Parse("test-dat", _ => null),
|
||||
clock,
|
||||
lighting,
|
||||
dispatcher: null,
|
||||
environmentCells: null,
|
||||
lightingUbo: null,
|
||||
new WorldRenderRangeState(4, 12),
|
||||
skyPes: null,
|
||||
persistentDaylight: () => persistentDaylight);
|
||||
WorldCameraFrame camera = CameraFrame(new FlyCamera());
|
||||
WorldRootFrame roots = default;
|
||||
SkyKeyframe midnight = sky.Interpolate(0f);
|
||||
SkyKeyframe noon = sky.Interpolate(0.5f);
|
||||
var foundation = new RenderFrameFoundation(
|
||||
PortalViewportVisible: false,
|
||||
Sky: midnight,
|
||||
Atmosphere: default);
|
||||
|
||||
environment.Prepare(
|
||||
in camera,
|
||||
in roots,
|
||||
in foundation,
|
||||
activeDayGroup: null);
|
||||
|
||||
Assert.Equal(noon.AmbientColor, lighting.CurrentAmbient.AmbientColor);
|
||||
Assert.NotNull(lighting.Sun);
|
||||
Assert.Equal(noon.SunColor, lighting.Sun!.ColorLinear);
|
||||
Assert.Equal(0d, clock.DayFraction, precision: 5);
|
||||
|
||||
persistentDaylight = false;
|
||||
environment.Prepare(
|
||||
in camera,
|
||||
in roots,
|
||||
in foundation,
|
||||
activeDayGroup: null);
|
||||
|
||||
Assert.Equal(midnight.AmbientColor, lighting.CurrentAmbient.AmbientColor);
|
||||
Assert.NotNull(lighting.Sun);
|
||||
Assert.Equal(midnight.SunColor, lighting.Sun!.ColorLinear);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Environment_preparation_keeps_lighting_snapshot_before_ubo_upload()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue