Talking to people: what the menus actually are

The menu is a table, not a state

An interaction menu is built from a per-character table of rows, filtered by two conditions in a fixed order (RoleInteract.GetNpcInteracts):

So "he wrote to me and there is nothing here" is always one of three things: no row of that kind on that character, a hide-condition that is currently true, or a show-condition that is not. The game never says which. The rows carry a 锁 description for some of the third case.

切磋 depends on which menu you opened, and 挑战 is a list someone wrote

There are two interaction menus, built by different code, and they do not offer the same things.

The world menu, the one the F key opens, is the surface a player means by sparring someone. It is built by NpcInteract.GetNpcInteracts, and 切磋 is gated there. The default 切磋 row carries param0 == 1, which that builder admits only for procedurally generated characters, so a named character offers it only by carrying a 切磋 row of its own, and 142 of them do. The handler enforces the same rule a second time: Handle_IT_TATAKAIYI counts the character's own 切磋 rows first, and on a named character with none it shows one stock line (5000117) and returns. Handle_IT_ROB does the same for 劫杀 — after paying the +30 胆识. So a mod that merely lets the default row through gets a button that answers with a refusal; the trainer's 切磋 is offered on anyone switch injects a row of its own instead, the way a commission does (CheckNpsIsNeedQieCuo). That row lands on your own teammates as well, and on them the stock spar cannot run: whether two units may hit each other is UnitController.IsHostile, and for two members of your team it answers from the team table, never from the hostile colour a spar paints — so the teammate stands with the weapon out, neither of you can land a blow, the F menu skips anyone mid-spar, and the 12 m walk is the only exit. The trainer makes the pair hostile to each other while the spar runs, and keeps NpcController.OnDead from filing the knockdown as a fallen party member, which it would otherwise do before the spar's win path is reached.

Measured over the trainer's /interacts, which reads that builder: 切磋 and 劫杀 were both offered by all three procedurally generated characters read (麒无邪 20006733, 香韩明 20005230, 梓凌瑶 20002725) and by none of the three named ones (张玄素 121401, 宁玉成 10009, 太初剑宗长老 121202).

The conversation menu is a separate surface with a builder of its own, InteractOnChat.InitData, where the injection is unconditional: IT_TATAKAIYI for every character except a teleport point, with no other test. That is a code reading and has not been checked in play.

挑战 is authored one character at a time. npc_interact carries 355 rows of type 86 (IT_TiaoZhan), one per character, each pointing back at itself, and only 4 of them carry a condition at all. Whether someone will take a challenge was decided by hand.

But the hand followed a pattern, and it is the opposite of the obvious guess. Cross-tabulated against the tables:

挑战 characters everyone named
carry a sect / genre 64% 17%
carry a title 38% 7%
on the 侠云录 5 of 355 86 exist
carry a sect-rank record (menpaiinfo) 0 of 355 44 exist

That last row is exact and worth stating plainly: the 44 characters who hold a formal position inside a sect — the ones MenPaiMenType sorts into 外门, 内门, 关门, 亲传 and 掌门 — are precisely the characters you cannot challenge. The two sets do not overlap by one member.

So the rule nobody wrote down is: 挑战 is for notable free agents — 荒冢剑魔, 青莲狂客, 白玉京魁首, the wandering sort — while anyone who holds office in a sect is reached through the sect's own systems instead. It has nothing to do with the 侠云录, where only 5 of the 86 listed characters carry an 挑战 row at all.

切磋 and 挑战 are not the same fight

Both end by calling SetCanBeWin() on the opponent, so neither can kill them. The difference is what happens to you, and it is one line:

public void EnterCompareState(...)   { m_PlayerEntity.SetCanBeWin(); StartCoroutine(RobCompareRoutine(...)); }
public void EnterTiaoZhanState(...)  {                               StartCoroutine(TiaoZhanRoutine(...)); }

切磋 protects you. 挑战 does not — lose it and you die.

The reward is the same for both, which is easy to get wrong because OnQieCuoFinished has two call sites and only one of them mentions 挑战:

// NpcController.LeaveCompare — the spar that ends without a defeat
if (pkResult && !IsSpecialQieCuoEventNpc(guid) && !m_TiaoZhanByPlayer)  OnQieCuoFinished(...);

// NpcController.Dead — the opponent actually goes down
if (m_Entity.m_CompareWithPlayer && !IsSpecialQieCuoEventNpc(guid))     OnQieCuoFinished(...);

挑战 sets m_CompareWithPlayer as well as m_TiaoZhanByPlayer, so the second site fires for it and pays the full purse: +8 好感 with the loser, +5 道德, the 悬赏 credit and the sect token whose count comes off the loser's 境界 band. The !m_TiaoZhanByPlayer on the first site stops the payout happening twice, not from happening at all.

The loot difference is not real either. SetCanBeWin resolves to EnterHostileRed(canDead: false), so a 挑战 opponent has m_CanDead = false exactly as a 切磋 one does — they cannot die. The loot branch reads !m_CompareWithPlayer || (m_TiaoZhanByPlayer && !pkResult), and its !pkResult half needs a real death, which that flag forbids. It is unreachable in a 挑战.

切磋 挑战
the opponent can die no no
you can die no yes
loot on the body none none — the branch cannot be reached
+8 好感, +5 道德, 悬赏 credit, sect token yes yes
your HP and MP afterwards full if you lost, untouched if you won same

That last row is the game's: PlayerController.OnDead refills you in full when an attacker knocked you down, and a win refills the opponent instead. The trainer's Winning a 切磋 refills you like losing one switch, on by default, gives a win the same refill.

One row differs in the fight. The approach to the fight is where 挑战 earns its keep, and the two handlers are barely related:

Handle_IT_TATAKAIYI (切磋) Handle_IT_TiaoZhan (挑战)
costs the target 15 vigor yes, and refuses below it no gate
can be turned down yes — a roll on the 战力 gap, softened by their 胆识 never
before the fight a 等待思考 random talk, an NpcChatView, then two NpcAutoChatViewXSY dialogs (5001006, 5001009), then the accept/refuse line, then the 烧香 salute and the 3-2-1 nothing — it collects the targets and calls EnterTiaoZhanState
how many opponents one one. The handler does collect every id in the row's paramString, but all 355 rows name only the character themselves, so the list holds the same person twice and the fight is one-on-one

So 挑战 is the fast, unrefusable version, and that is a real reason to prefer it: you trade the risk of dying for skipping a preamble that can also end in a refusal. The trainer's FastQieCuo option removes the 切磋 preamble, which narrows the gap to the refusal roll — and that roll only applies to a procedurally generated character; a named one always accepts.

That is bearable only because you almost never choose. The two are offered to disjoint hand-picked sets of characters, so in practice the person in front of you decides which fight you are allowed — and the ones who offer only 挑战 carry a death risk the others do not, for the same reward and with nothing on the menu to say so.

The other gates in the menu code are narrow and specific:

None of this is a rule you could infer from the person in front of you, and that is the honest summary of the whole surface: the menu is authored, not derived. Of the 11,596 named characters the F prompt will open on, 599 carry any of the three fight rows and the rest carry none. 挑战 is 355 hand-written rows, one per character. 切磋 reaches a named character only through one of the 142 rows written for it, 劫杀 through one of 210. The nine characters offering both 切磋 and 挑战 are nine copies of 太初弟子. Procedural or named, child or adult, on the board or not, bounty target or not — those are the only parts that follow a rule. The trainer's 切磋 is offered on anyone and 挑战 is offered on anyone switches, both off by default, put a row on every person who has none; the trainer guide says what that does to the economy.

The method that states the 好感 thresholds is never called. InteractOnChat.InitData(int npcid) has no callers anywhere in the assembly, and both call sites of IsReachNpcHaoGan sit inside it. So the only 好感 requirements the assembly spells out for an interaction, 拜师 at 40, 结缘 and 结义 at 70, and haogan >= 0 for everything else, describe nothing that runs.

情分索要 falls into the same hole, and what replaces the thresholds is simpler than they were. npc_interact carries exactly one row for it, on the default group, with no generated-characters-only flag and a condition that does not resolve. So the menu offers it to everyone, measured on 121401, 20006733 and 10009 alike. The 好感 50 and 情分 1000 in the unreachable method gate nothing; whatever validates a request runs when you use the interaction rather than when the menu is drawn, and that path was not traced.

The two second layers

The three or four buttons you first see (交谈, 交互, 交易, 行为) are a row of buttons. Choosing one opens a different kind of thing:

Both are reversed by the same call RoleInteract.ActivateInteracts(true), which is what the form's own backdrop is wired to on a first press. Anything that reverses only one of them will appear to work until you take the other route.

Choosing 交谈 closes the menu

InteractWithNpc keeps the interaction alive for a short list of types (赠送, 传送, 情分索要, 厨事, 巧取, 谈论, 地会) and calls DisableInteract for everything else, 交谈 included. So after a chat there is no menu behind it to step back to. The game took it down when you chose.

The gift picker is a separate view, and its exit leaves

The picker is InteractOther and its 容我再想想 calls RoleInteract.DisableInteract to leave the conversation outright rather than stepping back a layer. The pane describing the item you hover is a third view again, held by a field on the picker, which is why hiding the picker alone can leave a page of item text standing over the world.

The letter list has filters the dropdown never offers

MailView.OnRefresh takes a ShaiXuanType of None, HaoGan, ShengFen or Gang, and implements all four: HaoGan splits the correspondent list into the nine 好感 bands from −100..−75 up to 75..100, and Gang groups it by sect. The dropdown that drives it offers None and ShengFen. The other two are written, wired into the refresh, and unreachable from the interface.

Where the interaction menu lives

The menu is drawn inside the world HUD's form rather than in a panel of its own, so anything that reasons about "the top form" will conclude that no panel is open while you are standing in a conversation.