[Mod]LP mod增加3模式通用版

This commit is contained in:
LA
2026-01-25 18:48:11 +08:00
parent 198963bd4f
commit 532e2ccf20
7 changed files with 256 additions and 147 deletions

View File

@@ -12,6 +12,7 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Graphics;
using osu.Game.LAsEzExtensions.Mods;
using osu.Game.Localisation;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.Difficulty;
@@ -113,6 +114,13 @@ namespace osu.Game.Rulesets.Catch
{
switch (type)
{
case ModType.LA_Mod:
return new Mod[]
{
new ModNiceBPM(),
new UniversalLoopPlayClip(),
};
case ModType.DifficultyReduction:
return new Mod[]
{

View File

@@ -1,143 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.LAsEzExtensions.Configuration;
using osu.Game.LAsEzExtensions.Mods;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.EzOsu.Mods
{
public class OsuModLoopPlayClip : ModLoopPlayClip,
IApplicableAfterBeatmapConversion
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
Seed.Value ??= RNG.Next();
var rng = new Random((int)Seed.Value);
var osuBeatmap = (OsuBeatmap)beatmap;
osuBeatmap.Breaks.Clear();
var (cutTimeStart, cutTimeEnd, length) = ResolveSliceTimesForBeatmap(beatmap);
// compute break time (ms) from quarter-beat multiples at slice start
double breakTime;
try
{
var timing = beatmap.ControlPointInfo.TimingPointAt(cutTimeStart);
double quarterMs = timing.BeatLength / 4.0;
breakTime = quarterMs * Math.Max(1, BreakQuarter.Value);
}
catch
{
breakTime = 250 * Math.Max(1, BreakQuarter.Value);
}
var selectedPart = osuBeatmap.HitObjects.Where(h => h.StartTime > cutTimeStart && h.GetEndTime() < cutTimeEnd).ToList();
var newPart = new List<OsuHitObject>();
for (int timeIndex = 0; timeIndex < LoopCount.Value; timeIndex++)
{
double offset = timeIndex * (breakTime + length);
foreach (var note in selectedPart)
{
double baseOffset = offset - cutTimeStart;
if (note is HitCircle circle)
{
var nc = new HitCircle
{
StartTime = circle.StartTime + baseOffset,
Samples = circle.Samples.ToList(),
Position = circle.Position,
};
newPart.Add(nc);
}
else if (note is Slider slider)
{
var ns = new Slider
{
StartTime = slider.StartTime + baseOffset,
Path = slider.Path,
RepeatCount = slider.RepeatCount,
Samples = slider.Samples.ToList(),
Position = slider.Position,
SliderVelocityMultiplier = slider.SliderVelocityMultiplier,
};
if (slider.NodeSamples != null)
ns.NodeSamples = slider.NodeSamples.Select(n => (IList<HitSampleInfo>)n.ToList()).ToList();
newPart.Add(ns);
}
else if (note is Spinner spinner)
{
var ns = new Spinner
{
StartTime = spinner.StartTime + baseOffset,
Duration = spinner.Duration,
};
newPart.Add(ns);
}
else
{
// fallback: attempt to create a shallow instance of the same type and copy StartTime/Samples
var type = note.GetType();
try
{
var inst = (OsuHitObject?)Activator.CreateInstance(type);
if (inst != null)
{
inst.StartTime = note.StartTime + baseOffset;
inst.Samples = note.Samples.ToList();
newPart.Add(inst);
}
}
catch
{
// ignore and fallback to skipping
}
// As a last resort, skip adding this unknown object to avoid corrupting timing data.
}
}
}
// Ensure derived timing (slider velocity/endtime, nested objects) are populated
foreach (var h in newPart)
{
try
{
h.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.Difficulty);
}
catch
{
// Ignore per-object apply failures to avoid breaking the whole mod application.
}
}
osuBeatmap.HitObjects = newPart;
}
}
}

View File

@@ -28,7 +28,6 @@ using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.Osu.Difficulty;
using osu.Game.Rulesets.Osu.Edit;
using osu.Game.Rulesets.Osu.Edit.Setup;
using osu.Game.Rulesets.Osu.EzOsu.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
@@ -167,7 +166,7 @@ namespace osu.Game.Rulesets.Osu
return new Mod[]
{
new ModNiceBPM(),
new OsuModLoopPlayClip(),
new UniversalLoopPlayClip(),
};
case ModType.DifficultyReduction:

View File

@@ -34,6 +34,7 @@ using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning;
using osu.Game.Rulesets.Configuration;
using osu.Game.Configuration;
using osu.Game.LAsEzExtensions.Mods;
using osu.Game.Localisation;
using osu.Game.Rulesets.Scoring.Legacy;
using osu.Game.Rulesets.Taiko.Configuration;
@@ -131,6 +132,13 @@ namespace osu.Game.Rulesets.Taiko
{
switch (type)
{
case ModType.LA_Mod:
return new Mod[]
{
new ModNiceBPM(),
new UniversalLoopPlayClip(),
};
case ModType.DifficultyReduction:
return new Mod[]
{

View File

@@ -57,8 +57,11 @@ namespace osu.Game.LAsEzExtensions.Analysis
double interval = 240000.0 / bpm; // 4拍的时间间隔毫秒
double songEndTime = hitObjects[^1].StartTime;
// 预分配List容量以避免频繁扩容
// 预分配List容量以避免频繁扩容(防御性处理 NaN/Infinity/负数)
int estimatedIntervals = (int)((songEndTime / interval) + 1);
if (double.IsNaN(songEndTime) || double.IsInfinity(songEndTime) || double.IsNaN(interval) || double.IsInfinity(interval))
estimatedIntervals = 0;
estimatedIntervals = Math.Max(0, estimatedIntervals);
var kpsList = new List<double>(estimatedIntervals);
// 缓存hitObjects数组以提高访问性能
@@ -242,6 +245,9 @@ namespace osu.Game.LAsEzExtensions.Analysis
double songEndTime = hitObjects[^1].StartTime;
int estimatedIntervals = (int)((songEndTime / interval) + 1);
if (double.IsNaN(songEndTime) || double.IsInfinity(songEndTime) || double.IsNaN(interval) || double.IsInfinity(interval))
estimatedIntervals = 0;
estimatedIntervals = Math.Max(0, estimatedIntervals);
var kpsList = new List<double>(estimatedIntervals);
var columnCounts = new Dictionary<int, int>();
var holdNoteCounts = new Dictionary<int, int>();

View File

@@ -41,7 +41,7 @@ namespace osu.Game.LAsEzExtensions.Mods
// LAsMods - Mod Descriptions
// ====================================================================================================
public static readonly LocalisableString LoopPlayClip_Description = new EzLocalisableString("将谱面切割成片段用于循环练习。原版是YuLiangSSS的Duplicate Mod",
public static readonly LocalisableString LoopPlayClip_Description = new EzLocalisableString("将谱面切割成片段用于循环练习。",
"Cut the beatmap into a clip for loop practice. (The original is YuLiangSSS's Duplicate Mod)");
// SpaceBody

View File

@@ -0,0 +1,231 @@
// Universal loop play clip mod that applies to arbitrary rulesets using best-effort cloning.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.LAsEzExtensions.Mods
{
public class UniversalLoopPlayClip : ModLoopPlayClip,
IApplicableAfterBeatmapConversion
{
public void ApplyToBeatmap(IBeatmap beatmap)
{
var (cutTimeStart, cutTimeEnd, _) = ResolveSliceTimesForBeatmap(beatmap);
ApplyLoopToBeatmapStatic(beatmap, LoopCount.Value, cutTimeStart, cutTimeEnd, BreakQuarter.Value, Seed.Value);
}
public static void ApplyLoopToBeatmapStatic(IBeatmap beatmap, int loopCount, double cutTimeStart, double cutTimeEnd, int breakQuarter, int? seed = null)
{
if (beatmap == null) return;
try
{
var breaksProp = beatmap.GetType().GetProperty("Breaks");
if (breaksProp != null && breaksProp.CanWrite)
{
var breaks = breaksProp.GetValue(beatmap) as System.Collections.IList;
breaks?.Clear();
}
else
{
beatmap.Breaks.Clear();
}
}
catch
{
}
double breakTime;
try
{
var timing = beatmap.ControlPointInfo.TimingPointAt(cutTimeStart);
double quarterMs = timing.BeatLength / 4.0;
breakTime = quarterMs * Math.Max(1, breakQuarter);
}
catch
{
breakTime = 250 * Math.Max(1, breakQuarter);
}
var selectedPart = beatmap.HitObjects.Where(h => h.StartTime > cutTimeStart && h.GetEndTime() < cutTimeEnd).ToList();
var newPart = new List<HitObject>();
var rng = seed.HasValue ? new Random((int)seed.Value) : new Random();
double length = cutTimeEnd - cutTimeStart;
for (int timeIndex = 0; timeIndex < loopCount; timeIndex++)
{
double offset = timeIndex * (breakTime + length);
foreach (var note in selectedPart)
{
double baseOffset = offset - cutTimeStart;
var type = note.GetType();
try
{
var inst = (HitObject?)Activator.CreateInstance(type);
if (inst != null)
{
// StartTime
var startProp = type.GetProperty("StartTime");
if (startProp != null && startProp.CanWrite)
startProp.SetValue(inst, note.StartTime + baseOffset);
else
inst.StartTime = note.StartTime + baseOffset;
// Samples
var samplesProp = type.GetProperty("Samples");
if (samplesProp != null && samplesProp.CanWrite)
{
try
{
var s = note.Samples?.ToList();
samplesProp.SetValue(inst, s);
}
catch
{
inst.Samples = note.Samples?.ToList();
}
}
else
{
inst.Samples = note.Samples?.ToList();
}
newPart.Add(inst);
}
}
catch
{
// skip
}
}
}
foreach (var h in newPart)
{
try
{
h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
}
catch
{
}
}
var propHitObjects = beatmap.GetType().GetProperty("HitObjects");
if (propHitObjects != null && propHitObjects.CanWrite)
{
// Ensure we assign a list whose element type matches the property's generic argument
var propType = propHitObjects.PropertyType;
if (propType.IsGenericType)
{
var elementType = propType.GetGenericArguments()[0];
if (elementType == typeof(HitObject))
{
propHitObjects.SetValue(beatmap, newPart);
return;
}
try
{
var listType = typeof(List<>).MakeGenericType(elementType);
var listInstance = (System.Collections.IList)Activator.CreateInstance(listType)!;
foreach (var h in newPart)
{
if (h == null) continue;
if (elementType.IsAssignableFrom(h.GetType()))
{
listInstance.Add(h);
continue;
}
// Attempt best-effort conversion: create instance of elementType and copy common fields.
try
{
var target = Activator.CreateInstance(elementType) as HitObject;
if (target != null)
{
var startProp = elementType.GetProperty("StartTime");
if (startProp != null && startProp.CanWrite)
startProp.SetValue(target, h.StartTime);
else
target.StartTime = h.StartTime;
var samplesProp = elementType.GetProperty("Samples");
if (samplesProp != null && samplesProp.CanWrite)
{
try { samplesProp.SetValue(target, h.Samples?.ToList()); }
catch { target.Samples = h.Samples?.ToList(); }
}
else
{
target.Samples = h.Samples?.ToList();
}
listInstance.Add(target);
continue;
}
}
catch
{
// ignore conversion failure for this element
}
}
propHitObjects.SetValue(beatmap, listInstance);
return;
}
catch
{
// fallthrough to generic assignment attempt below
}
}
// Last resort: try to set directly (may fail if types mismatch)
try
{
propHitObjects.SetValue(beatmap, newPart);
return;
}
catch
{
// continue to other fallbacks
}
}
try
{
dynamic dyn = beatmap;
dyn.HitObjects = newPart;
}
catch
{
try
{
var current = beatmap.HitObjects as IList<HitObject>;
if (current != null)
{
current.Clear();
foreach (var h in newPart) current.Add(h);
}
}
catch
{
}
}
}
}
}