ppy/osu · error · InvalidOperationException
Attempted to remove an item which has already been played.
Error message
Attempted to remove an item which has already been played.
What it means
RemoveUserPlaylistItem refuses to remove items whose Expired flag is true. Once an item has been played it is considered historical and cannot be removed through the normal removal path.
Source
Thrown at osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs:602
public async Task RemoveUserPlaylistItem(int userId, long playlistItemId)
{
Debug.Assert(ServerRoom != null);
Debug.Assert(ServerAPIRoom != null);
var item = ServerRoom.Playlist.FirstOrDefault(i => i.ID == playlistItemId);
if (item == null)
throw new InvalidOperationException("Item does not exist in the room.");
if (item.Equals(currentItem))
throw new InvalidOperationException("The room's current item cannot be removed.");
if (item.OwnerID != userId)
throw new InvalidOperationException("Attempted to remove an item which is not owned by the user.");
if (item.Expired)
throw new InvalidOperationException("Attempted to remove an item which has already been played.");
ServerRoom.Playlist.Remove(item);
ServerAPIRoom.Playlist = ServerAPIRoom.Playlist.Where(i => i.ID != item.ID).ToArray();
await ((IMultiplayerClient)this).PlaylistItemRemoved(clone(playlistItemId)).ConfigureAwait(false);
await updateCurrentItem(ServerRoom).ConfigureAwait(false);
updateRoomStateIfRequired();
}
public override Task RemovePlaylistItem(long playlistItemId) => RemoveUserPlaylistItem(api.LocalUser.Value.OnlineID, clone(playlistItemId));
public override Task VoteToSkipIntro()
{
return UserVoteToSkipIntro(api.LocalUser.Value.OnlineID);
}
public async Task UserVoteToSkipIntro(int userId)
{
View on GitHub (pinned to d9c73e12ad)
Solutions
- Only attempt to remove non-expired items (item.Expired == false).
- Filter expired items out before issuing removal.
- Accept that expired items remain in history; do not treat them as removable.
Example fix
// before await client.RemovePlaylistItem(expiredItem.ID); // after var removable = ServerRoom.Playlist.FirstOrDefault(i => !i.Expired && i.ID != currentItem?.ID); if (removable != null) await client.RemovePlaylistItem(removable.ID);
Defensive patterns
Strategy: validation
Validate before calling
var item = ServerRoom.Playlist.FirstOrDefault(i => i.ID == playlistItemId);
if (item != null && item.Expired)
throw new InvalidOperationException("item already played; not removable");
await client.RemovePlaylistItem(playlistItemId); Prevention
- Filter removal candidates to non-expired items.
- Do not attempt to clean up historical/expired items via removal.
- Re-query item.Expired before each remove in long-running tests.
When it happens
Trigger: Removing a playlist item that has already been played (item.Expired == true).
Common situations: Test advanced past an item then tried to clean it up; removing a stale reference to a played item.
Related errors
- Attempted to change an item which has already been played.
- The room's current item cannot be removed.
- Attempted to change an item that doesn't exist.
- Item does not exist in the room.
- Accessing the client-side API room via {nameof(TestMultiplay
AI-assisted analysis of ppy/osu@d9c73e12ad (2026-08-13).
Data as JSON: /api/errors/2cf0d37294ebbc64.
Report an issue: GitHub.