Skip to content

Client can silently skip loading a connection scene when the server's scene handle collides with a client-side scene #1069

Closed

Description

@tindolt

Environment

  • FishNet 4.6.12 (Unity package, com.firstgeargames.fishnet)
  • Unity 6000.4.10f1, Windows, IL2CPP dedicated server
  • Multipass → WebTransport
  • Connection-scoped scene loads (LoadConnectionScenes), server stacks scenes with AllowStacking = true

Summary

A client can silently fail to load a connection scene when the server's scene handle happens to match the handle of an unrelated scene already loaded on the client. The scene is never loaded, no warning is raised, and the only downstream symptom is SceneId ... not found in SceneObjects for every networked scene object in it.

Because Unity scene handles are per-process instance IDs, whether this happens is luck of the draw on each server start. It appears and disappears on its own.

Mechanism

  1. The server builds a handle-based lookup to target a specific stacked instance:

    SceneLookupData lookupData = new SceneLookupData(instance.Handle);
    SceneLoadData sld = new SceneLoadData(lookupData) { ... };
    NetworkManager.SceneManager.LoadConnectionScenes(connection, sld);
  2. SceneManager broadcasts that lookup to the client with the server's handle intact. This looks intentional — the client echoes it back so the server can match the connection to the right instance:

    // Only the server needs to find scene handles to send to client. Client will send these back to the server.

    and on the client side:

    ClientScenesLoadedBroadcast msg = new()
    {
        SceneLookupDatas = sceneLoadData.SceneLookupDatas
    };
  3. But the client also resolves that handle locally. SceneLookupData.GetScene prefers Handle over Name:

    if (Handle != 0)
    {
        result = SceneManager.GetScene(Handle);
        if (!string.IsNullOrEmpty(result.name))
            foundByHandle = true;
    }
    if (!foundByHandle)
        result = SceneManager.GetScene(NameOnly, null, warnIfDuplicates);

    and SceneManager.GetScene(int) just scans the client's own loaded scenes:

    public static Scene GetScene(int sceneHandle)
    {
        int count = UnitySceneManager.sceneCount;
        for (int i = 0; i < count; i++)
        {
            Scene s = UnitySceneManager.GetSceneAt(i);
            if (s.handle == sceneHandle)
                return s;
        }
        return new();
    }
  4. If that scan happens to match some unrelated client scene, CanLoadScene treats the requested scene as already loaded and, for a client, refuses outright:

    bool alreadyLoaded = !string.IsNullOrEmpty(s.name);
    if (alreadyLoaded)
    {
        // Only servers can load the same scene multiple times for stacking.
        if (!qd.AsServer)
            return false;
        ...
    }

    The lookup never reaches loadableScenes, so the scene processor is never asked to load anything.

Evidence

Client log, subscribing to OnLoadStart / OnLoadEnd:

OnSceneStartLoad: scenes=[StartScene A|-322]
OnSceneEndLoad:   loaded=[] skipped=[ClientPreboot]
SceneId of 4480193996185786833 not found in SceneObjects   (x3)

The skipped list is the giveaway. It is built only from names of scenes GetScene() actually resolved:

List<string> skippedScenes = requestedLoadScenes.ToList();
for (int i = 0; i < loadedScenes.Count; i++)
    skippedScenes.Remove(loadedScenes[i].name);

The request was StartScene A. The resolved name was ClientPreboot — a completely unrelated bootstrap scene that happened to hold handle -322 in the client process.

Controlled confirmation

Restarting only the server, leaving the client process running and untouched, changed nothing but the handle the server sends:

Before After
Handle sent by server StartScene A|-322 StartScene A|-156
Result loaded=[] skipped=[ClientPreboot] loaded=[StartScene A] skipped=[]
SceneId not found errors 3 0

Same client, same build, same scene — only a server-side handle number differed.

Impact

The client ends up in a world with none of that scene's networked objects and no terrain. Nothing is logged on either side to say the load was skipped; the SceneId not found errors point at scene/observer configuration, which sends you looking in the wrong place.

Suggested fix

On a client — but not a host, where handles are genuinely shared — resolve incoming scene lookups by Name and ignore the handle, while leaving the handle untouched in the data echoed back to the server. That would cover the two places a received lookup is resolved: the requestedLoadSceneNames loop in LoadScenes_Internal, and CanLoadScene.

Clients do not stack scenes, so name matching should be sufficient for them, and the server still gets its own handle back for instance matching.

Failing that, even just a warning when a lookup resolves by handle to a scene whose name does not match SceneLookupData.Name would turn this from a silent failure into an obvious one.

Note

This project uses a custom SceneProcessorBase (Addressables-backed) rather than DefaultSceneProcessor. That should not be relevant here: CanLoadScene rejects the lookup before anything is added to loadableScenes, so the processor is never invoked. The only local modification to SceneManager.cs is in processor selection during initialization; the load and lookup paths quoted above are unmodified 4.6.12.

Activity

  1. tindolt commented on Aug 20, 2026

    @tindolt
    Author

    Verified still present on main (4.7.2), not just the 4.6.12 I reported from.

    I originally wrote this up against 4.6.12, so I went back and checked the current source. The handle type changed to ulong RawHandle and the lookup moved behind UnityCompatibility, but every link in the chain is unchanged:

    SceneLookupData.GetScene still prefers the handle:

    // Lookup by handle first.
    if (RawHandle != 0)
    {
        result = GetSceneByRawHandle(RawHandle);
        if (UnityCompatibility.HasValidSceneHandle(result))
            foundByHandle = true;
    }
    
    // If couldnt find handle try by string.
    if (!foundByHandle)
        result = SceneManager.GetScene(NameOnly, null, warnIfDuplicates);

    GetSceneByRawHandle still resolves it against the local process's scenes:

    private static Scene GetSceneByRawHandle(ulong rawHandle)
    {
        int count = UnityEngine.SceneManagement.SceneManager.sceneCount;
        for (int i = 0; i < count; i++)
        {
            Scene scene = UnityEngine.SceneManagement.SceneManager.GetSceneAt(i);
            if (scene.IsValid() && UnityCompatibility.GetSceneHandleRaw(scene) == rawHandle)
                return scene;
        }
        ...

    CanLoadScene still refuses outright for a client:

    if (alreadyLoaded)
    {
        // Only servers can load the same scene multiple times for stacking.
        if (!qd.AsServer)
            return false;

    And the server still sends its own handles, with the client echoing them back verbatim — which is why the handle can't simply be stripped on the wire:

    // Only the server needs to find scene handles to send to client. Client will send these back to the server.
    ClientScenesLoadedBroadcast msg = new()
    {
        SceneLookupDatas = sceneLoadData.SceneLookupDatas
    };

    So the collision window is the same on latest: a server-side handle that happens to match any scene loaded in the client process makes the client treat the requested scene as already loaded and skip it silently.

    Widening the handle to ulong does make an accidental collision less likely in principle, but GetSceneHandleRaw is still derived from Unity's per-process scene handles, so it does not remove the case — and it stays silent when it does happen.

  2. added
    Waiting For InformationNot enough information has been supplied to resolve this issue.
    Incomplete IssueRequired information is missing. See response for more information.
    on Aug 21, 2026
  3. FirstGearGames commented on Aug 21, 2026

    @FirstGearGames
    Owner

    This may be fixed via issue #1060

    There is no project attached to validate so in the mean time this will be put as Incomplete Issue / Waiting on information.

    Once 4.7.3 is out you can validate resolved and we'll close this out if so.

  4. FirstGearGames commented on Oct 4, 2026

    @FirstGearGames
    Owner

    Closed due to inactivity and no test package.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Incomplete IssueRequired information is missing. See response for more information.Waiting For InformationNot enough information has been supplied to resolve this issue.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions