done learn and upgrade skill

This commit is contained in:
VDH
2026-01-14 18:00:30 +07:00
parent f5a9227cc7
commit 57b5195c36
15 changed files with 2000 additions and 1719 deletions
@@ -1612,8 +1612,6 @@ namespace BrewMonster
public void SetAboutToDie(bool bFlag) { m_bAboutToDie = bFlag; }
public bool IsHangerOn() { return m_bHangerOn; }
// Show / hide wing
public void ShowWing(bool bShow)
{
@@ -1409,27 +1409,31 @@ namespace CSNetwork.S2CCommand
// public char data[1];
//};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_sevnpc_hello
{
public int id;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_normal_attack
{
public byte pvp_mask;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_select_target
{
public int id;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_sevnpc_serve
{
public int service_type;
public uint len;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct accept_task_CONTENT
{
public int idTask;
@@ -1437,21 +1441,25 @@ namespace CSNetwork.S2CCommand
public int idRefreshItem;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SevReturnTaskCONTENT
{
public int idTask;
public int iChoice;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SevTaskMatterCONTENT
{
public int idTask;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct SevLearnSkillCONTENT
{
public int idSkill;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_sevnpc_serve2
{
public int service_type;
@@ -1584,7 +1584,19 @@ namespace CSNetwork.GPDataType
{
return (id & 0x80000000) != 0 && (id & 0x40000000) == 0;
}
public static string ReplacePercentD(string fmt, params object[] args)
{
if (string.IsNullOrEmpty(fmt) || args == null || args.Length == 0)
return fmt;
for (int i = 0; i < args.Length; i++)
{
int idx = fmt.IndexOf("%d", StringComparison.Ordinal);
if (idx < 0) break; // hết %d
fmt = fmt.Substring(0, idx) + args[i]?.ToString() + fmt.Substring(idx + 2);
}
return fmt;
}
public static bool ISMONEYTID(int tid)
{
return tid == 3044;
@@ -583,8 +583,17 @@ namespace CSNetwork
break;
case CommandID.ERROR_MESSAGE:
_logger.Info($"### GameDataSend: ERROR_MESSAGE: {BitConverter.ToInt32(pDataBuf, 0)}");
{
int errRaw = BitConverter.ToInt32(pDataBuf, 0);
// Note: _logger may be configured as a file logger via SetLogPath(), so also log to console for visibility.
_logger.Info($"### GameDataSend: ERROR_MESSAGE: {errRaw}");
#if UNITY_EDITOR
BMLogger.LogError($"### GameDataSend: ERROR_MESSAGE: {errRaw}");
#endif
cmd_error_msg pCmd = GPDataTypeHelper.FromBytes<cmd_error_msg>(pDataBuf);
#if UNITY_EDITOR
BMLogger.LogError($"### GameDataSend: ERROR_MESSAGE parsed iMessage={pCmd.iMessage}");
#endif
if (pCmd.iMessage != 0)
{
@@ -639,6 +648,7 @@ namespace CSNetwork
}
break;
}
case CommandID.SELECT_TARGET:
case CommandID.UNSELECT:
@@ -670,6 +680,16 @@ namespace CSNetwork
break;
case CommandID.NPC_GREETING:
{
// If this greeting is from the skill-learn NPC, record it (C++ skill dialog relies on this).
try
{
cmd_npc_greeting greet = GPDataTypeHelper.FromBytes<cmd_npc_greeting>(pDataBuf);
CECHostSkillModel.Instance.OnNpcGreeting(greet.idObject);
}
catch (Exception ex)
{
_logger.Log(LogType.Warning, $"Failed to parse NPC_GREETING payload: {ex.Message}");
}
EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_NPCGREETING, MANAGER_INDEX.MAN_PLAYER, 0, pDataBuf, pCmdHeader);
break;
}
@@ -812,6 +832,14 @@ namespace CSNetwork
BMLogger.LogError("### GameDataSend: LEARN_SKILL");
EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_LEARNSKILL, MANAGER_INDEX.MAN_PLAYER, 0, pDataBuf, pCmdHeader);
break;
default:
#if UNITY_EDITOR
if (isDebug)
{
BMLogger.LogError($"### GameDataSend: Unhandled CMDID {pCmdHeader} (payloadBytes={pDataBuf?.Length ?? 0})");
}
#endif
break;
}
}
@@ -34,6 +34,7 @@ namespace BrewMonster.Scripts.Skills
private Dictionary<int, int> m_godRootMap = new Dictionary<int, int>();
private Dictionary<int, int> m_baseRootMap = new Dictionary<int, int>();
private int m_skillLearnNPCNID;
private bool m_bReceivedNPCGreeting;
private bool m_bInitialized;
private Octets m_npcListData;
@@ -52,8 +53,37 @@ namespace BrewMonster.Scripts.Skills
public CECHostSkillModel()
{
m_skillLearnNPCNID = 0;
m_bReceivedNPCGreeting = false;
m_bInitialized = false;
}
// NPC技能学习相关 / Skill-learn NPC helpers
public bool IsSkillLearnNPC(int nid) => nid == m_skillLearnNPCNID;
public bool IsSkillLearnNPCExsit() => m_skillLearnNPCNID != 0;
public bool IsReceivedNPCGreeting() => m_bReceivedNPCGreeting;
public void SetReceivedNPCGreeting(bool received) => m_bReceivedNPCGreeting = received;
public void OnNpcGreeting(int idObject)
{
// cmd_npc_greeting.idObject is the NPC/player id (nid)
if (idObject == m_skillLearnNPCNID && m_skillLearnNPCNID != 0)
{
m_bReceivedNPCGreeting = true;
//BMLogger.LogError($"[Skill] Received NPC_GREETING from skill-learn NPC nid={m_skillLearnNPCNID}");
}
}
public void SendHelloToSkillLearnNPC()
{
//BMLogger.LogError($"[Skill] Sent SEVNPC_HELLO to skill-learn NPC nid={m_skillLearnNPCNID}");
if (m_skillLearnNPCNID != 0)
{
// C++: g_pGame->GetGameSession()->c2s_CmdNPCSevHello(m_skillLearnNPCNID);
BrewMonster.Network.UnityGameSession.c2s_CmdNPCSevHello(m_skillLearnNPCNID);
//BMLogger.LogError($"[Skill] Sent SEVNPC_HELLO to skill-learn NPC nid={m_skillLearnNPCNID}");
}
}
public enumSkillLearnedState GetSkillLearnedState(int skillID)
{
CECSkill pSkill = CECGameRun.Instance.GetHostPlayer().GetNormalSkill(skillID);
@@ -82,7 +112,7 @@ namespace BrewMonster.Scripts.Skills
}
public void Initialize()
{
BMLogger.LogError("HoangDev CECHostSkillModel Initialize called");
//BMLogger.LogError("HoangDev CECHostSkillModel Initialize called");
// Çå¿ÕËùÓм¼ÄÜ£¬·ÀÖ¹ÒòΪ¶à¸ö½ÇÉ«µÇ¼µ¼ÖÂÖØ¸´¼ÓÔØ¼¼ÄÜ
Release();
@@ -99,7 +129,7 @@ namespace BrewMonster.Scripts.Skills
}
public void ProcessServiceList()
{
BMLogger.LogError("HoangDev: ProcessServiceList");
//BMLogger.LogError("HoangDev: ProcessServiceList");
if (m_npcListData == null)
{
BMLogger.LogError("CECHostSkillModel::ProcessServiceList, m_npcListData is null.");
@@ -126,20 +156,21 @@ namespace BrewMonster.Scripts.Skills
npcList.list[z] = GPDataTypeHelper.FromBytes<cmd_scene_service_npc_list.NpcEntry>(bodyBytes, offset);
offset += NpcEntrySize;
}
BMLogger.LogError("ProcessServiceList npcList.count:" + npcList.count);
BMLogger.LogError("ProcessServiceList m_allProfNPCs.count:" + m_allProfNPCs.Count);
//BMLogger.LogError("ProcessServiceList npcList.count:" + npcList.count);
//BMLogger.LogError("ProcessServiceList m_allProfNPCs.count:" + m_allProfNPCs.Count);
int i;
for (i = 0; i < npcList.count; i++)
{
int tid = npcList.list[i].tid;
BMLogger.LogError("ProcessServiceList tid:" + tid);
//BMLogger.LogError("ProcessServiceList tid:" + tid);
if (m_allProfNPCs.Contains(tid))
{
BMLogger.LogError("m_skillLearnNPCNID : " + m_skillLearnNPCNID);
BMLogger.LogError("npcList.list[i].nid : " + npcList.list[i].nid);
//BMLogger.LogError("m_skillLearnNPCNID : " + m_skillLearnNPCNID);
//BMLogger.LogError("npcList.list[i].nid : " + npcList.list[i].nid);
if (m_skillLearnNPCNID != npcList.list[i].nid)
{
m_skillLearnNPCNID = npcList.list[i].nid;
m_bReceivedNPCGreeting = false; // new NPC -> need greeting again
SetCurServiceSkills(tid);
var change = new CECSkillPanelChange(CECSkillPanelChange.enumChangeMask.CHANGE_SKILL_NPC, 0, 0);
//NotifyObservers(change);
@@ -157,7 +188,7 @@ namespace BrewMonster.Scripts.Skills
}
m_npcListData.Clear();
}
BMLogger.LogError("HoangDev: m_npcListData.Size :"+ m_npcListData.Size);
//BMLogger.LogError("HoangDev: m_npcListData.Size :"+ m_npcListData.Size);
}
private readonly HashSet<int> m_curServiceSkills = new HashSet<int>();
@@ -168,7 +199,7 @@ namespace BrewMonster.Scripts.Skills
}
public void SetCurServiceSkills(int tid)
{
BMLogger.LogError("SetCurServiceSkills " + tid);
//BMLogger.LogError("SetCurServiceSkills " + tid);
m_curServiceSkills.Clear();
if (tid == 0)
return;
@@ -193,8 +224,8 @@ namespace BrewMonster.Scripts.Skills
if (skillId != 0)
{ m_curServiceSkills.Add(skillId); }
}
BMLogger.LogError("SetCurServiceSkills m_curServiceSkills count:" + m_curServiceSkills.Count);
BMLogger.LogError("SetCurServiceSkills skillService.id_skills count:" + skillService.id_skills.Length);
//BMLogger.LogError("SetCurServiceSkills m_curServiceSkills count:" + m_curServiceSkills.Count);
//BMLogger.LogError("SetCurServiceSkills skillService.id_skills count:" + skillService.id_skills.Length);
}
public enumSkillFitLevelState GetSkillFitLevel(int skillID)
{
@@ -389,7 +420,7 @@ namespace BrewMonster.Scripts.Skills
DATA_TYPE dt = DATA_TYPE.DT_NPC_ESSENCE;
elementdataman pDB = ElementDataManProvider.GetElementDataMan();
var map = pDB.GetAllDataTypeWithType(ID_SPACE.ID_SPACE_ESSENCE, dt);
BMLogger.LogError("Hoang Dev map Count :" + map.Length);
//BMLogger.LogError("Hoang Dev map Count :" + map.Length);
foreach (var obj in map)
{
@@ -407,8 +438,8 @@ namespace BrewMonster.Scripts.Skills
if (pSkill == null)
{
BMLogger.LogError("Hoang Dev pSkill is null for skill id :" + skillService.id_skills[i]);
return;
//BMLogger.LogError($"Hoang Dev pSkill is null for skill {i} :" + skillService.id_skills[i]);
continue;
}
@@ -422,10 +453,10 @@ namespace BrewMonster.Scripts.Skills
}
if (profCorrect)
{
BMLogger.LogError("m_allProfNPCs.Add " + (int)npcEssence.id);
//BMLogger.LogError("m_allProfNPCs.Add " + (int)npcEssence.id);
m_allProfNPCs.Add((int)npcEssence.id);
}
BMLogger.LogError("Hoang Dev skillService.id_skills.Length :" + skillService.id_skills.Length);
//BMLogger.LogError("Hoang Dev skillService.id_skills.Length :" + skillService.id_skills.Length);
}
}
}
@@ -512,7 +543,7 @@ namespace BrewMonster.Scripts.Skills
private void Release()
{
BMLogger.LogError("HoangDev CECHostSkillModel Release called");
//BMLogger.LogError("HoangDev CECHostSkillModel Release called");
m_allProfSkills.Clear();
// Dọn sạch tất cả dictionary / map
@@ -0,0 +1,55 @@
using BrewMonster.Scripts.Skills;
using BrewMonster.UI;
using System;
using UnityEngine;
using UnityEngine.UI;
namespace BrewMonster
{
public class CDlgSkillAction : AUIDialog
{
[SerializeField] private Button uiSkillButton;
[SerializeField] private GameObject skillUI;
bool m_bOpenAction;
bool m_bReceivedNCPGreeting; // ÊÇ·ñÊÕµ½ÁËNPCµÄGreeting
public override void Awake()
{
base.Awake();
uiSkillButton.onClick.RemoveAllListeners();
uiSkillButton.onClick.AddListener(OnSkillButtonClicked);
}
private void OnSkillButtonClicked()
{
TryOpenDialog(false);
}
public void TryOpenDialog(bool bAction)
{
var boolll = skillUI.activeInHierarchy;
if (boolll)
{
skillUI.SetActive(!boolll);
return;
}
skillUI.SetActive(!boolll);
if (skillUI.activeInHierarchy)
if (!GetHostPlayer().IsTalkingWithNPC())
{
m_bOpenAction = bAction;
CECHostSkillModel.Instance.SendHelloToSkillLearnNPC();
SetReceivedNPCGreeting(false);
}
}
public void SetReceivedNPCGreeting(bool bReceived)
{
m_bReceivedNCPGreeting = bReceived;
}
public bool IsReceivedNPCGreeting()
{
return m_bReceivedNCPGreeting;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 3c007a40af961624cae5041be0798d18
@@ -179,6 +179,7 @@ namespace BrewMonster.UI
taoistRank != CECTaoistRank.GetEvilRankEnd();
taoistRank = taoistRank.GetNext())
{
BMLogger.LogError("ResetDialog Evil rank " + taoistRank.GetName());
AddDlgsOfOneRank(taoistRank);
}
@@ -201,7 +202,7 @@ namespace BrewMonster.UI
// ޸ijһ״̬ / Refresh a single skill sub dialog
private void UpdateOneSubDlg(int skillID)
{
BMLogger.LogError("UpdateOneSubDlg");
//BMLogger.LogError("UpdateOneSubDlg");
if (!m_skillSubDialogsMap.TryGetValue(skillID, out var pSub))
{
return;
@@ -47,8 +47,6 @@ namespace BrewMonster
/*CDlgQuickBar* pQuickBar = dynamic_cast<CDlgQuickBar*>(GetGameUIMan()->GetDialog(a_pszPanel[i]));
if (!pQuickBar || !pQuickBar->IsShow()) continue;*/
for (int j = 0; j < AUIImagePictureList.Count; j++)
{
pCell = AUIImagePictureList[j];
@@ -1,5 +1,6 @@
using BrewMonster.Scripts.Skills;
using BrewMonster.UI;
using CSNetwork.GPDataType;
using System;
using TMPro;
using UnityEngine;
@@ -10,9 +11,10 @@ using static TMPro.SpriteAssetUtilities.TexturePacker_JsonArray;
namespace BrewMonster
{
[DisallowMultipleComponent]
public class CDlgSkillSubListItem : MonoBehaviour
public class CDlgSkillSubListItem : AUIDialog
{
[SerializeField] private TextMeshProUGUI m_skillNameLbl;
[SerializeField] private TextMeshProUGUI skillLevel;
[SerializeField] private Image skillIcon;
[SerializeField] private GameObject m_highlight;
[SerializeField] private Button m_upgradeBtn;
@@ -92,19 +94,17 @@ namespace BrewMonster
int needMoney = CECHostSkillModel.Instance.GetSkillMoney(m_skillID, m_curLevel + 1);
int needSp = CECHostSkillModel.Instance.GetSkillSp(m_skillID, m_curLevel + 1);
string str = string.Format(GetStringFromTable(11326), needMoney, needSp);
string str = GPDataTypeHelper.ReplacePercentD(GetStringFromTable(11326), needMoney, needSp);
var messagebox = uiManager.ShowMessageBox("Game_LearnSkill", str);
messagebox.SetData((uint)m_skillID);
//GetGameUIMan()->MessageBox("Game_LearnSkill", str, //GetGameUIMan()->GetStringFromTable(231),
// MB_OKCANCEL, A3DCOLORRGBA(255, 255, 255, 160), &pMsgBox);
//pMsgBox->SetData(m_skillID);
// 򿪶ԻʱNPC͵HelloϢûյظٴη
/* if (!gameUIMan.m_pDlgSkillAction.IsReceivedNPCGreeting())
if (!uiManager.m_pDlgSkillAction.IsReceivedNPCGreeting())
{
CECHostSkillModel::Instance().SendHelloToSkillLearnNPC();
}*/
CECHostSkillModel.Instance.SendHelloToSkillLearnNPC();
}
}
else
{
@@ -116,7 +116,7 @@ namespace BrewMonster
public void UpdateSkill(int skillID)
{
BMLogger.LogError("HoangDev: CDlgSkillSubListItem UpdateSkill skillID " + skillID);
//BMLogger.LogError("HoangDev: CDlgSkillSubListItem UpdateSkill skillID " + skillID);
CECHostSkillModel model = CECHostSkillModel.Instance;
m_skillID = skillID;
m_curLevel = model.GetSkillCurrentLevel(m_skillID);
@@ -162,6 +162,16 @@ namespace BrewMonster
m_skillNameLbl.text = skillName;
UpdateUpgradeBtn();
if (enumSkillLearnedState.SKILL_NOT_LEARNED == learnedState)
{
skillLevel.gameObject.SetActive(false);
}
else
{
skillLevel.gameObject.SetActive(true);
skillLevel.text = GetStringFromTable(11323).Replace("%d", m_curLevel.ToString());
}
}
private void UpdateUpgradeBtn()
@@ -171,7 +181,7 @@ namespace BrewMonster
enumSkillLearnedState learnedState = model.GetSkillLearnedState(m_skillID);
int requiredItem = model.GetRequiredBook(m_skillID, m_curLevel + 1);
BMLogger.LogError($"UpdateUpgradeBtn learnedState:{learnedState}, fitLevel={fitLevel}, requiredItem:{requiredItem}, model.CheckPreItem(requiredItem):{model.CheckPreItem(requiredItem)}");
//BMLogger.LogError($"UpdateUpgradeBtn learnedState:{learnedState}, fitLevel={fitLevel}, requiredItem:{requiredItem}, model.CheckPreItem(requiredItem):{model.CheckPreItem(requiredItem)}");
if (enumSkillLearnedState.SKILL_FULL != learnedState &&
enumSkillFitLevelState.SKILL_FIT_LEVEL == fitLevel &&
(requiredItem == 0 || model.CheckPreItem(requiredItem)))
@@ -202,7 +212,7 @@ namespace BrewMonster
}
}
}
BMLogger.LogError($"HoangDev: UpdateUpgradeBtn m_skillID:{m_skillID} spOK=" + spOK + ", moneyOK=" + moneyOK + ", preSkillOK=" + preSkillOK);
//BMLogger.LogError($"HoangDev: UpdateUpgradeBtn m_skillID:{m_skillID} spOK=" + spOK + ", moneyOK=" + moneyOK + ", preSkillOK=" + preSkillOK);
if (spOK && moneyOK && preSkillOK)
{
m_upgradeBtn.interactable = true;
@@ -289,11 +299,5 @@ namespace BrewMonster
return CECGameRun.Instance.GetHostPlayer();
}
private string GetStringFromTable(int id)
{
// TODO: Implement this method to get localized strings
// This should look up the string ID in your localization system
return $"String_{id}";
}
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:93bd71a1075577b8cfeb184d2dd0dbe13c961132bb7dbffbf13f53c62f694dff
size 200524005
oid sha256:5a25370885c45257666371b86149f405172001d0bdbd259c8d89c58d721875ff
size 200526611
+4 -4
View File
@@ -289,7 +289,7 @@ namespace BrewMonster
public bool LoadResources()
{
BMLogger.LogError("HoangDev: CECHostPlayer::LoadResources");
//BMLogger.LogError("HoangDev: CECHostPlayer::LoadResources");
RoleInfo RoleInfo = UnityGameSession.Instance.GetRoleInfo();
m_iProfession = RoleInfo.occupation;
m_iGender = RoleInfo.gender;
@@ -5460,7 +5460,7 @@ namespace BrewMonster
}
// Is host player talking with NPC ?
bool IsTalkingWithNPC()
public bool IsTalkingWithNPC()
{
return m_bTalkWithNPC;
}
@@ -6444,7 +6444,7 @@ namespace BrewMonster
{
int iLevel = 1;
CECSkill pSkill = GetNormalSkill(idSkill);
if (pSkill !=null)
if (pSkill != null)
iLevel = pSkill.GetSkillLevel() + 1;
if (iLevel == 1 && bCheckBook)
@@ -6579,7 +6579,7 @@ namespace BrewMonster
if (pItem == null)
return false;
if(pItem is EC_IvtrEquip)
if (pItem is EC_IvtrEquip)
{
if ((pItem as EC_IvtrEquip).IsDestroying())
return false;
+3 -3
View File
@@ -23,10 +23,10 @@ public class CECUIManager : MonoSingleton<CECUIManager>
[SerializeField] private DialogScriptTableObject dialogResouce;
[SerializeField] private Canvas canvasDlg;
[SerializeField] private CDlgQuickBar cDlgQuickBar;
[SerializeField] private UnityEngine.UI.Button btnSecondClick;
[SerializeField] CDlgQuickBar m_pDlgQuickBar1;
CDlgMessageBox m_pDlgMessageBox;
public CDlgSkillAction m_pDlgSkillAction;
protected override void Awake()
{
@@ -39,7 +39,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
gameUI = new CECGameUIMan();
gameUI.SetDependency(dialogResouce, canvasDlg);
gameUI.Init();
m_pDlgSkillAction = GetComponent<CDlgSkillAction>();
// Wire up second-click button / 连接第二次点击按钮
if (btnSecondClick != null)
{
@@ -70,7 +70,7 @@ public class CECUIManager : MonoSingleton<CECUIManager>
}
public CDlgQuickBar GetCDlgQuickBar()
{
return cDlgQuickBar;
return m_pDlgQuickBar1;
}
private void TryHideUINPC(NPCDiedEvent obj)
{
File diff suppressed because one or more lines are too long
+29
View File
@@ -0,0 +1,29 @@
# Skill Learn Flow (C++)
1. **CDlgSkillSubListItem::OnCommand_Upgrade**
- Triggered when the player clicks the learn/upgrade button on a skill.
- Performs status checks (dead, trading, etc.), evaluates `CheckLearnCondition`, and shows the `Game_LearnSkill` confirmation dialog.
- If the NPC greeting has not been received yet, calls `CECHostSkillModel::SendHelloToSkillLearnNPC()`.
2. **CECHostSkillModel**
- Maintains `m_skillLearnNPCNID`, the NPC ID that currently offers skill learning.
- `ProcessServiceList()` scans `cmd_scene_service_npc_list` from the server, keeps the valid NPC, and resets `m_bReceivedNPCGreeting` when a new NPC is selected.
- `SendHelloToSkillLearnNPC()` issues `c2s_CmdNPCSevHello(m_skillLearnNPCNID)` to start the NPC service handshake.
- `OnNpcGreeting()` is invoked when `NPC_GREETING` (cmd 70) arrives; it marks the greeting as received so future learns skip re-hello.
3. **CDlgSkillAction::TryOpenDialog**
- Called when the skill dialog is being opened (skill vs action mode).
- If the host isnt currently “talking with the NPC,” it forces the dialog to open, sends `SendHelloToSkillLearnNPC()`, and clears the “received greeting” flag so the next dialog confirms again.
4. **Game UI dialog**
- Player confirms the `Game_LearnSkill` message box (`EC_GameUIMan::OnMessageBoxClose`).
- If `IDOK` and `CheckSkillLearnCondition` still returns zero, it sends `c2s_CmdNPCSevLearnSkill(skillID)`.
- The dialog name `"Game_LearnSkill"` ensures the server hasnt rejected the skill because it missed the handshake.
5. **Server round-trip**
- `SEVNPC_HELLO` establishes the NPC service context.
- Server replies with `NPC_GREETING`, parsed by `GameSession` and forwarded to `CECHostSkillModel::OnNpcGreeting`.
- Only after the greeting can `SEVNPC_SERVE` with `GP_NPCSEV_LEARN` be trusted; else, the server often drops it without a `LEARN_SKILL` response.
Use this sequence when troubleshooting missing `LEARN_SKILL` responses: show/confirm dialog → send `SEVNPC_HELLO` → wait for `NPC_GREETING` → send `GP_NPCSEV_LEARN`.