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
- Use ClientRoom for the client's received MultiplayerRoom state.
- Use ServerRoom for the authoritative server-side MultiplayerRoom.
- 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
- Treat the [Obsolete] throw as guidance, not something to swallow in try/catch.
- Assert against ServerRoom when you want authoritative state; ClientRoom for what the client received.
- Audit test helpers for `.Room` access on a TestMultiplayerClient.
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
- Accessing the client-side API room via {nameof(TestMultiplay
- 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/3d67bf3cb1111671.
Report an issue: GitHub.