Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
R1 the split bar's operand is the item's authored MaxStackSize —
three retail sites read pwd._maxStackSize directly (InqListSlotCount
pc:200052, buy-button cases pc:203996/204086) where ACE never fills
the desc stack and standard stock is unlimited. Threaded StackSizeMax
end to end with one shared resolver; the two literal _maxStackSize
sites are now byte-exact; AP-165 retired, AP-169 corrected.
R2 walk-to-vendor never opened because GetUseRadius used an UNCITED
3m Creature heuristic as the local stop distance while ACE's poll
demands the authored radius (default 0.6 m) — the walk stopped and
the Use fired far outside acceptance. Now reads the wire-authored
spawn UseRadius with ACE's exact fallback; heuristic constants
deleted. A first sabotage attempt was non-discriminating
(coincidental 0.6) and was corrected — the discriminating version is
what landed.
R3 the Buying/Selling purse summaries ("Buying %d %s worth %hsp" /
"You have %hsp") recovered from the binary data segment where BN
mis-attributes the Buy-side literal; wired to staging and money
changes on the four authored text elements; AP-166 narrowed to the
pending-sell highlight.
Clean-room complete solution: 11,528 passed / 4 skipped / 0 failed.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1073 lines
39 KiB
C#
1073 lines
39 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Interaction;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Selection;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.UI.Layout;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Combat;
|
|
using AcDream.Core.Items;
|
|
using AcDream.Core.Net;
|
|
using AcDream.Core.Net.Messages;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Core.World;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.App.Tests.Interaction;
|
|
|
|
public sealed class WorldSelectionQueryTests
|
|
{
|
|
private const uint Player = 0x5000_0001u;
|
|
private const uint Target = 0x7000_0001u;
|
|
private const uint Wielder = 0x7000_0100u;
|
|
private const uint RemoteWeapon = 0x7000_0101u;
|
|
private const uint OwnWeapon = 0x7000_0102u;
|
|
|
|
private sealed class Resources : ILiveEntityResourceLifecycle
|
|
{
|
|
public void Register(WorldEntity entity) { }
|
|
public void Unregister(WorldEntity entity) { }
|
|
}
|
|
|
|
private sealed class GeometrySource(RetailSelectionMesh mesh)
|
|
: IRetailSelectionGeometrySource
|
|
{
|
|
public RetailSelectionMesh? Resolve(uint gfxObjId) => mesh;
|
|
}
|
|
|
|
private sealed record Animation(WorldEntity Entity, uint CurrentMotion)
|
|
: ILiveEntityAnimationRuntime;
|
|
|
|
private sealed class Harness
|
|
{
|
|
public readonly ClientObjectTable Objects = new();
|
|
public readonly LiveEntityRuntime Runtime;
|
|
public readonly RetailSelectionScene Scene;
|
|
public readonly WorldSelectionQuery Query;
|
|
public PlayerInteractionPose? PlayerPose = new(0x0101_0001u, Vector3.Zero);
|
|
|
|
/// <summary>
|
|
/// Stands in for EntityEffectPoseRegistry: the composed equipped-child
|
|
/// root EquippedChildRenderController.PublishChildPose emits per frame.
|
|
/// </summary>
|
|
public readonly Dictionary<uint, Matrix4x4> ChildRoots = new();
|
|
|
|
public Harness()
|
|
{
|
|
var spatial = new GpuWorldState();
|
|
spatial.AddLandblock(new LoadedLandblock(
|
|
0x0101_FFFFu,
|
|
new LandBlock(),
|
|
Array.Empty<WorldEntity>()));
|
|
Runtime = LiveEntityRuntimeFixture.Create(spatial, new Resources());
|
|
Scene = new RetailSelectionScene(new GeometrySource(Mesh()));
|
|
Query = new WorldSelectionQuery(
|
|
Runtime,
|
|
Objects,
|
|
Scene,
|
|
() => Player,
|
|
Camera,
|
|
() => new Vector2(400f, 300f),
|
|
() => PlayerPose,
|
|
(_, _) => (0.5f, 2f),
|
|
_ => (new Vector3(1f, 0f, 0f), 2f),
|
|
localEntityId => ChildRoots.TryGetValue(localEntityId, out Matrix4x4 root)
|
|
? root
|
|
: null);
|
|
|
|
Add(Player, Vector3.Zero, ItemType.Creature, SelectedObjectHealthPolicy.BfPlayer);
|
|
}
|
|
|
|
public WorldEntity Add(
|
|
uint guid,
|
|
Vector3 position,
|
|
ItemType type,
|
|
uint publicFlags = 0u,
|
|
uint? useability = null,
|
|
uint objectDescriptionFlags = 0u,
|
|
ushort instance = 1,
|
|
float scale = 1f,
|
|
Quaternion? rotation = null,
|
|
float? useRadius = null)
|
|
{
|
|
WorldSession.EntitySpawn spawn = Spawn(guid, instance) with
|
|
{
|
|
ItemType = (uint)type,
|
|
Useability = useability,
|
|
ObjectDescriptionFlags = objectDescriptionFlags,
|
|
UseRadius = useRadius,
|
|
};
|
|
Runtime.RegisterLiveEntity(spawn);
|
|
WorldEntity entity = Runtime.MaterializeLiveEntity(
|
|
guid,
|
|
0x0101_0001u,
|
|
id => Entity(id, guid, position, scale, rotation ?? Quaternion.Identity))!;
|
|
Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = guid,
|
|
Name = $"Object {guid:X8}",
|
|
Type = type,
|
|
PublicWeenieBitfield = publicFlags,
|
|
});
|
|
return entity;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Materializes an equipped child exactly as
|
|
/// EquippedChildRenderController.TryRealize does: an Attached
|
|
/// projection whose bookkeeping Position/Rotation carry the PARENT's
|
|
/// composed world root, with the child's own composed root published
|
|
/// separately for effects and selection.
|
|
/// </summary>
|
|
public WorldEntity AddAttached(
|
|
uint guid,
|
|
Vector3 parentWorldPosition,
|
|
ItemType type,
|
|
uint wielderId,
|
|
Matrix4x4? childRoot = null,
|
|
float? objScale = null,
|
|
EquipMask equippedLocation = EquipMask.MeleeWeapon)
|
|
{
|
|
WorldSession.EntitySpawn spawn = Spawn(guid, 1) with
|
|
{
|
|
ItemType = (uint)type,
|
|
ObjScale = objScale,
|
|
};
|
|
Runtime.RegisterLiveEntity(spawn);
|
|
WorldEntity entity = Runtime.MaterializeLiveEntity(
|
|
guid,
|
|
0x0101_0001u,
|
|
id => Entity(
|
|
id,
|
|
guid,
|
|
parentWorldPosition,
|
|
1f,
|
|
Quaternion.Identity),
|
|
LiveEntityProjectionKind.Attached)!;
|
|
Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = guid,
|
|
Name = $"Object {guid:X8}",
|
|
Type = type,
|
|
WielderId = wielderId,
|
|
// CreateObject's PublicWeenieDesc carries CurrentWieldedLocation
|
|
// alongside Wielder for every equipped child; retail's
|
|
// pwd._location is what DeterminePositionState reads.
|
|
CurrentlyEquippedLocation = equippedLocation,
|
|
});
|
|
if (childRoot is { } root)
|
|
ChildRoots[entity.Id] = root;
|
|
return entity;
|
|
}
|
|
|
|
public void Publish(WorldEntity entity)
|
|
=> PublishParts((entity, entity.Position));
|
|
|
|
public void PublishParts(params (WorldEntity Entity, Vector3 PartWorld)[] parts)
|
|
{
|
|
SelectionCameraSnapshot camera = Camera();
|
|
Scene.BeginFrame();
|
|
Scene.SetViewFrustum(FrustumPlanes.FromViewProjection(
|
|
camera.View * camera.Projection));
|
|
foreach ((WorldEntity entity, Vector3 partWorld) in parts)
|
|
{
|
|
Scene.AddVisiblePart(
|
|
entity,
|
|
0,
|
|
0x0100_0001u,
|
|
Matrix4x4.CreateTranslation(partWorld));
|
|
}
|
|
Scene.CompleteFrame();
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void PickRejectsPublishedPartAfterGuidWasReused()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity oldTarget = h.Add(Target, new Vector3(0f, 0f, -5f), ItemType.Misc);
|
|
h.Publish(oldTarget);
|
|
Assert.Equal(Target, h.Query.PickAtCursor(includeSelf: false));
|
|
|
|
Assert.True(h.Runtime.UnregisterLiveEntity(
|
|
new DeleteObject.Parsed(Target, 1),
|
|
isLocalPlayer: false));
|
|
WorldEntity replacement = h.Add(
|
|
Target,
|
|
new Vector3(0f, 0f, -5f),
|
|
ItemType.Misc,
|
|
instance: 1);
|
|
|
|
Assert.NotEqual(oldTarget.Id, replacement.Id);
|
|
Assert.Null(h.Query.PickAtCursor(includeSelf: false));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0)]
|
|
[InlineData(1)]
|
|
[InlineData(2)]
|
|
public void PickRejectsPublishedPartAfterVisibilityLifetimeEnds(int transition)
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity target = h.Add(Target, new Vector3(0f, 0f, -5f), ItemType.Misc);
|
|
h.Publish(target);
|
|
Assert.Equal(Target, h.Query.PickAtCursor(includeSelf: false));
|
|
|
|
switch (transition)
|
|
{
|
|
case 0:
|
|
Assert.True(h.Runtime.TryApplyState(
|
|
new SetState.Parsed(
|
|
Target,
|
|
(uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden),
|
|
InstanceSequence: 1,
|
|
StateSequence: 2),
|
|
out _));
|
|
break;
|
|
case 1:
|
|
Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target));
|
|
break;
|
|
default:
|
|
Assert.True(h.Runtime.UnregisterLiveEntity(
|
|
new DeleteObject.Parsed(Target, 1),
|
|
isLocalPlayer: false));
|
|
break;
|
|
}
|
|
|
|
Assert.Null(h.Query.PickAtCursor(includeSelf: false));
|
|
}
|
|
|
|
[Fact]
|
|
public void ClosestTargetIncludesOnlyVisibleLivingHostileMonsters()
|
|
{
|
|
var h = new Harness();
|
|
h.Add(
|
|
0x7000_0010u,
|
|
new Vector3(8f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.Add(
|
|
0x7000_0011u,
|
|
new Vector3(2f, 0f, 0f),
|
|
ItemType.Creature,
|
|
publicFlags: 0u);
|
|
h.Add(
|
|
0x7000_0012u,
|
|
new Vector3(1f, 0f, 0f),
|
|
ItemType.Misc,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.Add(
|
|
0x7000_0013u,
|
|
new Vector3(3f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable | SelectedObjectHealthPolicy.BfPlayer);
|
|
WorldEntity pet = h.Add(
|
|
0x7000_0014u,
|
|
new Vector3(4f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.Objects.Get(pet.ServerGuid)!.PetOwnerId = Player;
|
|
WorldEntity dead = h.Add(
|
|
0x7000_0015u,
|
|
new Vector3(5f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.Runtime.SetAnimationRuntime(
|
|
dead.ServerGuid,
|
|
new Animation(dead, MotionCommand.Dead));
|
|
WorldEntity hidden = h.Add(
|
|
0x7000_0016u,
|
|
new Vector3(6f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
Assert.True(h.Runtime.TryApplyState(
|
|
new SetState.Parsed(
|
|
hidden.ServerGuid,
|
|
(uint)(PhysicsStateFlags.ReportCollisions | PhysicsStateFlags.Hidden),
|
|
InstanceSequence: 1,
|
|
StateSequence: 2),
|
|
out _));
|
|
WorldEntity pending = h.Add(
|
|
0x7000_0017u,
|
|
new Vector3(7f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
Assert.True(h.Runtime.WithdrawLiveEntityProjection(pending.ServerGuid));
|
|
|
|
ClosestCombatTarget? closest = h.Query.FindClosestHostileMonster();
|
|
|
|
Assert.Equal(0x7000_0010u, closest?.ServerGuid);
|
|
Assert.Equal(64f, closest?.DistanceSquared);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #298 follow-up: retail <c>ClientCombatSystem::UpdateTargetTracking
|
|
/// @ 0x0056A950</c> (pc:375691-375696) gates <c>CameraSet::TrackTarget</c>
|
|
/// on <c>ObjectIsAttackable @ 0x0056A600</c> — the SAME wide predicate as
|
|
/// <c>ExecuteAttack</c>, not the narrower monster-only
|
|
/// <c>IsHostileMonster</c>. A compatible-PK player under explicit
|
|
/// selection is a valid camera target; a non-PK player is not.
|
|
/// </summary>
|
|
[Fact]
|
|
public void CombatCameraTracksACompatiblePkPlayerButNotAnIncompatibleOne()
|
|
{
|
|
var h = new Harness();
|
|
h.Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = Player,
|
|
Name = $"Object {Player:X8}",
|
|
Type = ItemType.Creature,
|
|
PublicWeenieBitfield = SelectedObjectHealthPolicy.BfPlayer
|
|
| SelectedObjectHealthPolicy.BfPkLiteStatus,
|
|
});
|
|
const uint pkLiteOpponent = 0x7000_0050u;
|
|
const uint nonPkPlayer = 0x7000_0051u;
|
|
h.Add(
|
|
pkLiteOpponent,
|
|
new Vector3(2f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfPlayer
|
|
| SelectedObjectHealthPolicy.BfPkLiteStatus);
|
|
h.Add(
|
|
nonPkPlayer,
|
|
new Vector3(3f, 0f, 0f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfPlayer);
|
|
|
|
Assert.NotNull(h.Query.GetCombatCameraTargetPoint(pkLiteOpponent));
|
|
Assert.Null(h.Query.GetCombatCameraTargetPoint(nonPkPlayer));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(0.59f, true)]
|
|
[InlineData(0.61f, false)]
|
|
[InlineData(7.49f, false)]
|
|
[InlineData(7.50f, true)]
|
|
public void ApproachUsesRetailGroundItemRadiusAndAceChargeBoundary(
|
|
float distance,
|
|
bool expectedBoundary)
|
|
{
|
|
var h = new Harness();
|
|
h.Add(Target, new Vector3(distance, 0f, 0f), ItemType.Misc);
|
|
|
|
Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach));
|
|
|
|
if (distance < 1f)
|
|
Assert.Equal(expectedBoundary, approach.IsCloseRange);
|
|
else
|
|
Assert.Equal(expectedBoundary, approach.CanCharge);
|
|
Assert.Equal(0.6f, approach.UseRadius);
|
|
Assert.Equal(0.5f, approach.TargetRadius);
|
|
Assert.Equal(2f, approach.TargetHeight);
|
|
}
|
|
|
|
/// <summary>
|
|
/// R2 gate-finding fix (2026-08-08, grand-gate walk-to-vendor
|
|
/// regression): retail reads the TARGET's own wire-authored
|
|
/// <c>PublicWeenieDesc::_useRadius</c> directly for range/approach
|
|
/// purposes (<c>CPlayerSystem::RegisterObjectRangeHandler</c>,
|
|
/// <c>pc:195159</c>/<c>203677</c>/<c>210429</c> — <c>203677</c> is
|
|
/// <c>gmVendorUI::OpenVendor</c>'s own range-handler registration,
|
|
/// reading <c>eax->pwd._useRadius</c> for a VENDOR target
|
|
/// specifically). ACE's server-side acceptance test
|
|
/// (<c>WorldObject_Use.cs:47-55</c>, <c>IsWithinUseRadiusOf</c>) reads
|
|
/// the SAME wire field, falling back to 0.6f only when the object
|
|
/// genuinely doesn't author one.
|
|
/// <para>
|
|
/// A prior CLIENT-SIDE item-type heuristic (3m for any
|
|
/// <see cref="ItemType.Creature"/>, 2m for a "large object" flag
|
|
/// combination) ignored the wire field entirely. For a typical vendor
|
|
/// NPC (Creature-typed, authoring a much tighter UseRadius than 3m)
|
|
/// this made the client's own local "arrived" test satisfied well
|
|
/// outside ACE's real acceptance zone: <see cref="AcDream.Core.Physics.Motion.MoveToManager"/>
|
|
/// would stop walking and <see cref="SelectionInteractionController.RequestUse"/>
|
|
/// would dispatch Use the instant this too-loose local threshold was
|
|
/// crossed, long before the player was ever within ACE's own
|
|
/// poll-based <c>WithinUseRadius</c> check — reproducing the exact
|
|
/// "Use lost silently" defect the AP-170/G3 arrival-gate fix was
|
|
/// meant to close, just from a different angle (a too-loose LOCAL
|
|
/// arrival threshold, not a missing arrival gate).
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void ApproachUsesTheCreatureTargetsOwnWireAuthoredUseRadius()
|
|
{
|
|
var h = new Harness();
|
|
// A vendor NPC: Creature-typed, wire-authored UseRadius DIFFERENT
|
|
// from both the old 3m heuristic AND the 0.6f no-wire-value
|
|
// fallback, so this test actually discriminates "read the wire"
|
|
// from either the retired heuristic or a coincidental default match.
|
|
h.Add(Target, new Vector3(2f, 0f, 0f), ItemType.Creature, useRadius: 1.5f);
|
|
|
|
Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach));
|
|
|
|
Assert.Equal(1.5f, approach.UseRadius);
|
|
// 2m > 1.5m: genuinely out of range, must arm a walk — not the old
|
|
// heuristic's false "already close enough" (2m <= 3m).
|
|
Assert.False(approach.IsCloseRange);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Companion to <see cref="ApproachUsesTheCreatureTargetsOwnWireAuthoredUseRadius"/>:
|
|
/// when the wire genuinely carries no UseRadius (retail's zeroed-struct
|
|
/// default / ACE's <c>wo.UseRadius == null</c> case), the fallback is
|
|
/// ACE's own flat 0.6f default (<c>WorldObject_Use.cs:50</c>,
|
|
/// <c>useRadius ?? 0.6f</c>) — not a Creature-specific guess.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ApproachFallsBackToAceDefaultUseRadiusWhenTheWireOmitsIt()
|
|
{
|
|
var h = new Harness();
|
|
h.Add(Target, new Vector3(2f, 0f, 0f), ItemType.Creature);
|
|
|
|
Assert.True(h.Query.TryGetApproach(Target, out InteractionApproach approach));
|
|
|
|
Assert.Equal(0.6f, approach.UseRadius);
|
|
Assert.False(approach.IsCloseRange);
|
|
}
|
|
|
|
[Fact]
|
|
public void PickupAndUseabilityPreserveIndependentRetailGates()
|
|
{
|
|
var h = new Harness();
|
|
const uint stuckComponent = 0x7000_0020u;
|
|
const uint looseComponent = 0x7000_0021u;
|
|
h.Add(
|
|
stuckComponent,
|
|
Vector3.UnitX,
|
|
ItemType.SpellComponents,
|
|
useability: 1u,
|
|
objectDescriptionFlags: 0x0004u);
|
|
h.Add(
|
|
looseComponent,
|
|
Vector3.UnitX * 2f,
|
|
ItemType.SpellComponents,
|
|
useability: 1u);
|
|
|
|
Assert.False(h.Query.IsUseable(stuckComponent));
|
|
Assert.False(h.Query.IsPickupable(stuckComponent));
|
|
Assert.False(h.Query.IsUseable(looseComponent));
|
|
Assert.True(h.Query.IsPickupable(looseComponent));
|
|
}
|
|
|
|
[Fact]
|
|
public void UseabilityUsesRetailLowNoBitForAbsentAndExplicitValues()
|
|
{
|
|
var h = new Harness();
|
|
const uint absent = 0x7000_0022u;
|
|
const uint zero = 0x7000_0023u;
|
|
const uint no = 0x7000_0024u;
|
|
const uint neverWalk = 0x7000_0025u;
|
|
h.Add(absent, Vector3.UnitX, ItemType.Gem);
|
|
h.Add(zero, Vector3.UnitX * 2f, ItemType.Gem, useability: 0u);
|
|
h.Add(no, Vector3.UnitX * 3f, ItemType.Gem, useability: ItemUseability.No);
|
|
h.Add(
|
|
neverWalk,
|
|
Vector3.UnitX * 4f,
|
|
ItemType.Gem,
|
|
useability: ItemUseability.NeverWalk);
|
|
|
|
Assert.True(h.Query.IsUseable(absent));
|
|
Assert.True(h.Query.IsUseable(zero));
|
|
Assert.False(h.Query.IsUseable(no));
|
|
Assert.True(h.Query.IsUseable(neverWalk));
|
|
}
|
|
|
|
[Fact]
|
|
public void SelectionSphereAppliesSetupOffsetScaleAndRotation()
|
|
{
|
|
var h = new Harness();
|
|
h.Add(
|
|
Target,
|
|
new Vector3(10f, 20f, 3f),
|
|
ItemType.Misc,
|
|
scale: 2f,
|
|
rotation: Quaternion.CreateFromAxisAngle(Vector3.UnitZ, MathF.PI / 2f));
|
|
|
|
Assert.True(h.Query.TryGetSelectionSphere(Target, out Vector3 center, out float radius));
|
|
|
|
Assert.True(Vector3.Distance(new Vector3(10f, 22f, 3f), center) < 0.0001f);
|
|
Assert.Equal(4f, radius);
|
|
}
|
|
|
|
[Fact]
|
|
public void VividTargetMarkerWaitsForFreshProjectionWhenDropMovePrecedesPosition()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity entity =
|
|
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
|
|
|
|
Assert.NotNull(h.Query.ResolveVividTargetInfo(Target));
|
|
|
|
Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0));
|
|
Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target));
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
|
|
|
|
// ACE may confirm InventoryPutObjectIn3D before sending the fresh
|
|
// Position. The retained entity still has the old ground pose here.
|
|
Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1));
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
|
|
|
|
var newPosition = new Vector3(30f, 40f, 2f);
|
|
entity.SetPosition(newPosition);
|
|
Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u));
|
|
|
|
var marker = h.Query.ResolveVividTargetInfo(Target);
|
|
Assert.NotNull(marker);
|
|
Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter);
|
|
}
|
|
|
|
[Fact]
|
|
public void VividTargetMarkerUsesFreshPoseWhenPositionPrecedesDropMove()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity entity =
|
|
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
|
|
Assert.True(h.Objects.MoveItem(Target, Player, newSlot: 0));
|
|
Assert.True(h.Runtime.WithdrawLiveEntityProjection(Target));
|
|
|
|
var newPosition = new Vector3(30f, 40f, 2f);
|
|
entity.SetPosition(newPosition);
|
|
Assert.True(h.Runtime.RebucketLiveEntity(Target, 0x0101_0001u));
|
|
|
|
// The spatial pose is current, but ownership continues to suppress the
|
|
// marker until ServerSaysMoveItem publishes the world placement.
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
|
|
|
|
Assert.True(h.Objects.MoveItem(Target, 0u, newSlot: -1));
|
|
var marker = h.Query.ResolveVividTargetInfo(Target);
|
|
Assert.NotNull(marker);
|
|
Assert.Equal(newPosition + Vector3.UnitX, marker.Value.SelectionSphereCenter);
|
|
}
|
|
|
|
[Fact]
|
|
public void VividTargetMarkerRejectsRetainedProjectionInsideExternalContainer()
|
|
{
|
|
var h = new Harness();
|
|
const uint corpse = 0x7000_0040u;
|
|
h.Add(Target, new Vector3(3f, 4f, 0f), ItemType.Misc);
|
|
h.Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = corpse,
|
|
Name = "Corpse",
|
|
Type = ItemType.Container,
|
|
});
|
|
|
|
Assert.True(h.Objects.MoveItem(Target, corpse, newSlot: 0));
|
|
|
|
Assert.False(h.Objects.IsOwnedByObject(Target, Player));
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(Target));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Render::GfxObjUnderSelectionRay @ 0x0054C740 records each hit under the
|
|
/// drawn part's own CPhysicsObj id (CPhysicsPart::get_physobj_id @
|
|
/// 0x0050D490), and an equipped child is a first-class CPhysicsObj
|
|
/// (CPhysicsObj::add_child @ 0x0050F870). The closest hit therefore names
|
|
/// the WEAPON, not its wielder.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PickResolvesTheEquippedChildRatherThanItsWielder()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
h.PublishParts(
|
|
(wielder, wielder.Position),
|
|
(weapon, new Vector3(0f, 0f, -5f)));
|
|
|
|
Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false));
|
|
Assert.True(h.Query.TryCaptureIdentity(RemoteWeapon, out uint localId));
|
|
Assert.Equal(weapon.Id, localId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retail's pick keeps only the closest winner; there is no second-choice
|
|
/// walk to the wielder when the winner is gone.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PickRejectsAWithdrawnEquippedChildWithoutFallingBackToTheWielder()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
h.PublishParts(
|
|
(wielder, wielder.Position),
|
|
(weapon, new Vector3(0f, 0f, -5f)));
|
|
Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false));
|
|
|
|
Assert.True(h.Runtime.WithdrawLiveEntityProjection(RemoteWeapon));
|
|
|
|
Assert.Null(h.Query.PickAtCursor(includeSelf: false));
|
|
Assert.False(h.Query.IsCurrent(RemoteWeapon, weapon.Id));
|
|
Assert.False(h.Query.TryGetInteractionTarget(RemoteWeapon, out _));
|
|
}
|
|
|
|
[Fact]
|
|
public void PickRejectsAnEquippedChildWhoseIncarnationWasReplaced()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
h.PublishParts((weapon, new Vector3(0f, 0f, -5f)));
|
|
Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false));
|
|
|
|
Assert.True(h.Runtime.UnregisterLiveEntity(
|
|
new DeleteObject.Parsed(RemoteWeapon, 1),
|
|
isLocalPlayer: false));
|
|
WorldEntity replacement = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
|
|
Assert.NotEqual(weapon.Id, replacement.Id);
|
|
Assert.Null(h.Query.PickAtCursor(includeSelf: false));
|
|
}
|
|
|
|
/// <summary>
|
|
/// SmartBox::GetObjectBoundingBox @ 0x00452E20 pushes the picked object's
|
|
/// OWN m_position, which CPhysicsObj::UpdateChild @ 0x00512D50 composes
|
|
/// from Frame::combine(parent part frame, holding frame). The marker must
|
|
/// therefore track the hand, not the wielder's feet.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VividTargetMarkerAnchorsOnTheComposedChildRoot()
|
|
{
|
|
var h = new Harness();
|
|
var wielderRoot = new Vector3(1f, 2f, 3f);
|
|
h.Add(
|
|
Wielder,
|
|
wielderRoot,
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.AddAttached(
|
|
RemoteWeapon,
|
|
wielderRoot,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f));
|
|
|
|
VividTargetInfo? marker = h.Query.ResolveVividTargetInfo(RemoteWeapon);
|
|
|
|
Assert.NotNull(marker);
|
|
// Setup sphere origin (1,0,0) composed onto the child's own root.
|
|
Assert.Equal(new Vector3(8f, 8f, 9f), marker!.Value.SelectionSphereCenter);
|
|
Assert.Equal(2f, marker.Value.SelectionSphereRadius);
|
|
// The parent-derived bookkeeping pose would have produced (2,2,3).
|
|
Assert.NotEqual(
|
|
wielderRoot + Vector3.UnitX,
|
|
marker.Value.SelectionSphereCenter);
|
|
}
|
|
|
|
/// <summary>
|
|
/// No parent fallback: a child with no composed root published this tick
|
|
/// has no live frame of its own and therefore no selection sphere.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VividTargetMarkerRefusesAnEquippedChildWithNoComposedRoot()
|
|
{
|
|
var h = new Harness();
|
|
var wielderRoot = new Vector3(1f, 2f, 3f);
|
|
h.Add(
|
|
Wielder,
|
|
wielderRoot,
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielderRoot,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f));
|
|
|
|
Assert.True(h.ChildRoots.Remove(weapon.Id));
|
|
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(RemoteWeapon));
|
|
Assert.False(h.Query.TryGetSelectionSphere(RemoteWeapon, out _, out _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// VividTargetIndicator::SetSelected @ 0x004F5CE0 suppresses only
|
|
/// self/player-owned/IN_CONTAINER. A remote character's wielded item keeps
|
|
/// its brackets; the local player's own wielded item does not.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VividTargetMarkerSuppressesOnlyThePlayersOwnWieldedChild()
|
|
{
|
|
var h = new Harness();
|
|
h.Add(
|
|
Wielder,
|
|
new Vector3(1f, 2f, 3f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.AddAttached(
|
|
RemoteWeapon,
|
|
new Vector3(1f, 2f, 3f),
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f));
|
|
h.AddAttached(
|
|
OwnWeapon,
|
|
Vector3.Zero,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Player,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0.5f, 1f));
|
|
|
|
Assert.NotNull(h.Query.ResolveVividTargetInfo(RemoteWeapon));
|
|
Assert.Null(h.Query.ResolveVividTargetInfo(OwnWeapon));
|
|
Assert.True(h.Query.IsWieldedByPlayer(OwnWeapon));
|
|
Assert.False(h.Query.IsWieldedByPlayer(RemoteWeapon));
|
|
}
|
|
|
|
/// <summary>
|
|
/// CPhysicsObj::SetLighting @ 0x00511A80 is non-recursive: it lights the
|
|
/// clicked object's own part array only, so clicking a weapon never
|
|
/// flashes its wielder.
|
|
/// </summary>
|
|
[Fact]
|
|
public void LightingPulseStartsOnTheEquippedChildIdentityOnly()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
|
|
h.Query.BeginLightingPulse(RemoteWeapon);
|
|
|
|
Assert.True(h.Scene.TryGetLighting(RemoteWeapon, weapon.Id, out _));
|
|
Assert.False(h.Scene.TryGetLighting(Wielder, wielder.Id, out _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Regression guard for the Slice 4 scope boundary: picking is widened,
|
|
/// the interaction/radar/auto-target visible set is NOT. Retail's radar
|
|
/// has no wielded blips.
|
|
/// </summary>
|
|
[Fact]
|
|
public void EquippedChildStaysOutOfTheInteractionAndRadarVisibleSet()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
|
|
Assert.False(h.Runtime.TryGetInteractionEligibleEntity(RemoteWeapon, out _));
|
|
Assert.False(h.Runtime.TryGetInteractionEligibleRecord(RemoteWeapon, out _));
|
|
Assert.False(h.Runtime.TryGetInteractionEligibleRecord(
|
|
RemoteWeapon,
|
|
weapon.Id,
|
|
out _));
|
|
Assert.DoesNotContain(
|
|
h.Runtime.VisibleRecords,
|
|
record => record.ServerGuid == RemoteWeapon);
|
|
Assert.Contains(
|
|
h.Runtime.VisibleRecords,
|
|
record => record.ServerGuid == Wielder);
|
|
|
|
// Picking, and only picking, admits the same child.
|
|
Assert.True(h.Runtime.TryGetPickEligibleRecord(RemoteWeapon, out _));
|
|
Assert.True(h.Runtime.TryGetPickEligibleRecord(
|
|
RemoteWeapon,
|
|
weapon.Id,
|
|
out _));
|
|
Assert.False(h.Runtime.TryGetPickEligibleRecord(
|
|
RemoteWeapon,
|
|
weapon.Id + 1u,
|
|
out _));
|
|
}
|
|
|
|
/// <summary>
|
|
/// CPartArray::GetSelectionSphere @ 0x00518B80 scales the authored sphere
|
|
/// by the object's own part-array scale. An attached child carries that
|
|
/// scale on its spawn record, not on its parent-derived WorldEntity.
|
|
/// </summary>
|
|
[Fact]
|
|
public void EquippedChildSelectionSphereUsesItsOwnSpawnScale()
|
|
{
|
|
var h = new Harness();
|
|
h.Add(
|
|
Wielder,
|
|
Vector3.Zero,
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.AddAttached(
|
|
RemoteWeapon,
|
|
Vector3.Zero,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(7f, 8f, 9f),
|
|
objScale: 2f);
|
|
|
|
Assert.True(h.Query.TryGetSelectionSphere(
|
|
RemoteWeapon,
|
|
out Vector3 center,
|
|
out float radius));
|
|
|
|
Assert.Equal(new Vector3(9f, 8f, 9f), center);
|
|
Assert.Equal(4f, radius);
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>ACCWeenieObject::DeterminePositionState @ 0x0058BE70</c>:
|
|
/// BEING_REMOVED, else IN_CONTAINER when <c>pwd._containerID != 0</c>, else
|
|
/// <c>pwd._location != 0</c> selects WIELDED over IN_3D_VIEW. Both the
|
|
/// pickup-legality arm at <c>0x005872B7</c> and the IR_PICK_UP-vs-
|
|
/// IR_PUT_IN_CONTAINER split in
|
|
/// <c>ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680</c> read it.
|
|
/// </summary>
|
|
[Fact]
|
|
public void WieldedPositionStateFollowsRetailsContainerThenLocationOrder()
|
|
{
|
|
var h = new Harness();
|
|
const uint ground = 0x7000_0030u;
|
|
const uint stowed = 0x7000_0031u;
|
|
h.Add(ground, Vector3.UnitX, ItemType.MeleeWeapon);
|
|
h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.AddAttached(
|
|
RemoteWeapon,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
h.AddAttached(
|
|
OwnWeapon,
|
|
Vector3.Zero,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Player,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0.5f, 1f));
|
|
h.Objects.AddOrUpdate(new ClientObject
|
|
{
|
|
ObjectId = stowed,
|
|
Name = "Stowed weapon",
|
|
Type = ItemType.MeleeWeapon,
|
|
ContainerId = Player,
|
|
CurrentlyEquippedLocation = EquipMask.MeleeWeapon,
|
|
});
|
|
|
|
Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon));
|
|
Assert.True(h.Query.IsWieldedPositionState(OwnWeapon));
|
|
Assert.False(h.Query.IsWieldedPositionState(ground));
|
|
// IN_CONTAINER wins over a stale wielded location.
|
|
Assert.False(h.Query.IsWieldedPositionState(stowed));
|
|
// Unknown weenie: retail's GetWeenieObject miss rejects at 0x005870DC.
|
|
Assert.False(h.Query.IsWieldedPositionState(0x7000_00FFu));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The wielded-item pickup gate lives in
|
|
/// <c>ItemHolder::AttemptToPlaceInContainer_IsItemLegal @ 0x005870C0</c>,
|
|
/// NOT in the pick. Retail's pick is render-coupled and consults no
|
|
/// ownership or position state, so Slice 4's selection behavior stands.
|
|
/// </summary>
|
|
[Fact]
|
|
public void PickAndSelectionStayOpenOnARemotesWieldedItemTheGateWillRefuse()
|
|
{
|
|
var h = new Harness();
|
|
WorldEntity wielder = h.Add(
|
|
Wielder,
|
|
new Vector3(0f, 0f, -10f),
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
WorldEntity weapon = h.AddAttached(
|
|
RemoteWeapon,
|
|
wielder.Position,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(0f, 0f, -5f));
|
|
h.PublishParts(
|
|
(wielder, wielder.Position),
|
|
(weapon, new Vector3(0f, 0f, -5f)));
|
|
|
|
Assert.Equal(RemoteWeapon, h.Query.PickAtCursor(includeSelf: false));
|
|
Assert.True(h.Query.TryCaptureIdentity(RemoteWeapon, out uint localId));
|
|
Assert.Equal(weapon.Id, localId);
|
|
Assert.True(h.Query.IsCurrent(RemoteWeapon, weapon.Id));
|
|
Assert.True(h.Query.TryGetInteractionTarget(RemoteWeapon, out _));
|
|
Assert.NotNull(h.Query.ResolveVividTargetInfo(RemoteWeapon));
|
|
Assert.Equal($"Object {RemoteWeapon:X8}", h.Query.Describe(RemoteWeapon));
|
|
|
|
// The pick predicates are untouched: the retail gate is the wielded
|
|
// position state combined with ownership, not pickability.
|
|
Assert.True(h.Query.IsPickupable(RemoteWeapon));
|
|
Assert.True(h.Query.IsWieldedPositionState(RemoteWeapon));
|
|
Assert.False(h.Objects.IsOwnedByObject(RemoteWeapon, Player));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The world-pickup approach belongs to <c>PositionState.IN_3D_VIEW</c>
|
|
/// alone (<c>ACCWeenieObject::UIAttemptPutInContainer @ 0x0058D680</c>).
|
|
/// An attached child's bookkeeping pose is its wielder's root, so an
|
|
/// approach built from it would walk the player to the wielder.
|
|
/// </summary>
|
|
[Fact]
|
|
public void ApproachRefusesAnAttachedChildRatherThanAnchoringOnItsWielder()
|
|
{
|
|
var h = new Harness();
|
|
const uint ground = 0x7000_0032u;
|
|
var wielderRoot = new Vector3(20f, 0f, 0f);
|
|
h.Add(ground, new Vector3(3f, 0f, 0f), ItemType.MeleeWeapon);
|
|
h.Add(
|
|
Wielder,
|
|
wielderRoot,
|
|
ItemType.Creature,
|
|
SelectedObjectHealthPolicy.BfAttackable);
|
|
h.AddAttached(
|
|
RemoteWeapon,
|
|
wielderRoot,
|
|
ItemType.MeleeWeapon,
|
|
wielderId: Wielder,
|
|
childRoot: Matrix4x4.CreateTranslation(20f, 0f, 1.2f));
|
|
|
|
Assert.False(h.Query.TryGetApproach(RemoteWeapon, out _));
|
|
// The same call still succeeds for a genuine 3-D-view item.
|
|
Assert.True(h.Query.TryGetApproach(ground, out InteractionApproach loose));
|
|
Assert.Equal(ground, loose.Target.ServerGuid);
|
|
}
|
|
|
|
private static SelectionCameraSnapshot Camera()
|
|
{
|
|
Matrix4x4 view = Matrix4x4.CreateLookAt(
|
|
Vector3.Zero,
|
|
-Vector3.UnitZ,
|
|
Vector3.UnitY);
|
|
Matrix4x4 projection = Matrix4x4.CreatePerspectiveFieldOfView(
|
|
MathF.PI / 3f,
|
|
4f / 3f,
|
|
0.1f,
|
|
100f);
|
|
return new SelectionCameraSnapshot(view, projection, new Vector2(800f, 600f));
|
|
}
|
|
|
|
private static RetailSelectionMesh Mesh()
|
|
=> new(
|
|
Vector3.Zero,
|
|
2f,
|
|
[new RetailSelectionPolygon(
|
|
[
|
|
new(-1f, -1f, 0f),
|
|
new( 1f, -1f, 0f),
|
|
new( 1f, 1f, 0f),
|
|
new(-1f, 1f, 0f),
|
|
],
|
|
SingleSided: false)]);
|
|
|
|
private static WorldEntity Entity(
|
|
uint id,
|
|
uint guid,
|
|
Vector3 position,
|
|
float scale,
|
|
Quaternion rotation)
|
|
=> new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = guid,
|
|
SourceGfxObjOrSetupId = 0x0200_0001u,
|
|
Position = position,
|
|
Rotation = rotation,
|
|
Scale = scale,
|
|
MeshRefs = [],
|
|
};
|
|
|
|
private static WorldSession.EntitySpawn Spawn(uint guid, ushort instance)
|
|
{
|
|
var position = new CreateObject.ServerPosition(
|
|
0x0101_0001u,
|
|
10f,
|
|
10f,
|
|
5f,
|
|
1f,
|
|
0f,
|
|
0f,
|
|
0f);
|
|
var physics = new PhysicsSpawnData(
|
|
RawState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
Position: position,
|
|
Movement: null,
|
|
AnimationFrame: null,
|
|
SetupTableId: 0x0200_0001u,
|
|
MotionTableId: 0x0900_0001u,
|
|
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: new PhysicsTimestamps(1, 1, 1, 1, 0, 1, 0, 1, instance));
|
|
return new WorldSession.EntitySpawn(
|
|
guid,
|
|
position,
|
|
0x0200_0001u,
|
|
[],
|
|
[],
|
|
[],
|
|
null,
|
|
null,
|
|
"fixture",
|
|
null,
|
|
null,
|
|
0x0900_0001u,
|
|
PhysicsState: (uint)PhysicsStateFlags.ReportCollisions,
|
|
InstanceSequence: instance,
|
|
MovementSequence: 1,
|
|
ServerControlSequence: 1,
|
|
PositionSequence: 1,
|
|
Physics: physics);
|
|
}
|
|
}
|