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>
27 KiB
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 1–3 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.cs → HandleActionRemoveAllFriends (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 @0x006A5DD0 →
FriendData::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-346 → AcDream.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 AppearOffline → Player.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-17 →
SquelchManager.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 full0x01F4 SetSquelchDBreplacement, 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-355 →
AcDream.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(SendSetSingleCharacterOptionfor appear-offline). - Runtime commands (generation-gated, host-shared):
src/AcDream.Runtime/GameRuntimeCommands.cs:264-300(RuntimeFriendCommandAdd/Remove/Clear/RequestLegacyList,RuntimeSquelchCommandCharacter/Account/Global scopes,IRuntimeSocialCommands), executed atDirectGameRuntimeCommandAdapter.cs:722-800; graphical route bound atLiveSessionRuntimeFactory.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-12 →
Player_Fellowship.cs:33-48 — leader-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, namedShareFellowshipExpAndLuminancein ACE — luminance rides the same flag) is a create-time-only wire field:0x00A2 FellowshipCreate=[str16L name][u32 shareXP](retailEvent_Create @0x006A67A0reads the option at create; ACEFellowship.cs:51-54latchesDesiredShareXP/ShareXPin the constructor). There is no GameAction to change it later — the seven C→S fellowship opcodes are0x00A2–0x00A6,0x0290,0x0291, period (lane B §3; ACE'sGameActionType.cshas 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 = 0inside0x02BE. - 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 atCorpse.cs:192-198, and echoes it per-fellow as theshareLootu32 in theFellowrecord. 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 → immediate0x0005) before creating.
2.3 Panel visibility — 0x00A6 FellowshipUpdateRequest (not an option, but required)
[u32 panelOpen] from gmFellowshipUI::OnVisibilityChanged @0x0048E460.
ACE GameActionFellowshipUpdateRequest.cs:8-14 →
HandleFellowshipUpdateRequest (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-123 → AddConfirmedMember directly); otherwise a
Confirmation_Fellowship popup is enqueued. The
IgnoreFellowshipRequests filter fires earlier, on the inviter's
FellowshipRecruit (Player_Fellowship.cs:98-102 —
WeenieError.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):
:112IgnoreAllegianceRequests— options1,0x00000004, autoSave, default false:113IgnoreFellowshipRequests— options1,0x00000008, autoSave, default true:129FellowshipAutoAcceptRequests— options1,0x20000000, autoSave, default false:150AppearOffline— 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):
IgnoreFellowshipRequests—ACE.Server/WorldObjects/Player_Fellowship.cs:98(FellowshipRecruit:newPlayer.GetCharacterOption(...)→ inviter getsWeenieError.FellowshipIgnoringRequests). Pure server-side filter; the target's client never sees the attempt.AutomaticallyAcceptFellowshipRequests—ACE.Server/Entity/Fellowship.cs:120(AddFellowshipMember: bypasses theConfirmation_Fellowshippopup, joins immediately). Also settable via ACE's own/optionplayer command aliasAutoAcceptFellowRequest(PlayerCommands.cs:221).IgnoreAllegianceRequests—ACE.Server/WorldObjects/Player_Allegiance.cs:317(IsPledgable, checked during SwearAllegiance: swearer getsWeenieError.YourOfferOfAllegianceWasIgnored+ "Your offer of allegiance was ignored." text).AppearOffline—Player_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:
CPhysicsPart::Draw @0x0050D7A0(@0x0050D823-35): setsRender::check_curr_object = 1for any part whosephysobj->idis nonzero (orcreature_mode). No player-id comparison exists.Render::GfxObjUnderSelectionRay @0x0054C740: sphere test (CSphere::sphere_intersects_ray) then per-polygon (CPolygon::polygon_hits_ray), keeping the closest winner inRender::m_MouseSelectData. No identity filtering.UIElement_SmartBoxWrapper::RecvNotice_SmartBoxObjectFound @0x004E5AD0: for the ordinary reasons —sr_Select(left click, set@0x004E5805) andsr_Examine(right click,@0x004E58B8) — it pulses lighting and callsACCWeenieObject::SetSelectedObject(foundId, 0)unconditionally (@0x004E5BAE);sr_Examineadditionally callsClientUISystem::ExamineObjectwith the raw found id (@0x004E5C0A).ACCWeenieObject::SetSelectedObject @0x0058C2E0: no filter of any kind — it swapsselectedID, firesSendNotice_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:29—if (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)passesincludeSelf ? 0u : _playerGuid()). - Callers (
src/AcDream.App/Interaction/SelectionInteractionController.cs)::151PickAndStoreSelection— ordinary left click passes_items.IsAnyTargetModeActive, i.e. self is excluded except in spell/item target mode;:199PickSelectAndExamine— right-click examine passesincludeSelf: false— self-examine impossible;:101PlaceDraggedItemandInteractionRetainedUiComposition.cs:497passtrue.
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)
- 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. - 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). - 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. - Per-channel character squelch: no retail pcaps exist
(
SquelchInfo.cs:24-25); ACE accepts achatMessageTypemask but the panel only ever sendsAllChannels (1). RemoveAllFriendsS→C silence: ACE sends no0x0021after0x0025; retail clears its own list. acdream'sFriendsStatehas 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.)- Self-pick eligibility depth (§4.2): whether
TryGetPickEligibleRecordadmits the local player's record today was not verified — check before shipping the includeSelf flip.
6. Ten-line summary
- AddFriend
0x0018 [str16L name], RemoveFriend0x0017 [u32 guid], RemoveAllFriends0x0025[]; ACE answers with incremental0x0021 FriendsListUpdate(updateType LAST: 0/1/2/4) — acdream builders, WorldSession sends,RuntimeFriendCommand, and the0x0021parser ALL exist; only panel wiring is missing. - Appear-offline is CharacterOption
0x27(options20x1000), auto-save → immediate0x0005; ACE's handler routes it toSetAppearOffline, which pushes0x0021 FriendStatusChangedto everyone who friended you. No retail UI row exists (U4) — our checkbox is a new affordance on a retail wire path. - 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 FULL0x01F4 SetSquelchDBreplace — sender, Runtime command, and parser all shipped. - Fellowship post-create ops: openness is the ONLY one —
0x0291 [u32 isOpen], leader-only, refused when locked, answered by WeenieErrorWithString + full0x02BE. Builder +IRuntimeFellowshipCommands.SetOpenalready exist (FA1/FA2). - ShareXP is a create-time field of
0x00A2and ShareLoot is the leader's option0x11read once in ACE's Fellowship constructor — NO post-create wire op exists for either; the panel must dim them for an existing fellowship. 0x00A6is panel VISIBILITY (gates ACE's0x02C0vitals stream), not openness — keepSetPanelOpenwired to page show/hide.- The three bits: IgnoreAllegianceRequests
0x01/options10x04, IgnoreFellowshipRequests0x02/options10x08(default TRUE), FellowshipAutoAcceptRequests0x12/options10x20000000; all auto-save →0x0005immediate, never triggering0x01A1; the 0x02↔0x12 mutual exclusion emits the cleared option's0x0005first (already modeled, MF-2). - ACE reads all three server-side: recruit filter
Player_Fellowship.cs:98, auto-acceptFellowship.cs:120, swear filterPlayer_Allegiance.cs:317— client stays store-only (D6 stands). - 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). - acdream's self-exclusion is an unregistered divergence:
RetailWorldPicker.cs:29skip fed bySelectionInteractionController.cs:151/:199passing includeSelf=false — flip to true (after checking pick-eligibility admits the local player) or add a register row.