Repository navigation
Description
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 withAllowStacking = 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
-
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);
-
SceneManagerbroadcasts 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 };
-
But the client also resolves that handle locally.
SceneLookupData.GetSceneprefersHandleoverName: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(); }
-
If that scan happens to match some unrelated client scene,
CanLoadScenetreats 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
tindolt commentedon Aug 20, 2026
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 RawHandleand the lookup moved behindUnityCompatibility, but every link in the chain is unchanged:SceneLookupData.GetScenestill prefers the handle:GetSceneByRawHandlestill resolves it against the local process's scenes:CanLoadScenestill refuses outright for a client: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.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
ulongdoes make an accidental collision less likely in principle, butGetSceneHandleRawis still derived from Unity's per-process scene handles, so it does not remove the case — and it stays silent when it does happen.FirstGearGames commentedon Aug 21, 2026
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.
FirstGearGames commentedon Oct 4, 2026
Closed due to inactivity and no test package.