Files
test/Documentation/Server-to-Client_ElsePlayer_Skill_Cast_Flow.md
2026-03-13 16:03:47 +07:00

17 KiB

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

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:

case EC_MsgDef.MSG_PM_CASTSKILL:
    switch (Convert.ToInt32(Msg.dwParam2))
    {
        case CommandID.OBJECT_CAST_SKILL: 
            cid = (GPDataTypeHelper.FromBytes<cmd_object_cast_skill>((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

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

case CommandID.OBJECT_CAST_SKILL:
{
    cmd_object_cast_skill pCmd = GPDataTypeHelper.FromBytes<cmd_object_cast_skill>((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

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

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)

void OnMsgPlayerAtkResult(ECMSG Msg)
{
    cmd_object_atk_result pCmd = GPDataTypeHelper.FromBytes<cmd_object_atk_result>((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

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)

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

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

public void Play(int nSkillID, int nHostID, int nCastTargetID, 
                 List<TARGET_DATA> 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

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

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

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.