fix bug flash move skill

This commit is contained in:
VDH
2026-03-09 17:50:32 +07:00
parent 91bbfb6679
commit 574fc0c8c6
15 changed files with 1779 additions and 46 deletions
+2 -2
View File
@@ -1,3 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:d0362961b9d8f1c01d6aedb1be62d363d184135ac3c88879f13f26f078d18f46
size 309803
oid sha256:d381b291f0b51a7d9440c9129ad3176acc9c4e8a8ade93205cd8c87875c5b5db
size 309973
@@ -552,24 +552,17 @@ namespace BrewMonster
/// Load composer from file
/// 从文件加载组合器
/// </summary>
#if UNITY_EDITOR
public string hitGfxName;
public string flyGfxName;
public string hitGrdGfxName;
#endif
public async UniTask<bool> Load(SkillStub skillStub, string flyGFXPath, string hitGrdGFXPath, string hitGFXPath)
{
#if UNITY_EDITOR
flyGfxName = flyGFXPath;
hitGfxName = hitGFXPath;
hitGrdGfxName = hitGrdGFXPath;
#else
string flyGfxName = flyGFXPath;
string hitGfxName = hitGFXPath;
string hitGrdGfxName = hitGrdGFXPath;
#endif
// Load GFX prefabs / 加载GFX预制体
m_szFlyGfx = string.IsNullOrEmpty(flyGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + flyGfxName);
m_szHitGfx = string.IsNullOrEmpty(hitGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGfxName);
m_szHitGrndGfx = string.IsNullOrEmpty(hitGrdGfxName) ? null : await AddressableManager.Instance.LoadPrefabAsync("gfx/" + hitGrdGfxName);
@@ -1528,3 +1521,5 @@ public enum GfxSkillValType
@@ -212,7 +212,7 @@ namespace BrewMonster
m_fMoveTime = 0.0f;
iMoveMode |= (int)GPMoveMode.GP_MOVE_DEAD;
BMLogger.LogError($"HoangDev: SendMoveCmd m_wMoveStamp={m_wMoveStamp}");
UnityGameSession.Instance.c2s_CmdPlayerMove(vCurPos, vCurPos, iTime/* MOVECMD_INTERVAL */, fSpeed, iMoveMode, m_wMoveStamp++);
m_vLastSevPos = EC_Utility.ToA3DVECTOR3(vCurPos);
@@ -875,10 +875,10 @@ namespace CSNetwork.GPDataType
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_host_use_item
{
public byte byPackage;
public byte bySlot;
public int item_id;
public ushort use_count;
public byte byPackage;
public byte bySlot;
public int item_id;
public ushort use_count;
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -893,9 +893,9 @@ namespace CSNetwork.GPDataType
public string strMap;
public A3DVECTOR3 vPos;
//bool operator == (const ACString& rhsStr) const {return strMap == rhsStr;}
public bool Equals(string rhsStr) {return strMap == rhsStr;}
public bool Equals(string rhsStr) { return strMap == rhsStr; }
//override == method
}
}
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct A3DVECTOR3
@@ -1972,7 +1972,7 @@ namespace CSNetwork.GPDataType
public struct cmd_player_chgshape
{
public int idPlayer;
public byte shape;
public byte shape;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_host_notify_root
@@ -2604,7 +2604,11 @@ namespace CSNetwork.GPDataType
public int mount_id;
public ushort mount_color;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_set_move_stamp
{
public ushort move_stamp;
};
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_summon_plant_pet
{
@@ -2628,7 +2632,7 @@ namespace CSNetwork.GPDataType
public ushort equip_idx;
public uint cost;
};
// player leaves the world
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct cmd_player_leave_world
@@ -1227,6 +1227,10 @@ namespace CSNetwork
case CommandID.PLAYER_CHGSHAPE:
EC_ManMessage.PostMessage(EC_MsgDef.MSG_PM_PLAYERCHGSHAPE, MANAGER_INDEX.MAN_PLAYER, -1, pDataBuf, pCmdHeader);
break;
case CommandID.SET_MOVE_STAMP:
EC_ManMessage.PostMessage(EC_MsgDef.MSG_HST_SETMOVESTAMP, MANAGER_INDEX.MAN_PLAYER, 0, pDataBuf, pCmdHeader);
break;
default:
#if UNITY_EDITOR
if (isDebug)
@@ -590,7 +590,7 @@ namespace BrewMonster
int iSCType = GPDataTypeHelper.FromBytes<int>(pDataBuf, offset);
offset += sizeof(int);
BMLogger.Log("[MH] Loading shortcut slot: " + iSlot + " Type: " + iSCType);
//BMLogger.Log("[MH] Loading shortcut slot: " + iSlot + " Type: " + iSCType);
switch ((CECShortcut.ShortcutType)iSCType)
{
@@ -257,7 +257,7 @@ public class LitModelHolder : MonoSingleton<LitModelHolder>
}
if (!_candidatesForLoading.ContainsKey(_currentObjectToCheck))
{
BMLogger.Log($"LitModelHolder: Added object to candidates for loading: {_currentObjectToCheck.assetPath}");
//BMLogger.Log($"LitModelHolder: Added object to candidates for loading: {_currentObjectToCheck.assetPath}");
_candidatesForLoading[_currentObjectToCheck] = _realTimeSinceStartUp;
}
}
@@ -267,7 +267,7 @@ public class LitModelHolder : MonoSingleton<LitModelHolder>
{
if (!_objectsToUnload.Contains(_currentObjectToCheck))
{
BMLogger.Log($"LitModelHolder: Added object to unload: {_currentObjectToCheck.assetPath}");
//BMLogger.Log($"LitModelHolder: Added object to unload: {_currentObjectToCheck.assetPath}");
_objectsToUnload.Add(_currentObjectToCheck);
}
}
+122
View File
@@ -221,6 +221,11 @@ namespace BrewMonster
cmd_object_cast_skill pCmd =
GPDataTypeHelper.FromBytes<cmd_object_cast_skill>((byte[])Msg.dwParam1);
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_SKILL, skillID={pCmd.skill}, " +
// $"target={pCmd.target}, time={pCmd.time}, m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " +
// $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " +
// $"IsSpellingMagic={IsSpellingMagic()}");
if (m_pCurSkill != null)
{
m_pCurSkill.EndCharging();
@@ -235,6 +240,9 @@ namespace BrewMonster
Debug.Assert(m_pCurSkill != null, "Current skill should not be null");
return;
}
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_SKILL - Skill found, skillID={m_pCurSkill.GetSkillID()}, " +
// $"IsChargeable={m_pCurSkill.IsChargeable()}");
if (m_pCurSkill.IsChargeable())
m_pCurSkill.StartCharging(pCmd.time);
@@ -247,6 +255,9 @@ namespace BrewMonster
pWork.PrepareCast(pCmd.target, m_pCurSkill, iWaitTime);
m_pWorkMan.StartWork_p1(pWork);
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_SKILL - Created WORK_SPELLOBJECT, " +
// $"skillID={m_pCurSkill.GetSkillID()}, target={pCmd.target}, waitTime={iWaitTime}, " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
// Start time counter for some type skill
// 为某些类型的技能启动时间计数器
@@ -288,7 +299,12 @@ namespace BrewMonster
{
// Skill perform
// 技能执行
int prepSkillIDBefore = m_pPrepSkill != null ? m_pPrepSkill.GetSkillID() : 0;
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received SKILL_PERFORM, " +
// $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")} (clearing)");
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: SKILL_PERFORM cleared m_pPrepSkill (was skillID={prepSkillIDBefore}), now null");
if (m_pCurSkill != null && m_pCurSkill.IsDurative())
m_bSpellDSkill = true;
@@ -418,6 +434,20 @@ namespace BrewMonster
cmd_object_cast_pos_skill pCmd =
GPDataTypeHelper.FromBytes<cmd_object_cast_pos_skill>((byte[])Msg.dwParam1);
Debug.Assert(pCmd.caster == m_PlayerInfo.cid);
// Log position BEFORE flashmove processing
// 记录闪移处理前的位置
A3DVECTOR3 vHostPosBefore = EC_Utility.ToA3DVECTOR3(transform.position);
BMLogger.LogError($"[DISTANCE_DEBUG] OBJECT_CAST_POS_SKILL: Received, skillID={pCmd.skill}, " +
$"hostPosBefore=({vHostPosBefore.x:F2}, {vHostPosBefore.y:F2}, {vHostPosBefore.z:F2}), " +
$"destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " +
$"target={pCmd.target}, distanceBefore={A3d_Magnitude(pCmd.pos - vHostPosBefore):F2}");
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_POS_SKILL, skillID={pCmd.skill}, " +
// $"target={pCmd.target}, pos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " +
// $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, IsFlashMoving={IsFlashMoving()}");
CECSkill pSkill = GetNormalSkill(pCmd.skill);
if (pSkill == null) pSkill = GetEquipSkillByID(pCmd.skill);
@@ -426,6 +456,9 @@ namespace BrewMonster
Debug.Assert(pSkill != null, "Skill should not be null");
break;
}
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Skill found, skillID={pSkill.GetSkillID()}, " +
// $"type={pSkill.GetType()}, rangeType={pSkill.GetRangeType()}");
TurnFaceTo(pCmd.target);
@@ -531,9 +564,31 @@ namespace BrewMonster
m_pWorkMan.StartWork_p2(pWork);
iActionTime = nExecuteTime;
// Update position tracking immediately so distance checks use correct position
// The work will move the player visually over time, but we need position tracking
// updated now so normal attacks can check distance correctly
// 立即更新位置跟踪,以便距离检查使用正确的位置
// 工作将在时间上移动玩家,但我们需要现在更新位置跟踪,以便普通攻击可以正确检查距离
m_MoveCtrl.SetHostLastPos(pCmd.pos);
m_MoveCtrl.SetLastSevPos(pCmd.pos);
// Log position AFTER updating position tracking
// 记录更新位置跟踪后的位置
A3DVECTOR3 vHostPosAfter = EC_Utility.ToA3DVECTOR3(transform.position);
BMLogger.LogError($"[DISTANCE_DEBUG] OBJECT_CAST_POS_SKILL: After position update, skillID={pSkill.GetSkillID()}, " +
$"hostPosAfter=({vHostPosAfter.x:F2}, {vHostPosAfter.y:F2}, {vHostPosAfter.z:F2}), " +
$"SetHostLastPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " +
$"destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " +
$"executeTime={nExecuteTime}, positionChange={A3d_Magnitude(pCmd.pos - vHostPosBefore):F2}");
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Created WORK_FLASHMOVE, " +
// $"skillID={pSkill.GetSkillID()}, executeTime={nExecuteTime}, destPos=({pCmd.pos.x:F2}, {pCmd.pos.y:F2}, {pCmd.pos.z:F2}), " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
}
bActionStartSkill = true;
// Debug.Log($"[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: OBJECT_CAST_POS_SKILL - Completed, skillID={pCmd.skill}");
break;
}
case CommandID.PLAYER_CAST_RUNE_SKILL:
@@ -628,8 +683,47 @@ namespace BrewMonster
break;
}
case CommandID.ERROR_MESSAGE:
{
// Log error message from server for debugging distance issues
// 记录来自服务器的错误消息,用于调试距离问题
//cmd_error_message pCmd = GPDataTypeHelper.FromBytes<cmd_error_message>((byte[])Msg.dwParam1);
//int errorCode = pCmd.message;
// Common error codes:
// 2 = FIXMSG_NEEDMP (Need MP)
// 20 = FIXMSG_NEEDITEM (Need item)
// 21 = FIXMSG_TARGETISFAR (Target is too far)
// 22 = FIXMSG_TARGETTOOCLOSE (Target too close)
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position);
int idCurrentTarget = m_idSelTarget;
CECObject pTarget = idCurrentTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idCurrentTarget, 1) : null;
if (pTarget != null)
{
A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position);
float fDistance = A3d_Magnitude(vTargetPos - vHostPos);
BMLogger.LogError($"[DISTANCE_DEBUG] ERROR_MESSAGE from server: errorCode=, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDistance:F2}, targetID={idCurrentTarget}, " +
$"attackRange={m_ExtProps.ak.AttackRange:F2}, " +
$"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " +
$"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
}
else
{
BMLogger.LogError($"[DISTANCE_DEBUG] ERROR_MESSAGE from server: errorCode=, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetID={idCurrentTarget} (target object is null), " +
$"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, " +
$"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
}
bDoOtherThing = true;
break;
}
default:
Debug.Assert(false, "Unknown message type in OnMsgPlayerCastSkill");
@@ -692,6 +786,34 @@ namespace BrewMonster
return false;
}
// Log position and distance information for debugging
// 记录位置和距离信息用于调试
// Use server-tracked position instead of visual position for distance checks
// This ensures distance checks use the correct position immediately after flashmove
// 使用服务器跟踪的位置而不是视觉位置进行距离检查
// 这确保在闪移后立即使用正确的位置进行距离检查
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
CECObject pTarget = EC_ManMessageMono.Instance.GetObject(idTarget, 1);
if (pTarget != null && pTarget is CECNPC cECNPC)
{
A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position);
float fDistance = A3d_Magnitude(vTargetPos - vHostPos);
float fAttackRange = m_ExtProps.ak.AttackRange;
bool bCanTouch = CanTouchTarget(vHostPos,vTargetPos, cECNPC.GetTouchRadius(), 1); // 1 = melee
BMLogger.Log($"[DISTANCE_DEBUG] NormalAttackObject: Entry, idTarget={idTarget}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDistance:F2}, attackRange={fAttackRange:F2}, " +
$"targetRadius={cECNPC.GetTouchRadius():F2}, CanTouch={bCanTouch}, " +
$"bForceAttack={bForceAttack}, bMoreClose={bMoreClose}");
}
else
{
BMLogger.Log($"[DISTANCE_DEBUG] NormalAttackObject: Entry, idTarget={idTarget}, target object is null");
}
//if (!EC_Game.GetGameRun().GetWorld().GetObject(idTarget, 1))
// return false;
bool bStartNewWork = false;
+151 -7
View File
@@ -446,6 +446,9 @@ namespace BrewMonster
int idSelTarget = 0 /* 0 */, int iForceAtk = -1 /* -1 */)
{
//StackChecker::ACTrace(4);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Entry, skillID={idSkill}, bCombo={bCombo}, idSelTarget={idSelTarget}, iForceAtk={iForceAtk}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}, " +
// $"m_pCurSkill={(m_pCurSkill != null ? m_pCurSkill.GetSkillID().ToString() : "null")}, IsFlashMoving={IsFlashMoving()}");
if (m_pActionSwitcher != null)
m_pActionSwitcher.PostMessge((int)EMsgActionSwitcher.MSG_CASTSKILL);
@@ -462,10 +465,16 @@ namespace BrewMonster
// return SummonPlayer(idSelTarget, bCombo);
if (!CanDo(ActionCanDo.CANDO_SPELLMAGIC))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanDo(CANDO_SPELLMAGIC) returned false, skillID={idSkill}");
return false;
}
if (InSlidingState())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - InSlidingState() returned true, skillID={idSkill}");
return false;
}
if (!bCombo)
ClearComboSkill();
@@ -478,8 +487,12 @@ namespace BrewMonster
if (pSkill == null) pSkill = CECComboSkillState.Instance.GetInherentSkillByID((uint)idSkill);
if (pSkill == null)
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - Skill {idSkill} not found");
return false;
}
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Skill found, skillID={pSkill.GetSkillID()}, type={pSkill.GetType()}, " +
// $"ReadyToCast={pSkill.ReadyToCast()}, IsInstant={pSkill.IsInstant()}, IsFlashMove={pSkill.GetType() == (int)Skilltype.TYPE_FLASHMOVE}");
//// If we press a chargeable skill again when it's being charged,
//// we cast it out at once
@@ -492,8 +505,10 @@ namespace BrewMonster
}
int iCon = CheckSkillCastCondition(pSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: CheckSkillCastCondition returned {iCon} for skillID={idSkill}");
if (iCon != 0)
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CheckSkillCastCondition returned error {iCon}, skillID={idSkill}");
ProcessSkillCondition(iCon);
return false;
}
@@ -657,22 +672,49 @@ namespace BrewMonster
if (!IsMeleeing() && !IsSpellingMagic() &&
(iTargetType == 0 || idCastTarget == m_PlayerInfo.cid))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Entering main casting path, skillID={idSkill}, IsMeleeing={IsMeleeing()}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, iTargetType={iTargetType}, idCastTarget={idCastTarget}");
// Cast this skill need't checking cast distance
if (!pSkill.ReadyToCast())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - ReadyToCast() returned false, skillID={idSkill}");
return false;
}
// Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2)
// 检查技能是否可以立即施放(阻止在PRIORITY_2的闪移工作期间施放)
// Also check IsFlashMoving() as an additional safeguard since client-side work might finish
// before server processes the flashmove
// 同时检查 IsFlashMoving() 作为额外的保护,因为客户端工作可能在服务器处理闪移之前完成
if (IsFlashMoving() || !m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
bool hasWorkOnPriority2 = m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Work_priority.PRIORITY_2);
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - IsFlashMoving={IsFlashMoving()}, CanCastSkillImmediately={m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID())}, skillID={idSkill}, " +
// $"IsSpellingMagic={IsSpellingMagic()}, HasWorkOnPriority2={hasWorkOnPriority2}");
return false;
}
if (!pSkill.IsInstant() && pSkill.GetType() != (int)Skilltype.TYPE_FLASHMOVE)
{
if (!NaturallyStopMoving())
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - NaturallyStopMoving() returned false, skillID={idSkill}");
return false; // Couldn't stop naturally, so cancel casting skill
}
}
else if (pSkill.GetType() == (int)Skilltype.TYPE_FLASHMOVE)
{
if (!CanDo(ActionCanDo.CANDO_FLASHMOVE))
{
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanDo(CANDO_FLASHMOVE) returned false, skillID={idSkill}");
return false;
}
}
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: Setting prep skill and calling CastSkill, skillID={idSkill}, " +
// $"m_pPrepSkill before={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
m_pPrepSkill = pSkill;
// Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: m_pPrepSkill set to skillID={idSkill}, calling CastSkill");
CastSkill(m_PlayerInfo.cid, bForceAttack);
}
else if (IsSpellingMagic() && m_pCurSkill == pSkill)
@@ -750,9 +792,17 @@ namespace BrewMonster
public bool CastSkill(int idTarget, bool bForceAttack, CECObject pTarget = null)
{
int prepSkillID = m_pPrepSkill != null ? m_pPrepSkill.GetSkillID() : 0;
bool readyToCast = m_pPrepSkill != null ? m_pPrepSkill.ReadyToCast() : false;
bool isSpelling = IsSpellingMagic();
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Entry, prepSkillID={prepSkillID}, idTarget={idTarget}, bForceAttack={bForceAttack}, " +
// $"ReadyToCast={readyToCast}, IsSpellingMagic={isSpelling}, IsFlashMoving={IsFlashMoving()}");
// Check if prep skill is valid, ready to cast, and not currently spelling magic
if (m_pPrepSkill == null || !m_pPrepSkill.ReadyToCast() || IsSpellingMagic())
{
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: BLOCKED - Prep skill invalid or not ready, prepSkillID={prepSkillID}, " +
// $"ReadyToCast={readyToCast}, IsSpellingMagic={isSpelling}");
// Check if skill can change to melee attack
if (m_pPrepSkill != null && m_pPrepSkill.ChangeToMelee())
{
@@ -787,9 +837,14 @@ namespace BrewMonster
}
//TODO: Check cast condition - method not yet implemented
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: About to check conditions, m_pPrepSkill skillID={prepSkillID}, " +
// $"m_pPrepSkill={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
int iRet = CheckSkillCastCondition(m_pPrepSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: CheckSkillCastCondition returned {iRet} for skillID={prepSkillID}, " +
// $"checked skillID={(m_pPrepSkill != null ? m_pPrepSkill.GetSkillID().ToString() : "null")}");
if (iRet != 0)
{
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: BLOCKED - CheckSkillCastCondition returned error {iRet} (2=NeedMP, 8=NeedAP, 10=PackFull, 20=NeedItem, 12=HPUnsatisfied), skillID={prepSkillID}");
switch (iRet)
{
case 2: // Need MP
@@ -802,6 +857,9 @@ namespace BrewMonster
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_PACKFULL1);
break;
case 20: // Need item
// Debug.LogError($"[SKILL_CAST_DEBUG] CastSkill: ERROR 20 - Need item for skillID={prepSkillID}, " +
// $"ItemCost={m_pPrepSkill.SkillCore?.GetItemCost() ?? 0}, " +
// $"ItemTotalNum={(m_pPrepSkill.SkillCore != null ? GetPack().GetItemTotalNum(m_pPrepSkill.SkillCore.GetItemCost()) : 0)}");
// g_pGame.GetGameRun().AddFixedMessage(FIXMSG_NEEDITEM);
break;
case 12: // HP unsatisfied
@@ -815,7 +873,8 @@ namespace BrewMonster
byte byPVPMask = glb_BuildPVPMask(bForceAttack);
Debug.LogError($"HoangDev: Cast Skill ID={m_pPrepSkill.GetSkillID()}");
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending skill to server, skillID={prepSkillID}, idTarget={idTarget}, " +
// $"byPVPMask={byPVPMask}, IsInstant={m_pPrepSkill.IsInstant()}, IsFlashMove={m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_FLASHMOVE}");
// Handle instant skills
if (m_pPrepSkill.IsInstant())
@@ -823,9 +882,11 @@ namespace BrewMonster
int countTarget = 1;
targetsCastSkill = new int[countTarget];
targetsCastSkill[0] = idTarget;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastInstantSkill, skillID={prepSkillID}, target={idTarget}, count={countTarget}");
UnityGameSession.c2s_CmdCastInstantSkill(m_pPrepSkill.GetSkillID(), byPVPMask, countTarget,
targetsCastSkill);
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Instant skill sent, m_pPrepSkill cleared");
}
// Handle flash move skills (瞬移技能)
else if (m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_FLASHMOVE)
@@ -855,9 +916,22 @@ namespace BrewMonster
fDist = Mathf.Abs(fDist);
A3DVECTOR3 vDest = m_MoveCtrl.FlashMove(vDir, 100.0f, fDist);
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), EC_Utility.ToVector3(vDest),
byPVPMask, 0, 0);
UnityEngine.Vector3 vDestVec3 = EC_Utility.ToVector3(vDest);
// Log position information before sending flashmove
// 在发送闪移前记录位置信息
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position);
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove self), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"destPos=({vDestVec3.x:F2}, {vDestVec3.y:F2}, {vDestVec3.z:F2}), " +
$"flashDistance={fDist:F2}, byPVPMask={byPVPMask}");
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastPosSkill (flashmove self), skillID={prepSkillID}, " +
// $"pos=({vDestVec3.x:F2}, {vDestVec3.y:F2}, {vDestVec3.z:F2}), byPVPMask={byPVPMask}");
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), vDestVec3, byPVPMask, 0, 0);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (self) sent, clearing m_pPrepSkill (was skillID={prepSkillID})");
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (self) complete, m_pPrepSkill is now null");
}
else
{
@@ -979,12 +1053,39 @@ namespace BrewMonster
}
// 发送协议 (Send protocol)
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), EC_Utility.ToVector3(vMovePos),
byPVPMask, 1, idTarget);
UnityEngine.Vector3 vMovePosVec3 = EC_Utility.ToVector3(vMovePos);
// Log position information before sending flashmove
// 在发送闪移前记录位置信息
A3DVECTOR3 vHostPos2 = EC_Utility.ToA3DVECTOR3(transform.position);
CECObject pTarget2 = idTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idTarget, 1) : null;
if (pTarget2 != null)
{
A3DVECTOR3 vTargetPos2 = EC_Utility.ToA3DVECTOR3(pTarget2.transform.position);
float fDistance2 = A3d_Magnitude(vTargetPos2 - vHostPos2);
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
$"hostPos=({vHostPos2.x:F2}, {vHostPos2.y:F2}, {vHostPos2.z:F2}), " +
$"targetPos=({vTargetPos2.x:F2}, {vTargetPos2.y:F2}, {vTargetPos2.z:F2}), " +
$"destPos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), " +
$"distance={fDistance2:F2}, target={idTarget}, byPVPMask={byPVPMask}");
}
else
{
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
$"hostPos=({vHostPos2.x:F2}, {vHostPos2.y:F2}, {vHostPos2.z:F2}), " +
$"destPos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), " +
$"target={idTarget} (target object is null), byPVPMask={byPVPMask}");
}
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastPosSkill (flashmove target), skillID={prepSkillID}, " +
// $"pos=({vMovePosVec3.x:F2}, {vMovePosVec3.y:F2}, {vMovePosVec3.z:F2}), target={idTarget}, byPVPMask={byPVPMask}");
UnityGameSession.c2s_CmdCastPosSkill(m_pPrepSkill.GetSkillID(), vMovePosVec3, byPVPMask, 1, idTarget);
bSuccess = true;
}
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (target) sent, clearing m_pPrepSkill (was skillID={prepSkillID}), bSuccess={bSuccess}");
m_pPrepSkill = null;
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Flashmove (target) complete, m_pPrepSkill is now null");
return bSuccess;
}
}
@@ -995,7 +1096,39 @@ namespace BrewMonster
int targets = 1;
targetsCastSkill = new int[targets];
targetsCastSkill[0] = idTarget;
// Log position and distance information before sending skill cast
// Use server-tracked position instead of visual position for accurate distance checks
// 在发送技能施放前记录位置和距离信息
// 使用服务器跟踪的位置而不是视觉位置进行准确的距离检查
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
CECObject pTarget1 = idTarget > 0 ? EC_ManMessageMono.Instance.GetObject(idTarget, 1) : null;
if (pTarget != null && pTarget1 is CECNPC cECNPC)
{
A3DVECTOR3 vTargetPos = EC_Utility.ToA3DVECTOR3(pTarget.transform.position);
float fDistance = A3d_Magnitude(vTargetPos - vHostPos);
float fSkillRange = m_pPrepSkill.GetCastRange(m_ExtProps.ak.AttackRange, GetPrayDistancePlus());
bool bCanTouch = CanTouchTarget(vTargetPos, cECNPC.GetTouchRadius(), 2); // 2 = skill
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDistance:F2}, skillRange={fSkillRange:F2}, " +
$"targetRadius={cECNPC.GetTouchRadius():F2}, CanTouch={bCanTouch}, " +
$"target={idTarget}, byPVPMask={byPVPMask2}");
}
else
{
BMLogger.Log($"[DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"target={idTarget} (target object is null), byPVPMask={byPVPMask2}");
}
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastSkill (regular), skillID={prepSkillID}, " +
// $"target={idTarget}, count={targets}, byPVPMask={byPVPMask2}");
UnityGameSession.c2s_CmdCastSkill(m_pPrepSkill.GetSkillID(), byPVPMask2, targets, targetsCastSkill);
// Debug.Log($"[SKILL_CAST_DEBUG] CastSkill: Regular skill sent, m_pPrepSkill still set (will be cleared on server response)");
}
return true;
@@ -1155,12 +1288,18 @@ namespace BrewMonster
// 13=combo skill not active, 20=need item
public int CheckSkillCastCondition(CECSkill pSkill)
{
int skillID = pSkill != null ? pSkill.GetSkillID() : 0;
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Entry, skillID={skillID}, SkillCore={(pSkill.SkillCore != null ? "not null" : "null")}");
// Check if skill requires an item
if (pSkill.SkillCore != null)
{
int idItem = pSkill.SkillCore.GetItemCost();
if (idItem > 0 && GetPack().GetItemTotalNum(idItem) <= 0)
int itemTotalNum = GetPack().GetItemTotalNum(idItem);
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Item check, skillID={skillID}, idItem={idItem}, itemTotalNum={itemTotalNum}");
if (idItem > 0 && itemTotalNum <= 0)
{
// Debug.LogError($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: ERROR 20 - Need item, skillID={skillID}, requiredItem={idItem}, have={itemTotalNum}");
return 20; // Need item
}
}
@@ -1215,9 +1354,14 @@ namespace BrewMonster
if (pSkill.SkillCore != null)
{
return pSkill.SkillCore.Condition((uint)pSkill.GetSkillID(), Info, pSkill.GetSkillLevel());
int conditionResult = pSkill.SkillCore.Condition((uint)pSkill.GetSkillID(), Info, pSkill.GetSkillLevel());
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: SkillCore.Condition returned {conditionResult} for skillID={skillID}, " +
// $"MP={Info.mp}, AP={Info.ap}, HP={Info.hp}/{Info.max_hp}, weapon={Info.weapon}, arrow={Info.arrow}, " +
// $"is_combat={Info.is_combat}, move_env={Info.move_env}");
return conditionResult;
}
// Debug.Log($"[SKILL_CAST_DEBUG] CheckSkillCastCondition: Success (no SkillCore), skillID={skillID}");
return 0; // Success
}
+6 -1
View File
@@ -27,7 +27,12 @@ namespace BrewMonster
m_MoveCtrl.SetMoveStamp(pCmd.stamp);
}
public void OnMsgHstSetMoveStamp(ECMSG Msg)
{
cmd_set_move_stamp pCmd = GPDataTypeHelper.FromBytes<cmd_set_move_stamp>((byte[])Msg.dwParam1);
BMLogger.LogError($"OnMsgHstSetMoveStamp: Received move stamp {pCmd.move_stamp}");
m_MoveCtrl.SetMoveStamp(pCmd.move_stamp);
}
public void OnMsgHstGoto(in ECMSG Msg)
{
PopupManager.Instance.OnPlayerRevived();
+57 -6
View File
@@ -611,6 +611,8 @@ namespace BrewMonster
case EC_MsgDef.MSG_PM_DUELOPT: OnMsgHstDuelOpt(Msg); break;
case EC_MsgDef.MSG_PM_PLAYERCHGSHAPE :
OnMsgPlayerChgShape(Msg); break;
case EC_MsgDef.MSG_HST_SETMOVESTAMP: OnMsgHstSetMoveStamp(Msg); break;
default:
// Uncomment to debug unhandled messages
// Debug.LogWarning($"[CECHostPlayer] ProcessMessage: Unhandled message {msg}");
@@ -1411,11 +1413,14 @@ namespace BrewMonster
float fMaxCut = 1.0f)
{
float fDist = A3d_Magnitude(vTargetPos - vHostPos);
bool bResult = false;
float fRange = 0.0f;
string reasonStr = iReason == 1 ? "melee" : (iReason == 2 ? "cast magic" : (iReason == 3 ? "talk" : $"unknown({iReason})"));
switch (iReason)
{
case 1: // melee
{
float fRange;
if (fMaxCut >= 0.0f)
{
float fCutDist = m_ExtProps.ak.AttackRange * 0.3f;
@@ -1430,7 +1435,16 @@ namespace BrewMonster
}
if (fDist - fTargetRad <= fRange)
{
bResult = true;
BMLogger.Log($"[DISTANCE_DEBUG] CanTouchTarget: reason={reasonStr}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDist:F2}, targetRadius={fTargetRad:F2}, " +
$"requiredRange={fRange:F2}, effectiveDistance={fDist - fTargetRad:F2}, " +
$"result={bResult}, attackRange={m_ExtProps.ak.AttackRange:F2}");
return true;
}
break;
}
@@ -1440,35 +1454,69 @@ namespace BrewMonster
{
//TODO : Check this function GetCastRange
float fRange = m_pPrepSkill.GetCastRange(m_ExtProps.ak.AttackRange, GetPrayDistancePlus());
fRange = m_pPrepSkill.GetCastRange(m_ExtProps.ak.AttackRange, GetPrayDistancePlus());
if (fRange > 0.0f)
{
if (fDist - fTargetRad <= fRange)
{
bResult = true;
BMLogger.Log($"[DISTANCE_DEBUG] CanTouchTarget: reason={reasonStr}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDist:F2}, targetRadius={fTargetRad:F2}, " +
$"requiredRange={fRange:F2}, effectiveDistance={fDist - fTargetRad:F2}, " +
$"result={bResult}, skillID={m_pPrepSkill.GetSkillID()}, attackRange={m_ExtProps.ak.AttackRange:F2}");
return true;
}
}
else
{
bResult = true;
BMLogger.Log($"[DISTANCE_DEBUG] CanTouchTarget: reason={reasonStr}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDist:F2}, targetRadius={fTargetRad:F2}, " +
$"requiredRange=unlimited, result={bResult}, skillID={m_pPrepSkill.GetSkillID()}");
return true;
}
}
break;
}
case 3: // talk
{
if (fDist - fTargetRad <= 5.0f)
fRange = 5.0f;
if (fDist - fTargetRad <= fRange)
{
bResult = true;
return true;
}
break;
}
default: // no special reason
{
if (fDist < (fTargetRad + m_fTouchRad) * 3.0f)
fRange = (fTargetRad + m_fTouchRad) * 3.0f;
if (fDist < fRange)
{
bResult = true;
return true;
}
break;
}
}
// Log distance check result for debugging
// 记录距离检查结果用于调试
BMLogger.Log($"[DISTANCE_DEBUG] CanTouchTarget: reason={reasonStr}, " +
$"hostPos=({vHostPos.x:F2}, {vHostPos.y:F2}, {vHostPos.z:F2}), " +
$"targetPos=({vTargetPos.x:F2}, {vTargetPos.y:F2}, {vTargetPos.z:F2}), " +
$"distance={fDist:F2}, targetRadius={fTargetRad:F2}, " +
$"requiredRange={fRange:F2}, effectiveDistance={fDist - fTargetRad:F2}, " +
$"result={bResult}, attackRange={m_ExtProps.ak.AttackRange:F2}");
return false;
}
public void RemoveObjectFromTabSels(CECObject pObject)
@@ -1485,8 +1533,11 @@ namespace BrewMonster
public bool CanTouchTarget(A3DVECTOR3 vTargetPos, float fTargetRad, int iReason, float fMaxCut = 1.0f)
{
A3DVECTOR3 vector = new A3DVECTOR3(playerTransform.position.x, playerTransform.position.y,
playerTransform.position.z);
// Use server-tracked position instead of visual position for distance checks
// This ensures distance checks use the correct position immediately after flashmove
// 使用服务器跟踪的位置而不是视觉位置进行距离检查
// 这确保在闪移后立即使用正确的位置进行距离检查
A3DVECTOR3 vector = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
return CanTouchTarget(vector, vTargetPos, fTargetRad, iReason, fMaxCut);
}
File diff suppressed because one or more lines are too long
+78
View File
@@ -0,0 +1,78 @@
# Flash Move Skill Protocol Comparison: C++ vs C#
## Summary
**Both C++ and C# send the same command ID (89) for CAST_POS_SKILL**, but they use different protocol wrappers.
## C++ Implementation
### Log Evidence (EC.log)
```
[17:10:03.501] [FLASH_SKILL_PROTOCOL] c2s_SendCmdCastPosSkill: Sending protocol C2S::CAST_POS_SKILL (command ID=89), skillID=58, pos=(858.33, 60.93, -149.44), byPVPMask=0, targetCount=0
[17:10:03.501] [FLASH_SKILL_PROTOCOL] c2s_SendCmdCastPosSkill: Protocol packet size=20 bytes
[17:10:03.501] CLIENT - CAST_POS_SKILL(89)
```
### Code Location
- **File:** `EC_SendC2SCmds.cpp`
- **Function:** `c2s_SendCmdCastPosSkill()` (line 1030-1068)
- **Protocol:** Sends `C2S::CAST_POS_SKILL` directly as protocol command
- **Command ID:** 89 (from enum `C2S::CAST_POS_SKILL`)
- **Packet Structure:**
```cpp
[cmd_header] + [cmd_cast_pos_skill]
- cmd_header.cmd = C2S::CAST_POS_SKILL (89)
- cmd_cast_pos_skill contains: skill_id, pos, force_attack, target_count, targets[]
```
## C# Implementation
### Log Evidence (SessionLog)
```
[16:58:15.286] [DISTANCE_DEBUG] CastSkill: Before sending c2s_CmdCastPosSkill (flashmove self), skillID=58, hostPos=(860.71, 59.59, -130.28), destPos=(860.62, 60.73, -145.39), flashDistance=16.00, byPVPMask=0
[16:58:15.286] [GameSession] Sending protocol: gamedatasend (Type: PROTOCOL_GAMEDATASEND) + Type=34 - CMD_ID: CAST_POS_SKILL
```
### Code Location
- **File:** `C2SCommand.cs`
- **Enum:** `CommandID.CAST_POS_SKILL = 89` (line 118)
- **Protocol:** Wraps command in `PROTOCOL_GAMEDATASEND` (Type=34)
- **Command ID:** 89 (stored in first 2 bytes of Data field)
- **Packet Structure:**
```csharp
[PROTOCOL_GAMEDATASEND header] + [Data field]
- Protocol Type = 34 (PROTOCOL_GAMEDATASEND)
- Data[0-1] = CommandID.CAST_POS_SKILL (89) as ushort
- Data[2+] = CMD_CastPosSkill structure (skillId, pos, pvpMask, targetCount, targets[])
```
## Key Differences
| Aspect | C++ | C# |
|--------|-----|-----|
| **Protocol Type** | Direct command (89) | Wrapped in PROTOCOL_GAMEDATASEND (34) |
| **Command ID** | 89 (C2S::CAST_POS_SKILL) | 89 (CommandID.CAST_POS_SKILL) |
| **Packet Structure** | `[cmd_header][cmd_cast_pos_skill]` | `[PROTOCOL_GAMEDATASEND][Data with CMD_ID][CMD_CastPosSkill]` |
| **Packet Size** | 20 bytes (for skillID=58, no targets) | Larger (includes PROTOCOL_GAMEDATASEND wrapper) |
## Conclusion
**The command ID is the same (89)** in both implementations.
⚠️ **The protocol wrapper is different:**
- **C++** sends the command directly as protocol 89
- **C#** wraps it in `PROTOCOL_GAMEDATASEND` (34) and puts the command ID (89) in the Data field
This is a **protocol architecture difference**, not a bug. The C# implementation uses a unified `GameDataSend` protocol that wraps all game commands, while the C++ implementation sends commands directly. The server should handle both formats correctly if it recognizes:
1. Direct protocol 89 (C++ style)
2. Protocol 34 with command ID 89 in the data (C# style)
## Verification
Both logs show:
- **Skill ID:** 58 (same flash move skill)
- **Command ID:** 89 (CAST_POS_SKILL)
- **Position data:** Present in both
- **PVP Mask:** 0 in both
- **Target Count:** 0 in both
The actual command data is **identical**; only the protocol wrapper differs.
+262
View File
@@ -0,0 +1,262 @@
# Flash Move Skill: Server Protocol Responses Comparison
## Summary
After the client sends `CAST_POS_SKILL` (command ID 89), the server responds with a sequence of protocols. This document compares the server responses received by both C++ and C# clients.
## Client Request (Both Same)
### C++ Client
```
[17:10:03.501] CLIENT - CAST_POS_SKILL(89)
```
### C# Client
```
[16:58:15.286] [GameSession] Sending protocol: gamedatasend (Type: PROTOCOL_GAMEDATASEND) + Type=34 - CMD_ID: CAST_POS_SKILL
```
---
## Server Response Sequence
### Immediate Response (Within ~64ms)
| Order | C++ Log | C# Log | Command ID | Command Name | Status |
|-------|---------|--------|------------|--------------|--------|
| 1-9 | OBJECT_MOVE(15) × 9 | CMDID 15 × 6 | 15 | OBJECT_MOVE | ✅ Same |
| 10 | OBJECT_STOP_MOVE(35) | CMDID 35 | 35 | OBJECT_STOP_MOVE | ✅ Same |
| 11 | SET_MOVE_STAMP(205) | CMDID 205 (Unhandled) | 205 | SET_MOVE_STAMP | ⚠️ C# Unhandled |
| 12 | OBJECT_CAST_POS_SKILL(204) | CMDID 204 | 204 | OBJECT_CAST_POS_SKILL | ✅ Same |
| 13 | SKILL_PERFORM(88) | CMDID 88 | 88 | SKILL_PERFORM | ✅ Same |
| 14 | SET_COOLDOWN(198) | CMDID 198 | 198 | SET_COOLDOWN | ✅ Same |
| 15 | COMBO_SKILL_PREPARE(394) | CMDID 394 | 394 | COMBO_SKILL_PREPARE | ✅ Same |
| 16 | ENCHANT_RESULT(139) | CMDID 139 | 139 | ENCHANT_RESULT | ✅ Same |
| 17 | SELF_INFO_00(38) | CMDID 38 | 38 | SELF_INFO_00 | ✅ Same |
| 18 | UPDATE_EXT_STATE(124) | CMDID 124 (Unhandled) | 124 | UPDATE_EXT_STATE | ⚠️ C# Unhandled |
| 19 | ICON_STATE_NOTIFY(125) | CMDID 125 (Unhandled) | 125 | ICON_STATE_NOTIFY | ⚠️ C# Unhandled |
### Delayed Response (~1 second later)
| Order | C++ Log | C# Log | Command ID | Command Name | Status |
|-------|---------|--------|------------|--------------|--------|
| 20 | OBJECT_MOVE(15) × 2 | CMDID 15 × 2 | 15 | OBJECT_MOVE | ✅ Same |
| 21 | OBJECT_STOP_MOVE(35) | CMDID 35 | 35 | OBJECT_STOP_MOVE | ✅ Same |
| 22 | OBJECT_MOVE(15) × 2 | CMDID 15 × 2 | 15 | OBJECT_MOVE | ✅ Same |
| 23 | HOST_STOP_SKILL(123) | CMDID 123 | 123 | HOST_STOP_SKILL | ✅ Same |
| 24 | UPDATE_EXT_STATE(124) | CMDID 124 (Unhandled) | 124 | UPDATE_EXT_STATE | ⚠️ C# Unhandled |
| 25 | ICON_STATE_NOTIFY(125) | CMDID 125 (Unhandled) | 125 | ICON_STATE_NOTIFY | ⚠️ C# Unhandled |
---
## Detailed Protocol Analysis
### ✅ Fully Handled Protocols (Both Implementations)
#### 1. OBJECT_MOVE (15)
- **C++:** `SERVER - OBJECT_MOVE(15), size=21`
- **C#:** `### GameDataSend: CMDID 15`
- **Purpose:** Updates object position during flash move
- **Status:** ✅ Both handle correctly
#### 2. OBJECT_STOP_MOVE (35)
- **C++:** `SERVER - OBJECT_STOP_MOVE(35), size=20`
- **C#:** `### GameDataSend: CMDID 35`
- **Purpose:** Stops object movement
- **Status:** ✅ Both handle correctly
#### 3. OBJECT_CAST_POS_SKILL (204)
- **C++:** `SERVER - OBJECT_CAST_POS_SKILL(204), size=27`
- **C#:** `### GameDataSend: CMDID 204`
- **Purpose:** Confirms the position skill cast
- **Status:** ✅ Both handle correctly
- **C++ Debug Log:**
```
[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_POS_SKILL,
skillID=58, target=0, pos=(858.33, 60.93, -149.44)
```
- **C# Debug Log:**
```
[DISTANCE_DEBUG] OBJECT_CAST_POS_SKILL: Received, skillID=58,
hostPosBefore=(860.71, 59.59, -130.28), destPos=(860.62, 60.73, -145.39)
```
#### 4. SKILL_PERFORM (88)
- **C++:** `SERVER - SKILL_PERFORM(88), size=0`
- **C#:** `### GameDataSend: CMDID 88`
- **Purpose:** Signals skill execution completion
- **Status:** ✅ Both handle correctly
- **C++ Debug Log:**
```
[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received SKILL_PERFORM,
m_pCurSkill=0, m_pPrepSkill=0 (clearing)
```
#### 5. SET_COOLDOWN (198)
- **C++:** `SERVER - SET_COOLDOWN(198), size=8`
- **C#:** `### GameDataSend: CMDID 198`
- **Purpose:** Sets skill cooldown timer
- **Status:** ✅ Both handle correctly
#### 6. COMBO_SKILL_PREPARE (394)
- **C++:** `SERVER - COMBO_SKILL_PREPARE(394), size=20`
- **C#:** `### GameDataSend: CMDID 394`
- **Purpose:** Prepares combo skill chain
- **Status:** ✅ Both handle correctly
- **C++ Log:** `Receive COMBO_SKILL_PREPARE cmd from server.`
#### 7. ENCHANT_RESULT (139)
- **C++:** `SERVER - ENCHANT_RESULT(139), size=19`
- **C#:** `### GameDataSend: CMDID 139`
- **Purpose:** Returns enchantment/effect result
- **Status:** ✅ Both handle correctly
#### 8. SELF_INFO_00 (38)
- **C++:** `SERVER - SELF_INFO_00(38), size=36`
- **C#:** `### GameDataSend: CMDID 38`
- **Purpose:** Updates host player basic info
- **Status:** ✅ Both handle correctly
#### 9. HOST_STOP_SKILL (123)
- **C++:** `SERVER - HOST_STOP_SKILL(123), size=0`
- **C#:** `### GameDataSend: CMDID 123`
- **Purpose:** Stops the skill execution
- **Status:** ✅ Both handle correctly
- **C++ Log:** `CECHPWork::WORK_FLASHMOVE priority=2 killed`
---
### ⚠️ Unhandled Protocols in C# (But Received)
#### 1. SET_MOVE_STAMP (205)
- **C++:** `SERVER - SET_MOVE_STAMP(205), size=2`
- **C#:** `### GameDataSend: CMDID 205 (Unhandled CMDID 205 (payloadBytes=2))`
- **Purpose:** Sets movement synchronization stamp
- **Status:** ⚠️ C# receives but doesn't handle
- **Impact:** May cause movement desynchronization issues
#### 2. UPDATE_EXT_STATE (124)
- **C++:** `SERVER - UPDATE_EXT_STATE(124), size=28`
- **C#:** `### GameDataSend: Unhandled CMDID 124 (payloadBytes=28)`
- **Purpose:** Updates extended state (buffs, debuffs, effects)
- **Status:** ⚠️ C# receives but doesn't handle
- **Impact:** Extended states may not update correctly
#### 3. ICON_STATE_NOTIFY (125)
- **C++:** `SERVER - ICON_STATE_NOTIFY(125), size=10` (first) / `size=8` (second)
- **C#:** `### GameDataSend: Unhandled CMDID 125 (payloadBytes=10)` / `(payloadBytes=8)`
- **Purpose:** Notifies icon state changes (UI updates)
- **Status:** ⚠️ C# receives but doesn't handle
- **Impact:** UI icons may not reflect correct state
---
## Protocol Sequence Timeline
### C++ Timeline (EC.log)
```
[17:10:03.501] CLIENT - CAST_POS_SKILL(89)
[17:10:03.565] SERVER - OBJECT_MOVE(15) × 9
[17:10:03.565] SERVER - OBJECT_STOP_MOVE(35)
[17:10:03.565] SERVER - SET_MOVE_STAMP(205) ← 64ms delay
[17:10:03.565] SERVER - OBJECT_CAST_POS_SKILL(204)
[17:10:03.565] SERVER - SKILL_PERFORM(88)
[17:10:03.565] SERVER - SET_COOLDOWN(198)
[17:10:03.565] SERVER - COMBO_SKILL_PREPARE(394)
[17:10:03.565] SERVER - ENCHANT_RESULT(139)
[17:10:03.766] SERVER - SELF_INFO_00(38) ← 265ms delay
[17:10:03.766] SERVER - UPDATE_EXT_STATE(124)
[17:10:03.766] SERVER - ICON_STATE_NOTIFY(125)
[17:10:04.567] SERVER - OBJECT_MOVE(15) × 2 ← 1.066s delay
[17:10:04.567] SERVER - OBJECT_STOP_MOVE(35)
[17:10:04.567] SERVER - HOST_STOP_SKILL(123)
[17:10:04.766] SERVER - UPDATE_EXT_STATE(124) ← 1.265s delay
[17:10:04.766] SERVER - ICON_STATE_NOTIFY(125)
```
### C# Timeline (SessionLog)
```
[16:58:15.286] CLIENT - CAST_POS_SKILL (via PROTOCOL_GAMEDATASEND)
[16:58:15.402] SERVER - CMDID 205 (Unhandled) ← 116ms delay
[16:58:15.403] SERVER - CMDID 204
[16:58:15.404] SERVER - CMDID 88
[16:58:15.406] SERVER - CMDID 198
[16:58:15.407] SERVER - CMDID 394
[16:58:15.408] SERVER - CMDID 139
[16:58:15.409] SERVER - CMDID 38
[16:58:15.410] SERVER - CMDID 124 (Unhandled)
[16:58:15.411] SERVER - CMDID 125 (Unhandled)
[16:58:15.588] SERVER - CMDID 15 × 2 ← 302ms delay
[16:58:15.591] SERVER - CMDID 35
[16:58:16.388] SERVER - CMDID 38 ← 1.102s delay
[16:58:16.389] SERVER - CMDID 124 (Unhandled)
[16:58:16.391] SERVER - CMDID 125 (Unhandled)
[16:58:16.392] SERVER - CMDID 123
```
---
## Key Findings
### ✅ What Works the Same
1. **Core skill protocols** (204, 88, 198, 394, 139) are handled identically
2. **Movement protocols** (15, 35) work the same
3. **Player info** (38) updates correctly
4. **Skill stop** (123) is handled
### ⚠️ Missing Handlers in C#
1. **SET_MOVE_STAMP (205)** - May cause movement sync issues
2. **UPDATE_EXT_STATE (124)** - Extended states (buffs/debuffs) may not update
3. **ICON_STATE_NOTIFY (125)** - UI icon states may be incorrect
### 📊 Protocol Count Comparison
| Protocol | C++ Count | C# Count | Difference |
|----------|-----------|---------|------------|
| OBJECT_MOVE (15) | 11 | 8 | C++ has 3 more |
| OBJECT_STOP_MOVE (35) | 2 | 1 | C++ has 1 more |
| SET_MOVE_STAMP (205) | 1 | 1 (unhandled) | Same count |
| OBJECT_CAST_POS_SKILL (204) | 1 | 1 | ✅ Same |
| SKILL_PERFORM (88) | 1 | 1 | ✅ Same |
| SET_COOLDOWN (198) | 1 | 1 | ✅ Same |
| COMBO_SKILL_PREPARE (394) | 1 | 1 | ✅ Same |
| ENCHANT_RESULT (139) | 1 | 1 | ✅ Same |
| SELF_INFO_00 (38) | 1 | 1 | ✅ Same |
| UPDATE_EXT_STATE (124) | 2 | 2 (unhandled) | Same count |
| ICON_STATE_NOTIFY (125) | 2 | 2 (unhandled) | Same count |
| HOST_STOP_SKILL (123) | 1 | 1 | ✅ Same |
---
## Recommendations
### High Priority
1. **Implement SET_MOVE_STAMP (205) handler** in C#
- Critical for movement synchronization
- May cause desync between client and server positions
2. **Implement UPDATE_EXT_STATE (124) handler** in C#
- Important for buff/debuff/effect state management
- Affects gameplay mechanics
3. **Implement ICON_STATE_NOTIFY (125) handler** in C#
- Affects UI correctness
- Lower priority but improves user experience
### Medium Priority
1. **Investigate OBJECT_MOVE count difference**
- C++ receives 11 OBJECT_MOVE commands
- C# receives 8 OBJECT_MOVE commands
- May indicate timing or network differences
---
## Conclusion
**Overall Status:** ✅ **Most protocols match** - The core skill casting flow works correctly in both implementations.
**Issues Found:** ⚠️ **3 unhandled protocols in C#** that may cause:
- Movement synchronization problems (SET_MOVE_STAMP)
- Extended state update issues (UPDATE_EXT_STATE)
- UI icon state inconsistencies (ICON_STATE_NOTIFY)
The server sends the same protocols to both clients, but C# needs to implement handlers for commands 205, 124, and 125 to achieve full feature parity with the C++ client.
+532
View File
@@ -0,0 +1,532 @@
# Flashmove Skill Casting Issue - Debug Summary
## Problem Description
**Issue:** After casting a flashmove skill in C#, when attempting to cast another skill immediately after, the client receives error message ID 20 (FIXMSG_NEEDITEM - "Need item"), preventing skill casting. This issue does NOT occur in the C++ version.
**Error Code 20:** Returned by `CheckSkillCastCondition()` when a skill requires an item but the player doesn't have it.
## Investigation Status
### What We've Done
1. **Added comprehensive logging to C#** (`CECHostPlayer.Skill.cs` and `CECHostPlayer.Combat.cs`)
- Entry logging in `ApplySkillShortcut()` with full state information
- Detailed logging in `CastSkill()` for network commands and m_pPrepSkill state changes
- Detailed logging in `CheckSkillCastCondition()` for item requirements
- Server response logging in `OnMsgPlayerCastSkill()`
- Enhanced logging to track m_pPrepSkill state transitions (set/clear operations)
2. **Added matching logging to C++** (`EC_HostPlayer.cpp` and `EC_HostMsg.cpp`)
- Same comprehensive logging to enable direct comparison
3. **Removed non-C++ code** (2026-03-09)
- Removed safeguard code that cleared stale m_pPrepSkill at start of ApplySkillShortcut()
- This code didn't exist in C++ and was causing divergence from original behavior
- C# code now matches C++ behavior exactly for m_pPrepSkill handling
### Key Files Modified
#### C# Files:
- `e:\Projects\perfect-world-unity\Assets\Scripts\CECHostPlayer.Skill.cs`
- `ApplySkillShortcut()` - Lines ~445-744
- `CastSkill()` - Lines ~751-1002
- `CheckSkillCastCondition()` - Lines ~1208-1222
- `e:\Projects\perfect-world-unity\Assets\Scripts\CECHostPlayer.Combat.cs`
- `OnMsgPlayerCastSkill()` - Lines ~208-660
- `OBJECT_CAST_SKILL` handler - Lines ~219-285
- `OBJECT_CAST_POS_SKILL` handler - Lines ~414-538
- `SKILL_PERFORM` handler - Lines ~287-305
#### C++ Files:
- `perfect-world-source/perfect-world-source/CElement/CElementClient/EC_HostPlayer.cpp`
- `ApplySkillShortcut()` - Lines ~2468-2800
- `CastSkill()` - Lines ~6037-6250
- `CheckSkillCastCondition()` - Lines ~5779-5851
- `perfect-world-source/perfect-world-source/CElement/CElementClient/EC_HostMsg.cpp`
- `OnMsgPlayerCastSkill()` - Lines ~5865-6151
- `OBJECT_CAST_SKILL` handler - Lines ~5878-5932
- `OBJECT_CAST_POS_SKILL` handler - Lines ~6038-6151
- `SKILL_PERFORM` handler - Lines ~5933-5941
## Key Differences Found
### Flashmove Skill Handling
**C++ Behavior:**
- When `OBJECT_CAST_POS_SKILL` is received, creates `WORK_FLASHMOVE` work
- `m_pPrepSkill` is cleared in `SKILL_PERFORM` message (after skill execution)
- Flashmove skills use `c2s_CmdCastPosSkill()` network command
**C# Behavior:**
- Same as C++ for most parts
- **Potential Issue:** `m_pPrepSkill` might not be cleared properly when `OBJECT_CAST_POS_SKILL` is received
### Network Commands
**Flashmove Skills:**
- Self-target flashmove: `c2s_CmdCastPosSkill(skillID, position, byPVPMask, 0, NULL)`
- Target flashmove: `c2s_CmdCastPosSkill(skillID, position, byPVPMask, 1, &idTarget)`
**Regular Skills:**
- `c2s_CmdCastSkill(skillID, byPVPMask, targetCount, targets[])`
**Instant Skills:**
- `c2s_CmdCastInstantSkill(skillID, byPVPMask, targetCount, targets[])`
## Logging Format
All logs use the prefix `[SKILL_CAST_DEBUG]` for easy filtering.
### Key Log Points to Compare
1. **ApplySkillShortcut Entry:**
```
[SKILL_CAST_DEBUG] ApplySkillShortcut: Entry, skillID=X, IsSpellingMagic=Y, m_pPrepSkill=Z, m_pCurSkill=W, IsFlashMoving=V
```
2. **CheckSkillCastCondition:**
```
[SKILL_CAST_DEBUG] CheckSkillCastCondition: Entry, skillID=X
[SKILL_CAST_DEBUG] CheckSkillCastCondition: Item check, skillID=X, idItem=Y, itemTotalNum=Z
[SKILL_CAST_DEBUG] CheckSkillCastCondition: ERROR 20 - Need item, skillID=X, requiredItem=Y, have=Z
```
3. **CastSkill Network Commands:**
```
[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastPosSkill (flashmove self), skillID=X, pos=(x,y,z), byPVPMask=Y
[SKILL_CAST_DEBUG] CastSkill: Sending c2s_CmdCastSkill (regular), skillID=X, target=Y, byPVPMask=Z
```
4. **Server Responses:**
```
[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received OBJECT_CAST_POS_SKILL, skillID=X, m_pPrepSkill=Y, m_pCurSkill=Z
[SKILL_CAST_DEBUG] OnMsgPlayerCastSkill: Received SKILL_PERFORM, m_pPrepSkill=X (clearing)
```
## What to Check When Testing
### After Flashmove, Before Next Skill:
1. **State Variables:**
- Is `m_pPrepSkill` still set? (Should be NULL)
- Is `IsSpellingMagic()` returning true? (Should be false)
- Is `IsFlashMoving()` returning true? (Should be false)
- Is `m_pCurSkill` set? (May or may not be set)
2. **Server Response:**
- Did `OBJECT_CAST_POS_SKILL` arrive?
- Was `WORK_FLASHMOVE` created?
- Was `m_pPrepSkill` cleared when it should be?
3. **Next Skill Cast:**
- What does `CheckSkillCastCondition()` return? (Should be 0, not 20)
- If error 20: What item is required? What's the current count?
- Are network command parameters correct?
## Potential Root Causes
1. **m_pPrepSkill Not Cleared:**
- In C++, `m_pPrepSkill` is cleared in `SKILL_PERFORM` message
- In C#, might not be cleared properly after flashmove
- This could cause the next skill to check conditions with wrong skill reference
2. **Work State Issue:**
- `WORK_FLASHMOVE` might not be finishing properly
- `IsSpellingMagic()` or `IsFlashMoving()` might return true when they shouldn't
- This could block the next skill cast
3. **Item Requirement Check:**
- `CheckSkillCastCondition()` might be checking item requirements incorrectly
- The flashmove skill's item requirement might be incorrectly applied to the next skill
4. **Network Command Timing:**
- Client might be sending the next skill command before server confirms flashmove
- Server might reject it due to state mismatch
## Next Steps
1. **Run Test:**
- Cast flashmove skill
- Immediately cast another skill
- Collect logs from both C++ and C# versions
- **Note:** Logs from SessionLog_2026-03-09_10-48-05.txt show error 20 still occurs
2. **Compare Logs:**
- Filter by `[SKILL_CAST_DEBUG]`
- Compare state variables at each step
- Identify where C# behavior diverges from C++
- **Key:** Check if `[SKILL_CAST_DEBUG]` logs are appearing (they weren't in the test log)
3. **Focus Areas:**
- When is `m_pPrepSkill` cleared in C# vs C++? (Should be immediately after sending flashmove command)
- What is the state when `CheckSkillCastCondition()` returns error 20?
- Are network commands sent with correct parameters?
- **NEW:** Verify that debug logs are actually being written (may need to check Unity console or log file location)
4. **Server-Side Investigation:**
- Error 20 comes from server, not client `CheckSkillCastCondition()`
- Server might be checking wrong skill's item requirement
- Server might be receiving incorrect skill ID in network command
- Need to verify what skill ID the server receives vs what client sends
## Code References
### CheckSkillCastCondition Error 20:
```cpp
// C++: EC_HostPlayer.cpp line ~5781-5785
int idItem = pSkill->GetRequiredItem();
if (idItem > 0 && GetPack()->GetItemTotalNum(idItem) <= 0)
{
return 20; // Need item
}
```
```csharp
// C#: CECHostPlayer.Skill.cs line ~1161-1165
int idItem = pSkill.SkillCore.GetItemCost();
if (idItem > 0 && GetPack().GetItemTotalNum(idItem) <= 0)
{
return 20; // Need item
}
```
### Flashmove Skill Casting:
```cpp
// C++: EC_HostPlayer.cpp line ~6106-6130
else if (m_pPrepSkill->GetType() == CECSkill::TYPE_FLASHMOVE)
{
// ... flashmove logic ...
g_pGame->GetGameSession()->c2s_CmdCastPosSkill(...);
m_pPrepSkill = NULL;
}
```
```csharp
// C#: CECHostPlayer.Skill.cs line ~831-860
else if (m_pPrepSkill.GetType() == (int)CECSkill.SkillType.TYPE_FLASHMOVE)
{
// ... flashmove logic ...
UnityGameSession.c2s_CmdCastPosSkill(...);
m_pPrepSkill = null;
}
```
### Server Response Handling:
```cpp
// C++: EC_HostMsg.cpp line ~6038-6147
case OBJECT_CAST_POS_SKILL:
{
// Creates WORK_FLASHMOVE
CECHPWorkFMove* pWork = (CECHPWorkFMove*)m_pWorkMan->CreateWork(CECHPWork::WORK_FLASHMOVE);
// ...
}
```
```csharp
// C#: CECHostPlayer.Combat.cs line ~414-534
case CommandID.OBJECT_CAST_POS_SKILL:
{
// Creates WORK_FLASHMOVE
CECHPWorkFMove pWork = (CECHPWorkFMove)m_pWorkMan.CreateWork(CECHPWork.Host_work_ID.WORK_FLASHMOVE);
// ...
}
```
## Important Notes
- All logging uses `[SKILL_CAST_DEBUG]` prefix for easy filtering
- Error 20 = FIXMSG_NEEDITEM = "Need item" error
- Flashmove skills are TYPE_FLASHMOVE skill type
- `m_pPrepSkill` should be cleared after sending network command (for instant/flashmove) or on server response (for regular skills)
- `m_pCurSkill` is set when server responds with OBJECT_CAST_SKILL or OBJECT_CAST_POS_SKILL
## Questions to Answer
1. When exactly does error 20 occur? (Which skill triggers it?)
2. What item is required? (Check the logs for `requiredItem` value)
3. Does the player actually have the item? (Check `itemTotalNum` in logs)
4. Is `m_pPrepSkill` still set to the flashmove skill when checking the next skill?
5. Is `CheckSkillCastCondition()` being called with the correct skill object?
6. **NEW:** Why are `[SKILL_CAST_DEBUG]` logs not appearing in SessionLog_2026-03-09_10-48-05.txt?
7. **NEW:** Is the error coming from client-side `CheckSkillCastCondition()` or server-side validation?
## Recent Changes (2026-03-09)
### Removed Non-C++ Code
- **File:** `Assets/Scripts/CECHostPlayer.Skill.cs`
- **Lines removed:** 453-459 (safeguard code that cleared stale m_pPrepSkill)
- **Reason:** This code didn't exist in C++ and was causing divergence from original behavior
- **Status:** ✅ Removed - C# now matches C++ behavior
### Enhanced Logging
- **File:** `Assets/Scripts/CECHostPlayer.Skill.cs`
- Added detailed logging for m_pPrepSkill state transitions:
- When m_pPrepSkill is set in ApplySkillShortcut()
- When m_pPrepSkill is cleared in CastSkill() for flashmove skills
- Which skill ID is being checked in CheckSkillCastCondition()
- **File:** `Assets/Scripts/CECHostPlayer.Combat.cs`
- Added logging in SKILL_PERFORM handler to track when m_pPrepSkill is cleared
### Current Status
- ✅ Code now matches C++ behavior for m_pPrepSkill handling
- ❌ Bug still exists (error 20 when casting skill after flashmove)
- ⚠️ Debug logs not appearing in test session log (need to verify log output location)
- 🔍 Next: Compare C++ and C# logs side-by-side to find root cause
## Root Cause Analysis (2026-03-09)
### Key Finding: Error 20 is Server-Side, Not Client-Side
**Critical Discovery:** The error 20 (`FIXMSG_NEEDITEM`) is coming from the **SERVER**, not from client-side `CheckSkillCastCondition()`.
**Evidence from logs:**
- Unity log (SessionLog_2026-03-09_11-16-28.txt):
- Line 535: `CAST_SKILL` sent
- Line 536: `### GameDataSend: ERROR_MESSAGE: 20` ← **Server response**
- Line 537: `### GameDataSend: ERROR_MESSAGE parsed iMessage=20`
- C++ log (EC.log):
- Line 5448: Flashmove skill (skillID=58) cast
- Line 5472: Server responds with `OBJECT_CAST_POS_SKILL` and `SKILL_PERFORM` (almost immediately)
- Line 6208: Next skill (skillID=1) cast successfully - **NO ERROR**
### The Problem
**Timing Issue:** In the Unity version, the next skill is being cast **before the server has fully processed the flashmove skill**.
**Sequence in Unity:**
1. `11:16:57.856` - `CAST_POS_SKILL` sent (flashmove)
2. `11:17:00.858` - `CAST_SKILL` sent (next skill) ← **3 seconds later**
3. `11:17:00.972` - Server returns `ERROR_MESSAGE: 20`
**Sequence in C++ (working):**
1. `10:35:21.207` - Flashmove skill cast
2. `10:35:21.276` - Server responds with `OBJECT_CAST_POS_SKILL` and `SKILL_PERFORM` ← **Immediate response**
3. `10:35:24.438` - Next skill cast successfully ← **3+ seconds later, but AFTER server confirmed flashmove**
### Why This Happens
The server needs to:
1. Process the flashmove skill
2. Send `OBJECT_CAST_POS_SKILL` back to client
3. Send `SKILL_PERFORM` to clear the skill state
4. Only then is it ready for the next skill
If the next skill is sent **before** the server has sent `SKILL_PERFORM`, the server may still be in a state where it thinks the flashmove skill is active, causing it to check the wrong skill's item requirements.
### Missing Server Response in Unity Log
**Critical Observation:** The Unity log shows:
- `CAST_POS_SKILL` sent at `11:16:57.856`
- But **NO** `OBJECT_CAST_POS_SKILL` or `SKILL_PERFORM` response from server before the next skill is cast
This suggests either:
1. The server response is not being received/logged
2. The server response handler is not being called
3. There's a network timing issue
### Solution
**Option 1: Wait for Server Confirmation**
- Don't allow casting the next skill until `SKILL_PERFORM` is received for the flashmove skill
- This matches C++ behavior where the client waits for server confirmation
**Option 2: Check Server State**
- Before casting the next skill, verify that `m_pPrepSkill` is null (which should be set after `SKILL_PERFORM`)
- If `m_pPrepSkill` is still set, wait for `SKILL_PERFORM` before allowing next skill
**Option 3: Server-Side Fix**
- If the error is truly server-side, the server should handle the case where a new skill is sent while a flashmove is still processing
- However, since C++ works fine, this is likely a client-side timing issue
### Recommended Fix
**Immediate Fix:** Add a check in `ApplySkillShortcut()` or `CastSkill()` to prevent casting a new skill if:
- `m_pPrepSkill` is still set (waiting for server confirmation)
- OR `IsFlashMoving()` returns true (flashmove work is still active)
This will ensure the client waits for server confirmation before allowing the next skill cast, matching C++ behavior.
## Root Cause Found (2026-03-09)
### The Missing Check
**Problem:** The C# code was missing the `CanCastSkillImmediately()` check that exists in C++.
**C++ Code (EC_HostPlayer.cpp, line 2688-2694):**
```cpp
// Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2)
if (!m_pWorkMan->CanCastSkillImmediately(pSkill->GetSkillID()))
{
bool hasWorkOnPriority2 = m_pWorkMan->HasWorkRunningOnPriority(CECHPWorkMan::PRIORITY_2);
a_LogOutput(1, "[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanCastSkillImmediately returned false, skillID=%d, IsSpellingMagic=%d, HasWorkOnPriority2=%d",
idSkill, IsSpellingMagic() ? 1 : 0, hasWorkOnPriority2 ? 1 : 0);
return false;
}
```
**C# Code (BEFORE FIX):**
- Missing this check in the main casting path
- Only checked `ReadyToCast()` but not `CanCastSkillImmediately()`
- This allowed casting a new skill while flashmove work (PRIORITY_2) was still active
**Why This Causes Error 20:**
1. Flashmove skill is cast → `WORK_FLASHMOVE` created at PRIORITY_2
2. `m_pPrepSkill` is cleared immediately after sending network command (correct)
3. User tries to cast next skill → `CanCastSkillImmediately()` should return false (blocking it)
4. **BUT** the check was missing, so the skill is sent to server
5. Server receives the skill while flashmove is still processing
6. Server checks the wrong skill's item requirement → returns ERROR 20
### Fix Applied
**File:** `Assets/Scripts/CECHostPlayer.Skill.cs`
**Location:** Line ~683 (after `ReadyToCast()` check, before setting `m_pPrepSkill`)
**Added:**
```csharp
// Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2)
// 检查技能是否可以立即施放(阻止在PRIORITY_2的闪移工作期间施放)
if (!m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
bool hasWorkOnPriority2 = m_pWorkMan.HasWorkRunningOnPriority(CECHPWorkMan.Priority.PRIORITY_2);
Debug.Log($"[SKILL_CAST_DEBUG] ApplySkillShortcut: BLOCKED - CanCastSkillImmediately returned false, skillID={idSkill}, " +
$"IsSpellingMagic={IsSpellingMagic()}, HasWorkOnPriority2={hasWorkOnPriority2}");
return false;
}
```
This check prevents casting a new skill while `WORK_FLASHMOVE` (or any other PRIORITY_2 work) is still active, matching C++ behavior exactly.
### Status
- ✅ **Root cause identified:** Missing `CanCastSkillImmediately()` check
- ✅ **Fix applied:** Added the check to match C++ behavior
- ✅ **Enhanced fix:** Added `IsFlashMoving()` check as additional safeguard
- ❌ **Still occurring:** Error 20 still happens in test (SessionLog_2026-03-09_11-43-06.txt)
- 🔍 **Investigation:** The check may not be sufficient - client-side flashmove work might finish before server processes it
### Updated Fix (2026-03-09)
**Enhanced Check:** Added `IsFlashMoving()` check in addition to `CanCastSkillImmediately()`:
```csharp
// Check if skill can be cast immediately (blocks casting during flash move at PRIORITY_2)
// Also check IsFlashMoving() as an additional safeguard since client-side work might finish
// before server processes the flashmove
if (IsFlashMoving() || !m_pWorkMan.CanCastSkillImmediately(pSkill.GetSkillID()))
{
// BLOCKED
return false;
}
```
**Why this is needed:** The client-side flashmove work (`WORK_FLASHMOVE`) might finish before the server processes the flashmove command. When this happens:
- `CanCastSkillImmediately()` might return `true` (work is done on client)
- But server is still processing the flashmove
- Next skill sent to server → server checks wrong skill → ERROR 20
**Note:** Debug logs (`[SKILL_CAST_DEBUG]`) are not appearing in session log files. They may be going to Unity console instead. This makes it difficult to verify if the check is being executed.
## Additional Issue: "Distance Too Far" Error After Flashmove (2026-03-09)
### Problem
After casting a flashmove skill, normal attacks and skill casts fail with "distance too far" error (ERROR_MESSAGE: 2), even though the target should be in range.
### Root Cause
**The Real Issue:** `CanTouchTarget()` and distance calculation functions were using `transform.position` (the visual position) instead of the server-tracked position (`GetLastSevPos()`).
**What Happens:**
1. When flashmove happens, `SetHostLastPos()` and `SetLastSevPos()` ARE updated immediately (this was already fixed)
2. BUT `CanTouchTarget()` uses `transform.position` which is NOT updated immediately - it's moved gradually by `WORK_FLASHMOVE` work
3. So distance checks use the OLD visual position, while server uses the tracked position (which was updated)
4. Client thinks player is still at old position → distance check fails → ERROR 2 from server
### Fix Applied (2026-03-09)
**File 1:** `Assets/Scripts/CECHostPlayer.cs`
**Location:** Line ~1534 (CanTouchTarget overload)
**Changed:**
```csharp
// BEFORE (WRONG):
A3DVECTOR3 vector = new A3DVECTOR3(playerTransform.position.x, playerTransform.position.y,
playerTransform.position.z);
// AFTER (CORRECT):
// Use server-tracked position instead of visual position for distance checks
A3DVECTOR3 vector = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
```
**File 2:** `Assets/Scripts/CECHostPlayer.Combat.cs`
**Location:** Line ~791 (NormalAttackObject)
**Changed:**
```csharp
// BEFORE (WRONG):
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position);
// AFTER (CORRECT):
// Use server-tracked position instead of visual position for distance checks
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
```
**File 3:** `Assets/Scripts/CECHostPlayer.Skill.cs`
**Location:** Line ~1102 (CastSkill logging)
**Changed:**
```csharp
// BEFORE (WRONG):
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(transform.position);
// AFTER (CORRECT):
// Use server-tracked position instead of visual position for accurate distance checks
A3DVECTOR3 vHostPos = EC_Utility.ToA3DVECTOR3(m_MoveCtrl.GetLastSevPos());
```
**Why this works:**
- `GetLastSevPos()` returns the server-tracked position, which is updated immediately when flashmove happens
- `transform.position` is the visual position, which is moved gradually by `WORK_FLASHMOVE` work
- Using the tracked position ensures distance checks match what the server sees
**C++ Comparison:**
- **C++ (EC_HostPlayer.cpp, line 4106):** `CanTouchTarget(GetPos(), vTargetPos, ...)`
- **C# (BEFORE FIX):** `CanTouchTarget(transform.position, ...)` ❌ Wrong - uses visual position
- **C# (AFTER FIX):** `CanTouchTarget(GetLastSevPos(), ...)` ✅ Correct - uses server-tracked position
**Evidence from C++ Code:**
1. **Flashmove Position Update (EC_HostMsg.cpp, lines 6115-6178):**
- For flashmove skills using `WORK_FLASHMOVE`, `SetPos()` is **NOT** called immediately
- Only `SetHostLastPos()` and `SetLastSevPos()` are updated (via work system)
- The work moves the player visually over time
2. **Position Comparison (EC_HostPlayer.cpp, line 4162):**
```cpp
if (GetPos() != m_MoveCtrl.GetLastSevPos())
{
m_MoveCtrl.SendMoveCmd(GetPos(), ...);
}
```
This shows `GetPos()` and `GetLastSevPos()` **can be different**, but when they differ, the code sends a move command to sync them.
3. **Why C++ Works:**
- `GetPos()` in C++ appears to be synchronized with server position through the work system
- OR `GetPos()` might return `GetLastSevPos()` in certain contexts
- OR the work system updates `GetPos()` during flashmove execution
- The exact mechanism is unclear, but `CanTouchTarget(GetPos(), ...)` works correctly in C++
4. **Why C# Doesn't Work (Before Fix):**
- `transform.position` in Unity is the visual position, updated gradually by `WORK_FLASHMOVE`
- `GetPos()` in C# likely returns `transform.position` (visual position)
- This causes distance checks to use the old position, failing immediately after flashmove
**Conclusion:** Using `GetLastSevPos()` directly in C# matches the server-tracked position that C++ `GetPos()` appears to use for distance checks, ensuring consistent behavior.
### Status
- ✅ **Root cause identified:** Distance checks using visual position instead of tracked position
- ✅ **Fix applied:** All distance checks now use `GetLastSevPos()` instead of `transform.position`
- ⏳ **Testing needed:** Verify normal attacks and skill casts work correctly after flashmove