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

  1. Only attempt to remove non-expired items (item.Expired == false).
  2. Filter expired items out before issuing removal.
  3. 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

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


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