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

452 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.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 **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-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`
(`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-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`, 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-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`):
- `: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):**
- `IgnoreFellowshipRequests`
`ACE.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.
- `AutomaticallyAcceptFellowshipRequests`
`ACE.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`).
- `IgnoreAllegianceRequests`
`ACE.Server/WorldObjects/Player_Allegiance.cs:317`
(`IsPledgable`, checked during SwearAllegiance: swearer gets
`WeenieError.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:
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: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)` 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: false` — **self-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.