fix #FA4-mechanism-MUST-FIX-2: delete the non-retail client-side fellowship-invite intercept

RetailUiRuntime.TryAutoRespondToFellowshipInvite auto-declined/auto-accepted
fellowship invites based on IgnoreFellowshipRequests/FellowshipAutoAcceptRequests
before the dialog ever reached GameplayConfirmationController. Byte-verified
across Handle_Character__ConfirmationRequest @0x005640A0,
RecvNotice_FellowshipRequest @0x00490880, and MakeFellowRequestDialog
@0x00490620 (whose only guard is m_fellowRequestContext) plus a whole-file
sweep of both option accessors: retail's client reads neither bit on any
confirmation path. ACE filters both bits server-side, so the interceptor was
dead code against a correct ACE and actively harmful against a drifting one
(IgnoreFellowshipRequests defaults true, so it would silently swallow real
invites with no dialog and no chat line).

HandleConfirmationRequest now routes every confirmation type, including 4,
straight to the generic controller -- exactly like retail. No tests existed
for the deleted interceptor (nothing to remove); added a test proving the
type-4 dialog renders the server message verbatim (not "Continue?"-suffixed)
and sends accept/decline through the generic path.

Per the corrected plan D6 (docs/plans/2026-08-11-fellowship-allegiance-campaign.md).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-12 07:32:41 +02:00
parent e6d97516e5
commit 290f9b584a
2 changed files with 66 additions and 26 deletions

View file

@ -663,34 +663,35 @@ public sealed class RetailUiRuntime : IDisposable
}
/// <summary>
/// Campaign FA slice FA4, D6: a fellowship invite (<c>ConfirmationType.
/// Fellowship</c>, 4) is intercepted BEFORE it ever reaches the generic
/// <see cref="GameplayConfirmationController"/> dialog — retail's
/// <c>Fellowship.cs:121</c>-equivalent client-side mirror:
/// <c>IgnoreFellowshipRequests</c> auto-declines, <c>FellowshipAutoAcceptRequests</c>
/// auto-accepts, and the two are mutually exclusive
/// (<c>RuntimeCharacterState.TrySetOption</c>, already ported) so at
/// most one fires. Neither bit set → falls through to the generic
/// controller exactly as it already did before this slice (it already
/// matches the constants — see D6/lane A §5.3).
/// MUST-FIX 2 (FA4 fix round, 2026-08-12): every server confirmation
/// request — including a fellowship invite
/// (<c>ConfirmationType.Fellowship</c>, 4) — reaches the generic
/// <see cref="GameplayConfirmationController"/> dialog unconditionally,
/// exactly like retail. This slice originally shipped a client-side
/// interceptor here (D6) that auto-declined/auto-accepted based on
/// <c>IgnoreFellowshipRequests</c>/<c>FellowshipAutoAcceptRequests</c>
/// before the dialog. Byte-verified across
/// <c>Handle_Character__ConfirmationRequest @0x005640A0</c> (a bare
/// 7-way jump table; case 4 is one call, no option read),
/// <c>RecvNotice_FellowshipRequest @0x00490880</c> (copies the string,
/// tail-calls the dialog maker, no option read), and
/// <c>MakeFellowRequestDialog @0x00490620</c> (its ONLY guard is
/// <c>m_fellowRequestContext</c>) — plus a whole-file sweep of both
/// accessors — retail's client reads NEITHER option bit on ANY
/// confirmation path. ACE filters both bits SERVER-SIDE
/// (<c>Player_Fellowship.cs:98-102</c> refuses the recruiter outright;
/// <c>Fellowship.cs:121</c> auto-joins without ever sending a type-4
/// request) — so the interceptor was either dead code (against a
/// correct ACE, which never sends the request the interceptor would
/// act on) or actively harmful (against a drifting/misconfigured
/// server, where <c>IgnoreFellowshipRequests</c>'s client default of
/// <c>true</c> would silently swallow every real invite with no dialog
/// and no chat line). Deleted outright — see
/// docs/plans/2026-08-11-fellowship-allegiance-campaign.md D6's
/// correction and register row AD-78's fix-round addendum.
/// </summary>
public bool HandleConfirmationRequest(GameEvents.CharacterConfirmationRequest request)
{
if (request.Type == (uint)GameEvents.ConfirmationType.Fellowship
&& TryAutoRespondToFellowshipInvite(request))
return true;
return _gameplayConfirmationController?.HandleRequest(request) == true;
}
private bool TryAutoRespondToFellowshipInvite(GameEvents.CharacterConfirmationRequest request)
{
bool ignore = _bindings.Options.CurrentCharacterOption((uint)CharacterOptionId.IgnoreFellowshipRequests);
bool autoAccept = _bindings.Options.CurrentCharacterOption((uint)CharacterOptionId.FellowshipAutoAcceptRequests);
if (!ignore && !autoAccept) return false;
_bindings.Confirmations.SendResponse(request.Type, request.ContextId, autoAccept);
return true;
}
=> _gameplayConfirmationController?.HandleRequest(request) == true;
public bool HandleConfirmationDone(GameEvents.CharacterConfirmationDone done)
=> _gameplayConfirmationController?.HandleDone(done) == true;

View file

@ -33,6 +33,45 @@ public sealed class GameplayConfirmationControllerTests
Assert.Equal(0u, controller.ActiveDialogContext);
}
/// <summary>
/// MUST-FIX 2 (FA4 fix round, 2026-08-12): a fellowship invite
/// (<c>ConfirmationType.Fellowship</c>, type 4) reaches this generic
/// controller and opens a dialog exactly like any other type — no
/// client-side interceptor exists anymore (D6's correction: retail's
/// client reads neither option bit on the invite path;
/// <c>RetailUiRuntime.HandleConfirmationRequest</c> now routes every
/// type, including 4, straight here). Type 4 is NOT in the
/// 2/3/5/6 " Continue?"-suffix set, so the message renders verbatim —
/// matching <c>Handle_Character__ConfirmationRequest @0x005640A0</c>'s
/// case-4 arm, which is a single call with no text transformation.
/// </summary>
[Fact]
public void FellowshipInviteRequest_Type4_OpensDialog_MessageVerbatim_AndSendsAcceptOnClose()
{
var root = new UiRoot { Width = 800f, Height = 600f };
ImportedLayout? shown = null;
var factory = new RetailDialogFactory(root, _ =>
shown = FixtureLoader.LoadConfirmationDialog());
var responses = new List<(uint Type, uint Context, bool Accepted)>();
using var controller = new GameplayConfirmationController(
factory,
(type, context, accepted) => responses.Add((type, context, accepted)));
Assert.True(controller.HandleRequest(
new GameEvents.CharacterConfirmationRequest(4u, 7u, "Alice invites you to join their fellowship.")));
Assert.Equal(
"Alice invites you to join their fellowship.",
string.Join(" ", Assert.IsType<UiText>(shown!.FindElement(
RetailConfirmationDialogView.MessageElementId)).LinesProvider().Select(static line => line.Text)));
Assert.IsType<UiButton>(shown.FindElement(
RetailConfirmationDialogView.AcceptButtonId)).OnClick!();
Assert.Equal([(4u, 7u, true)], responses);
Assert.Equal(0u, controller.ActiveDialogContext);
}
[Fact]
public void MatchingConfirmationDoneClosesDialogAndUnmatchedTupleDoesNothing()
{