# C# vs C++ Comparison: Why Monster Destroy Error Occurs in C# but Not C++ ## Key Differences ### 1. **Memory Management Model** #### C++ (Raw Pointers) ```cpp // C++: EC_ManNPC.cpp line 774-787 void CECNPCMan::ReleaseNPC(CECNPC* pNPC) { if (pNPC) { pNPC->Release(); delete pNPC; // ← Object immediately destroyed, pointer becomes invalid pNPC = NULL; } } ``` **Behavior:** - `delete pNPC` immediately destroys the object - Pointer becomes **invalid** (dangling pointer) - Accessing deleted object = **immediate crash** (predictable) - Null check (`if (pNPC)`) is **sufficient** to prevent crashes #### C# Unity (Managed References) ```csharp // C#: CECNPCMan.cs line 214-226 void ReleaseNPC(CECNPC pNPC) { if (pNPC) { pNPC.Release(); pNPC.DestroySelf(); // ← Calls Destroy(gameObject) } } // CECNPC.cs line 578-581 public void DestroySelf() { Destroy(gameObject); // ← Unity marks for destruction, but reference persists } ``` **Behavior:** - `Destroy(gameObject)` **marks** GameObject for destruction at end of frame - C# reference (`pNPC`) **still exists** and is NOT set to null - Unity's `==` operator is **overloaded** - `pNPC == null` returns `true` for destroyed objects - But **direct property access** (`pNPC.transform`, `pNPC.GetPosVector3()`) **still throws error** - Null check (`if (pNPC != null)`) is **NOT sufficient** - need GameObject check too ### 2. **GetNPCFromAll Implementation** #### C++ Version ```cpp // C++: EC_ManNPC.cpp line 908-923 CECNPC* CECNPCMan::GetNPCFromAll(int nid) { CECNPC* pNPC = GetNPC(nid); if (pNPC) return pNPC; // Search from disappear array - NPCs still exist here! for (int i=0; i < m_aDisappearNPCs.GetSize(); i++) { CECNPC* pNPC = m_aDisappearNPCs[i]; if (pNPC->GetNPCID() == nid) return pNPC; // ← Returns NPC even if removed from main table } return NULL; } ``` **Key Points:** - Searches **disappear array** (`m_aDisappearNPCs`) after main table - NPCs in disappear array are **still valid** (not deleted yet) - Provides a **grace period** for GFX events to complete - Returns `NULL` only when NPC is truly gone #### C# Version ```csharp // C#: CECNPCMan.cs line 541-564 public CECNPC GetNPCFromAll(int nid) { CECNPC pNPC = GetNPC(nid); if (pNPC != null) return pNPC; // Search from disappear array - COMMENTED OUT! /*for (int i = 0; i < m_aDisappearNPCs.GetSize(); i++) { CECNPC* pNPC = m_aDisappearNPCs[i]; if (pNPC->GetNPCID() == nid) return pNPC; }*/ return null; // ← Returns null immediately if not in main table } ``` **Key Points:** - **Disappear array search is COMMENTED OUT** (lines 555-561) - Returns `null` immediately if NPC not in main table - **No grace period** - NPC removed from table = immediately unavailable - This is a **porting issue** - the safety mechanism was removed ### 3. **get_pos_by_id Implementation** #### C++ Version ```cpp // C++: EC_ManSkillGfx.cpp line 89-122 inline bool _get_pos_by_id(..., int nID, A3DVECTOR3& vPos, ...) { if (ISNPCID(nID)) { CECNPC* pNPC = pNPCMan->GetNPCFromAll(nID); if (pNPC){ // ← Simple null check is sufficient // Access pNPC methods directly vPos = pNPC->GetPos(); // ← Safe: if pNPC is null, we wouldn't be here return true; } } return false; } ``` **Why it works:** - `GetNPCFromAll()` may return NPC from disappear array (still valid) - If `pNPC` is null, we return false - **no access attempted** - If `pNPC` is non-null, it's **guaranteed valid** (C++ pointer semantics) - No need to check if object is "destroyed" - either it exists or pointer is null #### C# Version ```csharp // C#: CECSkillGfxMan.cs line 545-589 private static bool get_pos_by_id(..., int nID, out Vector3 vPos, ...) { if (GPDataTypeHelper.ISNPCID(nID)) { CECNPC pNPC = pNPCMan?.GetNPCFromAll(nID); if (pNPC != null) // ← This check passes even for destroyed GameObjects! { vPos = pNPC.GetPosVector3(); // ← ERROR: GameObject destroyed! // OR vPos = pNPC.transform.position; // ← ERROR: GameObject destroyed! } } return false; } ``` **Why it fails:** - `GetNPCFromAll()` returns `null` immediately (disappear array not checked) - But even if it returned a reference, Unity's destroyed objects are "fake null" - `pNPC != null` returns `true` even for destroyed GameObjects - Need **additional check**: `pNPC.gameObject != null` to detect destroyed objects ### 4. **Object Lifecycle Differences** #### C++ Lifecycle ``` 1. NPC created → Pointer stored in m_NPCTab 2. NPC dies → Moved to m_aDisappearNPCs (still valid) 3. Disappear timer expires → delete pNPC (immediate destruction) 4. Pointer becomes invalid → NULL check prevents access ``` **Timeline:** - NPC removed from main table → **Still accessible via disappear array** - NPC deleted → **Pointer immediately invalid** (predictable crash if accessed) #### C# Unity Lifecycle ``` 1. NPC created → GameObject + Component stored in m_NPCTab 2. NPC dies → Removed from m_NPCTab immediately 3. Destroy(gameObject) called → Marked for destruction 4. End of frame → GameObject destroyed, but C# reference persists 5. Reference is "fake null" → == null works, but property access throws ``` **Timeline:** - NPC removed from main table → **Immediately unavailable** (disappear array not checked) - GameObject destroyed → **Reference persists but is "fake null"** - Property access → **Throws "object has been destroyed" error** ### 5. **Null Check Behavior** #### C++ Null Check ```cpp CECNPC* pNPC = GetNPCFromAll(nid); if (pNPC) // ← True only if pointer is valid { vPos = pNPC->GetPos(); // ← Safe: pointer guaranteed valid } ``` **Semantics:** - `if (pNPC)` checks if pointer is **not NULL** - If true, pointer is **guaranteed valid** (C++ doesn't have "fake null") - Access is **safe** #### C# Unity Null Check ```csharp CECNPC pNPC = GetNPCFromAll(nid); if (pNPC != null) // ← True even for destroyed GameObjects! { vPos = pNPC.GetPosVector3(); // ← ERROR: GameObject may be destroyed } ``` **Semantics:** - `if (pNPC != null)` uses Unity's **overloaded == operator** - Returns `true` even for **destroyed GameObjects** (Unity's "fake null") - Need **additional check**: `pNPC.gameObject != null` ### 6. **Why C++ Doesn't Have This Problem** 1. **Disappear Array Safety Net** - C++ searches `m_aDisappearNPCs` which provides grace period - NPCs remain accessible even after removal from main table - GFX events can complete before NPC is actually deleted 2. **Immediate Destruction** - When `delete` is called, object is **immediately destroyed** - Pointer becomes invalid, null check prevents access - No "fake null" state - either valid or null 3. **Predictable Behavior** - Accessing deleted object = immediate crash (predictable) - Null check is sufficient protection - No need for GameObject existence checks ### 7. **Why C# Has This Problem** 1. **Disappear Array Not Used** - C# version has disappear array search **commented out** - NPCs become unavailable immediately when removed from main table - No grace period for GFX events 2. **Delayed Destruction** - `Destroy(gameObject)` marks for destruction at **end of frame** - C# reference persists in "fake null" state - Property access throws error even though `== null` returns true 3. **Unity's "Fake Null"** - Unity overloads `==` operator to handle destroyed objects - But direct property access doesn't use the overloaded operator - Need explicit GameObject check: `gameObject != null` ## Summary: Root Causes | Aspect | C++ | C# Unity | Impact | |--------|-----|----------|--------| | **Disappear Array** | ✅ Searched | ❌ Commented out | C# has no grace period | | **Null Check** | ✅ Sufficient | ❌ Not sufficient | C# needs GameObject check | | **Destruction** | Immediate (`delete`) | Delayed (`Destroy`) | C# has "fake null" state | | **Pointer/Reference** | Raw pointer (invalid when deleted) | Managed reference (persists when destroyed) | C# reference can point to destroyed object | ## The Fix To match C++ behavior, C# needs: 1. **Re-enable disappear array search** in `GetNPCFromAll()` 2. **Add GameObject checks** in `get_pos_by_id()`: `pNPC != null && pNPC.gameObject != null` 3. **Early termination** of GFX events when target is destroyed 4. **Target validation** before creating GFX events These changes will restore the safety mechanisms that existed in C++ but were lost during porting.