ppy/osu · error · InvalidOperationException

Already joined a room

Error message

Already joined a room

What it means

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.

Source

Thrown at osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs:236

                    break;
            }
        }

        public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability)
        {
            Debug.Assert(ServerRoom != null);

            var user = ServerRoom.Users.Single(u => u.UserID == userId);
            user.BeatmapAvailability = newBeatmapAvailability;

            ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(clone(userId), clone(user.BeatmapAvailability));
        }

        protected override async Task<MultiplayerRoom> JoinRoomInternal(long roomId, string? password = null)
        {
            if (RoomJoined || ServerAPIRoom != null)
                throw new InvalidOperationException("Already joined a room");

            roomId = clone(roomId);
            password = clone(password);

            ServerAPIRoom = ServerSideRooms.Single(r => r.RoomID == roomId);

            if (password != ServerAPIRoom.Password)
                throw new InvalidOperationException("Invalid password.");

            lastPlaylistItemId = ServerAPIRoom.Playlist.Max(item => item.ID);

            var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
            {
                User = api.LocalUser.Value
            };

            ServerRoom = new MultiplayerRoom(roomId)
            {

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Call LeaveRoom() before joining a new room.
  2. Add a teardown step that leaves the room so each test starts clean.
  3. If the prior join failed mid-way, explicitly null out state by leaving before retrying.

Example fix

// before
await client.JoinRoom(room1);
await client.JoinRoom(room2); // throws

// after
await client.JoinRoom(room1);
await client.LeaveRoom();
await client.JoinRoom(room2);
Defensive patterns

Strategy: validation

Validate before calling

if (client.RoomJoined || client.ServerAPIRoom != null)
    await client.LeaveRoom();
await client.JoinRoom(room);

Prevention

When it happens

Trigger: Calling JoinRoom a second time without an intervening LeaveRoom; reusing one TestMultiplayerClient instance across two room-join steps.

Common situations: 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.

Related errors


AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13). Data as JSON: /api/errors/ce34d8c5caad6ade. Report an issue: GitHub.