Files
Project-M/Assets/_Project/Tests/EditMode/ProjectileSpawnIdTests.cs
T
kronic 12f86c86de LANTERN P1 step 2: AbilityFireSystem socket restructure + SpawnId repack
The netcode core of the 4-socket kit (Phase1_Combat_Gym_Build_Spec §1,§2,§5; review NP-1/
RS-1/DB-1/DB-3):
- PlayerInput: +4 Socket0..3 InputEvents + Burst-safe GetSocket(i); legacy Fire kept vestigial.
- PlayerInputGatherSystem: gather sockets from keyboard 1..4 (+gamepad RT/LB/RB); socket 0 also
  fires on the legacy primary (right-click / pad LT) as a bridge.
- AbilityFireSystem: query dropped to 4 type args (PlayerInput/PlayerFacing/LocalTransform/
  GhostOwner) + BufferLookup<AbilitySocket>/ComponentLookup<SocketCooldown>/BufferLookup<
  EffectiveSocketStats> (the 7-arg-cap fix); loops 4 sockets; per-socket cooldown + per-socket
  effective stats; Cone + Projectile dispatch per socket (predict-spawn Projectile-only).
- ProjectileSpawnId.Pack: pure Burst-safe key owner14|socket2|fireCount12|fork4 so same-tick
  multi-socket projectiles never collide; AbilityFireSystem uses it.
L1 clean; L2 494/494 (+5 ProjectileSpawnId tests: distinct-per-socket, fork, owner, bit-ranges,
count-wrap). L3 two-player + rollback is the group-A gate (after step 2.5). Note: the client
feel layer (muzzle/anim) still reads the legacy cooldown until step 2.5; sockets bake empty
until content (step 4), so nothing fires in-game yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-14 10:59:45 -07:00

69 lines
3.0 KiB
C#

using NUnit.Framework;
using ProjectM.Simulation;
namespace ProjectM.Tests
{
/// <summary>
/// Pure tests for the predicted-projectile SpawnId pack (<see cref="ProjectileSpawnId.Pack"/>). The
/// load-bearing property (review NP-1/RS-1/DB-3): two ability sockets firing the same-prefab projectile on
/// ONE tick with EQUAL per-socket fire counts must produce DISTINCT SpawnIds, so
/// <c>ProjectileClassificationSystem</c> binds each predicted entity to its own server ghost instead of
/// cross-adopting one and orphaning the other (the visible mis-prediction snap the old 32-bit key caused).
/// </summary>
public class ProjectileSpawnIdTests
{
[Test]
public void DifferentSocket_SameOwnerCountFork_ProducesDistinctIds()
{
// The exact collision scenario: owner 1, every socket's first shot (count 1), fork 0.
uint a = ProjectileSpawnId.Pack(netId: 1, socket: 0, fireCount: 1, fork: 0);
uint b = ProjectileSpawnId.Pack(netId: 1, socket: 1, fireCount: 1, fork: 0);
uint c = ProjectileSpawnId.Pack(netId: 1, socket: 2, fireCount: 1, fork: 0);
uint d = ProjectileSpawnId.Pack(netId: 1, socket: 3, fireCount: 1, fork: 0);
Assert.AreNotEqual(a, b);
Assert.AreNotEqual(a, c);
Assert.AreNotEqual(a, d);
Assert.AreNotEqual(b, c);
Assert.AreNotEqual(b, d);
Assert.AreNotEqual(c, d);
}
[Test]
public void DifferentFork_ProducesDistinctIds()
{
uint s0 = ProjectileSpawnId.Pack(2, 1, 5, 0);
uint s1 = ProjectileSpawnId.Pack(2, 1, 5, 1);
uint s2 = ProjectileSpawnId.Pack(2, 1, 5, 7);
Assert.AreNotEqual(s0, s1);
Assert.AreNotEqual(s0, s2);
Assert.AreNotEqual(s1, s2);
}
[Test]
public void DifferentOwner_ProducesDistinctIds()
{
Assert.AreNotEqual(ProjectileSpawnId.Pack(1, 0, 0, 0), ProjectileSpawnId.Pack(2, 0, 0, 0));
}
[Test]
public void FieldsDecodeToTheirBitRanges()
{
uint id = ProjectileSpawnId.Pack(netId: 3, socket: 2, fireCount: 7, fork: 5);
Assert.AreEqual(5u, id & 0xFu, "fork in bits 0-3");
Assert.AreEqual(7u, (id >> 4) & 0x0FFFu, "fireCount in bits 4-15");
Assert.AreEqual(2u, (id >> 16) & 0x3u, "socket in bits 16-17");
Assert.AreEqual(3u, (id >> 18) & 0x3FFFu, "owner in bits 18-31");
}
[Test]
public void FireCountWrapsAt4096_WithoutBleedingIntoSocket()
{
// count 4096 wraps to 0 within its 12-bit field and must not flip the socket bits.
uint wrapped = ProjectileSpawnId.Pack(1, 1, 4096, 0);
uint zero = ProjectileSpawnId.Pack(1, 1, 0, 0);
Assert.AreEqual(zero, wrapped, "count 4096 wraps to 0 within its 12 bits");
Assert.AreEqual(1u, (wrapped >> 16) & 0x3u, "socket bit intact across the wrap");
}
}
}