{"record":{"id":"900dc85f6efdaae5","repo":"ppy/osu","slug":"setinfo-getdisplaystring-already-has-a-difficu","errorCode":null,"errorMessage":"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.","messagePattern":"(.+?) already has a difficulty with the name of '(.+?)'\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"osu.Game/Beatmaps/BeatmapManager.cs","lineNumber":562,"sourceCode":"            // Since now this is a locally-modified beatmap, we also set all relevant flags to indicate this.\r\n            beatmapInfo.LastLocalUpdate = DateTimeOffset.Now;\r\n            beatmapInfo.Status = BeatmapOnlineStatus.LocallyModified;\r\n\r\n            Realm.Write(r =>\r\n            {\r\n                using var stream = new MemoryStream();\r\n                using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))\r\n                    new LegacyBeatmapEncoder(beatmapContent, beatmapSkin, storyboard).Encode(sw);\r\n\r\n                stream.Seek(0, SeekOrigin.Begin);\r\n\r\n                // AddFile generally handles updating/replacing files, but this is a case where the filename may have also changed so let's delete for simplicity.\r\n                var existingFileInfo = beatmapInfo.Path != null ? setInfo.GetFile(beatmapInfo.Path) : null;\r\n                string targetFilename = createBeatmapFilenameFromMetadata(beatmapInfo);\r\n\r\n                // ensure that two difficulties from the set don't point at the same beatmap file.\r\n                if (setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID && string.Equals(b.Path, targetFilename, StringComparison.OrdinalIgnoreCase)))\r\n                    throw new InvalidOperationException($\"{setInfo.GetDisplayString()} already has a difficulty with the name of '{beatmapInfo.DifficultyName}'.\");\r\n\r\n                if (existingFileInfo != null)\r\n                    DeleteFile(setInfo, existingFileInfo);\r\n\r\n                string oldMd5Hash = beatmapInfo.MD5Hash;\r\n\r\n                beatmapInfo.MD5Hash = stream.ComputeMD5Hash();\r\n                beatmapInfo.Hash = stream.ComputeSHA2Hash();\r\n\r\n                AddFile(setInfo, stream, createBeatmapFilenameFromMetadata(beatmapInfo));\r\n\r\n                updateHashAndMarkDirty(setInfo);\r\n\r\n                var liveBeatmapSet = r.Find<BeatmapSetInfo>(setInfo.ID)!;\r\n\r\n                setInfo.CopyChangesToRealm(liveBeatmapSet);\r\n\r\n                if (transferCollections)\r","sourceCodeStart":544,"sourceCodeEnd":580,"githubUrl":"https://github.com/ppy/osu/blob/d9c73e12adff2feaae4a3e158d36fe5883faf6ca/osu.Game/Beatmaps/BeatmapManager.cs#L544-L580","documentation":"Thrown by BeatmapManager.Save when the filename computed from beatmap metadata (via createBeatmapFilenameFromMetadata) matches the Path of another difficulty already in the same beatmap set. Since difficulty filenames are derived from the difficulty name, this effectively means two difficulties share the same name (case-insensitive comparison).","triggerScenarios":"Renaming a difficulty to the same name as another difficulty in the same beatmap set, then calling Save — createBeatmapFilenameFromMetadata produces an identical filename, and the collision check at BeatmapManager.cs:561 catches it.","commonSituations":"User edits a difficulty name in the editor to match an existing difficulty in the set (e.g., both named 'Insane'); copying a difficulty and forgetting to rename it; case-variation collisions ('Hard' vs 'hard') since the comparison is OrdinalIgnoreCase.","solutions":["Rename the difficulty to a unique name within its beatmap set before saving.","Before calling Save, check whether any sibling difficulty (b.ID != beatmapInfo.ID) already uses the same DifficultyName (case-insensitive).","If the duplicate is a stale entry, remove or rename the conflicting difficulty from the set first."],"exampleFix":"// before\nbeatmapInfo.DifficultyName = \"Insane\";\nbeatmapManager.Save(beatmapInfo, beatmapContent); // throws if another 'Insane' exists\n\n// after\nbool nameExists = setInfo.Beatmaps.Any(b => b.ID != beatmapInfo.ID\n    && string.Equals(b.DifficultyName, \"Insane\", StringComparison.OrdinalIgnoreCase));\nif (nameExists)\n    throw new InvalidOperationException(\"A difficulty named 'Insane' already exists in this set.\");\nbeatmapInfo.DifficultyName = \"Insane\";\nbeatmapManager.Save(beatmapInfo, beatmapContent);","handlingStrategy":"validation","validationCode":"// Check for duplicate difficulty names before saving\nbool nameCollision = setInfo.Beatmaps.Any(b =>\n    b.ID != beatmapInfo.ID &&\n    string.Equals(b.DifficultyName, beatmapInfo.DifficultyName, StringComparison.OrdinalIgnoreCase));\nif (nameCollision)\n    throw new InvalidOperationException($\"Difficulty name '{beatmapInfo.DifficultyName}' already exists in this set\");","typeGuard":null,"tryCatchPattern":"try\n{\n    beatmapManager.Save(beatmapInfo, beatmapContent);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"already has a difficulty\"))\n{\n    // prompt user to pick a unique difficulty name\n}","preventionTips":["Enforce unique difficulty names at the editor UI layer before the user saves.","Compare names case-insensitively, matching the OrdinalIgnoreCase check in Save.","Validate after copy-difficulty operations, which are the most common source of name collisions."],"tags":["beatmap-management","difficulty-name","duplicate-detection","validation"],"backgroundTag":null,"analyzedSha":"d9c73e12adff2feaae4a3e158d36fe5883faf6ca","analyzedAt":"2026-08-13T14:12:54.015Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}