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

531 lines
17 KiB
Markdown

# 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<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`
```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<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
```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<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`
```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<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
```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.