531 lines
17 KiB
Markdown
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.
|