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>
This commit is contained in:
parent
ec2a7b0cce
commit
72ceddce2e
15 changed files with 1018 additions and 65 deletions
File diff suppressed because one or more lines are too long
452
docs/research/2026-08-13-social-wire-completion.md
Normal file
452
docs/research/2026-08-13-social-wire-completion.md
Normal file
|
|
@ -0,0 +1,452 @@
|
|||
# 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 **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
|
||||
`0x00A2–0x00A6`, `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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue