Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Assets/PurrDiction/Runtime/Core/PredictedIdentity.Module.cs
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,12 @@ private void TearDownAllModules()
_moduleHistory?.Clear();
}

private void TriggerModuleDestroyedEvents()
{
for (int i = 0; i < _modules.Count; i++)
_modules[i].TriggerDestroyedEvent();
}
Comment on lines +319 to +323

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid iterating the live _modules list during destroyed callbacks.

If a module disposes itself inside Destroyed(), _modules is mutated and later modules can be skipped. Dispatch from a snapshot (or iterate safely against mutation).

Suggested fix
 private void TriggerModuleDestroyedEvents()
 {
-    for (int i = 0; i < _modules.Count; i++)
-        _modules[i].TriggerDestroyedEvent();
+    var modulesSnapshot = new List<PredictedModule>(_modules);
+    for (int i = 0; i < modulesSnapshot.Count; i++)
+        modulesSnapshot[i].TriggerDestroyedEvent();
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void TriggerModuleDestroyedEvents()
{
for (int i = 0; i < _modules.Count; i++)
_modules[i].TriggerDestroyedEvent();
}
private void TriggerModuleDestroyedEvents()
{
var modulesSnapshot = new List<PredictedModule>(_modules);
for (int i = 0; i < modulesSnapshot.Count; i++)
modulesSnapshot[i].TriggerDestroyedEvent();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Assets/PurrDiction/Runtime/Core/PredictedIdentity.Module.cs` around lines 319
- 323, TriggerModuleDestroyedEvents currently iterates the live _modules list
and calls each module's TriggerDestroyedEvent, which can mutate _modules
(modules disposing themselves) and cause later modules to be skipped; fix by
iterating a stable snapshot of the collection (e.g., copy _modules to an array
or List first) and invoke TriggerDestroyedEvent on the snapshot so mutations
during callbacks do not affect which modules get destroyed. Ensure you reference
TriggerModuleDestroyedEvents, _modules, and TriggerDestroyedEvent when applying
the change.


private void TearDownAllDynamic()
{
if (_staticModuleCount < 0) return;
Expand Down
2 changes: 2 additions & 0 deletions Assets/PurrDiction/Runtime/Core/PredictedIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ protected virtual void Destroyed() {}
internal void TriggerDestroyedEvent()
{
Destroyed();
TriggerModuleDestroyedEvents();
}

public bool isServer { get; private set; }
Expand Down Expand Up @@ -126,6 +127,7 @@ internal virtual void Setup(NetworkManager manager, PredictionManager world, Pre
protected virtual void OnDestroy()
{
Destroyed();
TriggerModuleDestroyedEvents();
TearDownAllModules();

if (predictionManager)
Expand Down
11 changes: 11 additions & 0 deletions Assets/PurrDiction/Runtime/Core/PredictedModule.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ internal void OnRemovedInternal()
onDisposed?.Invoke();
}

internal void TriggerDestroyedEvent()
{
Destroyed();
}

/// <summary>
/// Invoked when the module is removed from its identity.
/// Use this to drop any external references to the module.
Expand All @@ -191,5 +196,11 @@ internal void OnRemovedInternal()
/// Override to release any owned resources.
/// </summary>
protected virtual void OnDisposed() { }

/// <summary>
/// Invoked when the owning identity is being despawned and cleaned up.
/// Allows for any necessary teardown or resource release to be handled.
/// </summary>
protected virtual void Destroyed() { }
}
}
23 changes: 23 additions & 0 deletions Assets/PurrDiction/Runtime/Core/PredictedModuleState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ public override string ToString()
return $"State:\n{fullPredictedState.state}";
}

private void ResetStateToInitialState()
{
fullPredictedState.prediction.wasOnSimulationStartCalled = false;
fullPredictedState.state = GetInitialState();
}
Comment on lines +40 to +44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Dispose the previous full state before resetting to a new initial state.

ResetStateToInitialState() overwrites fullPredictedState.state during setup without disposing the prior value, which can leak resources across respawns.

Suggested fix
 private void ResetStateToInitialState()
 {
+    fullPredictedState.Dispose();
+    fullPredictedState = default;
     fullPredictedState.prediction.wasOnSimulationStartCalled = false;
     fullPredictedState.state = GetInitialState();
 }

As per coding guidelines, "State types implementing IPredictedData must also implement IDisposable and IPackedAuto for auto-serialization codegen."

Also applies to: 61-69

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Assets/PurrDiction/Runtime/Core/PredictedModuleState.cs` around lines 40 -
44, ResetStateToInitialState currently overwrites fullPredictedState.state
without disposing the existing state, risking resource leaks; before assigning
fullPredictedState.state = GetInitialState() (and similarly in the other reset
method around the same area), call Dispose() on the existing
fullPredictedState.state if it implements IDisposable (or cast to IDisposable)
to release resources, then assign the new initial state; ensure state types
implementing IPredictedData also implement IDisposable and IPackedAuto as
required by codegen.

Source: Coding guidelines


internal override void OnCoreInitialize()
{
var tickRate = predictionManager.tickRate;
Expand All @@ -52,6 +58,16 @@ internal override void OnCoreInitialize()
);
}

protected override void Setup(PredictedIdentity parent, PredictionManager world)
{
base.Setup(parent, world);
ResetStateToInitialState();
_history?.Clear();
_viewState?.Dispose();
_viewState = null;
_interpolatedState?.Teleport(fullPredictedState.DeepCopy());
}

protected sealed override void UpdateView(float delta)
{
if (_interpolatedState == null) return;
Expand Down Expand Up @@ -135,6 +151,13 @@ protected override void Simulate(ulong tick, float delta)
/// </summary>
protected virtual void SimulationStart() { }

/// <summary>
/// Called when the module is first created.
/// Future updates will come only through Simulate.
/// </summary>
/// <returns>The initial state of the module.</returns>
protected virtual TState GetInitialState() => default;

/// <summary>
/// Executes the simulation logic for this module.
/// Modify the <paramref name="state"/> directly to advance the simulation. Or use the `currentState`
Expand Down
Loading