fix(headless): FA6 — name-match the Recruit bot instead of nearest-any-player

The second live gate run exposed a real environmental hazard: this shared
ACE dev instance has a THIRD player character online (+Je, guid
0x50000001), and after @teleallto it ended up nearer to the Leader bot than
the actual Recruit bot (+Horan, 0x5000000B). RuntimeFriendlyTargetQuery.
FindClosestOtherPlayer — "nearest ANY other player" — picked +Je, and the
fellowship recruit sent to it obviously never completed (confirmed live:
WaitRecruited/WaitForRecruit both timed out, both bots quarantined and
gracefully logged out cleanly).

RuntimeFriendlyTargetQuery.FindPlayerByName resolves the nearest player
whose streamed name matches exactly, with 3 new conformance tests
(preferring the named player over a closer stranger, returning null when
absent, and case-sensitivity/hidden/no-draw/self rejection).

FellowshipAllegianceGateCoordinator (AcDream.Headless.Policies) is a small
same-process, no-locking (single update thread) carrier for the Recruit
bot's own discovered character name — set by its own HeadlessSessionHost
the instant CharacterList selection resolves it, which IS D8's "discover it
live" mechanism, not a hard-coded value. Constructed once per
HeadlessProcessHost and threaded through HeadlessBotPolicyFactory.Create
into the Leader policy, which now name-matches instead of taking whichever
player entity happens to be closest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 09:53:46 +02:00
parent 11641597db
commit ab79b91f1b
5 changed files with 280 additions and 33 deletions

View file

@ -141,6 +141,106 @@ public sealed class RuntimeFriendlyTargetQueryTests
Assert.Null(RuntimeFriendlyTargetQuery.TryGetName(runtime, 0x50000099u));
}
/// <summary>
/// FA6 second gate attempt (live-run finding): a shared ACE dev
/// instance can have a THIRD player online and NEARER than the actual
/// expected counterpart — <see cref="RuntimeFriendlyTargetQuery.FindClosestOtherPlayer"/>
/// picked the wrong one live. <see cref="RuntimeFriendlyTargetQuery.FindPlayerByName"/>
/// must reject the closer stranger and resolve the named target even
/// though it is farther away.
/// </summary>
[Fact]
public void FindPlayerByName_PrefersTheNamedPlayerOverACloserStranger()
{
using GameRuntime runtime = Create();
runtime.PlayerIdentity.ServerGuid = Player;
Add(runtime, Player, 0x01010001u, 10f, 10f, "Self", PlayerBit);
Add(
runtime,
0x50000010u,
0x01010001u,
11f,
10f,
"+Je",
PlayerBit);
Add(
runtime,
0x50000011u,
0x01010001u,
30f,
10f,
"+Horan",
PlayerBit);
Assert.Equal(
0x50000011u,
RuntimeFriendlyTargetQuery.FindPlayerByName(runtime, "+Horan"));
// The plain nearest-any-player query still (correctly) prefers the
// closer stranger — this test pins that FindPlayerByName diverges
// from it deliberately, not that the sibling method is "buggy".
Assert.Equal(
0x50000010u,
RuntimeFriendlyTargetQuery.FindClosestOtherPlayer(runtime));
}
[Fact]
public void FindPlayerByName_ReturnsNullWhenNoPlayerHasThatName()
{
using GameRuntime runtime = Create();
runtime.PlayerIdentity.ServerGuid = Player;
Add(runtime, Player, 0x01010001u, 10f, 10f, "Self", PlayerBit);
Add(runtime, 0x50000010u, 0x01010001u, 11f, 10f, "+Je", PlayerBit);
Assert.Null(
RuntimeFriendlyTargetQuery.FindPlayerByName(runtime, "+Horan"));
}
[Fact]
public void FindPlayerByName_IsCaseSensitiveAndSkipsHiddenNoDrawAndSelf()
{
using GameRuntime runtime = Create();
runtime.PlayerIdentity.ServerGuid = Player;
Add(runtime, Player, 0x01010001u, 10f, 10f, "+Horan", PlayerBit);
Add(
runtime,
0x50000010u,
0x01010001u,
11f,
10f,
"+horan",
PlayerBit);
Add(
runtime,
0x50000011u,
0x01010001u,
12f,
10f,
"+Horan",
PlayerBit,
PhysicsStateFlags.Hidden);
Add(
runtime,
0x50000012u,
0x01010001u,
13f,
10f,
"+Horan",
PlayerBit,
PhysicsStateFlags.NoDraw);
Add(
runtime,
0x50000013u,
0x01010001u,
14f,
10f,
"+Horan",
PlayerBit);
Assert.Equal(
0x50000013u,
RuntimeFriendlyTargetQuery.FindPlayerByName(runtime, "+Horan"));
}
private static GameRuntime Create()
{
var operations = new Operations();