From 0c35fb49cf4594b9de2a7052210c02de02dfea1d Mon Sep 17 00:00:00 2001 From: LA <1245661240@qq.com> Date: Wed, 4 Mar 2026 22:33:52 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DPS=E7=B3=BB=E5=88=97mod?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2=E4=B8=AD=E6=BD=9C=E5=9C=A8=E6=BC=8F=E6=B4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Mods/LAsMods/ManiaKeyPatternHelp.cs | 110 +++++++++++++++++- .../Mods/LAsMods/ManiaModPatternShift.cs | 22 +++- .../Mods/LAsMods/ManiaModPatternShiftDump.cs | 36 +++++- .../Mods/LAsMods/ManiaModPatternShiftJack.cs | 20 ++-- .../Mods/LAsMods/ManiaNoteCleanupTool.cs | 76 +++++------- 5 files changed, 193 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaKeyPatternHelp.cs b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaKeyPatternHelp.cs index 27719f9ffe..2220b2dee4 100644 --- a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaKeyPatternHelp.cs +++ b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaKeyPatternHelp.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods public static class ManiaKeyPatternHelp { + private const double add_note_alignment_tolerance = 6.0; + private const double add_note_min_column_gap = 8.0; + internal readonly struct WindowContext { public readonly double BeatLength; @@ -475,17 +478,22 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods internal static bool TryAddNoteFromOrderedCandidates(ManiaBeatmap beatmap, IReadOnlyList candidates, double time) { + double alignedTime = AlignTimeToNearbyRow(beatmap, time, add_note_alignment_tolerance); + for (int i = 0; i < candidates.Count; i++) { int column = candidates[i]; - if (IsHoldOccupyingColumn(beatmap, column, time)) + if (IsHoldOccupyingColumn(beatmap, column, alignedTime)) continue; - if (HasNoteAtTime(beatmap, column, time)) + if (HasNoteAtTime(beatmap, column, alignedTime)) return false; - beatmap.HitObjects.Add(new Note { Column = column, StartTime = time }); + if (hasAnyObjectInColumnNearTime(beatmap, column, alignedTime, add_note_min_column_gap)) + return false; + + beatmap.HitObjects.Add(new Note { Column = column, StartTime = alignedTime }); return true; } @@ -498,7 +506,9 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods Random rng, double time) { - int pickedIndex = pickIndexAvoidingHolds(beatmap, candidates, weights, rng, time); + double alignedTime = AlignTimeToNearbyRow(beatmap, time, add_note_alignment_tolerance); + + int pickedIndex = pickIndexAvoidingHolds(beatmap, candidates, weights, rng, alignedTime); if (pickedIndex < 0) return false; @@ -506,13 +516,55 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods candidates.RemoveAt(pickedIndex); weights?.RemoveAt(pickedIndex); - if (HasNoteAtTime(beatmap, column, time)) + if (HasNoteAtTime(beatmap, column, alignedTime)) return false; - beatmap.HitObjects.Add(new Note { Column = column, StartTime = time }); + if (hasAnyObjectInColumnNearTime(beatmap, column, alignedTime, add_note_min_column_gap)) + return false; + + beatmap.HitObjects.Add(new Note { Column = column, StartTime = alignedTime }); return true; } + internal static double AlignTimeToNearbyRow(ManiaBeatmap beatmap, double time, double tolerance = 3.0) + { + double bestTime = time; + double bestDelta = tolerance; + + foreach (var obj in beatmap.HitObjects) + { + double delta = Math.Abs(obj.StartTime - time); + + if (delta > bestDelta) + continue; + + bestDelta = delta; + bestTime = obj.StartTime; + } + + return bestTime; + } + + private static bool hasAnyObjectInColumnNearTime(ManiaBeatmap beatmap, int column, double time, double minGap, ManiaHitObject? ignore = null) + { + if (minGap <= 0) + return false; + + foreach (var obj in beatmap.HitObjects) + { + if (ReferenceEquals(obj, ignore)) + continue; + + if (obj.Column != column) + continue; + + if (Math.Abs(obj.StartTime - time) < minGap) + return true; + } + + return false; + } + // Placement helpers (shared rule: hold occupied -> reselect, note exists -> skip). internal static bool HasNoteAtTime(ManiaBeatmap beatmap, int column, double time, ManiaHitObject? ignore = null, double tolerance = 0.5) @@ -632,6 +684,52 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods return false; } + internal static bool IsWithinBeatFraction(double deltaTime, ManiaBeatmap beatmap, double referenceTime, double beatFraction, double tolerance = 0.5) + { + double beatLength = beatmap.ControlPointInfo.TimingPointAt(referenceTime).BeatLength; + + if (beatLength <= 0) + return false; + + double threshold = beatLength * Math.Max(0, beatFraction); + return deltaTime <= threshold + tolerance; + } + + internal static void RemoveSameColumnRunsWithinBeatFraction(ManiaBeatmap beatmap, double windowStart, double windowEnd, double beatFraction, double tolerance = 0.5) + { + var toRemove = new HashSet(); + + foreach (var group in beatmap.HitObjects.OfType().GroupBy(o => o.Column)) + { + Note? previous = null; + + foreach (var note in group.OrderBy(o => o.StartTime)) + { + if (note.StartTime < windowStart - tolerance || note.StartTime > windowEnd + tolerance) + continue; + + if (previous == null) + { + previous = note; + continue; + } + + double delta = note.StartTime - previous.StartTime; + + if (IsWithinBeatFraction(delta, beatmap, previous.StartTime, beatFraction, tolerance)) + { + toRemove.Add(note); + continue; + } + + previous = note; + } + } + + foreach (var obj in toRemove) + beatmap.HitObjects.Remove(obj); + } + internal static void Shuffle(IList list, Random rng) { for (int i = list.Count - 1; i > 0; i--) diff --git a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShift.cs b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShift.cs index a9f7bcf048..3fd595d83e 100644 --- a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShift.cs +++ b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShift.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods { public class ManiaModPatternShift : Mod, IApplicableAfterBeatmapConversion, IApplicableToBeatmapConverter, IHasSeed, IHasApplyOrder { + private const double min_column_spacing_ms = 8; + public override string Name => "Pattern Shift"; public override string Acronym => "PS"; @@ -250,14 +252,14 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods foreach (var note in chord.Notes) { - int column = chooseColumn(keyCount, lastColumnTime, lastNote, rng, note.StartTime); + int column = chooseColumn(keyCount, lastColumnTime, lastNote, rng, note.StartTime, min_column_spacing_ms); if (column < 0) continue; if (usedColumns.Contains(column)) continue; - if (hasAssignedNoteAtTime(placedNotes, column, note.StartTime)) + if (hasAssignedNoteAtTime(placedNotes, column, note.StartTime, min_column_spacing_ms)) continue; note.AssignedColumn = column; @@ -272,16 +274,28 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods } } - private static int chooseColumn(int keys, double[] lastUsedTime, int lastNote, Random rng, double currentTime) + private static int chooseColumn(int keys, double[] lastUsedTime, int lastNote, Random rng, double currentTime, double minSpacingMs) { var candidates = new List(); + double safeTime = currentTime - Math.Max(0, minSpacingMs); + for (int i = 0; i < keys; i++) { - if (lastUsedTime[i] <= currentTime) + if (lastUsedTime[i] <= safeTime) candidates.Add(i); } + // 若严格最小间隔下无可用列,退化到旧逻辑,避免过多丢 note。 + if (candidates.Count == 0) + { + for (int i = 0; i < keys; i++) + { + if (lastUsedTime[i] <= currentTime) + candidates.Add(i); + } + } + if (candidates.Count == 0) return -1; diff --git a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftDump.cs b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftDump.cs index 93ed5ecdb3..2788e3b238 100644 --- a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftDump.cs +++ b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftDump.cs @@ -101,6 +101,8 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods int direction = lastCol == firstCol ? (rng.Next(0, 2) == 0 ? -1 : 1) : (lastCol > firstCol ? 1 : -1); int prevTarget = firstCol; + int lastAssignedColumn = firstCol; + double lastAssignedTime = groups[0].time; for (int g = 1; g < groups.Count; g++) { @@ -108,25 +110,48 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods if (time < windowStart - TIME_TOLERANCE || time > windowEnd + TIME_TOLERANCE) continue; + double delta = time - lastAssignedTime; + bool forbidSameColumnWithPrevious = ManiaKeyPatternHelp.IsWithinBeatFraction(delta, beatmap, lastAssignedTime, 1.0 / 2.0, TIME_TOLERANCE); + + int? forbiddenColumn = forbidSameColumnWithPrevious ? lastAssignedColumn : null; + int minCol = direction > 0 ? prevTarget + 1 : 0; int maxCol = direction > 0 ? totalColumns - 1 : prevTarget - 1; - if (!tryPickNearestColumn(beatmap, cur, time, minCol, maxCol, out int chosen)) + if (!tryPickNearestColumn(beatmap, cur, time, minCol, maxCol, out int chosen, forbiddenColumn)) { // 允许保持不下降的单调(非严格) minCol = direction > 0 ? prevTarget : 0; maxCol = direction > 0 ? totalColumns - 1 : prevTarget; - if (!tryPickNearestColumn(beatmap, cur, time, minCol, maxCol, out chosen)) - continue; + if (!tryPickNearestColumn(beatmap, cur, time, minCol, maxCol, out chosen, forbiddenColumn)) + { + // 最后兜底:允许在全列找一个可用列,但仍禁止与前一个在 1/2 beat 内同列。 + if (!tryPickNearestColumn(beatmap, cur, time, 0, totalColumns - 1, out chosen, forbiddenColumn)) + { + if (forbidSameColumnWithPrevious && cur.Column == lastAssignedColumn) + { + beatmap.HitObjects.Remove(cur); + continue; + } + + lastAssignedColumn = cur.Column; + lastAssignedTime = time; + prevTarget = cur.Column; + continue; + } + } } cur.Column = chosen; prevTarget = chosen; + lastAssignedColumn = chosen; + lastAssignedTime = time; } // 大间隙单调滑梯:首尾间隔至少 1/2 拍才添加 tryAddLargeGapSlide(beatmap, windowObjects, groups, beatLength, rng, settings); + ManiaKeyPatternHelp.RemoveSameColumnRunsWithinBeatFraction(beatmap, windowStart, windowEnd, 1.0 / 2.0, TIME_TOLERANCE); } private static void tryAddLargeGapSlide(ManiaBeatmap beatmap, @@ -195,7 +220,8 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods double time, int minCol, int maxCol, - out int chosen) + out int chosen, + int? forbiddenColumn = null) { int totalColumns = Math.Max(1, beatmap.TotalColumns); int clampedMin = Math.Clamp(minCol, 0, totalColumns - 1); @@ -209,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods for (int col = clampedMin; col <= clampedMax; col++) { - if (!isColumnAvailable(beatmap, obj, col, time)) + if ((forbiddenColumn.HasValue && col == forbiddenColumn.Value) || !isColumnAvailable(beatmap, obj, col, time)) continue; int dist = Math.Abs(col - obj.Column); diff --git a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftJack.cs b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftJack.cs index d1944887f1..f4133b2609 100644 --- a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftJack.cs +++ b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaModPatternShiftJack.cs @@ -268,6 +268,8 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods Random rng, double timeTolerance) { + double alignedTime = ManiaKeyPatternHelp.AlignTimeToNearbyRow(beatmap, time, timeTolerance); + // 不允许在整拍(1/1)上添加 note。 if (addNote) { @@ -275,14 +277,14 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods if (beatLength > 0) { - double mod = time % beatLength; + double mod = alignedTime % beatLength; if (mod <= timeTolerance || Math.Abs(beatLength - mod) <= timeTolerance) return false; } } int totalColumns = Math.Max(1, beatmap.TotalColumns); - var sourceNotes = ManiaKeyPatternHelp.GetNotesAtTime(windowObjects, time, timeTolerance); + var sourceNotes = ManiaKeyPatternHelp.GetNotesAtTime(windowObjects, alignedTime, timeTolerance); if (sourceNotes.Count == 0) return false; @@ -292,8 +294,8 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods for (int column = 0; column < totalColumns; column++) { - bool hasPrev = time - quarter >= 0 && ManiaKeyPatternHelp.HasNoteAtTime(beatmap, column, time - quarter, null, timeTolerance); - bool hasNext = ManiaKeyPatternHelp.HasNoteAtTime(beatmap, column, time + quarter, null, timeTolerance); + bool hasPrev = alignedTime - quarter >= 0 && ManiaKeyPatternHelp.HasNoteAtTime(beatmap, column, alignedTime - quarter, null, timeTolerance); + bool hasNext = ManiaKeyPatternHelp.HasNoteAtTime(beatmap, column, alignedTime + quarter, null, timeTolerance); if (hasPrev && hasNext) bothSides.Add(column); @@ -339,18 +341,18 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods if (addNote) { - if (ManiaKeyPatternHelp.HasNoteAtTime(beatmap, targetColumn, time, null, timeTolerance)) + if (ManiaKeyPatternHelp.HasNoteAtTime(beatmap, targetColumn, alignedTime, null, timeTolerance)) continue; - if (ManiaKeyPatternHelp.IsHoldOccupyingColumn(beatmap, targetColumn, time, null, timeTolerance)) + if (ManiaKeyPatternHelp.IsHoldOccupyingColumn(beatmap, targetColumn, alignedTime, null, timeTolerance)) continue; } else { - if (ManiaKeyPatternHelp.HasNoteAtTime(beatmap, targetColumn, time, source, timeTolerance)) + if (ManiaKeyPatternHelp.HasNoteAtTime(beatmap, targetColumn, alignedTime, source, timeTolerance)) continue; - if (ManiaKeyPatternHelp.IsHoldOccupyingColumn(beatmap, targetColumn, time, source, timeTolerance)) + if (ManiaKeyPatternHelp.IsHoldOccupyingColumn(beatmap, targetColumn, alignedTime, source, timeTolerance)) continue; } @@ -365,7 +367,7 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods available.RemoveAt(targetIndex); if (addNote) - beatmap.HitObjects.Add(new Note { Column = target, StartTime = time }); + beatmap.HitObjects.Add(new Note { Column = target, StartTime = alignedTime }); else source.Column = target; diff --git a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaNoteCleanupTool.cs b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaNoteCleanupTool.cs index 7bc516c445..aab0b25192 100644 --- a/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaNoteCleanupTool.cs +++ b/osu.Game.Rulesets.Mania/LAsEzMania/Mods/LAsMods/ManiaNoteCleanupTool.cs @@ -32,12 +32,12 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods // beatmap.HitObjects.AddRange(resolved); // } - // 2) 去除重叠并降低密度 + // 去除重叠并降低密度 CleanOverlapNotes(beatmap); - // 3) 在同一列中强制最小间隙 - EnforceMinimumGaps(beatmap); - // 4) Enforce hold-release gap and convert too-short holds to notes. + // Enforce hold-release gap and convert too-short holds to notes. EnforceHoldReleaseGap(beatmap); + // 调整/转换可能再次引入过小间距,做一次最终兜底。 + EnforceMinimumGaps(beatmap); } /// @@ -84,9 +84,21 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods return 0; double startTime = beatmap.HitObjects.Min(h => h.StartTime); - double beatLength = beatmap.ControlPointInfo.TimingPointAt(startTime).BeatLength; - double gap = beatLength / beat; - return Math.Clamp(gap, 30, beatLength / beat * 2); + return getMinimumGapAtTime(beatmap, startTime, beat); + } + + private static double getMinimumGapAtTime(ManiaBeatmap beatmap, double time, int beat = 8) + { + int safeBeatDivisor = Math.Max(1, beat); + double beatLength = beatmap.ControlPointInfo.TimingPointAt(time).BeatLength; + + if (beatLength <= 0) + return 30; + + double beatGap = beatLength / safeBeatDivisor; + + // 保留“按节拍”判定,同时使用 30ms 作为保守下限,避免高速段阈值过小。 + return Math.Max(30, beatGap); } /// @@ -98,7 +110,6 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods if (beatmap.HitObjects.Count == 0) return; - double minGapMs = GetMinimumGapMs(beatmap); int targetKeys = beatmap.TotalColumns; var byColumn = beatmap.HitObjects.ToList().GroupBy(o => Math.Clamp(o.Column, 0, Math.Max(0, targetKeys - 1))); @@ -107,53 +118,24 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods foreach (var colGroup in byColumn) { var list = colGroup.OrderBy(o => o.StartTime).ToList(); - if (list.Count == 0) continue; + if (list.Count == 0) + continue; - // 第一步:预先移除极度密集的音符(> 1/8 拍)作为快速预处理 - var denseFiltered = new List { list[0] }; + var kept = new List { list[0] }; for (int i = 1; i < list.Count; i++) - { - var prev = denseFiltered.Last(); - var curr = list[i]; - - if (curr.StartTime - prev.StartTime < minGapMs) - continue; // 丢弃当前音符(过于密集) - - denseFiltered.Add(curr); - } - - // 第二步:应用最小间隙规则,包含向前查看的三连处理 - var kept = new List { denseFiltered[0] }; - int idx = 0; - - while (++idx < denseFiltered.Count) { var prev = kept.Last(); - var cur = denseFiltered[idx]; + var current = list[i]; - double prevEnd = prev is HoldNote ph ? ph.EndTime : prev.StartTime; - double gap = cur.StartTime - prevEnd; + double prevEnd = prev is HoldNote prevHold ? prevHold.EndTime : prev.StartTime; + double requiredGap = getMinimumGapAtTime(beatmap, prevEnd); + double actualGap = current.StartTime - prevEnd; - if (gap >= minGapMs) - { - kept.Add(cur); + if (actualGap < requiredGap) continue; - } - // 间隙过小:尝试向前查看并删除中间音符 - if (idx + 1 < denseFiltered.Count) - { - var next = denseFiltered[idx + 1]; - double nextGap = next.StartTime - prevEnd; - - if (nextGap >= minGapMs) - { - // 删除当前(中间)音符,前进到下一个 - idx++; // will move to next in outer loop - kept.Add(next); - } - } + kept.Add(current); } survivors.AddRange(kept); @@ -172,7 +154,6 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods /// internal static void EnforceHoldReleaseGap(ManiaBeatmap beatmap, int beat = 8) { - double minGapMs = GetMinimumGapMs(beatmap, beat); if (beatmap.HitObjects.Count == 0) return; @@ -188,6 +169,7 @@ namespace osu.Game.Rulesets.Mania.LAsEzMania.Mods.LAsMods continue; var next = list[i + 1]; + double minGapMs = getMinimumGapAtTime(beatmap, next.StartTime, beat); double gap = next.StartTime - hold.EndTime; if (gap >= minGapMs)