ppy/osu · error · InvalidOperationException

Accessing the client-side API room via {nameof(TestMultiplay

Error message

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

What it means

TestMultiplayerClient is a test double that holds BOTH a server-side Room (ServerAPIRoom) and the inherited client-side Room (exposed as ClientAPIRoom). The inherited base MultiplayerClient.APIRoom would return the client view, which is ambiguous and often not what a test asserts against, so it is shadowed with `new` and marked [Obsolete] to throw. The throw forces the author to disambiguate by naming the room explicitly.

Source

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

        public Room? ClientAPIRoom => base.APIRoom;

        /// <summary>
        /// The local client's <see cref="MultiplayerRoom"/>. This is not always equivalent to the server-side room.
        /// </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;

View on GitHub (pinned to d9c73e12ad)

Solutions

  1. Use ClientAPIRoom when you want what the local client received.
  2. Use ServerAPIRoom when you want the authoritative server state the test double maintains.
  3. If you truly need the inherited behavior, note that ClientAPIRoom already returns base.APIRoom, so prefer it.

Example fix

// before
Room? room = multiplayerClient.APIRoom;

// after
Room? room = multiplayerClient.ServerAPIRoom; // authoritative server view
// or: multiplayerClient.ClientAPIRoom; // client view
Defensive patterns

Strategy: validation

Validate before calling

// never read the shadowed base APIRoom; pick the explicit view up front
Room? room = wantServerView ? multiplayerClient.ServerAPIRoom : multiplayerClient.ClientAPIRoom;

Prevention

When it happens

Trigger: Reading `client.APIRoom` (the protected inherited property) in a multiplayer test scene or helper, instead of ClientAPIRoom (client view) or ServerAPIRoom (server/authoritative view).

Common situations: Code copy-pasted from production MultiplayerClient consumers into a test; an IDE auto-completing to the shadowed base property; refactors that previously used base.APIRoom.

Related errors


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