Skip to content

fix: Unify SyncVar and SyncCollection replay across host visibility, scene re-observation, and pooled respawn - #4118

Open
MrGadget1024 with Copilot wants to merge 52 commits into
masterfrom
copilot/fix-host-client-only-bugs
Open

MrGadget1024 with Copilot wants to merge 52 commits into
masterfrom
copilot/fix-host-client-only-bugs

Conversation

Copilot AI commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

SyncVar hooks and SyncCollection callbacks could diverge between host and remote clients when visibility, scene-object reuse, or pooled respawn got involved. This PR brings those paths back into alignment so observation-driven replay behaves consistently across host mode, remote clients, AOI hide/show, and runtime UnSpawn/respawn reuse.

  • Preserve correct SyncVar baselines

    • Weaver-generated hooked SyncVars now capture the real pre-visibility field value, including declaration initializers and non-primitive types.
    • Host replay uses that captured baseline so the first visible hook sees the same oldValue a fresh observer would.
  • Defer host callbacks until actual observation

    • Host-mode SyncVar hooks and SyncCollection actions now stay deferred until the host client has a local player and the object is actually observed.
    • Re-entering AOI or observing after a delayed player spawn replays state once, instead of firing early or double-firing during host spawn transitions.
  • Replay re-observation as a fresh observation

    • Host and remote clients now treat scene-object hide/show as a new observation for hooked SyncVars.
    • SyncCollections replay the current visible state as Add actions again on re-observation, instead of remembering prior observation state.
  • Handle pooled runtime-object reuse in host mode

    • Host-mode NetworkServer.UnSpawn/respawn of pooled runtime objects now marks SyncVar hooks and SyncCollection replays pending before visibility is lost.
    • Respawning the same object replays one-time-assigned SyncVars (such as RandomColor) and current SyncCollection contents again, even without AOI.
  • Preserve server delta queues and avoid duplicate callbacks

    • Host visibility replay no longer deserializes the shared server/host SyncObject state or consumes pending remote deltas.
    • Replay-pending tracking is now scoped per hooked SyncVar / SyncObject, preventing unrelated members from replaying twice.
    • SyncObject callback clearing is limited to reused client scene-object paths so client re-observation does not accumulate handlers and server pooling keeps its subscriptions.
  • Regression coverage

    • Added and corrected focused editor tests for host visibility deferral, AOI leave/re-enter, remote scene-object re-observation, ready-without-player flows, AddPlayer/spawn boundary cases, pooled runtime respawn, and double-fire prevention for both SyncVars and SyncCollections.

Example behavior after this change:

[SyncVar(hook = nameof(OnValueChanged))]
public int value = 42;

// Mutated before the host can observe it
value = 100;
value = 200;

// When the host first observes or re-observes the object:
// OnValueChanged(42, 200)

Copilot AI changed the title [WIP] Fix host-client-only bugs in Mirror sync state delivery Fix host-client deferred SyncVar and SyncCollection delivery semantics Jul 2, 2026
Copilot AI requested a review from MrGadget1024 July 2, 2026 12:42
@MrGadget1024

Copy link
Copy Markdown
Collaborator

@copilot Error: Assets/Mirror/Core/SyncDictionary.cs(371,42): error CS0103: The name 'newItem' does not exist in the current context
Should be TValue capturedNew = item;

Copilot AI commented Jul 5, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Error: Assets/Mirror/Core/SyncDictionary.cs(371,42): error CS0103: The name 'newItem' does not exist in the current context
Should be TValue capturedNew = item;

Addressed in 485ce90.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

@copilot Fix errors related to failed tests

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Fix errors related to failed tests

Addressed in 7ee058b.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

Copilot AI commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.94357% with 111 lines in your changes missing coverage. Please review.
✅ Project coverage is 42.84%. Comparing base (31163ab) to head (2ce684a).
⚠️ Report is 9 commits behind head on master.

Files with missing lines Patch % Lines
Assets/Mirror/Core/NetworkBehaviour.cs 65.33% 104 Missing ⚠️
Assets/Mirror/Core/SyncObject.cs 75.00% 3 Missing ⚠️
Assets/Mirror/Core/NetworkClient.cs 95.00% 2 Missing ⚠️
Assets/Mirror/Core/SyncDictionary.cs 96.42% 1 Missing ⚠️
Assets/Mirror/Core/SyncSet.cs 96.15% 1 Missing ⚠️

❌ Your patch check has failed because the patch coverage (74.94%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #4118      +/-   ##
==========================================
+ Coverage   42.22%   42.84%   +0.61%     
==========================================
  Files         156      156              
  Lines       15100    15496     +396     
==========================================
+ Hits         6376     6639     +263     
- Misses       8724     8857     +133     
Flag Coverage Δ
unittests 42.84% <74.94%> (+0.61%) ⬆️
unity-6000.5.1f1 42.84% <74.94%> (+0.61%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
Assets/Mirror/Core/NetworkIdentity.cs 91.07% <100.00%> (+0.05%) ⬆️
Assets/Mirror/Core/NetworkServer.cs 69.00% <100.00%> (+0.53%) ⬆️
Assets/Mirror/Core/SyncList.cs 91.91% <100.00%> (+0.77%) ⬆️
Assets/Mirror/Core/SyncDictionary.cs 89.83% <96.42%> (+2.09%) ⬆️
Assets/Mirror/Core/SyncSet.cs 92.37% <96.15%> (+1.82%) ⬆️
Assets/Mirror/Core/NetworkClient.cs 94.14% <95.00%> (+0.34%) ⬆️
Assets/Mirror/Core/SyncObject.cs 78.57% <75.00%> (-21.43%) ⬇️
Assets/Mirror/Core/NetworkBehaviour.cs 74.37% <65.33%> (-11.35%) ⬇️

... and 7 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

@copilot Extend the new tests to move host client player out of AOI range, Assert that the test object is no longer observed, change the SyncVar value or make a change to the Sync Collection, Assert no hooks / actions, move host client player into range again, Assert hooks / actions invoked again the same as previously, e.g. same original oldValue as if the host client is seeing it for the first time again.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

@Lymdun We good now?

@MrGadget1024

Copy link
Copy Markdown
Collaborator

@copilot In the Room example, there is a Spawner static class Assets/Mirror/Examples/Room/Scripts/Spawner.cs that uses a Pool Assets/Mirror/Core/Tools/Pool.cs, and a Reward script Assets/Mirror/Examples/Room/Scripts/Reward.cs. The Reward prefab has RandomColor, and the color is set once when the Reward is initially created by the Spawner and never changes. OnTriggerEnter in Reward script recycles the reward item by Unspawning it into the pool to be respawned after a delay at a new position. The host client should fire the SyncVar hook of RandomColor when it's respawned, but it's not firing. There is no AOI in this example. If the reward had a SyncCollection, the Add actions should be invoked again too.

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot In the Room example, there is a Spawner static class Assets/Mirror/Examples/Room/Scripts/Spawner.cs that uses a Pool Assets/Mirror/Core/Tools/Pool.cs, and a Reward script `Assets/Mirror/E...

Addressed in 1fab2fc with the test follow-up in 310425d. Host-mode UnSpawn now marks SyncVar and SyncCollection replay pending before the shared object leaves observation, so respawning the same runtime object replays the RandomColor hook again, and SyncCollection Add actions replay again too. I also added focused host/no-AOI regression tests for both runtime-object SyncVar hook replay and SyncCollection replay after unspawn/respawn.

Copilot AI changed the title Fix SyncVar and SyncCollection visibility replay semantics Fix SyncVar and SyncCollection replay semantics across visibility and respawn Jul 15, 2026
@Lymdun

Lymdun commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

@Lymdun We good now?

@MrGadget1024 re-ran all evals, all good now ✅

@MrGadget1024 MrGadget1024 added Awaiting Review and removed work in progress Need more time to decide. Nothing to do here for now. labels Jul 15, 2026
@MrGadget1024

Copy link
Copy Markdown
Collaborator

@copilot Update the Title and Description of this PR reflecting the goals we achieved and describing the work completed.

Copilot AI commented Jul 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Update the Title and Description of this PR reflecting the goals we achieved and describing the work completed.

Use this updated PR metadata:

Title
Unify SyncVar and SyncCollection replay across host visibility, scene re-observation, and pooled respawn

Description
SyncVar hooks and SyncCollection callbacks could diverge between host and remote clients when visibility, scene-object reuse, or pooled respawn got involved. This PR brings those paths back into alignment so observation-driven replay behaves consistently across host mode, remote clients, AOI hide/show, and runtime UnSpawn/respawn reuse.

  • Preserve correct SyncVar baselines

    • Weaver-generated hooked SyncVars now capture the real pre-visibility field value, including declaration initializers and non-primitive types.
    • Host replay uses that captured baseline so the first visible hook sees the same oldValue a fresh observer would.
  • Defer host callbacks until actual observation

    • Host-mode SyncVar hooks and SyncCollection actions now stay deferred until the host client has a local player and the object is actually observed.
    • Re-entering AOI or observing after a delayed player spawn replays state once, instead of firing early or double-firing during host spawn transitions.
  • Replay re-observation as a fresh observation

    • Host and remote clients now treat scene-object hide/show as a new observation for hooked SyncVars.
    • SyncCollections replay the current visible state as Add actions again on re-observation, instead of remembering prior observation state.
  • Handle pooled runtime-object reuse in host mode

    • Host-mode NetworkServer.UnSpawn/respawn of pooled runtime objects now marks SyncVar hooks and SyncCollection replays pending before visibility is lost.
    • Respawning the same object replays one-time-assigned SyncVars (such as RandomColor) and current SyncCollection contents again, even without AOI.
  • Preserve server delta queues and avoid duplicate callbacks

    • Host visibility replay no longer deserializes the shared server/host SyncObject state or consumes pending remote deltas.
    • Replay-pending tracking is now scoped per hooked SyncVar / SyncObject, preventing unrelated members from replaying twice.
    • SyncObject callback clearing is limited to reused client scene-object paths so client re-observation does not accumulate handlers and server pooling keeps its subscriptions.
  • Regression coverage

    • Added and corrected focused editor tests for host visibility deferral, AOI leave/re-enter, remote scene-object re-observation, ready-without-player flows, AddPlayer/spawn boundary cases, pooled runtime respawn, and double-fire prevention for both SyncVars and SyncCollections.

Example behavior after this change:

[SyncVar(hook = nameof(OnValueChanged))]
public int value = 42;

// Mutated before the host can observe it
value = 100;
value = 200;

// When the host first observes or re-observes the object:
// OnValueChanged(42, 200)

That wording matches the behavior covered by the final test set and the host/remote example scenarios fixed in this PR.

@MrGadget1024 MrGadget1024 changed the title Fix SyncVar and SyncCollection replay semantics across visibility and respawn fix: Unify SyncVar and SyncCollection replay across host visibility, scene re-observation, and pooled respawn Jul 15, 2026
@GoToNightmare

GoToNightmare commented Sep 14, 2026

Copy link
Copy Markdown

Suggestion to add FieldAttributes.NotSerialized into SyncVarAttributeProcessor bcs newly generated flag fields would trigger unity error message with unsupported enum type.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

Suggestion to add FieldAttributes.NotSerialized into SyncVarAttributeProcessor bcs newly generated flag fields would trigger unity error message with unsupported enum type.

Please provide a more concrete example?

@GoToNightmare

GoToNightmare commented Sep 15, 2026

Copy link
Copy Markdown

@MrGadget1024 Sorry wasnt able to replicate it, but error message was
Unsupported enum type 'GameData.GameEnums.ModifierStatusEffects' used for field '_Mirror_SyncVarHookOriginal_thisEntityStatusEffects' in class 'CombatEntity'

where CombatEntity is networkbeh and original field is

	[SyncVar(hook = nameof(StatusEffectChangedOnClient))]
	private GameEnums.ModifierStatusEffects thisEntityStatusEffects;
		[Flags]
		public enum ModifierStatusEffects : ulong

Edit: Nvm, might be bad suggestion.

@MrGadget1024

Copy link
Copy Markdown
Collaborator

@GoToNightmare Unity (and Mirror) serialize enums perfectly, even those with non-int bases. We have our own examples that use byte.

@GoToNightmare

Copy link
Copy Markdown

@MrGadget1024 I mean unity out of the box doesnt support serialization for enum flags with undeling type long/ulong, and that error I got is exactly that. Mirror generated field Unsupported enum type 'GameData.GameEnums.ModifierStatusEffects' used for field '_Mirror_SyncVarHookOriginal_thisEntityStatusEffects' in class 'CombatEntity' and unity message it as error.

But im not sure if u should care about it on mirrors side.

@MrGadget1024

MrGadget1024 commented Sep 16, 2026

Copy link
Copy Markdown
Collaborator

@GoToNightmare I can reproduce it with the PlayerTest example. That's a Unity bug and should be reported as such. Annoying, but not a Mirror issue. I notice the inspector still works fine, and the flags work fine, it's erroneous error spam.

EDIT: Apparently Unity isn't going to fix this...long standing issue that I hadn't run into before.
https://docs.unity3d.com/6000.6/Documentation/Manual/script-serialization-analyzer.html#uac1011
Still...not our pasture, not our BS.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants