ppy/osu · error · InvalidOperationException

Accessing the client-side room via {nameof(TestMultiplayerCl

Error message

Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. Use {nameof(ClientRoom)} if this was intended.

What it means

Same disambiguation guard as the API Room, but for the in-memory MultiplayerRoom state. TestMultiplayerClient maintains a server-side MultiplayerRoom (ServerRoom) and an inherited client-side one (exposed as ClientRoom). The base MultiplayerClient.Room is shadowed with `new` + [Obsolete] and throws so tests do not accidentally assert against the wrong room object.

Source

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

        /// </summary>
        public MultiplayerRoom? ClientRoom => base.Room;

        /// <summary>
        /// The server's <see cref="Room"/>. This is always up-to-date.
        /// </summary>
        public Room? ServerAPIRoom { get; private set; }

        /// <summary>
        /// The server's <see cref="MultiplayerRoom"/>. This is always up-to-date.
        /// </summary>
        public MultiplayerRoom? ServerRoom { get; private set; }

        [Obsolete]
        protected new Room APIRoom => throw new InvalidOperationException($"Accessing the client-side API room via {nameof(TestMultiplayerClient)} is unsafe. "
                                                                          + $"Use {nameof(ClientAPIRoom)} if this was intended.");

        [Obsolete]
        public new MultiplayerRoom Room => throw new InvalidOperationException($"Accessing the client-side room via {nameof(TestMultiplayerClient)} is unsafe. "
                                                                               + $"Use {nameof(ClientRoom)} if this was intended.");

        public new MultiplayerRoomUser? LocalUser => ServerRoom?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id);

        public Action<MultiplayerRoom>? RoomSetupAction;

        public bool RoomJoined { get; private set; }

        [Resolved]
        private IAPIProvider api { get; set; } = null!;

        private MultiplayerPlaylistItem? currentItem => ServerRoom?.Playlist[currentIndex];
        private int currentIndex;
        private long lastPlaylistItemId;
        private int lastCountdownId;

        private readonly Dictionary<int, long> matchmakingUserPicks = new Dictionary<int, long>();

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Use ClientRoom for the client's received MultiplayerRoom state.
  2. Use ServerRoom for the authoritative server-side MultiplayerRoom.
  3. Remember ClientRoom already wraps base.Room, so it is the safe equivalent of the old property.

Example fix

// before
MultiplayerRoom? room = multiplayerClient.Room;

// after
MultiplayerRoom? room = multiplayerClient.ServerRoom; // authoritative server view
// or: multiplayerClient.ClientRoom; // client view
Defensive patterns

Strategy: validation

Validate before calling

// never read the shadowed base Room; pick the explicit view up front
MultiplayerRoom? room = wantServerView ? multiplayerClient.ServerRoom : multiplayerClient.ClientRoom;

Prevention

When it happens

Trigger: Reading `client.Room` (the inherited public property) instead of ClientRoom (client view) or ServerRoom (server view) in test code.

Common situations: Porting code that used MultiplayerClient.Room into a test; autocomplete selecting the shadowed member; assuming Room is the server state.

Related errors


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