fix(world): retire motion links at hidden transitions
Port CPhysicsObj::set_hidden's PartArray HandleEnterWorld boundary on both Hidden and UnHide. This strips linked recall/casting animations and aborts their pending completions before cell visibility changes, preventing spell-recall tails after portal travel without arrival resets or action classifiers.\n\nCo-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
75acae02d6
commit
bfb4b26dff
6 changed files with 185 additions and 21 deletions
|
|
@ -1,11 +1,15 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content.Vfx;
|
||||
using AcDream.Core.Net;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter;
|
||||
using DatReaderWriter.DBObjs;
|
||||
using DatReaderWriter.Options;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace AcDream.App.Tests.World;
|
||||
|
||||
|
|
@ -36,6 +40,11 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
fixture.TypedPlays);
|
||||
Assert.Equal([(Fixture.Guid, true)], fixture.ChildNoDraw);
|
||||
Assert.Equal([Fixture.Guid], fixture.InvalidTargets);
|
||||
Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld);
|
||||
Assert.Equal([0], fixture.PartArrayShadowCounts);
|
||||
Assert.Equal(
|
||||
["effect:76", "children:True", "part-array", "target"],
|
||||
fixture.PresentationOrder);
|
||||
Assert.Equal(0, fixture.Shadows.TotalRegistered);
|
||||
Assert.False(fixture.Entity.IsDrawVisible);
|
||||
|
||||
|
|
@ -52,11 +61,87 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
],
|
||||
fixture.TypedPlays);
|
||||
Assert.Equal([(Fixture.Guid, true), (Fixture.Guid, false)], fixture.ChildNoDraw);
|
||||
Assert.Equal(
|
||||
[fixture.Entity.Id, fixture.Entity.Id],
|
||||
fixture.PartArrayEnterWorld);
|
||||
Assert.Equal([0, 0], fixture.PartArrayShadowCounts);
|
||||
Assert.Equal(
|
||||
[
|
||||
"effect:76", "children:True", "part-array", "target",
|
||||
"effect:75", "children:False", "part-array",
|
||||
],
|
||||
fixture.PresentationOrder);
|
||||
Assert.Equal(1, fixture.Shadows.TotalRegistered);
|
||||
Assert.True(fixture.Entity.IsDrawVisible);
|
||||
Assert.Same(fixture.Entity, Assert.Single(fixture.Runtime.WorldEntities).Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SpellRecall_HiddenAndUnHide_RetireMagicTimelineThroughController()
|
||||
{
|
||||
const uint humanSetup = 0x02000001u;
|
||||
const uint humanMotionTable = 0x09000001u;
|
||||
const uint magic = 0x80000049u;
|
||||
const uint ready = 0x41000003u;
|
||||
const uint magicPortal = 0x40000038u;
|
||||
string datDir = @"C:\Turbine\Asheron's Call";
|
||||
if (!File.Exists(Path.Combine(datDir, "client_portal.dat")))
|
||||
throw SkipException.ForSkip("Installed retail DATs are required.");
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
Setup setup = Assert.IsType<Setup>(dats.Get<Setup>(humanSetup));
|
||||
MotionTable table = Assert.IsType<MotionTable>(dats.Get<MotionTable>(humanMotionTable));
|
||||
var sequencer = new AnimationSequencer(
|
||||
setup,
|
||||
table,
|
||||
new RetailAnimationLoader(dats));
|
||||
var fixture = new Fixture(
|
||||
PhysicsStateFlags.ReportCollisions,
|
||||
_ => sequencer.Manager.HandleEnterWorld());
|
||||
Assert.True(fixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
|
||||
sequencer.SetCycle(magic, ready);
|
||||
sequencer.PlayAction(magicPortal);
|
||||
Assert.False(sequencer.CurrentNodeDiag.IsLooping);
|
||||
|
||||
Assert.True(fixture.Runtime.TryApplyState(
|
||||
new SetState.Parsed(
|
||||
Fixture.Guid,
|
||||
(uint)(PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions),
|
||||
1,
|
||||
2),
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(fixture.Controller.OnStateAccepted(Fixture.Guid));
|
||||
Assert.True(sequencer.CurrentNodeDiag.IsLooping);
|
||||
Assert.Empty(sequencer.Manager.PendingAnimations);
|
||||
|
||||
// A motion accepted while the mesh is hidden is retired again at
|
||||
// retail's matching UnHide PartArray boundary before cell visibility
|
||||
// returns.
|
||||
var unhideSequencer = new AnimationSequencer(
|
||||
setup,
|
||||
table,
|
||||
new RetailAnimationLoader(dats));
|
||||
var unhideFixture = new Fixture(
|
||||
PhysicsStateFlags.Hidden | PhysicsStateFlags.ReportCollisions,
|
||||
_ => unhideSequencer.Manager.HandleEnterWorld());
|
||||
Assert.True(unhideFixture.Controller.OnLiveEntityReady(Fixture.Guid));
|
||||
unhideSequencer.SetCycle(magic, ready);
|
||||
unhideSequencer.PlayAction(magicPortal);
|
||||
Assert.False(unhideSequencer.CurrentNodeDiag.IsLooping);
|
||||
Assert.True(unhideFixture.Runtime.TryApplyState(
|
||||
new SetState.Parsed(Fixture.Guid, 0u, 1, 3),
|
||||
out _,
|
||||
out _));
|
||||
Assert.True(unhideFixture.Controller.OnStateAccepted(Fixture.Guid));
|
||||
Assert.True(unhideSequencer.CurrentNodeDiag.IsLooping);
|
||||
Assert.Empty(unhideSequencer.Manager.PendingAnimations);
|
||||
Assert.Equal([0], fixture.PartArrayShadowCounts);
|
||||
Assert.Equal([0, 0], unhideFixture.PartArrayShadowCounts);
|
||||
Assert.Equal(1, unhideFixture.Shadows.TotalRegistered);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IdenticalOrStaleState_DoesNotReplayHiddenEffect()
|
||||
{
|
||||
|
|
@ -76,6 +161,7 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
Assert.Single(fixture.TypedPlays);
|
||||
Assert.Equal(LiveEntityPresentationController.HiddenScriptType,
|
||||
fixture.TypedPlays[0].Type);
|
||||
Assert.Equal([fixture.Entity.Id], fixture.PartArrayEnterWorld);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -166,13 +252,18 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
public List<(uint Owner, uint Type, float Intensity)> TypedPlays { get; } = [];
|
||||
public List<(uint Parent, bool NoDraw)> ChildNoDraw { get; } = [];
|
||||
public List<uint> InvalidTargets { get; } = [];
|
||||
public List<uint> PartArrayEnterWorld { get; } = [];
|
||||
public List<int> PartArrayShadowCounts { get; } = [];
|
||||
public List<string> PresentationOrder { get; } = [];
|
||||
public GpuWorldState Spatial { get; }
|
||||
public LiveEntityRuntime Runtime { get; }
|
||||
public ShadowObjectRegistry Shadows { get; } = new();
|
||||
public WorldEntity Entity { get; }
|
||||
public LiveEntityPresentationController Controller { get; }
|
||||
|
||||
public Fixture(PhysicsStateFlags initialState)
|
||||
public Fixture(
|
||||
PhysicsStateFlags initialState,
|
||||
Action<uint>? onPartArrayEnterWorld = null)
|
||||
{
|
||||
Spatial = new GpuWorldState();
|
||||
Spatial.AddLandblock(new LoadedLandblock(
|
||||
|
|
@ -214,11 +305,27 @@ public sealed class LiveEntityPresentationControllerTests
|
|||
(owner, type, intensity) =>
|
||||
{
|
||||
TypedPlays.Add((owner, type, intensity));
|
||||
PresentationOrder.Add($"effect:{type:X2}");
|
||||
return true;
|
||||
},
|
||||
(parent, noDraw) => ChildNoDraw.Add((parent, noDraw)),
|
||||
InvalidTargets.Add,
|
||||
() => (1, 1));
|
||||
(parent, noDraw) =>
|
||||
{
|
||||
ChildNoDraw.Add((parent, noDraw));
|
||||
PresentationOrder.Add($"children:{noDraw}");
|
||||
},
|
||||
guid =>
|
||||
{
|
||||
InvalidTargets.Add(guid);
|
||||
PresentationOrder.Add("target");
|
||||
},
|
||||
() => (1, 1),
|
||||
handlePartArrayEnterWorld: localEntityId =>
|
||||
{
|
||||
PartArrayEnterWorld.Add(localEntityId);
|
||||
PartArrayShadowCounts.Add(Shadows.TotalRegistered);
|
||||
PresentationOrder.Add("part-array");
|
||||
onPartArrayEnterWorld?.Invoke(localEntityId);
|
||||
});
|
||||
}
|
||||
|
||||
internal static WorldSession.EntitySpawn Spawn(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue