Own render, physics, DAT-static presentation, and retained static-resource rebinds behind retryable receipts so a failed publication resumes its exact unfinished suffix without replaying retail create-time defaults. Co-authored-by: Codex <noreply@openai.com>
309 lines
11 KiB
C#
309 lines
11 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.Core.Lighting;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Plugins;
|
|
using AcDream.Core.Rendering;
|
|
using AcDream.Core.Terrain;
|
|
using AcDream.Core.Vfx;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
using DatReaderWriter.Types;
|
|
using Xunit;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class LandblockStaticPresentationPublisherTests
|
|
{
|
|
private const uint LandblockId = 0xA9B4FFFFu;
|
|
private const uint AdjacentLandblockId = 0xAAB4FFFFu;
|
|
private const uint SetupId = 0x02000042u;
|
|
private static readonly float[] HeightTable = new float[256];
|
|
|
|
[Fact]
|
|
public void Reapply_ReplacesLightsAndRefreshesPluginWithoutSpawnReplay()
|
|
{
|
|
var fixture = Fixture();
|
|
int spawnCount = 0;
|
|
fixture.Events.EntitySpawned += _ => spawnCount++;
|
|
WorldEntity first = Entity(0x80A9B401u, new Vector3(12f, 12f, 0f));
|
|
WorldEntity moved = Entity(first.Id, new Vector3(36f, 12f, 0f));
|
|
|
|
Publish(fixture, Build(LandblockId, [first], BundleWithLight()));
|
|
Assert.Equal(1, fixture.Lights.RegisteredCount);
|
|
Assert.Equal(1, spawnCount);
|
|
Assert.Equal(first.Position, Assert.Single(fixture.World.Entities).Position);
|
|
|
|
fixture.Lighting.SetOwnerLighting(first.Id, enabled: false);
|
|
fixture.Translucency.StartPartFade(first.Id, 0u, 0f, 0.6f, 0f);
|
|
fixture.Translucency.StartPartFade(first.Id, 1u, 0f, 1f, 2f);
|
|
fixture.Translucency.AdvanceAll(0.5f);
|
|
Assert.True(fixture.Translucency.TryGetCurrentValue(
|
|
first.Id,
|
|
1u,
|
|
out float activeFadeBefore));
|
|
Publish(fixture, Build(LandblockId, [moved], BundleWithLight()));
|
|
|
|
Assert.Equal(1, fixture.Lights.RegisteredCount);
|
|
Assert.Equal(1, spawnCount);
|
|
Assert.Equal(moved.Position, Assert.Single(fixture.World.Entities).Position);
|
|
Assert.False(Assert.Single(fixture.Lighting.GetOwnedLights(first.Id)!).IsLit);
|
|
Assert.Equal(1, fixture.Lighting.RetainedOwnerStateCount);
|
|
Assert.True(fixture.Translucency.TryGetCurrentValue(
|
|
first.Id,
|
|
0u,
|
|
out float settled));
|
|
Assert.Equal(0.6f, settled);
|
|
Assert.True(fixture.Translucency.TryGetCurrentValue(
|
|
first.Id,
|
|
1u,
|
|
out float activeFadeAfter));
|
|
Assert.Equal(activeFadeBefore, activeFadeAfter);
|
|
Assert.Equal(1, fixture.Publisher.Diagnostics.PluginSpawnCount);
|
|
Assert.Equal(1, fixture.Publisher.Diagnostics.PluginRefreshCount);
|
|
Assert.Equal(2, fixture.Publisher.Diagnostics.LightReplacementCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reapply_OmittedStaticRemovesLightTranslucencyAndPluginProjection()
|
|
{
|
|
var fixture = Fixture();
|
|
WorldEntity entity = Entity(0x80A9B401u, new Vector3(12f, 12f, 0f));
|
|
Publish(fixture, Build(LandblockId, [entity], BundleWithLight()));
|
|
fixture.Lighting.SetOwnerLighting(entity.Id, enabled: false);
|
|
fixture.Translucency.StartPartFade(entity.Id, 0u, 0f, 1f, 2f);
|
|
|
|
Publish(
|
|
fixture,
|
|
Build(LandblockId, Array.Empty<WorldEntity>(), PhysicsDatBundle.Empty));
|
|
|
|
Assert.Equal(0, fixture.Lights.RegisteredCount);
|
|
Assert.Equal(0, fixture.Lighting.RetainedOwnerStateCount);
|
|
Assert.False(fixture.Translucency.TryGetCurrentValue(entity.Id, 0u, out _));
|
|
Assert.Empty(fixture.World.Entities);
|
|
Assert.Equal(1, fixture.Publisher.Diagnostics.PluginRemovalCount);
|
|
Assert.Equal(0, fixture.Publisher.Diagnostics.ActiveLandblockCount);
|
|
Assert.Equal(0, fixture.Publisher.Diagnostics.ActiveEntityCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RetirementThenReload_EmitsNewLogicalPluginSpawn()
|
|
{
|
|
var fixture = Fixture();
|
|
int spawnCount = 0;
|
|
fixture.Events.EntitySpawned += _ => spawnCount++;
|
|
WorldEntity entity = Entity(0x80A9B401u, new Vector3(12f, 12f, 0f));
|
|
Publish(fixture, Build(LandblockId, [entity], BundleWithLight()));
|
|
|
|
fixture.Publisher.RemoveLighting(entity);
|
|
fixture.Publisher.RemoveTranslucency(entity);
|
|
fixture.Publisher.RemovePluginProjection(entity);
|
|
Assert.Equal(0, fixture.Publisher.Diagnostics.ActiveEntityCount);
|
|
|
|
Publish(fixture, Build(LandblockId, [entity], BundleWithLight()));
|
|
|
|
Assert.Equal(2, spawnCount);
|
|
Assert.Equal(1, fixture.Lights.RegisteredCount);
|
|
Assert.Equal(1, fixture.Publisher.Diagnostics.ActiveEntityCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepare_RejectsDuplicateAndCrossLandblockStaticIdsBeforeMutation()
|
|
{
|
|
var fixture = Fixture();
|
|
WorldEntity first = Entity(0x80A9B401u, Vector3.Zero);
|
|
LandblockPhysicsPublication duplicatePhysics = PhysicsPrefix(
|
|
fixture,
|
|
Build(LandblockId, [first, Entity(first.Id, Vector3.One)]));
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Publisher.PreparePublication(duplicatePhysics));
|
|
Assert.Empty(fixture.World.Entities);
|
|
Assert.Equal(0, fixture.Lights.RegisteredCount);
|
|
|
|
Publish(fixture, Build(LandblockId, [first]));
|
|
LandblockPhysicsPublication adjacentPhysics = PhysicsPrefix(
|
|
fixture,
|
|
Build(AdjacentLandblockId, [Entity(first.Id, new Vector3(192f, 0f, 0f))]));
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
fixture.Publisher.PreparePublication(adjacentPhysics));
|
|
Assert.Single(fixture.World.Entities);
|
|
}
|
|
|
|
[Fact]
|
|
public void LiveProjectionInBuild_IsIgnoredByDatStaticPresentation()
|
|
{
|
|
var fixture = Fixture();
|
|
WorldEntity live = Entity(7u, Vector3.Zero, serverGuid: 0x80000007u);
|
|
|
|
Publish(fixture, Build(LandblockId, [live], BundleWithLight()));
|
|
|
|
Assert.Empty(fixture.World.Entities);
|
|
Assert.Equal(0, fixture.Lights.RegisteredCount);
|
|
Assert.Equal(0, fixture.Publisher.Diagnostics.ActiveEntityCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ForeignReceipt_IsRejectedWithoutChangingOwnerState()
|
|
{
|
|
var first = Fixture();
|
|
var second = Fixture();
|
|
LandblockPhysicsPublication physics = PhysicsPrefix(
|
|
first,
|
|
Build(LandblockId, [Entity(1u, Vector3.Zero)]));
|
|
LandblockStaticPresentationPublication receipt =
|
|
first.Publisher.PreparePublication(physics);
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
second.Publisher.BeginPublication(receipt));
|
|
Assert.Equal(0, second.Publisher.Diagnostics.BeginCount);
|
|
}
|
|
|
|
private static void Publish(FixtureState fixture, LandblockBuild build)
|
|
{
|
|
LandblockPhysicsPublication physics = PhysicsPrefix(fixture, build);
|
|
fixture.Render.CompletePublication(fixture.LastRender!);
|
|
LandblockStaticPresentationPublication presentation =
|
|
fixture.Publisher.PreparePublication(physics);
|
|
fixture.Publisher.BeginPublication(presentation);
|
|
fixture.Physics.CompletePublication(
|
|
physics,
|
|
entity => fixture.Publisher.PrepareEntityBeforeCollision(
|
|
presentation,
|
|
entity));
|
|
fixture.Publisher.CompletePublication(presentation);
|
|
}
|
|
|
|
private static LandblockPhysicsPublication PhysicsPrefix(
|
|
FixtureState fixture,
|
|
LandblockBuild build)
|
|
{
|
|
LandblockRenderPublication render = fixture.Render.BeginPublication(
|
|
build,
|
|
EmptyMesh());
|
|
fixture.LastRender = render;
|
|
LandblockPhysicsPublication physics =
|
|
fixture.Physics.PreparePublication(render);
|
|
fixture.Physics.BeginPublication(physics);
|
|
return physics;
|
|
}
|
|
|
|
private static FixtureState Fixture()
|
|
{
|
|
var cache = new PhysicsDataCache();
|
|
var engine = new PhysicsEngine { DataCache = cache };
|
|
var gpuState = new GpuWorldState();
|
|
var render = new LandblockRenderPublisher(
|
|
static (_, _, _) => { },
|
|
static _ => { },
|
|
new CellVisibility(),
|
|
gpuState);
|
|
var physics = new LandblockPhysicsPublisher(engine, HeightTable);
|
|
var lights = new LightManager();
|
|
var lighting = new LightingHookSink(lights, new NullPoseSource());
|
|
var translucency = new TranslucencyFadeManager();
|
|
var world = new WorldGameState();
|
|
var events = new WorldEvents();
|
|
return new FixtureState(
|
|
render,
|
|
physics,
|
|
new LandblockStaticPresentationPublisher(
|
|
lighting,
|
|
translucency,
|
|
world,
|
|
events),
|
|
lights,
|
|
lighting,
|
|
translucency,
|
|
world,
|
|
events);
|
|
}
|
|
|
|
private static PhysicsDatBundle BundleWithLight()
|
|
{
|
|
var setup = new Setup();
|
|
setup.Lights[0] = new LightInfo
|
|
{
|
|
ViewSpaceLocation = new Frame { Orientation = Quaternion.Identity },
|
|
Color = new ColorARGB
|
|
{
|
|
Red = 255,
|
|
Green = 255,
|
|
Blue = 255,
|
|
Alpha = 255,
|
|
},
|
|
Intensity = 1f,
|
|
Falloff = 8f,
|
|
};
|
|
return new PhysicsDatBundle(
|
|
new LandBlockInfo(),
|
|
new Dictionary<uint, EnvCell>(),
|
|
new Dictionary<uint, DatReaderWriter.DBObjs.Environment>(),
|
|
new Dictionary<uint, Setup> { [SetupId] = setup },
|
|
new Dictionary<uint, GfxObj>());
|
|
}
|
|
|
|
private static LandblockBuild Build(
|
|
uint landblockId,
|
|
IReadOnlyList<WorldEntity> entities,
|
|
PhysicsDatBundle? bundle = null) => new(
|
|
new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock
|
|
{
|
|
Terrain = new TerrainInfo[81],
|
|
Height = new byte[81],
|
|
},
|
|
entities,
|
|
bundle ?? PhysicsDatBundle.Empty),
|
|
Origin: new LandblockBuildOrigin(0xA9, 0xB4));
|
|
|
|
private static WorldEntity Entity(
|
|
uint id,
|
|
Vector3 position,
|
|
uint serverGuid = 0u) => new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = SetupId,
|
|
Position = position,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs = Array.Empty<MeshRef>(),
|
|
};
|
|
|
|
private static LandblockMeshData EmptyMesh() => new(
|
|
Array.Empty<TerrainVertex>(),
|
|
Array.Empty<uint>());
|
|
|
|
private sealed record FixtureState(
|
|
LandblockRenderPublisher Render,
|
|
LandblockPhysicsPublisher Physics,
|
|
LandblockStaticPresentationPublisher Publisher,
|
|
LightManager Lights,
|
|
LightingHookSink Lighting,
|
|
TranslucencyFadeManager Translucency,
|
|
WorldGameState World,
|
|
WorldEvents Events)
|
|
{
|
|
public LandblockRenderPublication? LastRender { get; set; }
|
|
}
|
|
|
|
private sealed class NullPoseSource : IEntityEffectPoseSource
|
|
{
|
|
public bool TryGetRootPose(uint localEntityId, out Matrix4x4 rootWorld)
|
|
{
|
|
rootWorld = default;
|
|
return false;
|
|
}
|
|
|
|
public bool TryGetPartPose(
|
|
uint localEntityId,
|
|
int partIndex,
|
|
out Matrix4x4 partLocal)
|
|
{
|
|
partLocal = default;
|
|
return false;
|
|
}
|
|
}
|
|
}
|