using System.Numerics;
using AcDream.App.Physics;
using AcDream.App.Streaming;
using AcDream.App.World;
using AcDream.Core.Items;
using AcDream.Core.Net;
using AcDream.Core.Net.Messages;
using AcDream.Core.Physics;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
namespace AcDream.App.Tests.Physics;
///
/// #297 second edge: keeps a live
/// entity's collision flags synced with
/// the 's live
/// after a
/// PropertyInt(PlayerKillerStatus) update, instead of staying frozen at
/// whatever the spawn-time CreateObject captured.
///
public sealed class LiveEntityPvpBitfieldSyncTests
{
private sealed class RecordingResources : ILiveEntityResourceLifecycle
{
public void Register(WorldEntity entity) { }
public void Unregister(WorldEntity entity) { }
}
private static WorldEntity Entity(uint id, uint guid) => new()
{
Id = id,
ServerGuid = guid,
SourceGfxObjOrSetupId = 0x02000001u,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs = Array.Empty(),
};
// Mirrors LiveEntityRuntimeTests.Spawn's proven Register+Materialize
// shape verbatim: a PhysicsSpawnData block whose timestamps/position
// agree exactly with the flattened EntitySpawn fields is required by
// RuntimeEntityObjectLifetime.HasConsistentCreateIdentityAndParent.
private static WorldSession.EntitySpawn Spawn(uint guid, uint cell)
{
var position = new CreateObject.ServerPosition(cell, 10f, 10f, 5f, 1f, 0f, 0f, 0f);
var timestamps = new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, 1);
var physics = new PhysicsSpawnData(
RawState: (uint)PhysicsStateFlags.ReportCollisions,
Position: position,
Movement: null,
AnimationFrame: null,
SetupTableId: 0x02000001u,
MotionTableId: 0x09000001u,
SoundTableId: null,
PhysicsScriptTableId: null,
Parent: null,
Children: null,
Scale: null,
Friction: null,
Elasticity: null,
Translucency: null,
Velocity: null,
Acceleration: null,
AngularVelocity: null,
DefaultScriptType: null,
DefaultScriptIntensity: null,
Timestamps: timestamps);
return new WorldSession.EntitySpawn(
guid,
position,
0x02000001u,
Array.Empty(),
Array.Empty(),
Array.Empty(),
null,
null,
"fixture",
(uint)ItemType.Creature,
null,
0x09000001u,
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
InstanceSequence: 1,
MovementSequence: 1,
ServerControlSequence: 1,
PositionSequence: 1,
Physics: physics);
}
/// Registers and materializes one live entity, mirroring
/// LiveEntityRuntimeTests.RegisterRebucketWithdrawAndRestore_UsesOneLogicalCreate's
/// proven Register+Materialize sequence.
private static (LiveEntityRuntime Runtime, WorldEntity Entity) MaterializedEntity(uint guid)
{
var spatial = new GpuWorldState();
spatial.AddLandblock(new LoadedLandblock(
0x0101FFFFu, new LandBlock(), Array.Empty()));
var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources());
WorldSession.EntitySpawn spawn = Spawn(guid, cell: 0x01010001u);
runtime.RegisterLiveEntity(spawn);
WorldEntity? entity = runtime.MaterializeLiveEntity(
spawn.Guid,
spawn.Position!.Value.LandblockId,
id => Entity(id, spawn.Guid));
return (runtime, entity!);
}
[Fact]
public void ObjectUpdated_WithLiveBitfield_RefreshesRegisteredTargetFlags()
{
(LiveEntityRuntime runtime, WorldEntity entity) = MaterializedEntity(0x70000010u);
var shadows = new ShadowObjectRegistry();
shadows.Register(
entity.Id,
0x01000005u,
new Vector3(12f, 12f, 50f),
Quaternion.Identity,
1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B40000u,
flags: EntityCollisionFlags.HasWeenie | EntityCollisionFlags.IsPlayer,
seedCellId: 0xA9B40001u);
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0x70000010u,
PublicWeenieBitfield = 0x8u, // BF_PLAYER only — spawn-time snapshot
});
using var sync = new LiveEntityPvpBitfieldSync(objects, runtime, shadows);
// Live PropertyInt(PlayerKillerStatus) = PKLite arrives.
objects.UpdateIntProperty(
0x70000010u,
ClientObjectTable.PlayerKillerStatusPropertyId,
value: PlayerKillerStatusBitfield.PkLite);
ShadowEntry after = Assert.Single(shadows.GetObjectsInCell(0xA9B40001u));
Assert.Equal(
EntityCollisionFlags.HasWeenie
| EntityCollisionFlags.IsPlayer
| EntityCollisionFlags.IsPKLite,
after.Flags);
}
[Fact]
public void ObjectUpdated_UnrelatedProperty_NoBitfield_DoesNotThrowOrRegisterUnknownEntity()
{
var spatial = new GpuWorldState();
var runtime = LiveEntityRuntimeFixture.Create(spatial, new RecordingResources());
var shadows = new ShadowObjectRegistry();
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject { ObjectId = 0x70000011u });
using var sync = new LiveEntityPvpBitfieldSync(objects, runtime, shadows);
// No PublicWeenieBitfield yet, and no matching live entity — must
// no-op harmlessly rather than throw.
objects.UpdateIntProperty(0x70000011u, propertyId: 18u, value: 1);
Assert.Equal(0, shadows.TotalRegistered);
}
[Fact]
public void Dispose_UnsubscribesFromObjectUpdated()
{
(LiveEntityRuntime runtime, WorldEntity entity) = MaterializedEntity(0x70000012u);
var shadows = new ShadowObjectRegistry();
shadows.Register(
entity.Id,
0x01000005u,
new Vector3(12f, 12f, 50f),
Quaternion.Identity,
1f,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0xA9B40000u,
flags: EntityCollisionFlags.HasWeenie | EntityCollisionFlags.IsPlayer,
seedCellId: 0xA9B40001u);
var objects = new ClientObjectTable();
objects.AddOrUpdate(new ClientObject
{
ObjectId = 0x70000012u,
PublicWeenieBitfield = 0x8u,
});
var sync = new LiveEntityPvpBitfieldSync(objects, runtime, shadows);
sync.Dispose();
objects.UpdateIntProperty(
0x70000012u,
ClientObjectTable.PlayerKillerStatusPropertyId,
value: PlayerKillerStatusBitfield.PkLite);
// Disposed sync must not have reacted — flags stay exactly as
// Register captured (no IsPKLite bit picked up).
ShadowEntry after = Assert.Single(shadows.GetObjectsInCell(0xA9B40001u));
Assert.Equal(
EntityCollisionFlags.HasWeenie | EntityCollisionFlags.IsPlayer,
after.Flags);
}
}