{"record":{"id":"0918219871c022c3","repo":"ppy/osu","slug":"cannot-toggle-spectate-when-in-localuser-state","errorCode":null,"errorMessage":"Cannot toggle spectate when in {localUser.State}","messagePattern":"Cannot toggle spectate when in (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"warning","filePath":"osu.Game/Online/Multiplayer/MultiplayerClient.cs","lineNumber":474,"sourceCode":"        {\r\n            var localUser = LocalUser;\r\n\r\n            if (localUser == null)\r\n                return;\r\n\r\n            switch (localUser.State)\r\n            {\r\n                case MultiplayerUserState.Idle:\r\n                case MultiplayerUserState.Ready:\r\n                    await ChangeState(MultiplayerUserState.Spectating).ConfigureAwait(false);\r\n                    return;\r\n\r\n                case MultiplayerUserState.Spectating:\r\n                    await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false);\r\n                    return;\r\n\r\n                default:\r\n                    throw new InvalidOperationException($\"Cannot toggle spectate when in {localUser.State}\");\r\n            }\r\n        }\r\n\r\n        public abstract Task TransferHost(int userId);\r\n\r\n        public abstract Task KickUser(int userId);\r\n\r\n        public abstract Task ChangeSettings(MultiplayerRoomSettings settings);\r\n\r\n        public abstract Task ChangeState(MultiplayerUserState newState);\r\n\r\n        public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability);\r\n\r\n        public abstract Task ChangeUserStyle(int? beatmapId, int? rulesetId);\r\n\r\n        /// <summary>\r\n        /// Change the local user's mods in the currently joined room.\r\n        /// </summary>\r","sourceCodeStart":456,"sourceCodeEnd":492,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Online/Multiplayer/MultiplayerClient.cs#L456-L492","documentation":"Thrown by MultiplayerClient.ToggleSpectate when the local user's state is not Idle, Ready, or Spectating. The toggle only flips between spectating and idle from those entry points; mid-match states are rejected.","triggerScenarios":"Calling ToggleSpectate() while localUser.State is Loaded, Playing, or Results.","commonSituations":"Spectate toggle clicked during an active match; UI not disabled while the user is playing.","solutions":["Gate the spectate toggle on localUser.State being Idle, Ready, or Spectating.","Disable the spectate button while in Loaded/Playing/Results.","If encountered, defer the toggle until the match ends and state returns to Idle."],"exampleFix":"// before\nspectateButton.Action = () => client.ToggleSpectate();\n\n// after\nspectateButton.Action = () =>\n{\n    var s = client.LocalUser?.State;\n    if (s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready || s == MultiplayerUserState.Spectating)\n        client.ToggleSpectate();\n};","handlingStrategy":"validation","validationCode":"var s = client.LocalUser?.State;\nif (s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready || s == MultiplayerUserState.Spectating)\n    await client.ToggleSpectate();","typeGuard":"bool CanToggleSpectate(MultiplayerUserState? s)\n    => s == MultiplayerUserState.Idle || s == MultiplayerUserState.Ready || s == MultiplayerUserState.Spectating;","tryCatchPattern":"try { await client.ToggleSpectate(); }\ncatch (InvalidOperationException) { /* invalid state: ignore the click */ }","preventionTips":["Disable the spectate button during Loaded/Playing/Results.","Subscribe to LocalUser state changes to refresh button state.","Avoid toggling spectate mid-match."],"tags":["multiplayer","state-machine","spectate","invalid-operation"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}