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
- Use ClientAPIRoom when you want what the local client received.
- Use ServerAPIRoom when you want the authoritative server state the test double maintains.
- 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
- Treat the [Obsolete] throw as a compile/lint nudge, not a runtime path to catch.
- Search tests for `.APIRoom` and redirect each to ClientAPIRoom or ServerAPIRoom.
- When porting production MultiplayerClient code into a test, audit every base-property access.
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
- Accessing the client-side room via {nameof(TestMultiplayerCl
- Already joined a room
- Invalid password.
- Local user is not the room host.
- Attempted to change an item that doesn't exist.
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/9653d96a92168e1d.
Report an issue: GitHub.