{"record":{"id":"ce34d8c5caad6ade","repo":"ppy/osu","slug":"already-joined-a-room","errorCode":null,"errorMessage":"Already joined a room","messagePattern":"Already joined a room","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs","lineNumber":236,"sourceCode":"\r\n                    break;\r\n            }\r\n        }\r\n\r\n        public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability)\r\n        {\r\n            Debug.Assert(ServerRoom != null);\r\n\r\n            var user = ServerRoom.Users.Single(u => u.UserID == userId);\r\n            user.BeatmapAvailability = newBeatmapAvailability;\r\n\r\n            ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability));\r\n        }\r\n\r\n        protected override async Task<MultiplayerRoom> JoinRoomInternal(long roomId, string? password = null)\r\n        {\r\n            if (RoomJoined || ServerAPIRoom != null)\r\n                throw new InvalidOperationException(\"Already joined a room\");\r\n\r\n            roomId = clone(roomId);\r\n            password = clone(password);\r\n\r\n            ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId);\r\n\r\n            if (password != ServerAPIRoom.Password)\r\n                throw new InvalidOperationException(\"Invalid password.\");\r\n\r\n            lastPlaylistItemId = ServerAPIRoom.Playlist.Max(item => item.ID);\r\n\r\n            var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)\r\n            {\r\n                User = api.LocalUser.Value\r\n            };\r\n\r\n            ServerRoom = new MultiplayerRoom(roomId)\r\n            {\r","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs#L218-L254","documentation":"JoinRoomInternal enforces a one-active-room-per-client invariant (mirroring the real server): if RoomJoined is already true OR ServerAPIRoom is non-null, joining again throws before any state mutates. The client must leave its current room before joining another.","triggerScenarios":"Calling JoinRoom a second time without an intervening LeaveRoom; reusing one TestMultiplayerClient instance across two room-join steps.","commonSituations":"Test setup that joins a room but never tears it down; sequential join steps in one test without cleanup; a previous join left partial state after a failure.","solutions":["Call LeaveRoom() before joining a new room.","Add a teardown step that leaves the room so each test starts clean.","If the prior join failed mid-way, explicitly null out state by leaving before retrying."],"exampleFix":"// before\nawait client.JoinRoom(room1);\nawait client.JoinRoom(room2); // throws\n\n// after\nawait client.JoinRoom(room1);\nawait client.LeaveRoom();\nawait client.JoinRoom(room2);","handlingStrategy":"validation","validationCode":"if (client.RoomJoined || client.ServerAPIRoom != null)\n    await client.LeaveRoom();\nawait client.JoinRoom(room);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair a JoinRoom with a LeaveRoom in test teardown.","Before joining a second room, check RoomJoined and leave first.","Keep one TestMultiplayerClient responsible for at most one active room at a time."],"tags":["test-infra","multiplayer","room-lifecycle"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}