acdream/docs/research/2026-08-13-social-wire-completion.md
Erik 72ceddce2e fix: social panel completion batch (user gate 2026-08-13, "fix all")
One user-ordered batch across the FA social panel + world selection.
Every root cause was probe-proven before the fix (new
ProbeSocialClickRouting in SocialPanelLiveMountProbeTests - production
window mount + real UiRoot hit-tests + a synthetic click):

1. STUCK CHECKBOXES (fellowship x4, allegiance x1, "always checked /
   can't change any options"): the authored checkboxes carry DAT
   ToggleBehavior, so UiButton SELF-FLIPS Selected at MouseUp - the old
   handlers read the flipped value and wrote the ORIGINAL back, snapping
   every click to where it started (the probe recorded (id, oldValue)).
   Fix: SuppressSelfToggle (the CH6a/b mirror discipline) + derive the
   next value from the STORE; the per-tick seeding mirrors it back.
2. UNCLICKABLE ROSTER ROWS ("only get the move window cursor"): the row
   name text is display-text ClickThrough=true, which the hit-test walk
   skips regardless of HandlesClick - the wired OnClick was unreachable.
   Fix: UiText.OnClick assignment now clears ClickThrough (central,
   documented); the stats text gains the same select handler so most of
   the row's width selects the fellow.
3. TRUNCATED EMPTY-STATE ("You do not belong... To create MISSING"):
   the authored string resolves COMPLETE (three sentences) but embedded
   '\n's rendered as one clipped line. DatWidgetFactory now splits
   authored strings into one Line per newline, with the provider still
   re-reading DefaultColor live (the state-color contract - caught by
   BuildText_AuthoredLineTracksStateFontColor).
4. FELLOW NAMES WHITE (user-directed): the AD-82 invented leader-gold +
   selection-blue tints are deleted; names always white (register row
   narrowed).
5. ALLEGIANCE HEADER LABELS: bare "0"/"0" -> "Followers: N" / "Rank: [N]"
   (user-specified format; the full retail StringInfo composition stays
   AD-85's gap), monarch block matching.
6. FRIENDS/SQUELCH LIVE (AD-79 mostly retired): Add friend (name box ->
   0x0018, retail clears the box - Request_AddFriend @0x0048D240),
   Remove (row-click selection -> 0x0017), Appear Offline (CharacterOption
   0x27 via the immediate 0x0005 auto-save, ACE pushes FriendStatusChanged
   to your friend-of list), Squelch Character/Account add-by-name
   (0x0058 guid0/type AllChannels + 0x0059) and Remove for the selected
   row. The wire beneath (builders, WorldSession sends, Runtime commands,
   parsers) existed end-to-end since J4.1/FA1 - this is panel wiring only
   (docs/research/2026-08-13-social-wire-completion.md, committed here).
   Send Tell stays inert (not in the order; AD-79's remainder).
7. WORLD SELF-SELECTION ("clicking my own char should select myself"):
   retail has NO self-exclusion (CPhysicsPart::Draw @0x0050D823 arms
   every physobj; RecvNotice_SmartBoxObjectFound @0x004E5BAE selects
   unconditionally) - the includeSelf gate was an unregistered
   divergence, now removed on both the left-click and right-click paths.

Element roles were probe-measured, never guessed (Add 0x10000514 /
Remove 0x10000515 / Send Tell 0x10000516 / Appear Offline 0x1000052C /
name field 0x1000051B; Squelch: field 0x10000540, Remove 0x10000547,
Squelch Character 0x1000054B, Squelch Account 0x1000054C).

Register: AD-79 mostly retired, AD-82 narrowed. Known remainder, filed
not hidden: the fellowship page's authored 600px content vs the 362px
viewport leaves Dismiss/Assign-Leader below the fold until the window is
resized taller (probe-measured; candidate follow-up).

Tests: Checkbox_Click fact rewritten to the mirror contract (both
directions), monarch-followers label updated, includeSelf expectation
updated, probe extended (click routing, synthetic click, action-widget
role dump). App suite 4,976/3 skips.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-13 19:30:51 +02:00

27 KiB
Raw Blame History

Social-panel completion batch — exact wire mechanics (2026-08-13)

Read-only research for the user-ordered social-panel completion features: friends add/remove, appear-offline, squelch add, fellowship option operations, the three fellowship/allegiance character-option bits, and world self-selection. Every claim below is cited to primary source; the usual three-way agreement rule (retail decomp + ACE + a third reference) is applied per message. str16L = [u16 byteLen][Windows-1252 bytes][pad to 4-byte multiple counting the 2 length bytes] throughout (see 2026-08-11-fa-fellowship-wire.md §3 for the envelope conventions — C→S GameAction body = [u32 0xF7B1][u32 seq][u32 subOpcode][payload], S→C GameEvent body = [u32 0xF7B0][u32 targetGuid][u32 eventSeq][u32 eventType][payload]; both already shipped in acdream).

Headline finding: the entire C→S sender layer, the inbound S→C parser/state layer, AND the generation-gated Runtime command surface for features 13 already exist in acdream (built for the CH command registry and Campaign FA). The implementation gap is almost purely panel wiring: the Friends/Squelch pages are deliberately read-only (FA slice FA3 scope) and no UI affordance drives the existing commands. Feature 4 (self-selection) is the one place acdream diverges from retail behavior today.


1. Add friend / remove friend / appear offline / squelch add

1.1 AddFriend — C→S 0x0018

Body [u32 0x0018][str16L characterName]
Retail sender CM_Social::Event_AddFriend @0x006A5C10 (buffer strPackSize + 0xC; opcode u32, then PStringBase<char>::Pack)
Retail UI caller gmFriendsUI::Request_AddFriend @0x0048D240 — Add button (idElement 0x10000514) reads the name edit box in gmFriendsUI::ListenToElementMessage @0x0048D520, clears it, dims the Add button (SetState(0xD)). Chat-command route: gmFriendsUI::RecvNotice_ChatCommand_AddFriend @0x0048D470
ACE handler references/ACE/Source/ACE.Server/Network/GameAction/Actions/GameActionAddFriend.cs:7-12 (GameActionType.AddFriend = 0x0018, GameActionType.cs:12) → Player.HandleActionAddFriend, ACE.Server/WorldObjects/Player_Character.cs:141-172
ACE validation rejects self-add ("can't be friends with yourself"), unknown name ("That character does not exist"), duplicate ("already in your friends list") — each as a ChatPacket.SendServerMessage broadcast text, NOT a WeenieError
ACE success flow DB add → incremental GameEventFriendsListUpdate with FriendsUpdateTypeFlag.FriendAdded (0x0001) (Player_Character.cs:169) + a "{name} has been added to your friends list." text
Cross-check Chorizite Messages/C2S/Actions/Social_AddFriend.generated.cs:24-26 (str16L only). holtburger: opcode known but commented out / no sender (crates/holtburger-protocol/src/opcodes.rs:427)
acdream builder src/AcDream.Core.Net/Messages/ClientCommandRequests.cs:132-133 (BuildAddFriend, retail address cited in-file)

1.2 RemoveFriend — C→S 0x0017, RemoveAllFriends — C→S 0x0025

RemoveFriend body [u32 0x0017][u32 friendGuid]guid, not name. The retail Remove button resolves the selected list row's InstanceID attribute (GetAttribute_InstanceID(…, 0x10000085, …) at @0x0048D67D) and sends CM_Social::Event_RemoveFriend @0x006A5650 (buffer 0x10). The /friends remove <name> chat route (RecvNotice_ChatCommand_RemoveFriend @0x0048DAC0) resolves name→guid client-side first (@0x0048dbde).
RemoveAllFriends body [u32 0x0025] — parameterless. CM_Social::Event_ClearFriends @0x006A55C0 (buffer 0xC), chat route RecvNotice_ChatCommand_RemoveAllFriends @0x0048E020.
ACE handlers GameActionRemoveFriend.cs:6-11 (ReadUInt32 guid) → HandleActionRemoveFriend (Player_Character.cs:178-198): not-found → text error; success → incremental 0x0021 with FriendRemoved (0x0002) + text. GameActionRemoveAllFriends.csHandleActionRemoveAllFriends (Player_Character.cs:203-211): DB clear, NO S→C list update at all (the client is expected to clear its own list — retail does, in the /friends removeall handler).
Cross-check Chorizite Social_RemoveFriend.generated.cs:18-25 (ObjectId u32).
acdream builders ClientCommandRequests.cs:135-139 (BuildRemoveFriend, BuildClearFriends).

1.3 S→C FriendsListUpdate — 0x0021 (verified; it IS the 0x0021 family)

GameEventType.FriendsListUpdate = 0x0021 (ACE.Server/Network/GameEvent/GameEventType.cs:9; Chorizite Social_FriendsUpdate.generated.cs). Writer: GameEventFriendsListUpdate.cs:56-99:

u32 count
count × FriendData:
    u32 friendId
    u32 online          (0/1; forced 0 when the friend has AppearOffline set — :74-79)
    u32 appearOffline   (ACE always writes 0 — :84)
    str16L friendName
    u32 numFriendsOfFriend   (ACE: always 0 / TODO)  [u32 ids…]
    u32 numFriendOf          (ACE: always 0 / TODO)  [u32 ids…]
u32 updateType      — LAST field, after the list:
                      0=FullList  1=FriendAdded  2=FriendRemoved  4=FriendStatusChanged

Retail parser: CM_Social::DispatchUI_FriendsUpdate @0x006A5DD0FriendData::UnPack @0x005B9D20; UI application: gmFriendsUI::ServerSays_AddFriend @0x0048DEF0 / ServerSays_RemoveFriend @0x0048DFB0 / ServerSays_UpdateFriend @0x0048DCD0. The full list arrives unprompted at login (Player_Networking.cs:117 sends it with the login bundle).

acdream already parses and owns this: src/AcDream.Core.Net/Messages/SocialStateMessages.cs:13-43 (ParseFriendsUpdate, retail addresses cited in-file) → registered at GameEventWiring.cs:341-346AcDream.Core/Social/FriendsState.cs (Apply handles Full/Add/Remove/OnlineStatus), owned by J4.1's RuntimeCommunicationState. Nothing to build inbound.

1.4 Appear offline — a character option, NOT a dedicated GameAction

There is no standalone appear-offline opcode. AppearOffline is PlayerOption 0x27, stored in options2_ bit 12 (mask 0x00001000 — retail PlayerModule::GetOption switch @0x005D3D56-67; ACE CharacterOptions2.cs:25). It is in retail's 21-entry auto-save table (CPlayerModule::IsAutoSaveOption @0x0059A600, byte-verified list in 2026-08-10-character-options-map.md §1.3 — id 0x27 IS auto-save), so setting it sends SetSingleCharacterOption (0x0005) [u32 0x27][u32 value] immediately via CM_Character::Event_PlayerOptionChangedEvent @0x006A1510, dispatched from CPlayerModule::OnChanged @0x0059A8E0 (@0x0059a99d-9b6).

ACE side (GameActionSetSingleCharacterOption.cs:16-18): the 0x0005 handler special-cases AppearOfflinePlayer.SetAppearOffline (Player_Character.cs:221-226), which flips the option and calls SendFriendStatusUpdates (Player_Networking.cs:274-294): every online player who has YOU friended receives an incremental 0x0021 with FriendStatusChanged (0x0004) and the forced online flag plus a "{Name} has gone offline." text — i.e., other players genuinely see you go offline. Login/logout honor it too (PlayerManager.cs:430,464), and the full-list writer masks you out of other players' lists (GameEventFriendsListUpdate.cs:76-79). ACE deliberately does not persist it (Player_Character.cs:219 comment).

Open retail question (carried, not new): the 2013 client has NO UI row and no SetAppearOffline caller outside the generic SetOption switch — how retail's own client ever set 0x27 is UNKNOWN (options map §3 row 0x27 + §8 item U4). Any acdream checkbox for it is an acdream-authored affordance driving a fully retail wire path. HeadlessConfigurationLoader.cs:45 already declares it by name for bots.

1.5 Squelch add/remove (by name) — C→S 0x0058 / 0x0059 / 0x005B

Opcode Body Retail sender
0x0058 ModifyCharacterSquelch [u32 add(0/1)][u32 characterGuid][str16L characterName][u32 chatMessageType] CM_Communication::Event_ModifyCharacterSquelch @0x006A42D0 (buffer strPackSize + 0x18; field order byte-read from the body: opcode, arg1=add, arg2=guid, Pack(name), arg4=type)
0x0059 ModifyAccountSquelch [u32 add][str16L characterName] Event_ModifyAccountSquelch @0x006A41E0
0x005B ModifyGlobalSquelch [u32 add][u32 chatMessageType] Event_ModifyGlobalSquelch @0x006A3D00

Add-by-name is native: retail's squelch panel sends guid = 0 with the typed name (gmSquelchUI::ListenToElementMessage @0x0048C860, add call @0x0048CA0D: Event_ModifyCharacterSquelch(1, 0, &name, 1)chatMessageType 1 = AllChannels, ACE ChatMessageType.cs:47). The account-checkbox variant sends 0x0059 instead (@0x0048C99C). The chat-window speaker toggle goes through gmMainChatUI::ToggleSquelchOnCurrentSpeakableTarget @0x004CD230 (@0x004CD2F6).

ACE (GameActionModifyCharacterSquelch.cs:12-17SquelchManager.HandleActionModifyCharacterSquelch, ACE.Server/WorldObjects/Managers/SquelchManager.cs:74-124):

  • channel legality check (IsLegalChannel, :78) → text error if bad;
  • guid≠0 looks up by guid; guid==0 falls back to name lookup (:86-107) — so name-only add works;
  • self-squelch rejected (:109-113);
  • on success UpdateSquelchDB() + SendSquelchDB() (:121-123) — the S→C answer is always a full 0x01F4 SetSquelchDB replacement, never an incremental. Account (:195-250) and global (:255-279) do the same.

S→C 0x01F4 SetSquelchDB (GameEventType.cs:51; writer GameEventCommunicationSetSquelch.cs + Network/Structure/SquelchDB.cs:158-165

  • SquelchInfo.cs:50-65):
PackableHashTable<string,u32> accounts   — ALWAYS EMPTY in retail pcaps (ACE writes header 0/0)
PackableHashTable<u32, SquelchInfo> characters  — account squelches folded in with Account=true
SquelchInfo globals
SquelchInfo = [u32 filterWordCount][u32 mask × count][str16L playerName][u32 isAccount]

ACE writes each character's filter list as the same mask 4× (SquelchInfo.cs:21-27 — "if not sent 4x, the checkbox in the chat menu doesn't toggle"; why retail wants 4 words is an ACE-acknowledged UNKNOWN). Chorizite Communication_SetSquelchDB.generated.cs agrees on the envelope. acdream already parses it — SocialStateMessages.ParseSquelchDatabase (:45-61, retail SquelchDB::UnPack @0x006B1900 cited) → GameEventWiring.cs:350-355AcDream.Core/Social/SquelchState.cs (full Replace semantics, matching the wire).

1.6 acdream seam — feature 1 is wiring-only

Already shipped (verify, don't rebuild):

  • Builders: ClientCommandRequests.cs:132-188 (all six, retail addresses cited).
  • Session sends: WorldSession.cs:2235-2274 (SendAddFriend, SendRemoveFriend, SendClearFriends, SendModifyCharacterSquelch, SendModifyAccountSquelch, SendModifyGlobalSquelch) and :2202 (SendSetSingleCharacterOption for appear-offline).
  • Runtime commands (generation-gated, host-shared): src/AcDream.Runtime/GameRuntimeCommands.cs:264-300 (RuntimeFriendCommand Add/Remove/Clear/RequestLegacyList, RuntimeSquelchCommand Character/Account/Global scopes, IRuntimeSocialCommands), executed at DirectGameRuntimeCommandAdapter.cs:722-800; graphical route bound at LiveSessionRuntimeFactory.cs:520-525/595-599.
  • Option path: RuntimeCharacterOptionsState.TrySetOption (RuntimeCharacterState.cs:806-838) auto-save dispatch → SendSetSingleCharacterOption (adapter :677).

To build: Friends-page Add (edit box + button → RuntimeFriendCommand Add with the typed name), Remove (selected row's guid → Remove), appear-offline checkbox (option id 0x27 through the existing option seam — note U4: no retail UI row to copy), Squelch-page add-by-name (RuntimeSquelchCommand Character scope, guid 0, type 1 = AllChannels, account checkbox → Account scope). The pages (SocialFriendsPageController.cs, SocialSquelchPageController.cs) are currently read-only by FA-D1 design.


2. Fellowship option operations

Full derivation in 2026-08-11-fa-fellowship-wire.md (lane B); this section resolves the specific "change after creation" questions.

2.1 What CAN change after creation — openness only: 0x0291

FellowshipChangeOpenness [u32 isOpen]CM_Fellowship::Event_ChangeFellowOpeness @0x006A6040, Open button case 9 in gmFellowshipUI (client pre-toggles its local _open_fellow optimistically @0x0049038A). ACE: GameActionFellowshipChangeOpenness.cs:7-12Player_Fellowship.cs:33-48leader-only (WeenieError.YouMustBeLeaderOfFellowship 0x050F-family refusal) and refused when locked (WeenieError.FellowshipIsLocked). Success → Fellowship.UpdateOpenness (Fellowship.cs:410-415) → SendWeenieErrorWithStringAndUpdate(_IsNowOpenFellowship / _IsNowClosedFellowship, name) — i.e. every member gets the WeenieErrorWithString plus a full 0x02BE FellowshipFullUpdate. acdream: builder SocialActions.BuildFellowshipChangeOpenness (SocialActions.cs:223-231), Runtime command IRuntimeFellowshipCommands.SetOpen (GameRuntimeCommands.cs:339-341) — both already shipped (FA1/FA2).

2.2 What CANNOT change after creation — ShareXP and ShareLoot

  • ShareXP (retail option FellowshipShareXP 0x0F, named ShareFellowshipExpAndLuminance in ACE — luminance rides the same flag) is a create-time-only wire field: 0x00A2 FellowshipCreate = [str16L name][u32 shareXP] (retail Event_Create @0x006A67A0 reads the option at create; ACE Fellowship.cs:51-54 latches DesiredShareXP/ShareXP in the constructor). There is no GameAction to change it later — the seven C→S fellowship opcodes are 0x00A20x00A6, 0x0290, 0x0291, period (lane B §3; ACE's GameActionType.cs has nothing else fellowship-shaped). Toggling the 0x0F option after creation changes only what your NEXT create sends. Level-spread proportional-vs-even demotion is server-side (Fellowship.CalculateXPSharing), reflected to clients as _share_xp = 0 inside 0x02BE.
  • ShareLoot is not on the create message at all: ACE reads the LEADER's ShareFellowshipLoot (0x11) character option once, in the Fellowship constructor (Fellowship.cs:56-57), enforces it at Corpse.cs:192-198, and echoes it per-fellow as the shareLoot u32 in the Fellow record. No post-create wire op exists in either ACE or the retail client (lane B master-table row 31). The only lever is flipping option 0x11 (auto-save → immediate 0x0005) before creating.

2.3 Panel visibility — 0x00A6 FellowshipUpdateRequest (not an option, but required)

[u32 panelOpen] from gmFellowshipUI::OnVisibilityChanged @0x0048E460. ACE GameActionFellowshipUpdateRequest.cs:8-14HandleFellowshipUpdateRequest (Player_Fellowship.cs:142-148): sets FellowshipPanelOpen, replies with a full 0x02BE when open, and the flag gates the whole 0x02C0 member-vitals stream (Fellowship.cs:723). Already shipped: BuildFellowshipUpdateRequest (SocialActions.cs:190-198) and IRuntimeFellowshipCommands.SetPanelOpen — the fellowship page's show/hide seam must keep calling it.

2.4 Auto-accept — server-side, driven by the option bits (see §3)

Fellowship.AddFellowshipMember (Fellowship.cs:76-132): inviter-side recruit → busy check → AutomaticallyAcceptFellowshipRequests on the TARGET short-circuits the confirmation dialog (Fellowship.cs:120-123AddConfirmedMember directly); otherwise a Confirmation_Fellowship popup is enqueued. The IgnoreFellowshipRequests filter fires earlier, on the inviter's FellowshipRecruit (Player_Fellowship.cs:98-102WeenieError.FellowshipIgnoringRequests + "not accepting fellowship requests" text). Neither bit generates any wire traffic of its own at recruit time — they are read from the server's stored option words.


3. The three character-option bits — values, wire class, ACE reads

Option PlayerOption id (0x0005 payload) Word Mask Retail accessor Auto-save? Client default
IgnoreAllegianceRequests 0x01 options1 0x00000004 (options map §3) yes false
IgnoreFellowshipRequests 0x02 options1 0x00000008 @0x005D2B30 / @0x005D2B40 yes true
FellowshipAutoAcceptRequests (ACE: AutomaticallyAcceptFellowshipRequests) 0x12 options1 0x20000000 @0x005D3020 / @0x005D3030 yes false
(context) AppearOffline 0x27 options2 0x00001000 @0x005D3740 yes false

Sources: retail bit map PlayerModule::GetOption @0x005D3AA0 switch; ACE CharacterOptions1.cs:16-17, CharacterOption.cs:18-22,70, CharacterOptions2.cs:25; auto-save membership from the byte-verified IsAutoSaveOption @0x0059A600 table (2026-08-10-character-options-map.md §1.3 — all four ids are in the 21-entry auto-save set).

Wire class: all three (plus AppearOffline) ride 0x0005 SetSingleCharacterOption immediately[u32 optionId][u32 value] — and never trigger the batched 0x01A1 PlayerModule blob. (Their bits still appear inside the blob's options1/options2 words whenever some other dirty option flushes it — that is correct and harmless; ACE stores the whole word.)

Mutual exclusion (CPlayerModule::OnChanged @0x0059A8E0, case 2 / case 0x12, decomp lines @0x0059a971-98d): setting IgnoreFellowshipRequests clears FellowshipAutoAcceptRequests and vice versa, each clear going through the real accessor so the CLEARED option's own 0x0005 is emitted first. acdream models this exactly — RuntimeCharacterState.cs:821-834 (MF-2), and HeadlessConfigurationLoader.cs:254-266 rejects both-true bot configs.

acdream option table (src/AcDream.Runtime/Gameplay/CharacterOptionTable.cs):

  • :112 IgnoreAllegianceRequests — options1, 0x00000004, autoSave, default false
  • :113 IgnoreFellowshipRequests — options1, 0x00000008, autoSave, default true
  • :129 FellowshipAutoAcceptRequests — options1, 0x20000000, autoSave, default false
  • :150 AppearOffline — options2, 0x00001000, autoSave, default false

Id enum: CharacterOptionId (SocialActions.cs:425-480). Send seam: RuntimeCharacterOptionsState.TrySetOption (RuntimeCharacterState.cs:806-838, entry.IsAutoSave → sendAutoSave(id, value)) wired to WorldSession.SendSetSingleCharacterOption (WorldSession.cs:2202) at DirectGameRuntimeCommandAdapter.cs:677. Headless bots declare all of them by name (HeadlessConfigurationLoader.cs:30-45).

ACE READS each bit server-side (the D6 finding, confirmed with code):

  • IgnoreFellowshipRequestsACE.Server/WorldObjects/Player_Fellowship.cs:98 (FellowshipRecruit: newPlayer.GetCharacterOption(...) → inviter gets WeenieError.FellowshipIgnoringRequests). Pure server-side filter; the target's client never sees the attempt.
  • AutomaticallyAcceptFellowshipRequestsACE.Server/Entity/Fellowship.cs:120 (AddFellowshipMember: bypasses the Confirmation_Fellowship popup, joins immediately). Also settable via ACE's own /option player command alias AutoAcceptFellowRequest (PlayerCommands.cs:221).
  • IgnoreAllegianceRequestsACE.Server/WorldObjects/Player_Allegiance.cs:317 (IsPledgable, checked during SwearAllegiance: swearer gets WeenieError.YourOfferOfAllegianceWasIgnored + "Your offer of allegiance was ignored." text).
  • AppearOfflinePlayer_Character.cs:213-226, GameEventFriendsListUpdate.cs:76-79, PlayerManager.cs:430,464 (see §1.4).

The client-side consumer status in the FA fix round stands: these three are store-only on the acdream client (CharacterOptionsPageController.cs:244-249 — D6 correction comments); the server does all the filtering.


4. World self-selection — retail allows it; acdream blocks it

4.1 Retail: no self-exclusion anywhere in the selection pipeline

The retail pick is render-driven: SmartBox::find_object @0x00451C60 arms Render::set_selection_cursor + lookingForObject = 1; during the next frame every drawn part runs the ray test; SmartBox::DrawNoBlit @0x00454C20 (@0x00454C4E-70) reads the raw winner (Render::GetMouseSelectionObjectID) and publishes it via ECM_UI::SendNotice_SmartBoxObjectFound. The eligibility chain:

  1. CPhysicsPart::Draw @0x0050D7A0 (@0x0050D823-35): sets Render::check_curr_object = 1 for any part whose physobj->id is nonzero (or creature_mode). No player-id comparison exists.
  2. Render::GfxObjUnderSelectionRay @0x0054C740: sphere test (CSphere::sphere_intersects_ray) then per-polygon (CPolygon::polygon_hits_ray), keeping the closest winner in Render::m_MouseSelectData. No identity filtering.
  3. UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0: for the ordinary reasons — sr_Select (left click, set @0x004E5805) and sr_Examine (right click, @0x004E58B8) — it pulses lighting and calls ACCWeenieObject::SetSelectedObject(foundId, 0) unconditionally (@0x004E5BAE); sr_Examine additionally calls ClientUISystem::ExamineObject with the raw found id (@0x004E5C0A).
  4. ACCWeenieObject::SetSelectedObject @0x0058C2E0: no filter of any kind — it swaps selectedID, fires SendNotice_SelectionChanged, and notifies plugins.

The only self-related gate in the whole flow is the double-click-use branch (search reason 4): if (found->pwd._wielderID != SmartBox::player_id) before ItemHolder::UseObject (@0x004E5BE9) — clicking your own wielded item selects/flashes but does not Use. acdream already ports that exact gate (SelectionInteractionController.cs:169-180 comment + IsWieldedByPlayer).

Conclusion: retail lets you left-click-select and right-click-examine your OWN character whenever your model is drawn (third person). In first person the model simply isn't rendered, so it can't win the ray — an emergent property, not a filter.

4.2 acdream: the exclusion is ours, and it is deliberate but retail-divergent

  • Skip mechanism: src/AcDream.Core/Selection/RetailWorldPicker.cs:29if (part.ServerGuid == 0u || part.ServerGuid == skipServerGuid) continue;
  • Fed from src/AcDream.App/Rendering/Selection/RetailSelectionScene.cs:220-233 (Pick(..., uint skipServerGuid)) ← src/AcDream.App/Interaction/WorldSelectionQuery.cs:145-154 (PickAt(..., bool includeSelf) passes includeSelf ? 0u : _playerGuid()).
  • Callers (src/AcDream.App/Interaction/SelectionInteractionController.cs):
    • :151 PickAndStoreSelection — ordinary left click passes _items.IsAnyTargetModeActive, i.e. self is excluded except in spell/item target mode;
    • :199 PickSelectAndExamine — right-click examine passes includeSelf: falseself-examine impossible;
    • :101 PlaceDraggedItem and InteractionRetainedUiComposition.cs:497 pass true.

To match retail, the select (:151) and examine (:199) paths should pass includeSelf: true (target-mode already does). If the exclusion is retained instead, it needs a retail-divergence-register.md row — none exists today (the register has no row for self-picking; this is an unregistered deviation). Downstream note for the implementer: WorldSelectionQuery.PickAt verifies the winner through LiveEntityRuntime.TryGetPickEligibleRecord; confirm the local player's record passes that eligibility check before assuming the one-line flag flip is sufficient.


5. UNKNOWNs (explicit)

  1. U4 (carried): how retail's 2013 client sets AppearOffline 0x27 — no UI row, no non-generic setter call site (2026-08-10-character-options-map.md §3 row 0x27, §8 U4). Any acdream checkbox is a new affordance on a retail wire path.
  2. SquelchInfo 4× filter list: why retail wants the mask repeated four times (SquelchInfo.cs:21-27 — ACE's own "not sure"). Parse side already tolerant (acdream reads ≤4 words).
  3. Account-squelch key semantics: ACE is unsure whether the (always empty) top-level accounts table keyed on account name or character name (SquelchDB.cs:20-28). Moot for a client — never populated.
  4. Per-channel character squelch: no retail pcaps exist (SquelchInfo.cs:24-25); ACE accepts a chatMessageType mask but the panel only ever sends AllChannels (1).
  5. RemoveAllFriends S→C silence: ACE sends no 0x0021 after 0x0025; retail clears its own list. acdream's FriendsState has no "clear on 0x0025 ack" hook — the panel action should clear the local state the way retail's chat handler does, or simply re-request. (Behavior choice for the implementer, flagged, not researched deeper.)
  6. Self-pick eligibility depth (§4.2): whether TryGetPickEligibleRecord admits the local player's record today was not verified — check before shipping the includeSelf flip.

6. Ten-line summary

  1. AddFriend 0x0018 [str16L name], RemoveFriend 0x0017 [u32 guid], RemoveAllFriends 0x0025 []; ACE answers with incremental 0x0021 FriendsListUpdate (updateType LAST: 0/1/2/4) — acdream builders, WorldSession sends, RuntimeFriendCommand, and the 0x0021 parser ALL exist; only panel wiring is missing.
  2. Appear-offline is CharacterOption 0x27 (options2 0x1000), auto-save → immediate 0x0005; ACE's handler routes it to SetAppearOffline, which pushes 0x0021 FriendStatusChanged to everyone who friended you. No retail UI row exists (U4) — our checkbox is a new affordance on a retail wire path.
  3. Squelch add-by-name is native: 0x0058 [u32 add][u32 guid=0][str16L name][u32 type=1 AllChannels]; ACE falls back to name lookup and always answers with a FULL 0x01F4 SetSquelchDB replace — sender, Runtime command, and parser all shipped.
  4. Fellowship post-create ops: openness is the ONLY one — 0x0291 [u32 isOpen], leader-only, refused when locked, answered by WeenieErrorWithString + full 0x02BE. Builder + IRuntimeFellowshipCommands.SetOpen already exist (FA1/FA2).
  5. ShareXP is a create-time field of 0x00A2 and ShareLoot is the leader's option 0x11 read once in ACE's Fellowship constructor — NO post-create wire op exists for either; the panel must dim them for an existing fellowship.
  6. 0x00A6 is panel VISIBILITY (gates ACE's 0x02C0 vitals stream), not openness — keep SetPanelOpen wired to page show/hide.
  7. The three bits: IgnoreAllegianceRequests 0x01/options1 0x04, IgnoreFellowshipRequests 0x02/options1 0x08 (default TRUE), FellowshipAutoAcceptRequests 0x12/options1 0x20000000; all auto-save → 0x0005 immediate, never triggering 0x01A1; the 0x02↔0x12 mutual exclusion emits the cleared option's 0x0005 first (already modeled, MF-2).
  8. ACE reads all three server-side: recruit filter Player_Fellowship.cs:98, auto-accept Fellowship.cs:120, swear filter Player_Allegiance.cs:317 — client stays store-only (D6 stands).
  9. Retail has NO self-selection exclusion: any drawn physobj is ray-eligible (CPhysicsPart::Draw @0x0050D823), and found ids are selected/examined unconditionally (@0x004E5BAE/0x004E5C0A); the only self gate is the wielded-item double-click Use check (@0x004E5BE9, already ported).
  10. acdream's self-exclusion is an unregistered divergence: RetailWorldPicker.cs:29 skip fed by SelectionInteractionController.cs:151/:199 passing includeSelf=false — flip to true (after checking pick-eligibility admits the local player) or add a register row.