{"record":{"id":"0b7ad3ea0491697d","repo":"stride3d/stride","slug":"all-nameof-contactchangedchannels-should-have-hashsets","errorCode":null,"errorMessage":"All {nameof(contactChangedChannels)} should have hashsets associated to them","messagePattern":"All (.+?) should have hashsets associated to them","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"sources/engine/Stride.Physics/Simulation.cs","lineNumber":354,"sourceCode":"            foreach (var (coll, hashset) in contactsUpToDate)\n            {\n                if (contactChangedChannels.TryGetValue(coll, out var tuple))\n                {\n                    contactChangedChannels[coll] = (tuple.Channel, hashset);\n                    previousSets++;\n                }\n                else\n                {\n                    hashset.Clear();\n                    contactsPool.Push(hashset);\n                }\n            }\n\n            contactsUpToDate.Clear();\n\n            if (previousSets != contactChangedChannels.Count)\n            {\n                throw new InvalidOperationException($\"All {nameof(contactChangedChannels)} should have hashsets associated to them\");\n            }\n\n            foreach (var collision in markedAsNewColl)\n            {\n                if (IncludeStaticAgainstStaticCollisions == false\n                    && collision.ColliderA is StaticColliderComponent\n                    && collision.ColliderB is StaticColliderComponent)\n                {\n                    continue;\n                }\n\n                collision.ColliderA.Collisions.Add( collision );\n                collision.ColliderB.Collisions.Add( collision );\n                \n                while (collision.ColliderA.NewPairChannel.Balance < 0)\n                {\n                    collision.ColliderA.NewPairChannel.Send(collision);\n                }","sourceCodeStart":336,"sourceCodeEnd":372,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/engine/Stride.Physics/Simulation.cs#L336-L372","documentation":"During SendEvents, the Simulation compares the number of previous contact sets against contactChangedChannels.Count and requires a one-to-one mapping between each contact-changed channel and an associated HashSet of contacts. If the counts differ, the internal bookkeeping invariant is broken and InvalidOperationException is thrown.","triggerScenarios":"Calling/triggering Simulation.SendEvents when a contact-changed channel was registered in contactChangedChannels without a corresponding HashSet entry (or vice versa), e.g. by adding a channel key without its set or removing a set while keeping the channel.","commonSituations":"Custom engine modifications or extensions that register contact channels incorrectly; race conditions or partial initialization where channel registration and set creation steps were separated.","solutions":["Ensure every channel added to contactChangedChannels gets its HashSet populated at registration time (atomic add: dictionary[key] = new HashSet<...>())","Audit code that removes or clears contact sets so the channel entry is removed together with its set","If this arises in stock Stride code without modifications, report it as a bug — the invariant should hold in unmodified engine code"],"exampleFix":"// before\ncontactChangedChannels.Add(channel); // channel without hashset\n// after\ncontactChangedChannels[channel] = new HashSet<Contact>();","handlingStrategy":"try-catch","validationCode":"// ensure every registered channel has an associated hashset before running physics ticks\nforeach (var key in contactChangedChannels.Keys)\n    Debug.Assert(GetHashSetFor(key) != null);","typeGuard":null,"tryCatchPattern":"try\n{\n    simulation.SendEvents();\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"contactChangedChannels\"))\n{\n    // rebuild channel -> hashset mapping and re-register contacts\n    logger.Error(ex, \"Contact channel bookkeeping corrupted\");\n}","preventionTips":["Always add channels and their hashsets atomically as a single dictionary entry","Never remove a hashset while keeping its channel key registered","In stock Stride code, treat this as an engine bug and update the Stride package if hit"],"tags":["physics","invariant","internal-state","events"],"backgroundTag":"internal-invariant-violation","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}