# Server-to-Client ElsePlayer Skill Cast Flow Implementation ## Overview This document describes the complete implementation of the server-to-client flow for when other players (else players) cast skills in the Unity C# client. This includes message handling, animation playback, and GFX (graphics effects) triggering. ## Architecture ### Key Components | Component | File | Purpose | |-----------|------|---------| | `EC_ElsePlayer` | `Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs` | Handles skill casting for other players | | `EC_ManPlayer` | `Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs` | Routes messages to correct player instances | | `CECPlayer` | `Assets/PerfectWorld/Scripts/Move/CECPlayer.cs` | Base class with `PlayAttackEffect()` and `PlaySkillCastAction()` | | `CECAttacksMan` | Attack Manager | Creates attack events that trigger GFX | | `A3DSkillGfxComposerMan` | `Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs` | Manages skill GFX composition and spawning | ## Complete Flow Diagram ```mermaid flowchart TD A[Server sends OBJECT_CAST_SKILL] --> B[EC_GameDataPrtc routes to MSG_PM_CASTSKILL] B --> C[EC_ManPlayer.TransmitMessage extracts caster ID] C --> D{Caster is Host?} D -->|Yes| E[CECHostPlayer.ProcessMessage] D -->|No| F[EC_ElsePlayer.ProcessMessage] F --> G[OnMsgPlayerCastSkill handler] G --> H[Parse cmd_object_cast_skill] H --> I[PlaySkillCastAction - play animation] I --> J[Create CECSkill object for tracking] J --> K[Set m_pCurSkill and m_idCurSkillTarget] K --> L[Server sends SKILL_PERFORM] L --> M[SKILL_PERFORM handler - skill execution] M --> N[Server sends Attack Result] N --> O{Message Type?} O -->|MSG_PM_PLAYERATKRESULT| P[OnMsgPlayerAtkResult] O -->|OBJECT_SKILL_ATTACK_RESULT| Q[OnMsgPlayerCastSkill with different commandID] P --> R[Get skillID from m_pCurSkill] Q --> R R --> S[PlayAttackEffect with skillID] S --> T[CECAttacksMan.AddSkillAttack] T --> U[CECAttackEvent created] U --> V[Event.DoFire triggers GFX] V --> W[A3DSkillGfxComposerMan.Play] W --> X[GFX spawned at hook positions] ``` ## Implementation Details ### 1. Message Routing **File**: `Assets/PerfectWorld/Scripts/Managers/EC_ManPlayer.cs` The message routing system receives `MSG_PM_CASTSKILL` and routes it to the appropriate player: ```csharp case EC_MsgDef.MSG_PM_CASTSKILL: switch (Convert.ToInt32(Msg.dwParam2)) { case CommandID.OBJECT_CAST_SKILL: cid = (GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1)).caster; break; // ... other cases } break; ``` The `TransmitMessage()` method extracts the caster ID and routes to either: - `CECHostPlayer.ProcessMessage()` if caster is the host player - `EC_ElsePlayer.ProcessMessage()` if caster is another player ### 2. Message Handler Registration **File**: `Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs` ```csharp public bool ProcessMessage(ECMSG Msg) { switch (Msg.dwMsg) { case EC_MsgDef.MSG_PM_CASTSKILL: OnMsgPlayerCastSkill(Msg); break; case EC_MsgDef.MSG_PM_PLAYERATKRESULT: OnMsgPlayerAtkResult(Msg); break; // ... other cases } return true; } ``` ### 3. Skill Cast Handler **File**: `Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs` **Method**: `OnMsgPlayerCastSkill(ECMSG Msg)` Handles multiple command types: #### 3.1 OBJECT_CAST_SKILL ```csharp case CommandID.OBJECT_CAST_SKILL: { cmd_object_cast_skill pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); int skillID = pCmd.skill; // Store target m_idCurSkillTarget = pCmd.target; // Face target TurnFaceTo(pCmd.target); // Play cast animation PlaySkillCastAction(skillID); // Create temporary skill object for tracking if (m_pCurSkill == null || m_pCurSkill.GetSkillID() != skillID) { m_pCurSkill = new CECSkill(skillID, 1); } EnterFightState(); break; } ``` **Key Points**: - Else players don't maintain skill lists like host player - Creates temporary `CECSkill` objects for tracking only - Uses `PlaySkillCastAction()` from base class `CECPlayer` - Stores skill and target for later use in attack result handler #### 3.2 OBJECT_CAST_INSTANT_SKILL Similar to `OBJECT_CAST_SKILL` but for instant skills (no cast time). #### 3.3 OBJECT_CAST_POS_SKILL For position-based skills (e.g., flash move). Target is a position, not an object. #### 3.4 SKILL_PERFORM ```csharp case CommandID.SKILL_PERFORM: { // Skill has finished casting and is being executed // For durative skills, keep m_pCurSkill active // For non-durative skills, wait for attack result break; } ``` #### 3.5 SKILL_INTERRUPTED ```csharp case CommandID.SKILL_INTERRUPTED: { // Clear casting state if (m_pCurSkill != null) { StopSkillCastAction(); m_pCurSkill = null; } m_idCurSkillTarget = 0; break; } ``` ### 4. Attack Result Handler **File**: `Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs` **Method**: `OnMsgPlayerAtkResult(ECMSG Msg)` ```csharp void OnMsgPlayerAtkResult(ECMSG Msg) { cmd_object_atk_result pCmd = GPDataTypeHelper.FromBytes((byte[])Msg.dwParam1); TurnFaceTo(pCmd.target_id); // Get skill ID from current skill (set during cast) int idSkill = 0; int skillLevel = 0; if (m_pCurSkill != null) { idSkill = m_pCurSkill.GetSkillID(); skillLevel = m_pCurSkill.GetSkillLevel(); } // Trigger attack effect (this triggers GFX system) int attackTime = int.MinValue; PlayAttackEffect(pCmd.target_id, idSkill, skillLevel, -1, (uint)pCmd.attack_flag, pCmd.speed * 50, ref attackTime); // Only start melee work for melee attacks (idSkill == 0) if (idSkill == 0) { // Start melee attack work } else { // Skill attack - GFX will be triggered via CECAttacksMan // Keep m_pCurSkill for potential multi-hit skills } EnterFightState(); } ``` **Key Points**: - Uses `m_pCurSkill` (set during cast) to get skill ID - Calls `PlayAttackEffect()` with skill ID to trigger GFX - For melee attacks (`idSkill == 0`), starts melee work - For skill attacks, GFX system handles the rest ### 5. GFX System Integration **Flow**: `PlayAttackEffect()` → `CECAttacksMan.AddSkillAttack()` → `CECAttackEvent.DoFire()` → `A3DSkillGfxComposerMan.Play()` #### 5.1 PlayAttackEffect (Base Class) **File**: `Assets/PerfectWorld/Scripts/Move/CECPlayer.cs` ```csharp public void PlayAttackEffect(int idTarget, int idSkill, int skillLevel, int nDamage, uint dwModifier, int nAttackSpeed, ref int piAttackTime, int nSection = 0) { if (idSkill == 0) { // Melee attack handling } else { // Skill attack - create attack event CECAttackEvent pAttack = CECAttacksMan.Instance.AddSkillAttack( GetPlayerInfo().cid, m_idCurSkillTarget, idTarget, GetWeaponID(), idSkill, skillLevel, dwModifier, nDamage); if (pAttack != null) { pAttack.SetSkillSection(nSection); PlaySkillAttackAction(idSkill, nAttackSpeed, ref unusedInt, nSection, pAttack); } } } ``` #### 5.2 Attack Event Creation **File**: `CECAttacksMan.cs` (Attack Manager) ```csharp CECAttackEvent AddSkillAttack(int idHost, int idCastTarget, int idTarget, int idWeapon, int idSkill, int skillLevel, uint dwModifier, int nDamage) { // Creates attack event with delay (timeToBeFired = 200ms) CECAttackEvent newEvent = new CECAttackEvent(...); m_AttackList.AddTail(newEvent); return newEvent; } ``` #### 5.3 GFX Triggering **File**: `CECAttackEvent.cs` ```csharp bool DoFire() { if (m_idSkill != 0) { // Trigger GFX composer m_pManager->GetSkillGfxComposerMan()->Play( m_idSkill, m_idHost, m_idCastTarget, m_targets); } } ``` #### 5.4 GFX Spawning **File**: `Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs` ```csharp public void Play(int nSkillID, int nHostID, int nCastTargetID, List Targets, bool bIsGoblinSkill = false) { if (m_ComposerMap.TryGetValue(nSkillID, out var composer)) { composer.Play(nHostID, nCastTargetID, Targets, bIsGoblinSkill); } } ``` The composer loads GFX resources and spawns them at hook positions on character skeletons. ## Data Structures ### Command Structures #### cmd_object_cast_skill ```csharp struct cmd_object_cast_skill { int caster; // Player ID casting the skill int skill; // Skill ID int target; // Target ID (0 for self/position) int time; // Cast time in milliseconds } ``` #### cmd_object_atk_result ```csharp struct cmd_object_atk_result { int attacker_id; // Player ID who attacked int target_id; // Target ID int damage; // Damage dealt (-1 if miss) int speed; // Attack speed int attack_flag; // Attack flags (crit, miss, etc.) } ``` ## State Management ### Key Variables in EC_ElsePlayer | Variable | Type | Purpose | |----------|------|---------| | `m_pCurSkill` | `CECSkill` | Current skill being cast (temporary object) | | `m_idCurSkillTarget` | `int` | Target ID for current skill cast | | `m_pEPWorkMan` | `CECEPWorkMan` | Work manager for else player actions | ### Skill Object Lifecycle 1. **Created**: When `OBJECT_CAST_SKILL` is received 2. **Maintained**: During casting and until attack result 3. **Cleared**: When `SKILL_INTERRUPTED` is received or new skill is cast **Note**: Else players don't maintain skill lists. `CECSkill` objects are created temporarily for tracking purposes only. Actual skill data comes from `ElementSkill` static methods. ## Message Flow Sequence ### Normal Skill Cast Flow ``` 1. Server → OBJECT_CAST_SKILL (commandID=85) ↓ 2. EC_ManPlayer.TransmitMessage() extracts caster ID ↓ 3. EC_ElsePlayer.ProcessMessage() routes to OnMsgPlayerCastSkill() ↓ 4. OnMsgPlayerCastSkill() handles OBJECT_CAST_SKILL: - Parses cmd_object_cast_skill - Plays cast animation (PlaySkillCastAction) - Creates CECSkill object - Sets m_pCurSkill and m_idCurSkillTarget ↓ 5. Server → SKILL_PERFORM (commandID=88) ↓ 6. OnMsgPlayerCastSkill() handles SKILL_PERFORM: - Marks skill as performing - Keeps m_pCurSkill for attack result ↓ 7. Server → MSG_PM_PLAYERATKRESULT (or OBJECT_SKILL_ATTACK_RESULT) ↓ 8. EC_ElsePlayer.OnMsgPlayerAtkResult() (or OnMsgPlayerCastSkill()): - Gets skillID from m_pCurSkill - Calls PlayAttackEffect() with skillID ↓ 9. PlayAttackEffect() → CECAttacksMan.AddSkillAttack() ↓ 10. CECAttackEvent.DoFire() → A3DSkillGfxComposerMan.Play() ↓ 11. GFX spawned at hook positions ✨ ``` ### Instant Skill Flow Similar to normal flow but: - `OBJECT_CAST_INSTANT_SKILL` instead of `OBJECT_CAST_SKILL` - No `SKILL_PERFORM` message (skill executes immediately) - Attack result may arrive immediately after cast ### Position-Based Skill Flow Similar to normal flow but: - `OBJECT_CAST_POS_SKILL` instead of `OBJECT_CAST_SKILL` - Target is a position (Vector3), not an object ID - `m_idCurSkillTarget` may be 0 ## Debugging and Logging ### Log Prefix All logs use the prefix `[ELSEPLAYER_SKILL_FLOW]` for easy filtering. ### Key Log Points 1. **ProcessMessage Entry** ``` [ELSEPLAYER_SKILL_FLOW] ProcessMessage: Received message, playerID=X, msgType=Y, param2=Z ``` 2. **OnMsgPlayerCastSkill Entry** ``` [ELSEPLAYER_SKILL_FLOW] OnMsgPlayerCastSkill: Entry, playerID=X, commandID=Y ``` 3. **OBJECT_CAST_SKILL** ``` [ELSEPLAYER_SKILL_FLOW] OBJECT_CAST_SKILL: playerID=X, skillID=Y, target=Z, time=W [ELSEPLAYER_SKILL_FLOW] PlaySkillCastAction result: true/false [ELSEPLAYER_SKILL_FLOW] Created new CECSkill: skillID=X, level=1 ``` 4. **OnMsgPlayerAtkResult Entry** ``` [ELSEPLAYER_SKILL_FLOW] OnMsgPlayerAtkResult: Entry, attackerID=X, targetID=Y, m_pCurSkill=Z [ELSEPLAYER_SKILL_FLOW] OnMsgPlayerAtkResult: Skill attack detected, skillID=X [ELSEPLAYER_SKILL_FLOW] Calling PlayAttackEffect: target=X, skillID=Y ``` 5. **Unknown Command IDs** ``` [ELSEPLAYER_SKILL_FLOW] ProcessMessage: Unknown commandID=X in MSG_PM_CASTSKILL! ``` ## Known Issues and Limitations ### Issue 1: Missing Attack Result Messages **Problem**: The server may not send `MSG_PM_PLAYERATKRESULT` for else players' skill attacks. According to documentation, `OBJECT_SKILL_ATTACK_RESULT` should be sent to observers instead. **Status**: ⚠️ **Investigation Needed** **Possible Solutions**: 1. Add handler for `OBJECT_SKILL_ATTACK_RESULT` command ID in `OnMsgPlayerCastSkill()` 2. Check if attack results come through a different message type 3. Implement fallback to trigger GFX from `SKILL_PERFORM` if no attack result arrives **Detection**: Logs will show warning if `SKILL_PERFORM` is received but no attack result follows. ### Issue 2: PlaySkillCastAction Returns False **Problem**: In logs, `PlaySkillCastAction` sometimes returns `false`, indicating animation may not play. **Status**: ⚠️ **Non-Critical** - Animation may fail but skill state is still tracked **Impact**: Visual animation may not play, but GFX should still spawn when attack result arrives. ### Issue 3: Skill Level Unknown **Problem**: Else players don't know the actual skill level, so we create `CECSkill` with level 1. **Status**: ✅ **Expected Behavior** **Impact**: GFX may use default level settings, but should still work correctly. ## Differences from Host Player Implementation | Aspect | Host Player | Else Player | |--------|-------------|-------------| | **Skill Lists** | Maintains full skill lists (`m_aPtSkills`, `m_aPsSkills`) | No skill lists - creates temporary objects | | **Prep Skill** | Has `m_pPrepSkill` for skill preparation | No prep skill - direct cast | | **Work System** | Uses `CECHPWorkMan` with complex work system | Uses `CECEPWorkMan` with simpler work system | | **Attack Result** | Receives `HOST_SKILL_ATTACK_RESULT` | Should receive `OBJECT_SKILL_ATTACK_RESULT` (needs verification) | | **State Management** | Complex state with work priorities | Simpler state management | ## Testing Checklist - [ ] Verify `OBJECT_CAST_SKILL` message is received and processed - [ ] Verify cast animation plays (`PlaySkillCastAction` returns true) - [ ] Verify `m_pCurSkill` is set correctly - [ ] Verify `SKILL_PERFORM` is received (if applicable) - [ ] Verify attack result message is received (`MSG_PM_PLAYERATKRESULT` or `OBJECT_SKILL_ATTACK_RESULT`) - [ ] Verify `PlayAttackEffect()` is called with correct skill ID - [ ] Verify GFX spawns at hook positions - [ ] Verify GFX follows target if target moves - [ ] Test with multiple else players casting simultaneously - [ ] Test instant skills - [ ] Test position-based skills - [ ] Test skill interruption ## Files Modified 1. **Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs** - Added `MSG_PM_CASTSKILL` case in `ProcessMessage()` - Implemented `OnMsgPlayerCastSkill()` method (~150 lines) - Enhanced `OnMsgPlayerAtkResult()` to handle skill attacks - Added comprehensive logging throughout ## Related Documentation - [Skill Flow Documentation.md](Skill%20Flow%20Documentation.md) - Complete C++ to C# conversion reference - [CECHostPlayer.Skill.cs](Assets/Scripts/CECHostPlayer.Skill.cs) - Host player skill implementation reference - [A3DSkillGfxComposerMan.cs](Assets/PerfectWorld/Scripts/Vfx/A3DSkillGfxComposerMan.cs) - GFX composer implementation ## Future Improvements 1. **Add OBJECT_SKILL_ATTACK_RESULT Handler** - Once command ID is identified, add handler in `OnMsgPlayerCastSkill()` - Parse `cmd_object_skill_attack_result` structure - Call `PlayAttackEffect()` with skill ID and damage 2. **Improve Animation Handling** - Investigate why `PlaySkillCastAction` sometimes returns false - Add fallback animation handling 3. **Add Skill Level Detection** - If possible, get actual skill level from server - Use level for more accurate GFX scaling 4. **Optimize Skill Object Creation** - Consider pooling `CECSkill` objects instead of creating new ones - Reuse objects when same skill is cast multiple times ## Summary The implementation successfully handles the server-to-client flow for else player skill casting: ✅ **Message Routing**: Correctly routes `MSG_PM_CASTSKILL` to else players ✅ **Cast Animation**: Plays skill cast animations ✅ **State Management**: Tracks current skill and target ✅ **Attack Results**: Handles attack result messages (when received) ✅ **GFX Integration**: Triggers GFX system via `PlayAttackEffect()` ✅ **Logging**: Comprehensive logging for debugging ⚠️ **Known Issue**: Attack result messages may not be arriving for else players - needs investigation to identify correct message type/command ID.