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:
parent
11641597db
commit
ab79b91f1b
5 changed files with 280 additions and 33 deletions
|
|
@ -215,7 +215,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
HeadlessProcessContentOwner.HeadlessProcessContentLease?
|
||||
contentLease = null,
|
||||
IHeadlessBotPolicy? policyOverride = null,
|
||||
IRuntimePlacementProjectionSink? placementSinkOverride = null)
|
||||
IRuntimePlacementProjectionSink? placementSinkOverride = null,
|
||||
FellowshipAllegianceGateCoordinator? gateCoordinator = null)
|
||||
{
|
||||
_descriptor = descriptor
|
||||
?? throw new ArgumentNullException(nameof(descriptor));
|
||||
|
|
@ -288,7 +289,26 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
_ => { },
|
||||
runtime.ActionOwner.Combat.Clear),
|
||||
new LiveSessionEnteredWorldBindings(
|
||||
name => ActiveCharacterName = name,
|
||||
name =>
|
||||
{
|
||||
ActiveCharacterName = name;
|
||||
// FA6: only the Recruit half publishes its own
|
||||
// discovered name — the Leader's proximity
|
||||
// stage reads this to disambiguate the actual
|
||||
// counterpart bot from any other player entity
|
||||
// a shared ACE dev instance happens to have
|
||||
// online (live-run finding, see
|
||||
// RuntimeFriendlyTargetQuery.FindPlayerByName's
|
||||
// doc). Gating on role keeps two sessions
|
||||
// writing the SAME field from ever racing —
|
||||
// only one role ever writes it.
|
||||
if (descriptor.Policy.Role
|
||||
== HeadlessBotPolicyRole.Recruit
|
||||
&& gateCoordinator is not null)
|
||||
{
|
||||
gateCoordinator.RecruitCharacterName = name;
|
||||
}
|
||||
},
|
||||
() => { },
|
||||
() => { },
|
||||
_ => { },
|
||||
|
|
@ -323,7 +343,8 @@ internal sealed class HeadlessSessionHost : IDisposable
|
|||
descriptor.Policy,
|
||||
runtime,
|
||||
() => _pendingConfirmation,
|
||||
RespondToConfirmation);
|
||||
RespondToConfirmation,
|
||||
gateCoordinator);
|
||||
policySubscription = runtime.Subscribe(policy);
|
||||
diagnostics.Lifecycle(
|
||||
descriptor.Id,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue