mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 06:36:31 +09:00
Add few hitsounds check
This commit is contained in:
@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality(),
|
||||
new CheckMutedObjects(),
|
||||
new CheckFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
|
161
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
161
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
@ -0,0 +1,161 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckFewHitsounds : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
|
||||
/// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good.
|
||||
/// </summary>
|
||||
private const int negligible_threshold_time = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song.
|
||||
/// This is ok if the section is a quiet intro, for example.
|
||||
/// </summary>
|
||||
private const int warning_threshold_time = 8000;
|
||||
|
||||
/// <summary>
|
||||
/// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song.
|
||||
/// </summary>
|
||||
private const int problem_threshold_time = 24000;
|
||||
|
||||
// Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too).
|
||||
private const int warning_threshold_objects = 4;
|
||||
private const int problem_threshold_objects = 16;
|
||||
|
||||
private static readonly string[] hitsound_types = { HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_FINISH };
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateLongPeriodProblem(this),
|
||||
new IssueTemplateLongPeriodWarning(this),
|
||||
new IssueTemplateNoHitsounds(this)
|
||||
};
|
||||
|
||||
private bool hasHitsounds;
|
||||
private int objectsWithoutHitsounds;
|
||||
private double lastHitsoundTime;
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (!context.Beatmap.HitObjects.Any())
|
||||
yield break;
|
||||
|
||||
hasHitsounds = false;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
|
||||
|
||||
var hitObjectCount = context.Beatmap.HitObjects.Count;
|
||||
|
||||
for (int i = 0; i < hitObjectCount; ++i)
|
||||
{
|
||||
var hitObject = context.Beatmap.HitObjects[i];
|
||||
|
||||
// Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
|
||||
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
||||
{
|
||||
foreach (var issue in applyHitsoundUpdate(nestedHitObject))
|
||||
yield return issue;
|
||||
}
|
||||
|
||||
// This is used to perform an update at the end so that the period after the last hitsounded object can be an issue.
|
||||
bool isLastObject = i == hitObjectCount - 1;
|
||||
|
||||
foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject))
|
||||
yield return issue;
|
||||
}
|
||||
|
||||
if (!hasHitsounds)
|
||||
yield return new IssueTemplateNoHitsounds(this).Create();
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
|
||||
{
|
||||
var time = hitObject.GetEndTime();
|
||||
|
||||
// Only generating issues on hitsounded or last objects ensures we get one issue per long period.
|
||||
// If there are no hitsounds we let the "No hitsounds" template take precedence.
|
||||
if (hasHitsound(hitObject) || isLastObject && hasHitsounds)
|
||||
{
|
||||
var timeWithoutHitsounds = time - lastHitsoundTime;
|
||||
|
||||
if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
}
|
||||
|
||||
if (hasHitsound(hitObject))
|
||||
{
|
||||
hasHitsounds = true;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = time;
|
||||
}
|
||||
else if (couldHaveHitsound(hitObject))
|
||||
++objectsWithoutHitsounds;
|
||||
}
|
||||
|
||||
private bool hasHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitsound);
|
||||
private bool couldHaveHitsound(HitObject hitObject) => hitObject.Samples.Any(isHitnormal);
|
||||
|
||||
private bool isHitsound(HitSampleInfo sample) => hitsound_types.Any(sample.Name.Contains);
|
||||
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
public abstract class IssueTemplateLongPeriod : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateLongPeriod(ICheck check, IssueType type)
|
||||
: base(check, type, "Long period without hitsounds ({0:F1} seconds).")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time };
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodProblem(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodWarning(ICheck check)
|
||||
: base(check, IssueType.Warning)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodNegligible(ICheck check)
|
||||
: base(check, IssueType.Negligible)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateNoHitsounds : IssueTemplate
|
||||
{
|
||||
public IssueTemplateNoHitsounds(ICheck check)
|
||||
: base(check, IssueType.Problem, "There are no hitsounds.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create() => new Issue(this);
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user