Skip to content

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

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.

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