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 playerEC_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
CECSkillobjects for tracking only - Uses
PlaySkillCastAction()from base classCECPlayer - 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
- Created: When
OBJECT_CAST_SKILLis received - Maintained: During casting and until attack result
- Cleared: When
SKILL_INTERRUPTEDis 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_SKILLinstead ofOBJECT_CAST_SKILL- No
SKILL_PERFORMmessage (skill executes immediately) - Attack result may arrive immediately after cast
Position-Based Skill Flow
Similar to normal flow but:
OBJECT_CAST_POS_SKILLinstead ofOBJECT_CAST_SKILL- Target is a position (Vector3), not an object ID
m_idCurSkillTargetmay be 0
Debugging and Logging
Log Prefix
All logs use the prefix [ELSEPLAYER_SKILL_FLOW] for easy filtering.
Key Log Points
-
ProcessMessage Entry
[ELSEPLAYER_SKILL_FLOW] ProcessMessage: Received message, playerID=X, msgType=Y, param2=Z -
OnMsgPlayerCastSkill Entry
[ELSEPLAYER_SKILL_FLOW] OnMsgPlayerCastSkill: Entry, playerID=X, commandID=Y -
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 -
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 -
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:
- Add handler for
OBJECT_SKILL_ATTACK_RESULTcommand ID inOnMsgPlayerCastSkill() - Check if attack results come through a different message type
- Implement fallback to trigger GFX from
SKILL_PERFORMif 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_SKILLmessage is received and processed - Verify cast animation plays (
PlaySkillCastActionreturns true) - Verify
m_pCurSkillis set correctly - Verify
SKILL_PERFORMis received (if applicable) - Verify attack result message is received (
MSG_PM_PLAYERATKRESULTorOBJECT_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
- Assets/PerfectWorld/Scripts/Players/EC_ElsePlayer.cs
- Added
MSG_PM_CASTSKILLcase inProcessMessage() - Implemented
OnMsgPlayerCastSkill()method (~150 lines) - Enhanced
OnMsgPlayerAtkResult()to handle skill attacks - Added comprehensive logging throughout
- Added
Related Documentation
- Skill Flow Documentation.md - Complete C++ to C# conversion reference
- CECHostPlayer.Skill.cs - Host player skill implementation reference
- A3DSkillGfxComposerMan.cs - GFX composer implementation
Future Improvements
-
Add OBJECT_SKILL_ATTACK_RESULT Handler
- Once command ID is identified, add handler in
OnMsgPlayerCastSkill() - Parse
cmd_object_skill_attack_resultstructure - Call
PlayAttackEffect()with skill ID and damage
- Once command ID is identified, add handler in
-
Improve Animation Handling
- Investigate why
PlaySkillCastActionsometimes returns false - Add fallback animation handling
- Investigate why
-
Add Skill Level Detection
- If possible, get actual skill level from server
- Use level for more accurate GFX scaling
-
Optimize Skill Object Creation
- Consider pooling
CECSkillobjects instead of creating new ones - Reuse objects when same skill is cast multiple times
- Consider pooling
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.