mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 15:44:04 +09:00
Merge branch 'refactor-combo-colour-retrieval' into legacy-beatmap-combo-offset
This commit is contained in:
@ -8,11 +8,11 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
{
|
||||
public class DifficultyAttributes
|
||||
{
|
||||
public Mod[] Mods;
|
||||
public Skill[] Skills;
|
||||
public Mod[] Mods { get; set; }
|
||||
public Skill[] Skills { get; set; }
|
||||
|
||||
public double StarRating;
|
||||
public int MaxCombo;
|
||||
public double StarRating { get; set; }
|
||||
public int MaxCombo { get; set; }
|
||||
|
||||
public DifficultyAttributes()
|
||||
{
|
||||
|
@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// <returns>A structure describing the difficulty of the beatmap.</returns>
|
||||
public DifficultyAttributes Calculate(params Mod[] mods)
|
||||
{
|
||||
mods = mods.Select(m => m.CreateCopy()).ToArray();
|
||||
mods = mods.Select(m => m.DeepClone()).ToArray();
|
||||
|
||||
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, mods);
|
||||
|
||||
@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
|
||||
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
var skills = CreateSkills(beatmap, mods);
|
||||
var skills = CreateSkills(beatmap, mods, clockRate);
|
||||
|
||||
if (!beatmap.HitObjects.Any())
|
||||
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
|
||||
@ -180,7 +180,8 @@ namespace osu.Game.Rulesets.Difficulty
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
|
||||
/// <param name="mods">Mods to calculate difficulty with.</param>
|
||||
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
|
||||
/// <returns>The <see cref="Skill"/>s.</returns>
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
|
||||
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
@ -23,15 +22,18 @@ namespace osu.Game.Rulesets.Edit
|
||||
// Audio
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality(),
|
||||
new CheckMutedObjects(),
|
||||
new CheckFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
new CheckConcurrentObjects()
|
||||
new CheckConcurrentObjects(),
|
||||
new CheckZeroLengthObjects(),
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
return checks.SelectMany(check => check.Run(playableBeatmap, workingBeatmap));
|
||||
return checks.SelectMany(check => check.Run(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
38
osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs
Normal file
38
osu.Game/Rulesets/Edit/BeatmapVerifierContext.cs
Normal file
@ -0,0 +1,38 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the context provided by the beatmap verifier to the checks it runs.
|
||||
/// Contains information about what is being checked and how it should be checked.
|
||||
/// </summary>
|
||||
public class BeatmapVerifierContext
|
||||
{
|
||||
/// <summary>
|
||||
/// The playable beatmap instance of the current beatmap.
|
||||
/// </summary>
|
||||
public readonly IBeatmap Beatmap;
|
||||
|
||||
/// <summary>
|
||||
/// The working beatmap instance of the current beatmap.
|
||||
/// </summary>
|
||||
public readonly IWorkingBeatmap WorkingBeatmap;
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty level which the current beatmap is considered to be.
|
||||
/// </summary>
|
||||
public DifficultyRating InterpretedDifficulty;
|
||||
|
||||
public BeatmapVerifierContext(IBeatmap beatmap, IWorkingBeatmap workingBeatmap, DifficultyRating difficultyRating = DifficultyRating.ExpertPlus)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
WorkingBeatmap = workingBeatmap;
|
||||
InterpretedDifficulty = difficultyRating;
|
||||
}
|
||||
}
|
||||
}
|
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
protected override CheckCategory Category => CheckCategory.Audio;
|
||||
protected override string TypeOfFile => "audio";
|
||||
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.AudioFile;
|
||||
protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata?.AudioFile;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
@ -26,13 +25,13 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
new IssueTemplateNoBitrate(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var audioFile = playableBeatmap.Metadata?.AudioFile;
|
||||
var audioFile = context.Beatmap.Metadata?.AudioFile;
|
||||
if (audioFile == null)
|
||||
yield break;
|
||||
|
||||
var track = workingBeatmap.Track;
|
||||
var track = context.WorkingBeatmap.Track;
|
||||
|
||||
if (track?.Bitrate == null || track.Bitrate.Value == 0)
|
||||
yield return new IssueTemplateNoBitrate(this).Create();
|
||||
|
@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
protected override CheckCategory Category => CheckCategory.Resources;
|
||||
protected override string TypeOfFile => "background";
|
||||
protected override string GetFilename(IBeatmap playableBeatmap) => playableBeatmap.Metadata?.BackgroundFile;
|
||||
protected override string GetFilename(IBeatmap beatmap) => beatmap.Metadata?.BackgroundFile;
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
@ -30,13 +29,13 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
new IssueTemplateTooUncompressed(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var backgroundFile = playableBeatmap.Metadata?.BackgroundFile;
|
||||
var backgroundFile = context.Beatmap.Metadata?.BackgroundFile;
|
||||
if (backgroundFile == null)
|
||||
yield break;
|
||||
|
||||
var texture = workingBeatmap.Background;
|
||||
var texture = context.WorkingBeatmap.Background;
|
||||
if (texture == null)
|
||||
yield break;
|
||||
|
||||
@ -48,8 +47,8 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
else if (texture.Width < low_width || texture.Height < low_height)
|
||||
yield return new IssueTemplateLowResolution(this).Create(texture.Width, texture.Height);
|
||||
|
||||
string storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
|
||||
double filesizeMb = workingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
|
||||
string storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(backgroundFile);
|
||||
double filesizeMb = context.WorkingBeatmap.GetStream(storagePath).Length / (1024d * 1024d);
|
||||
|
||||
if (filesizeMb > max_filesize_mb)
|
||||
yield return new IssueTemplateTooUncompressed(this).Create(filesizeMb);
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -22,15 +21,17 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
new IssueTemplateConcurrentDifferent(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
for (int i = 0; i < playableBeatmap.HitObjects.Count - 1; ++i)
|
||||
{
|
||||
var hitobject = playableBeatmap.HitObjects[i];
|
||||
var hitObjects = context.Beatmap.HitObjects;
|
||||
|
||||
for (int j = i + 1; j < playableBeatmap.HitObjects.Count; ++j)
|
||||
for (int i = 0; i < hitObjects.Count - 1; ++i)
|
||||
{
|
||||
var hitobject = hitObjects[i];
|
||||
|
||||
for (int j = i + 1; j < hitObjects.Count; ++j)
|
||||
{
|
||||
var nextHitobject = playableBeatmap.HitObjects[j];
|
||||
var nextHitobject = hitObjects[j];
|
||||
|
||||
// Accounts for rulesets with hitobjects separated by columns, such as Mania.
|
||||
// In these cases we only care about concurrent objects within the same column.
|
||||
|
164
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
164
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
@ -0,0 +1,164 @@
|
||||
// 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;
|
||||
|
||||
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 IssueTemplateLongPeriodNegligible(this),
|
||||
new IssueTemplateNoHitsounds(this)
|
||||
};
|
||||
|
||||
private bool mapHasHitsounds;
|
||||
private int objectsWithoutHitsounds;
|
||||
private double lastHitsoundTime;
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (!context.Beatmap.HitObjects.Any())
|
||||
yield break;
|
||||
|
||||
mapHasHitsounds = false;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
|
||||
|
||||
var hitObjectsIncludingNested = new List<HitObject>();
|
||||
|
||||
foreach (var hitObject in context.Beatmap.HitObjects)
|
||||
{
|
||||
// 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)
|
||||
hitObjectsIncludingNested.Add(nestedHitObject);
|
||||
|
||||
hitObjectsIncludingNested.Add(hitObject);
|
||||
}
|
||||
|
||||
var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
|
||||
var hitObjectCount = hitObjectsByEndTime.Count;
|
||||
|
||||
for (int i = 0; i < hitObjectCount; ++i)
|
||||
{
|
||||
var hitObject = hitObjectsByEndTime[i];
|
||||
|
||||
// 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 (!mapHasHitsounds)
|
||||
yield return new IssueTemplateNoHitsounds(this).Create();
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
|
||||
{
|
||||
var time = hitObject.GetEndTime();
|
||||
bool hasHitsound = hitObject.Samples.Any(isHitsound);
|
||||
bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal);
|
||||
|
||||
// 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 || (isLastObject && mapHasHitsounds))
|
||||
{
|
||||
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)
|
||||
{
|
||||
mapHasHitsounds = true;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = time;
|
||||
}
|
||||
else if (couldHaveHitsound)
|
||||
++objectsWithoutHitsounds;
|
||||
}
|
||||
|
||||
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.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);
|
||||
}
|
||||
}
|
||||
}
|
@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
protected abstract CheckCategory Category { get; }
|
||||
protected abstract string TypeOfFile { get; }
|
||||
protected abstract string GetFilename(IBeatmap playableBeatmap);
|
||||
protected abstract string GetFilename(IBeatmap beatmap);
|
||||
|
||||
public CheckMetadata Metadata => new CheckMetadata(Category, $"Missing {TypeOfFile}");
|
||||
|
||||
@ -21,9 +21,9 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
new IssueTemplateDoesNotExist(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var filename = GetFilename(playableBeatmap);
|
||||
var filename = GetFilename(context.Beatmap);
|
||||
|
||||
if (filename == null)
|
||||
{
|
||||
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
}
|
||||
|
||||
// If the file is set, also make sure it still exists.
|
||||
var storagePath = playableBeatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
|
||||
var storagePath = context.Beatmap.BeatmapInfo.BeatmapSet.GetPathForFile(filename);
|
||||
if (storagePath != null)
|
||||
yield break;
|
||||
|
||||
|
158
osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
Normal file
158
osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
Normal file
@ -0,0 +1,158 @@
|
||||
// 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.Utils;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckMutedObjects : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Volume percentages lower than or equal to this are typically inaudible.
|
||||
/// </summary>
|
||||
private const int muted_threshold = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume.
|
||||
/// </summary>
|
||||
private const int low_volume_threshold = 20;
|
||||
|
||||
private enum EdgeType
|
||||
{
|
||||
Head,
|
||||
Repeat,
|
||||
Tail,
|
||||
None
|
||||
}
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateMutedActive(this),
|
||||
new IssueTemplateLowVolumeActive(this),
|
||||
new IssueTemplateMutedPassive(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
foreach (var hitObject in context.Beatmap.HitObjects)
|
||||
{
|
||||
// Worth keeping in mind: The samples of an object always play at its end time.
|
||||
// Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
|
||||
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
||||
{
|
||||
foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
|
||||
yield return issue;
|
||||
}
|
||||
|
||||
foreach (var issue in getVolumeIssues(hitObject))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
|
||||
{
|
||||
sampledHitObject ??= hitObject;
|
||||
if (!sampledHitObject.Samples.Any())
|
||||
yield break;
|
||||
|
||||
// Samples that allow themselves to be overridden by control points have a volume of 0.
|
||||
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
|
||||
double samplePlayTime = sampledHitObject.GetEndTime();
|
||||
|
||||
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
|
||||
// We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
|
||||
if (edgeType == EdgeType.None)
|
||||
yield break;
|
||||
|
||||
string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null;
|
||||
|
||||
if (maxVolume <= muted_threshold)
|
||||
{
|
||||
if (edgeType == EdgeType.Head)
|
||||
yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
else
|
||||
yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
}
|
||||
else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head)
|
||||
{
|
||||
yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
}
|
||||
}
|
||||
|
||||
private EdgeType getEdgeAtTime(HitObject hitObject, double time)
|
||||
{
|
||||
if (Precision.AlmostEquals(time, hitObject.StartTime, 1f))
|
||||
return EdgeType.Head;
|
||||
if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f))
|
||||
return EdgeType.Tail;
|
||||
|
||||
if (hitObject is IHasRepeats hasRepeats)
|
||||
{
|
||||
double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
|
||||
if (spanDuration <= 0)
|
||||
// Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist.
|
||||
return EdgeType.None;
|
||||
|
||||
double spans = (time - hitObject.StartTime) / spanDuration;
|
||||
double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above.
|
||||
|
||||
if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) ||
|
||||
Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference))
|
||||
{
|
||||
return EdgeType.Repeat;
|
||||
}
|
||||
}
|
||||
|
||||
return EdgeType.None;
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateMuted : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
|
||||
: base(check, type, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
|
||||
{
|
||||
string objectName = hitobject.GetType().Name;
|
||||
if (!string.IsNullOrEmpty(postfix))
|
||||
objectName += " " + postfix;
|
||||
|
||||
return new Issue(hitobject, this, objectName, volume) { Time = time };
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateMutedActive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateMutedActive(ICheck check)
|
||||
: base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLowVolumeActive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateLowVolumeActive(ICheck check)
|
||||
: base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateMutedPassive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateMutedPassive(ICheck check)
|
||||
: base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@ -22,11 +21,11 @@ namespace osu.Game.Rulesets.Edit.Checks
|
||||
new IssueTemplateSmallUnsnap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
var controlPointInfo = playableBeatmap.ControlPointInfo;
|
||||
var controlPointInfo = context.Beatmap.ControlPointInfo;
|
||||
|
||||
foreach (var hitobject in playableBeatmap.HitObjects)
|
||||
foreach (var hitobject in context.Beatmap.HitObjects)
|
||||
{
|
||||
double startUnsnap = hitobject.StartTime - controlPointInfo.GetClosestSnappedTime(hitobject.StartTime);
|
||||
string startPostfix = hitobject is IHasDuration ? "start" : "";
|
||||
|
47
osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs
Normal file
47
osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// 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 osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckZeroLengthObjects : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration can be this low before being treated as having no length, in case of precision errors. Unit is milliseconds.
|
||||
/// </summary>
|
||||
private const double leniency = 0.5d;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Zero-length hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateZeroLength(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
foreach (var hitObject in context.Beatmap.HitObjects)
|
||||
{
|
||||
if (!(hitObject is IHasDuration hasDuration))
|
||||
continue;
|
||||
|
||||
if (hasDuration.Duration < leniency)
|
||||
yield return new IssueTemplateZeroLength(this).Create(hitObject, hasDuration.Duration);
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateZeroLength : IssueTemplate
|
||||
{
|
||||
public IssueTemplateZeroLength(ICheck check)
|
||||
: base(check, IssueType.Problem, "{0} has a duration of {1:0}.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, double duration) => new Issue(hitobject, this, hitobject.GetType(), duration);
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
@ -24,8 +23,7 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
/// <summary>
|
||||
/// Runs this check and returns any issues detected for the provided beatmap.
|
||||
/// </summary>
|
||||
/// <param name="playableBeatmap">The playable beatmap of the beatmap to run the check on.</param>
|
||||
/// <param name="workingBeatmap">The working beatmap of the beatmap to run the check on.</param>
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap);
|
||||
/// <param name="context">The beatmap verifier context associated with the beatmap.</param>
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context);
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
/// <summary> An error occurred and a complete check could not be made. </summary>
|
||||
Error,
|
||||
|
||||
// TODO: Negligible issues should be hidden by default.
|
||||
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
|
||||
Negligible,
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
// 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.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.Edit;
|
||||
@ -52,15 +54,21 @@ namespace osu.Game.Rulesets.Edit
|
||||
if (changeHandler != null)
|
||||
{
|
||||
// for now only regenerate replay on a finalised state change, not HitObjectUpdated.
|
||||
changeHandler.OnStateChange += updateReplay;
|
||||
changeHandler.OnStateChange += () => Scheduler.AddOnce(regenerateAutoplay);
|
||||
}
|
||||
else
|
||||
{
|
||||
beatmap.HitObjectUpdated += _ => updateReplay();
|
||||
beatmap.HitObjectUpdated += _ => Scheduler.AddOnce(regenerateAutoplay);
|
||||
}
|
||||
|
||||
Scheduler.AddOnce(regenerateAutoplay);
|
||||
}
|
||||
|
||||
private void updateReplay() => drawableRuleset.RegenerateAutoplay();
|
||||
private void regenerateAutoplay()
|
||||
{
|
||||
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();
|
||||
drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
|
||||
}
|
||||
|
||||
private void addHitObject(HitObject hitObject)
|
||||
{
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Input;
|
||||
@ -43,6 +44,9 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
protected readonly Ruleset Ruleset;
|
||||
|
||||
// Provides `Playfield`
|
||||
private DependencyContainer dependencies;
|
||||
|
||||
[Resolved]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
|
||||
@ -60,15 +64,20 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
private InputManager inputManager;
|
||||
|
||||
private RadioButtonCollection toolboxCollection;
|
||||
private EditorRadioButtonCollection toolboxCollection;
|
||||
|
||||
private FillFlowContainer togglesCollection;
|
||||
|
||||
private IBindable<bool> hasTiming;
|
||||
|
||||
protected HitObjectComposer(Ruleset ruleset)
|
||||
{
|
||||
Ruleset = ruleset;
|
||||
}
|
||||
|
||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) =>
|
||||
dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -88,6 +97,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
return;
|
||||
}
|
||||
|
||||
dependencies.CacheAs(Playfield);
|
||||
|
||||
const float toolbar_width = 200;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
@ -118,7 +129,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
new ToolboxGroup("toolbox (1-9)")
|
||||
{
|
||||
Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X }
|
||||
Child = toolboxCollection = new EditorRadioButtonCollection { RelativeSizeAxes = Axes.X }
|
||||
},
|
||||
new ToolboxGroup("toggles (Q~P)")
|
||||
{
|
||||
@ -152,6 +163,14 @@ namespace osu.Game.Rulesets.Edit
|
||||
base.LoadComplete();
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
hasTiming = EditorBeatmap.HasTiming.GetBoundCopy();
|
||||
hasTiming.BindValueChanged(timing =>
|
||||
{
|
||||
// it's important this is performed before the similar code in EditorRadioButton disables the button.
|
||||
if (!timing.NewValue)
|
||||
setSelectTool();
|
||||
});
|
||||
}
|
||||
|
||||
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
|
||||
@ -211,7 +230,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
if (item != null)
|
||||
{
|
||||
item.Select();
|
||||
if (!item.Selected.Disabled)
|
||||
item.Select();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -9,24 +9,23 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public abstract class OverlaySelectionBlueprint : SelectionBlueprint<HitObject>
|
||||
public abstract class HitObjectSelectionBlueprint : SelectionBlueprint<HitObject>
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="DrawableHitObject"/> which this <see cref="OverlaySelectionBlueprint"/> applies to.
|
||||
/// The <see cref="DrawableHitObject"/> which this <see cref="HitObjectSelectionBlueprint"/> applies to.
|
||||
/// </summary>
|
||||
public readonly DrawableHitObject DrawableObject;
|
||||
public DrawableHitObject DrawableObject { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the blueprint should be shown even when the <see cref="DrawableObject"/> is not alive.
|
||||
/// </summary>
|
||||
protected virtual bool AlwaysShowWhenSelected => false;
|
||||
|
||||
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
|
||||
protected override bool ShouldBeAlive => (DrawableObject?.IsAlive == true && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
|
||||
|
||||
protected OverlaySelectionBlueprint(DrawableHitObject drawableObject)
|
||||
: base(drawableObject.HitObject)
|
||||
protected HitObjectSelectionBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
{
|
||||
DrawableObject = drawableObject;
|
||||
}
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.ReceivePositionalInputAt(screenSpacePos);
|
||||
@ -35,4 +34,15 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
public override Quad SelectionQuad => DrawableObject.ScreenSpaceDrawQuad;
|
||||
}
|
||||
|
||||
public abstract class HitObjectSelectionBlueprint<T> : HitObjectSelectionBlueprint
|
||||
where T : HitObject
|
||||
{
|
||||
public T HitObject => (T)Item;
|
||||
|
||||
protected HitObjectSelectionBlueprint(T item)
|
||||
: base(item)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
@ -12,6 +11,6 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
public interface IBeatmapVerifier
|
||||
{
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap);
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
[Resolved(canBeNull: true)]
|
||||
protected EditorClock EditorClock { get; private set; }
|
||||
|
||||
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
|
||||
[Resolved]
|
||||
private EditorBeatmap beatmap { get; set; }
|
||||
|
||||
private Bindable<double> startTimeBindable;
|
||||
|
||||
@ -58,10 +59,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(IBindable<WorkingBeatmap> beatmap)
|
||||
private void load()
|
||||
{
|
||||
this.beatmap.BindTo(beatmap);
|
||||
|
||||
startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy();
|
||||
startTimeBindable.BindValueChanged(_ => ApplyDefaultsToHitObject(), true);
|
||||
}
|
||||
@ -113,7 +112,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// Invokes <see cref="Objects.HitObject.ApplyDefaults(ControlPointInfo,BeatmapDifficulty, CancellationToken)"/>,
|
||||
/// refreshing <see cref="Objects.HitObject.NestedHitObjects"/> and parameters for the <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.Value.Beatmap.ControlPointInfo, beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty);
|
||||
protected void ApplyDefaultsToHitObject() => HitObject.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty);
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent?.ReceivePositionalInputAt(screenSpacePos) ?? false;
|
||||
|
||||
|
32
osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs
Normal file
32
osu.Game/Rulesets/Edit/ScrollingToolboxGroup.cs
Normal file
@ -0,0 +1,32 @@
|
||||
// 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 osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public class ScrollingToolboxGroup : ToolboxGroup
|
||||
{
|
||||
protected readonly OsuScrollContainer Scroll;
|
||||
|
||||
protected override Container<Drawable> Content { get; }
|
||||
|
||||
public ScrollingToolboxGroup(string title, float scrollAreaHeight)
|
||||
: base(title)
|
||||
{
|
||||
base.Content.Add(Scroll = new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = scrollAreaHeight,
|
||||
Child = Content = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -105,34 +105,34 @@ namespace osu.Game.Rulesets.Edit
|
||||
protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;
|
||||
|
||||
/// <summary>
|
||||
/// Selects this <see cref="OverlaySelectionBlueprint"/>, causing it to become visible.
|
||||
/// Selects this <see cref="SelectionBlueprint{T}"/>, causing it to become visible.
|
||||
/// </summary>
|
||||
public void Select() => State = SelectionState.Selected;
|
||||
|
||||
/// <summary>
|
||||
/// Deselects this <see cref="OverlaySelectionBlueprint"/>, causing it to become invisible.
|
||||
/// Deselects this <see cref="HitObjectSelectionBlueprint"/>, causing it to become invisible.
|
||||
/// </summary>
|
||||
public void Deselect() => State = SelectionState.NotSelected;
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the selection state of this <see cref="OverlaySelectionBlueprint"/>.
|
||||
/// Toggles the selection state of this <see cref="HitObjectSelectionBlueprint"/>.
|
||||
/// </summary>
|
||||
public void ToggleSelection() => State = IsSelected ? SelectionState.NotSelected : SelectionState.Selected;
|
||||
|
||||
public bool IsSelected => State == SelectionState.Selected;
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="MenuItem"/>s to be displayed in the context menu for this <see cref="OverlaySelectionBlueprint"/>.
|
||||
/// The <see cref="MenuItem"/>s to be displayed in the context menu for this <see cref="HitObjectSelectionBlueprint"/>.
|
||||
/// </summary>
|
||||
public virtual MenuItem[] ContextMenuItems => Array.Empty<MenuItem>();
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space point that causes this <see cref="OverlaySelectionBlueprint"/> to be selected via a drag.
|
||||
/// The screen-space point that causes this <see cref="HitObjectSelectionBlueprint"/> to be selected via a drag.
|
||||
/// </summary>
|
||||
public virtual Vector2 ScreenSpaceSelectionPoint => ScreenSpaceDrawQuad.Centre;
|
||||
|
||||
/// <summary>
|
||||
/// The screen-space quad that outlines this <see cref="OverlaySelectionBlueprint"/> for selections.
|
||||
/// The screen-space quad that outlines this <see cref="HitObjectSelectionBlueprint"/> for selections.
|
||||
/// </summary>
|
||||
public virtual Quad SelectionQuad => ScreenSpaceDrawQuad;
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
@ -32,18 +31,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
|
||||
private readonly Container aboveHitObjectsContent;
|
||||
|
||||
/// <summary>
|
||||
/// Duration of initial fade in.
|
||||
/// </summary>
|
||||
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
|
||||
protected virtual double FadeInDuration => 100;
|
||||
|
||||
/// <summary>
|
||||
/// Duration to wait until fade out begins. Defaults to <see cref="FadeInDuration"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")]
|
||||
protected virtual double FadeOutDelay => FadeInDuration;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
|
||||
/// </summary>
|
||||
@ -130,7 +117,7 @@ namespace osu.Game.Rulesets.Judgements
|
||||
|
||||
LifetimeStart = Result.TimeAbsolute;
|
||||
|
||||
using (BeginAbsoluteSequence(Result.TimeAbsolute, true))
|
||||
using (BeginAbsoluteSequence(Result.TimeAbsolute))
|
||||
{
|
||||
// not sure if this should remain going forward.
|
||||
JudgementBody.ResetAnimation();
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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 osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
@ -69,14 +68,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
/// </summary>
|
||||
public double MaxHealthIncrease => HealthIncreaseFor(MaxResult);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric score representation of a <see cref="HitResult"/>.
|
||||
/// </summary>
|
||||
/// <param name="result">The <see cref="HitResult"/> to find the numeric score representation for.</param>
|
||||
/// <returns>The numeric score representation of <paramref name="result"/>.</returns>
|
||||
[Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328
|
||||
protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the numeric score representation of a <see cref="JudgementResult"/>.
|
||||
/// </summary>
|
||||
|
112
osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
Normal file
112
osu.Game/Rulesets/Mods/DifficultyAdjustSettingsControl.cs
Normal file
@ -0,0 +1,112 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class DifficultyAdjustSettingsControl : SettingsItem<float?>
|
||||
{
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Used to track the display value on the setting slider.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When the mod is overriding a default, this will match the value of <see cref="Current"/>.
|
||||
/// When there is no override (ie. <see cref="Current"/> is null), this value will match the beatmap provided default via <see cref="updateCurrentFromSlider"/>.
|
||||
/// </remarks>
|
||||
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
|
||||
|
||||
protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent);
|
||||
|
||||
/// <summary>
|
||||
/// Guards against beatmap values displayed on slider bars being transferred to user override.
|
||||
/// </summary>
|
||||
private bool isInternalChange;
|
||||
|
||||
private DifficultyBindable difficultyBindable;
|
||||
|
||||
public override Bindable<float?> Current
|
||||
{
|
||||
get => base.Current;
|
||||
set
|
||||
{
|
||||
// Intercept and extract the internal number bindable from DifficultyBindable.
|
||||
// This will provide bounds and precision specifications for the slider bar.
|
||||
difficultyBindable = ((DifficultyBindable)value).GetBoundCopy();
|
||||
sliderDisplayCurrent.BindTo(difficultyBindable.CurrentNumber);
|
||||
|
||||
base.Current = difficultyBindable;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Current.BindValueChanged(current => updateCurrentFromSlider());
|
||||
beatmap.BindValueChanged(b => updateCurrentFromSlider(), true);
|
||||
|
||||
sliderDisplayCurrent.BindValueChanged(number =>
|
||||
{
|
||||
// this handles the transfer of the slider value to the main bindable.
|
||||
// as such, should be skipped if the slider is being updated via updateFromDifficulty().
|
||||
if (!isInternalChange)
|
||||
Current.Value = number.NewValue;
|
||||
});
|
||||
}
|
||||
|
||||
private void updateCurrentFromSlider()
|
||||
{
|
||||
if (Current.Value != null)
|
||||
{
|
||||
// a user override has been added or updated.
|
||||
sliderDisplayCurrent.Value = Current.Value.Value;
|
||||
return;
|
||||
}
|
||||
|
||||
var difficulty = beatmap.Value.BeatmapInfo.BaseDifficulty;
|
||||
|
||||
if (difficulty == null)
|
||||
return;
|
||||
|
||||
// generally should always be implemented, else the slider will have a zero default.
|
||||
if (difficultyBindable.ReadCurrentFromDifficulty == null)
|
||||
return;
|
||||
|
||||
isInternalChange = true;
|
||||
sliderDisplayCurrent.Value = difficultyBindable.ReadCurrentFromDifficulty(difficulty);
|
||||
isInternalChange = false;
|
||||
}
|
||||
|
||||
private class SliderControl : CompositeDrawable, IHasCurrentValue<float?>
|
||||
{
|
||||
// This is required as SettingsItem relies heavily on this bindable for internal use.
|
||||
// The actual update flow is done via the bindable provided in the constructor.
|
||||
public Bindable<float?> Current { get; set; } = new Bindable<float?>();
|
||||
|
||||
public SliderControl(BindableNumber<float> currentNumber)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new SettingsSlider<float>
|
||||
{
|
||||
ShowsDefaultIndicator = false,
|
||||
Current = currentNumber,
|
||||
}
|
||||
};
|
||||
|
||||
AutoSizeAxes = Axes.Y;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
133
osu.Game/Rulesets/Mods/DifficultyBindable.cs
Normal file
133
osu.Game/Rulesets/Mods/DifficultyBindable.cs
Normal file
@ -0,0 +1,133 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class DifficultyBindable : Bindable<float?>
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether the extended limits should be applied to this bindable.
|
||||
/// </summary>
|
||||
public readonly BindableBool ExtendedLimits = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// An internal numeric bindable to hold and propagate min/max/precision.
|
||||
/// The value of this bindable should not be set.
|
||||
/// </summary>
|
||||
internal readonly BindableFloat CurrentNumber = new BindableFloat
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// A function that can extract the current value of this setting from a beatmap difficulty for display purposes.
|
||||
/// </summary>
|
||||
public Func<BeatmapDifficulty, float> ReadCurrentFromDifficulty;
|
||||
|
||||
public float Precision
|
||||
{
|
||||
set => CurrentNumber.Precision = value;
|
||||
}
|
||||
|
||||
public float MinValue
|
||||
{
|
||||
set => CurrentNumber.MinValue = value;
|
||||
}
|
||||
|
||||
private float maxValue;
|
||||
|
||||
public float MaxValue
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == maxValue)
|
||||
return;
|
||||
|
||||
maxValue = value;
|
||||
updateMaxValue();
|
||||
}
|
||||
}
|
||||
|
||||
private float? extendedMaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// The maximum value to be used when extended limits are applied.
|
||||
/// </summary>
|
||||
public float? ExtendedMaxValue
|
||||
{
|
||||
set
|
||||
{
|
||||
if (value == extendedMaxValue)
|
||||
return;
|
||||
|
||||
extendedMaxValue = value;
|
||||
updateMaxValue();
|
||||
}
|
||||
}
|
||||
|
||||
public DifficultyBindable()
|
||||
: this(null)
|
||||
{
|
||||
}
|
||||
|
||||
public DifficultyBindable(float? defaultValue = null)
|
||||
: base(defaultValue)
|
||||
{
|
||||
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
|
||||
}
|
||||
|
||||
public override float? Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
// Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated.
|
||||
if (value != null)
|
||||
CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value);
|
||||
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMaxValue()
|
||||
{
|
||||
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
|
||||
}
|
||||
|
||||
public override void BindTo(Bindable<float?> them)
|
||||
{
|
||||
if (!(them is DifficultyBindable otherDifficultyBindable))
|
||||
throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}.");
|
||||
|
||||
ReadCurrentFromDifficulty = otherDifficultyBindable.ReadCurrentFromDifficulty;
|
||||
|
||||
// the following max value copies are only safe as long as these values are effectively constants.
|
||||
MaxValue = otherDifficultyBindable.maxValue;
|
||||
ExtendedMaxValue = otherDifficultyBindable.extendedMaxValue;
|
||||
|
||||
ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits;
|
||||
|
||||
// the actual values need to be copied after the max value constraints.
|
||||
CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber;
|
||||
base.BindTo(them);
|
||||
}
|
||||
|
||||
public override void UnbindFrom(IUnbindable them)
|
||||
{
|
||||
if (!(them is DifficultyBindable otherDifficultyBindable))
|
||||
throw new InvalidOperationException($"Cannot unbind from a non-{nameof(DifficultyBindable)}.");
|
||||
|
||||
base.UnbindFrom(them);
|
||||
|
||||
CurrentNumber.UnbindFrom(otherDifficultyBindable.CurrentNumber);
|
||||
ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits);
|
||||
}
|
||||
|
||||
public new DifficultyBindable GetBoundCopy() => new DifficultyBindable { BindTarget = this };
|
||||
}
|
||||
}
|
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a <see cref="Mod"/> that applies changes to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
public interface IApplicableToBeatmapProcessor : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this <see cref="Mod"/> to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor);
|
||||
}
|
||||
}
|
@ -10,13 +10,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// </summary>
|
||||
public interface IApplicableToDifficulty : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when a beatmap is changed. Can be used to read default values.
|
||||
/// Any changes made will not be preserved.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The difficulty to read from.</param>
|
||||
void ReadFromDifficulty(BeatmapDifficulty difficulty);
|
||||
|
||||
/// <summary>
|
||||
/// Called post beatmap conversion. Can be used to apply changes to difficulty attributes.
|
||||
/// </summary>
|
||||
|
@ -1,7 +1,6 @@
|
||||
// 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 osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
@ -9,13 +8,12 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// <summary>
|
||||
/// An interface for <see cref="Mod"/>s that can be applied to <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public interface IApplicableToDrawableHitObjects : IApplicableMod
|
||||
public interface IApplicableToDrawableHitObject : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this <see cref="IApplicableToDrawableHitObjects"/> to a list of <see cref="DrawableHitObject"/>s.
|
||||
/// Applies this <see cref="IApplicableToDrawableHitObject"/> to a <see cref="DrawableHitObject"/>.
|
||||
/// This will only be invoked with top-level <see cref="DrawableHitObject"/>s. Access <see cref="DrawableHitObject.NestedHitObjects"/> if adjusting nested objects is necessary.
|
||||
/// </summary>
|
||||
/// <param name="drawables">The list of <see cref="DrawableHitObject"/>s to apply to.</param>
|
||||
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
|
||||
void ApplyToDrawableHitObject(DrawableHitObject drawable);
|
||||
}
|
||||
}
|
||||
|
18
osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
Normal file
18
osu.Game/Rulesets/Mods/IApplicableToDrawableHitObjects.cs
Normal file
@ -0,0 +1,18 @@
|
||||
// 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 osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
[Obsolete(@"Use the singular version IApplicableToDrawableHitObject instead.")] // Can be removed 20211216
|
||||
public interface IApplicableToDrawableHitObjects : IApplicableToDrawableHitObject
|
||||
{
|
||||
void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables);
|
||||
|
||||
void IApplicableToDrawableHitObject.ApplyToDrawableHitObject(DrawableHitObject drawable) => ApplyToDrawableHitObjects(drawable.Yield());
|
||||
}
|
||||
}
|
14
osu.Game/Rulesets/Mods/ICreateReplay.cs
Normal file
14
osu.Game/Rulesets/Mods/ICreateReplay.cs
Normal file
@ -0,0 +1,14 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public interface ICreateReplay
|
||||
{
|
||||
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
|
||||
}
|
||||
}
|
12
osu.Game/Rulesets/Mods/IHasSeed.cs
Normal file
12
osu.Game/Rulesets/Mods/IHasSeed.cs
Normal file
@ -0,0 +1,12 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public interface IHasSeed
|
||||
{
|
||||
Bindable<int?> Seed { get; }
|
||||
}
|
||||
}
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// The base class for gameplay modifiers.
|
||||
/// </summary>
|
||||
[ExcludeFromDynamicCompile]
|
||||
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable
|
||||
public abstract class Mod : IMod, IEquatable<Mod>, IJsonSerializable, IDeepCloneable<Mod>
|
||||
{
|
||||
/// <summary>
|
||||
/// The name of this mod.
|
||||
@ -108,9 +108,13 @@ namespace osu.Game.Rulesets.Mods
|
||||
public virtual bool HasImplementation => this is IApplicableMod;
|
||||
|
||||
/// <summary>
|
||||
/// Returns if this mod is ranked.
|
||||
/// Whether this mod is playable by an end user.
|
||||
/// Should be <c>false</c> for cases where the user is not interacting with the game (so it can be excluded from mutliplayer selection, for example).
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public virtual bool UserPlayable => true;
|
||||
|
||||
[Obsolete("Going forward, the concept of \"ranked\" doesn't exist. The only exceptions are automation mods, which should now override and set UserPlayable to false.")] // Can be removed 20211009
|
||||
public virtual bool Ranked => false;
|
||||
|
||||
/// <summary>
|
||||
@ -128,7 +132,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// <summary>
|
||||
/// Creates a copy of this <see cref="Mod"/> initialised to a default state.
|
||||
/// </summary>
|
||||
public virtual Mod CreateCopy()
|
||||
public virtual Mod DeepClone()
|
||||
{
|
||||
var result = (Mod)Activator.CreateInstance(GetType());
|
||||
result.CopyFrom(this);
|
||||
|
@ -7,22 +7,11 @@ using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T>
|
||||
where T : HitObject
|
||||
{
|
||||
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
|
||||
{
|
||||
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class ModAutoplay : Mod, IApplicableFailOverride
|
||||
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
|
||||
{
|
||||
public override string Name => "Autoplay";
|
||||
public override string Acronym => "AT";
|
||||
@ -35,6 +24,8 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public bool RestartOnFail => false;
|
||||
|
||||
public override bool UserPlayable => false;
|
||||
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) };
|
||||
|
||||
public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0;
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>();
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
|
@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
public override string Description => "Feeling nostalgic?";
|
||||
|
||||
public override bool Ranked => false;
|
||||
|
||||
public override ModType Type => ModType.Conversion;
|
||||
}
|
||||
}
|
||||
|
@ -1,13 +1,12 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -33,24 +32,24 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
protected const int LAST_SETTING_ORDER = 2;
|
||||
|
||||
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
|
||||
public BindableNumber<float> DrainRate { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable DrainRate { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.DrainRate,
|
||||
};
|
||||
|
||||
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
|
||||
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloatWithLimitExtension
|
||||
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
|
||||
public DifficultyBindable OverallDifficulty { get; } = new DifficultyBindable
|
||||
{
|
||||
Precision = 0.1f,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Default = 5,
|
||||
Value = 5,
|
||||
ExtendedMaxValue = 11,
|
||||
ReadCurrentFromDifficulty = diff => diff.OverallDifficulty,
|
||||
};
|
||||
|
||||
[SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
|
||||
@ -58,17 +57,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
|
||||
protected ModDifficultyAdjust()
|
||||
{
|
||||
ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Changes the difficulty adjustment limits. Occurs when the value of <see cref="ExtendedLimits"/> is changed.
|
||||
/// </summary>
|
||||
/// <param name="extended">Whether limits should extend beyond sane ranges.</param>
|
||||
protected virtual void ApplyLimits(bool extended)
|
||||
{
|
||||
DrainRate.MaxValue = extended ? 11 : 10;
|
||||
OverallDifficulty.MaxValue = extended ? 11 : 10;
|
||||
foreach (var (_, property) in this.GetOrderedSettingsSourceProperties())
|
||||
{
|
||||
if (property.GetValue(this) is DifficultyBindable diffAdjustBindable)
|
||||
diffAdjustBindable.ExtendedLimits.BindTo(ExtendedLimits);
|
||||
}
|
||||
}
|
||||
|
||||
public override string SettingDescription
|
||||
@ -86,146 +79,20 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
private BeatmapDifficulty difficulty;
|
||||
|
||||
public void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
{
|
||||
if (this.difficulty == null || this.difficulty.ID != difficulty.ID)
|
||||
{
|
||||
TransferSettings(difficulty);
|
||||
this.difficulty = difficulty;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyToDifficulty(BeatmapDifficulty difficulty) => ApplySettings(difficulty);
|
||||
|
||||
/// <summary>
|
||||
/// Transfer initial settings from the beatmap to settings.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap's initial values.</param>
|
||||
protected virtual void TransferSettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
TransferSetting(DrainRate, difficulty.DrainRate);
|
||||
TransferSetting(OverallDifficulty, difficulty.OverallDifficulty);
|
||||
}
|
||||
|
||||
private readonly Dictionary<IBindable, bool> userChangedSettings = new Dictionary<IBindable, bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Transfer a setting from <see cref="BeatmapDifficulty"/> to a configuration bindable.
|
||||
/// Only performs the transfer if the user is not currently overriding.
|
||||
/// </summary>
|
||||
protected void TransferSetting<T>(BindableNumber<T> bindable, T beatmapDefault)
|
||||
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
|
||||
{
|
||||
bindable.UnbindEvents();
|
||||
|
||||
userChangedSettings.TryAdd(bindable, false);
|
||||
|
||||
bindable.Default = beatmapDefault;
|
||||
|
||||
// users generally choose a difficulty setting and want it to stick across multiple beatmap changes.
|
||||
// we only want to value transfer if the user hasn't changed the value previously.
|
||||
if (!userChangedSettings[bindable])
|
||||
bindable.Value = beatmapDefault;
|
||||
|
||||
bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault;
|
||||
}
|
||||
|
||||
internal override void CopyAdjustedSetting(IBindable target, object source)
|
||||
{
|
||||
// if the value is non-bindable, it's presumably coming from an external source (like the API) - therefore presume it is not default.
|
||||
// if the value is bindable, defer to the source's IsDefault to be able to tell.
|
||||
userChangedSettings[target] = !(source is IBindable bindableSource) || !bindableSource.IsDefault;
|
||||
base.CopyAdjustedSetting(target, source);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a setting from a configuration bindable using <paramref name="applyFunc"/>, if it has been changed by the user.
|
||||
/// </summary>
|
||||
protected void ApplySetting<T>(BindableNumber<T> setting, Action<T> applyFunc)
|
||||
where T : struct, IComparable<T>, IConvertible, IEquatable<T>
|
||||
{
|
||||
if (userChangedSettings.TryGetValue(setting, out bool userChangedSetting) && userChangedSetting)
|
||||
applyFunc.Invoke(setting.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply all custom settings to the provided beatmap.
|
||||
/// </summary>
|
||||
/// <param name="difficulty">The beatmap to have settings applied.</param>
|
||||
protected virtual void ApplySettings(BeatmapDifficulty difficulty)
|
||||
{
|
||||
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
|
||||
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
|
||||
}
|
||||
|
||||
public override void ResetSettingsToDefaults()
|
||||
{
|
||||
base.ResetSettingsToDefaults();
|
||||
|
||||
if (difficulty != null)
|
||||
{
|
||||
// base implementation potentially overwrite modified defaults that came from a beatmap selection.
|
||||
TransferSettings(difficulty);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableDouble"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableDoubleWithLimitExtension : BindableDouble
|
||||
{
|
||||
public override double Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableFloat"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableFloatWithLimitExtension : BindableFloat
|
||||
{
|
||||
public override float Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A <see cref="BindableInt"/> that extends its min/max values to support any assigned value.
|
||||
/// </summary>
|
||||
protected class BindableIntWithLimitExtension : BindableInt
|
||||
{
|
||||
public override int Value
|
||||
{
|
||||
get => base.Value;
|
||||
set
|
||||
{
|
||||
if (value < MinValue)
|
||||
MinValue = value;
|
||||
if (value > MaxValue)
|
||||
MaxValue = value;
|
||||
base.Value = value;
|
||||
}
|
||||
}
|
||||
if (DrainRate.Value != null) difficulty.DrainRate = DrainRate.Value.Value;
|
||||
if (OverallDifficulty.Value != null) difficulty.OverallDifficulty = OverallDifficulty.Value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModDoubletime;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Zoooooooooom...";
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModHalfTime)).ToArray();
|
||||
|
||||
|
@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModEasy;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override bool Ranked => true;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) };
|
||||
|
||||
public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty)
|
||||
|
@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModFlashlight;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Restricted view area.";
|
||||
public override bool Ranked => true;
|
||||
|
||||
internal ModFlashlight()
|
||||
{
|
||||
|
@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.ModHalftime;
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override string Description => "Less zoom...";
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModDoubleTime)).ToArray();
|
||||
|
||||
|
@ -1,9 +1,7 @@
|
||||
// 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 osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@ -16,15 +14,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "HD";
|
||||
public override IconUsage? Icon => OsuIcon.ModHidden;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override bool Ranked => true;
|
||||
|
||||
/// <summary>
|
||||
/// Check whether the provided hitobject should be considered the "first" hideable object.
|
||||
/// Can be used to skip spinners, for instance.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hitobject to check.</param>
|
||||
[Obsolete("Use IsFirstAdjustableObject() instead.")] // Can be removed 20210506
|
||||
protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true;
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
@ -46,39 +35,5 @@ namespace osu.Game.Rulesets.Mods
|
||||
return rank;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
ApplyFirstObjectIncreaseVisibilityState(hitObject, state);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
#pragma warning disable 618
|
||||
ApplyHiddenState(hitObject, state);
|
||||
#pragma warning restore 618
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a special visibility state to the first object in a beatmap, if the user chooses to turn on the "increase first object visibility" setting.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hit object to apply the state change to.</param>
|
||||
/// <param name="state">The state of the hit object.</param>
|
||||
[Obsolete("Use ApplyIncreasedVisibilityState() instead.")] // Can be removed 20210506
|
||||
protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply a hidden state to the provided object.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hit object to apply the state change to.</param>
|
||||
/// <param name="state">The state of the hit object.</param>
|
||||
[Obsolete("Use ApplyNormalVisibilityState() instead.")] // Can be removed 20210506
|
||||
protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyReduction;
|
||||
public override string Description => "You can't fail, no matter what.";
|
||||
public override double ScoreMultiplier => 0.5;
|
||||
public override bool Ranked => true;
|
||||
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) };
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override string Acronym => "PF";
|
||||
public override IconUsage? Icon => OsuIcon.ModPerfect;
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override bool Ranked => true;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override string Description => "SS or quit.";
|
||||
|
||||
|
@ -1,17 +1,27 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModRandom : Mod
|
||||
public abstract class ModRandom : Mod, IHasSeed
|
||||
{
|
||||
public override string Name => "Random";
|
||||
public override string Acronym => "RD";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override IconUsage? Icon => OsuIcon.Dice;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
|
||||
public Bindable<int?> Seed { get; } = new Bindable<int?>
|
||||
{
|
||||
Default = null,
|
||||
Value = null
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.DifficultyIncrease;
|
||||
public override string Description => "Miss and fail.";
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override bool Ranked => true;
|
||||
|
||||
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray();
|
||||
|
||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
/// A <see cref="Mod"/> which applies visibility adjustments to <see cref="DrawableHitObject"/>s
|
||||
/// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting.
|
||||
/// </summary>
|
||||
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects
|
||||
public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The first adjustable object.
|
||||
@ -73,19 +73,16 @@ namespace osu.Game.Rulesets.Mods
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
public virtual void ApplyToDrawableHitObject(DrawableHitObject dho)
|
||||
{
|
||||
foreach (var dho in drawables)
|
||||
dho.ApplyCustomUpdateState += (o, state) =>
|
||||
{
|
||||
dho.ApplyCustomUpdateState += (o, state) =>
|
||||
{
|
||||
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
|
||||
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
|
||||
ApplyIncreasedVisibilityState(o, state);
|
||||
else
|
||||
ApplyNormalVisibilityState(o, state);
|
||||
};
|
||||
}
|
||||
// Increased visibility is applied to the entire first object, including all of its nested hitobjects.
|
||||
if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject))
|
||||
ApplyIncreasedVisibilityState(o, state);
|
||||
else
|
||||
ApplyNormalVisibilityState(o, state);
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
Mods = mods;
|
||||
}
|
||||
|
||||
public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray());
|
||||
public override Mod DeepClone() => new MultiMod(Mods.Select(m => m.DeepClone()).ToArray());
|
||||
|
||||
public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray();
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Audio;
|
||||
@ -157,10 +156,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// If <c>null</c>, a hitobject is expected to be later applied via <see cref="PoolableDrawableWithLifetime{TEntry}.Apply"/> (or automatically via pooling).
|
||||
/// </param>
|
||||
protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null)
|
||||
: base(initialHitObject != null ? new SyntheticHitObjectEntry(initialHitObject) : null)
|
||||
{
|
||||
if (Entry != null)
|
||||
ensureEntryHasResult();
|
||||
if (initialHitObject == null) return;
|
||||
|
||||
Entry = new SyntheticHitObjectEntry(initialHitObject);
|
||||
ensureEntryHasResult();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -172,7 +172,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
base.AddInternal(Samples = new PausableSkinnableSound());
|
||||
|
||||
CurrentSkin = skinSource;
|
||||
CurrentSkin.SourceChanged += onSkinSourceChanged;
|
||||
CurrentSkin.SourceChanged += skinSourceChanged;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
skinChanged();
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
@ -187,7 +193,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
/// <summary>
|
||||
/// Applies a hit object to be represented by this <see cref="DrawableHitObject"/>.
|
||||
/// </summary>
|
||||
[Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")]
|
||||
[Obsolete("Use either overload of Apply that takes a single argument of type HitObject or HitObjectLifetimeEntry")] // Can be removed 20211021.
|
||||
public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry)
|
||||
{
|
||||
if (lifetimeEntry != null)
|
||||
@ -305,6 +311,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Invoked for this <see cref="DrawableHitObject"/> to take on any values from a newly-applied <see cref="HitObject"/>.
|
||||
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
|
||||
/// </summary>
|
||||
protected virtual void OnApply()
|
||||
{
|
||||
@ -312,6 +319,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Invoked for this <see cref="DrawableHitObject"/> to revert any values previously taken on from the currently-applied <see cref="HitObject"/>.
|
||||
/// This is also fired after any changes which occurred via an <see cref="osu.Game.Rulesets.Objects.HitObject.ApplyDefaults"/> call.
|
||||
/// </summary>
|
||||
protected virtual void OnFree()
|
||||
{
|
||||
@ -396,18 +404,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
clearExistingStateTransforms();
|
||||
|
||||
using (BeginAbsoluteSequence(transformTime, true))
|
||||
using (BeginAbsoluteSequence(transformTime))
|
||||
UpdateInitialTransforms();
|
||||
|
||||
using (BeginAbsoluteSequence(StateUpdateTime, true))
|
||||
using (BeginAbsoluteSequence(StateUpdateTime))
|
||||
UpdateStartTimeStateTransforms();
|
||||
|
||||
#pragma warning disable 618
|
||||
using (BeginAbsoluteSequence(StateUpdateTime + (Result?.TimeOffset ?? 0), true))
|
||||
UpdateStateTransforms(newState);
|
||||
#pragma warning restore 618
|
||||
|
||||
using (BeginAbsoluteSequence(HitStateUpdateTime, true))
|
||||
using (BeginAbsoluteSequence(HitStateUpdateTime))
|
||||
UpdateHitStateTransforms(newState);
|
||||
|
||||
state.Value = newState;
|
||||
@ -437,12 +440,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
/// <summary>
|
||||
/// Apply (generally fade-in) transforms leading into the <see cref="HitObject"/> start time.
|
||||
/// The local drawable hierarchy is recursively delayed to <see cref="LifetimeEntry.LifetimeStart"/> for convenience.
|
||||
///
|
||||
/// By default this will fade in the object from zero with no duration.
|
||||
/// By default, this will fade in the object from zero with no duration.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is called once before every <see cref="UpdateStateTransforms"/>. This is to ensure a good state in the case
|
||||
/// This is called once before every <see cref="UpdateHitStateTransforms"/>. This is to ensure a good state in the case
|
||||
/// the <see cref="JudgementResult.TimeOffset"/> was negative and potentially altered the pre-hit transforms.
|
||||
/// </remarks>
|
||||
protected virtual void UpdateInitialTransforms()
|
||||
@ -450,16 +451,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
this.FadeInFromZero();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply transforms based on the current <see cref="ArmedState"/>. Previous states are automatically cleared.
|
||||
/// In the case of a non-idle <see cref="ArmedState"/>, and if <see cref="Drawable.LifetimeEnd"/> was not set during this call, <see cref="Drawable.Expire"/> will be invoked.
|
||||
/// </summary>
|
||||
/// <param name="state">The new armed state.</param>
|
||||
[Obsolete("Use UpdateStartTimeStateTransforms and UpdateHitStateTransforms instead")] // Can be removed 20210504
|
||||
protected virtual void UpdateStateTransforms(ArmedState state)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply passive transforms at the <see cref="HitObject"/>'s StartTime.
|
||||
/// This is called each time <see cref="State"/> changes.
|
||||
@ -495,7 +486,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
protected ISkinSource CurrentSkin { get; private set; }
|
||||
|
||||
private void onSkinSourceChanged() => Scheduler.AddOnce(() =>
|
||||
private void skinSourceChanged() => Scheduler.AddOnce(skinChanged);
|
||||
|
||||
private void skinChanged()
|
||||
{
|
||||
UpdateComboColour();
|
||||
|
||||
@ -503,7 +496,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
if (IsLoaded)
|
||||
updateState(State.Value, true);
|
||||
});
|
||||
}
|
||||
|
||||
protected void UpdateComboColour()
|
||||
{
|
||||
@ -512,23 +505,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
AccentColour.Value = combo.GetComboColour(CurrentSkin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to retrieve the combo colour. Automatically assigned to <see cref="AccentColour"/>.
|
||||
/// Defaults to using <see cref="IHasComboInformation.ComboIndex"/> to decide on a colour.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will only be called if the <see cref="HitObject"/> implements <see cref="IHasComboInformation"/>.
|
||||
/// </remarks>
|
||||
/// <param name="comboColours">A list of combo colours provided by the beatmap or skin. Can be null if not available.</param>
|
||||
[Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527
|
||||
protected virtual Color4 GetComboColour(IReadOnlyList<Color4> comboColours)
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation combo))
|
||||
throw new InvalidOperationException($"{nameof(HitObject)} must implement {nameof(IHasComboInformation)}");
|
||||
|
||||
return comboColours?.Count > 0 ? comboColours[combo.ComboIndex % comboColours.Count] : Color4.White;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when a change is made to the skin.
|
||||
/// </summary>
|
||||
@ -612,23 +588,17 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action);
|
||||
|
||||
/// <summary>
|
||||
/// A safe offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
|
||||
/// An offset prior to the start time of <see cref="HitObject"/> at which this <see cref="DrawableHitObject"/> may begin displaying contents.
|
||||
/// By default, <see cref="DrawableHitObject"/>s are assumed to display their contents within 10 seconds prior to the start time of <see cref="HitObject"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only used as an optimisation to delay the initial update of this <see cref="DrawableHitObject"/> and may be tuned more aggressively if required.
|
||||
/// It is indirectly used to decide the automatic transform offset provided to <see cref="UpdateInitialTransforms"/>.
|
||||
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="LoadComplete"/>, for example).
|
||||
/// <para>
|
||||
/// Only has an effect if this <see cref="DrawableHitObject"/> is not being pooled.
|
||||
/// For pooled <see cref="DrawableHitObject"/>s, use <see cref="HitObjectLifetimeEntry.InitialLifetimeOffset"/> instead.
|
||||
/// </para>
|
||||
/// The initial transformation (<see cref="UpdateInitialTransforms"/>) starts at this offset before the start time of <see cref="HitObject"/>.
|
||||
/// </remarks>
|
||||
protected virtual double InitialLifetimeOffset => 10000;
|
||||
|
||||
/// <summary>
|
||||
/// The time at which state transforms should be applied that line up to <see cref="HitObject"/>'s StartTime.
|
||||
/// This is used to offset calls to <see cref="UpdateStateTransforms"/>.
|
||||
/// This is used to offset calls to <see cref="UpdateStartTimeStateTransforms"/>.
|
||||
/// </summary>
|
||||
public double StateUpdateTime => HitObject.StartTime;
|
||||
|
||||
@ -746,7 +716,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
if (HitObject != null)
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
CurrentSkin.SourceChanged -= onSkinSourceChanged;
|
||||
if (CurrentSkin != null)
|
||||
CurrentSkin.SourceChanged -= skinSourceChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,31 +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 osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// An interface that exposes properties required for scrolling hit objects to be properly displayed.
|
||||
/// </summary>
|
||||
internal interface IScrollingHitObject : IDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Time offset before the hit object start time at which this <see cref="IScrollingHitObject"/> becomes visible and the time offset
|
||||
/// after the hit object's end time after which it expires.
|
||||
///
|
||||
/// <para>
|
||||
/// This provides only a default life time range, however classes inheriting from <see cref="IScrollingHitObject"/> should override
|
||||
/// their life times if more tight control is desired.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
BindableDouble LifetimeOffset { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Axes which this <see cref="IScrollingHitObject"/> will scroll through.
|
||||
/// This is set by the container which this scrolls through.
|
||||
/// </summary>
|
||||
Axes ScrollingAxes { set; }
|
||||
}
|
||||
}
|
@ -35,7 +35,11 @@ namespace osu.Game.Rulesets.Objects
|
||||
HitObject = hitObject;
|
||||
|
||||
startTimeBindable.BindTo(HitObject.StartTimeBindable);
|
||||
startTimeBindable.BindValueChanged(onStartTimeChanged, true);
|
||||
startTimeBindable.BindValueChanged(_ => SetInitialLifetime(), true);
|
||||
|
||||
// Subscribe to this event before the DrawableHitObject so that the local callback is invoked before the entry is re-applied as a result of DefaultsApplied.
|
||||
// This way, the DrawableHitObject can use OnApply() to overwrite the LifetimeStart that was set inside setInitialLifetime().
|
||||
HitObject.DefaultsApplied += _ => SetInitialLifetime();
|
||||
}
|
||||
|
||||
// The lifetime, as set by the hitobject.
|
||||
@ -82,15 +86,14 @@ namespace osu.Game.Rulesets.Objects
|
||||
/// By default, <see cref="HitObject"/>s are assumed to display their contents within 10 seconds prior to their start time.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is only used as an optimisation to delay the initial update of the <see cref="HitObject"/> and may be tuned more aggressively if required.
|
||||
/// It is indirectly used to decide the automatic transform offset provided to <see cref="DrawableHitObject.UpdateInitialTransforms"/>.
|
||||
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set for further optimisation (in <see cref="DrawableHitObject.LoadComplete"/>, for example).
|
||||
/// This is only used as an optimisation to delay the initial application of the <see cref="HitObject"/> to a <see cref="DrawableHitObject"/>.
|
||||
/// A more accurate <see cref="LifetimeEntry.LifetimeStart"/> should be set on the hit object application, for further optimisation.
|
||||
/// </remarks>
|
||||
protected virtual double InitialLifetimeOffset => 10000;
|
||||
|
||||
/// <summary>
|
||||
/// Resets <see cref="LifetimeEntry.LifetimeStart"/> according to the change in start time of the <see cref="HitObject"/>.
|
||||
/// Set <see cref="LifetimeEntry.LifetimeStart"/> using <see cref="InitialLifetimeOffset"/>.
|
||||
/// </summary>
|
||||
private void onStartTimeChanged(ValueChangedEvent<double> startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
internal void SetInitialLifetime() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset;
|
||||
}
|
||||
}
|
||||
|
@ -2,15 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!catch Hit-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertHit : ConvertHitObject, IHasCombo, IHasXPosition
|
||||
internal sealed class ConvertHit : ConvertHitObject, IHasPosition, IHasCombo
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float X => Position.X;
|
||||
|
||||
public float Y => Position.Y;
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public bool NewCombo { get; set; }
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
|
||||
return new ConvertHit
|
||||
{
|
||||
X = position.X,
|
||||
Position = position,
|
||||
NewCombo = newCombo,
|
||||
};
|
||||
}
|
||||
@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
|
||||
return new ConvertSlider
|
||||
{
|
||||
X = position.X,
|
||||
Position = position,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
NodeSamples = nodeSamples,
|
||||
|
@ -2,15 +2,20 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
{
|
||||
/// <summary>
|
||||
/// Legacy osu!catch Slider-type, used for parsing Beatmaps.
|
||||
/// </summary>
|
||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo
|
||||
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasPosition, IHasCombo
|
||||
{
|
||||
public float X { get; set; }
|
||||
public float X => Position.X;
|
||||
|
||||
public float Y => Position.Y;
|
||||
|
||||
public Vector2 Position { get; set; }
|
||||
|
||||
public bool NewCombo { get; set; }
|
||||
}
|
||||
|
@ -3,7 +3,9 @@
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
|
||||
@ -15,27 +17,59 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// <typeparam name="TEntry">The <see cref="LifetimeEntry"/> type storing state and controlling this drawable.</typeparam>
|
||||
public abstract class PoolableDrawableWithLifetime<TEntry> : PoolableDrawable where TEntry : LifetimeEntry
|
||||
{
|
||||
private TEntry? entry;
|
||||
|
||||
/// <summary>
|
||||
/// The entry holding essential state of this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// </summary>
|
||||
protected TEntry? Entry { get; private set; }
|
||||
/// <remarks>
|
||||
/// If a non-null value is set before loading is started, the entry is applied when the loading is completed.
|
||||
/// It is not valid to set an entry while this <see cref="PoolableDrawableWithLifetime{TEntry}"/> is loading.
|
||||
/// </remarks>
|
||||
public TEntry? Entry
|
||||
{
|
||||
get => entry;
|
||||
set
|
||||
{
|
||||
if (LoadState == LoadState.NotLoaded)
|
||||
entry = value;
|
||||
else if (value != null)
|
||||
Apply(value);
|
||||
else if (HasEntryApplied)
|
||||
free();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether <see cref="Entry"/> is applied to this <see cref="PoolableDrawableWithLifetime{TEntry}"/>.
|
||||
/// When an initial entry is specified in the constructor, <see cref="Entry"/> is set but not applied until loading is completed.
|
||||
/// When an <see cref="Entry"/> is set during initialization, it is not applied until loading is completed.
|
||||
/// </summary>
|
||||
protected bool HasEntryApplied { get; private set; }
|
||||
|
||||
public override double LifetimeStart
|
||||
{
|
||||
get => base.LifetimeStart;
|
||||
set => setLifetime(value, LifetimeEnd);
|
||||
set
|
||||
{
|
||||
if (Entry == null && LifetimeStart != value)
|
||||
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
|
||||
|
||||
if (Entry != null)
|
||||
Entry.LifetimeStart = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override double LifetimeEnd
|
||||
{
|
||||
get => base.LifetimeEnd;
|
||||
set => setLifetime(LifetimeStart, value);
|
||||
set
|
||||
{
|
||||
if (Entry == null && LifetimeEnd != value)
|
||||
throw new InvalidOperationException($"Cannot modify lifetime of {nameof(PoolableDrawableWithLifetime<TEntry>)} when entry is not set");
|
||||
|
||||
if (Entry != null)
|
||||
Entry.LifetimeEnd = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool RemoveWhenNotAlive => false;
|
||||
@ -50,9 +84,9 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
base.LoadAsyncComplete();
|
||||
|
||||
// Apply the initial entry given in the constructor.
|
||||
// Apply the initial entry.
|
||||
if (Entry != null && !HasEntryApplied)
|
||||
Apply(Entry);
|
||||
apply(Entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -61,15 +95,10 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
/// </summary>
|
||||
public void Apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
if (LoadState == LoadState.Loading)
|
||||
throw new InvalidOperationException($"Cannot apply a new {nameof(TEntry)} while currently loading.");
|
||||
|
||||
setLifetime(entry.LifetimeStart, entry.LifetimeEnd);
|
||||
Entry = entry;
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
apply(entry);
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
@ -95,16 +124,18 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
}
|
||||
|
||||
private void setLifetime(double start, double end)
|
||||
private void apply(TEntry entry)
|
||||
{
|
||||
base.LifetimeStart = start;
|
||||
base.LifetimeEnd = end;
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
if (Entry != null)
|
||||
{
|
||||
Entry.LifetimeStart = start;
|
||||
Entry.LifetimeEnd = end;
|
||||
}
|
||||
this.entry = entry;
|
||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||
setLifetimeFromEntry(entry);
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
private void free()
|
||||
@ -113,10 +144,19 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
|
||||
OnFree(Entry);
|
||||
|
||||
Entry = null;
|
||||
setLifetime(double.MaxValue, double.MaxValue);
|
||||
Entry.LifetimeChanged -= setLifetimeFromEntry;
|
||||
entry = null;
|
||||
base.LifetimeStart = double.MinValue;
|
||||
base.LifetimeEnd = double.MaxValue;
|
||||
|
||||
HasEntryApplied = false;
|
||||
}
|
||||
|
||||
private void setLifetimeFromEntry(LifetimeEntry entry)
|
||||
{
|
||||
Debug.Assert(entry == Entry);
|
||||
base.LifetimeStart = entry.LifetimeStart;
|
||||
base.LifetimeEnd = entry.LifetimeEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,163 @@
|
||||
// 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.
|
||||
|
||||
#nullable enable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
/// <summary>
|
||||
/// A container of <typeparamref name="TDrawable"/>s dynamically added/removed by model <typeparamref name="TEntry"/>s.
|
||||
/// When an entry became alive, a drawable corresponding to the entry is obtained (potentially pooled), and added to this container.
|
||||
/// The drawable is removed when the entry became dead.
|
||||
/// </summary>
|
||||
/// <typeparam name="TEntry">The type of entries managed by this container.</typeparam>
|
||||
/// <typeparam name="TDrawable">The type of drawables corresponding to the entries.</typeparam>
|
||||
public abstract class PooledDrawableWithLifetimeContainer<TEntry, TDrawable> : CompositeDrawable
|
||||
where TEntry : LifetimeEntry
|
||||
where TDrawable : Drawable
|
||||
{
|
||||
/// <summary>
|
||||
/// All entries added to this container, including dead entries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The enumeration order is undefined.
|
||||
/// </remarks>
|
||||
public IEnumerable<TEntry> Entries => allEntries;
|
||||
|
||||
/// <summary>
|
||||
/// All alive entries and drawables corresponding to the entries.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The enumeration order is undefined.
|
||||
/// </remarks>
|
||||
public IEnumerable<(TEntry Entry, TDrawable Drawable)> AliveEntries => aliveDrawableMap.Select(x => (x.Key, x.Value));
|
||||
|
||||
/// <summary>
|
||||
/// Whether to remove an entry when clock goes backward and crossed its <see cref="LifetimeEntry.LifetimeStart"/>.
|
||||
/// Used when entries are dynamically added at its <see cref="LifetimeEntry.LifetimeStart"/> to prevent duplicated entries.
|
||||
/// </summary>
|
||||
protected virtual bool RemoveRewoundEntry => false;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time prior to the current time within which entries should be considered alive.
|
||||
/// </summary>
|
||||
internal double PastLifetimeExtension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time after the current time within which entries should be considered alive.
|
||||
/// </summary>
|
||||
internal double FutureLifetimeExtension { get; set; }
|
||||
|
||||
private readonly Dictionary<TEntry, TDrawable> aliveDrawableMap = new Dictionary<TEntry, TDrawable>();
|
||||
private readonly HashSet<TEntry> allEntries = new HashSet<TEntry>();
|
||||
|
||||
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||
|
||||
protected PooledDrawableWithLifetimeContainer()
|
||||
{
|
||||
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||
lifetimeManager.EntryCrossedBoundary += entryCrossedBoundary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a <typeparamref name="TEntry"/> to be managed by this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The aliveness of the entry is not updated until <see cref="CheckChildrenLife"/>.
|
||||
/// </remarks>
|
||||
public virtual void Add(TEntry entry)
|
||||
{
|
||||
allEntries.Add(entry);
|
||||
lifetimeManager.AddEntry(entry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a <typeparamref name="TEntry"/> from this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the entry was alive, the corresponding drawable is removed.
|
||||
/// </remarks>
|
||||
/// <returns>Whether the entry was in this container.</returns>
|
||||
public virtual bool Remove(TEntry entry)
|
||||
{
|
||||
if (!lifetimeManager.RemoveEntry(entry)) return false;
|
||||
|
||||
allEntries.Remove(entry);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize new <typeparamref name="TDrawable"/> corresponding <paramref name="entry"/>.
|
||||
/// </summary>
|
||||
/// <returns>The <typeparamref name="TDrawable"/> corresponding to the entry.</returns>
|
||||
protected abstract TDrawable GetDrawable(TEntry entry);
|
||||
|
||||
private void entryBecameAlive(LifetimeEntry lifetimeEntry)
|
||||
{
|
||||
var entry = (TEntry)lifetimeEntry;
|
||||
Debug.Assert(!aliveDrawableMap.ContainsKey(entry));
|
||||
|
||||
TDrawable drawable = GetDrawable(entry);
|
||||
aliveDrawableMap[entry] = drawable;
|
||||
AddDrawable(entry, drawable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> to this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Invoked when the entry became alive and a <typeparamref name="TDrawable"/> is obtained by <see cref="GetDrawable"/>.
|
||||
/// </remarks>
|
||||
protected virtual void AddDrawable(TEntry entry, TDrawable drawable) => AddInternal(drawable);
|
||||
|
||||
private void entryBecameDead(LifetimeEntry lifetimeEntry)
|
||||
{
|
||||
var entry = (TEntry)lifetimeEntry;
|
||||
Debug.Assert(aliveDrawableMap.ContainsKey(entry));
|
||||
|
||||
TDrawable drawable = aliveDrawableMap[entry];
|
||||
aliveDrawableMap.Remove(entry);
|
||||
RemoveDrawable(entry, drawable);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove a <typeparamref name="TDrawable"/> corresponding to <paramref name="entry"/> from this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Invoked when the entry became dead.
|
||||
/// </remarks>
|
||||
protected virtual void RemoveDrawable(TEntry entry, TDrawable drawable) => RemoveInternal(drawable);
|
||||
|
||||
private void entryCrossedBoundary(LifetimeEntry lifetimeEntry, LifetimeBoundaryKind kind, LifetimeBoundaryCrossingDirection direction)
|
||||
{
|
||||
if (RemoveRewoundEntry && kind == LifetimeBoundaryKind.Start && direction == LifetimeBoundaryCrossingDirection.Backward)
|
||||
Remove((TEntry)lifetimeEntry);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Remove all <typeparamref name="TEntry"/>s.
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
foreach (var entry in Entries.ToArray())
|
||||
Remove(entry);
|
||||
|
||||
Debug.Assert(aliveDrawableMap.Count == 0);
|
||||
}
|
||||
|
||||
protected override bool CheckChildrenLife()
|
||||
{
|
||||
bool aliveChanged = base.CheckChildrenLife();
|
||||
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
|
||||
return aliveChanged;
|
||||
}
|
||||
}
|
||||
}
|
19
osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs
Normal file
19
osu.Game/Rulesets/Objects/Types/IHasDisplayColour.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// 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 osu.Framework.Bindables;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A HitObject which has a preferred display colour. Will be used for editor timeline display.
|
||||
/// </summary>
|
||||
public interface IHasDisplayColour
|
||||
{
|
||||
/// <summary>
|
||||
/// The current display colour of this hit object.
|
||||
/// </summary>
|
||||
Bindable<Color4> DisplayColour { get; }
|
||||
}
|
||||
}
|
@ -1,40 +1,36 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays
|
||||
{
|
||||
public abstract class AutoGenerator : IAutoGenerator
|
||||
public abstract class AutoGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the auto replay and returns it.
|
||||
/// Every subclass of OsuAutoGeneratorBase should implement this!
|
||||
/// The default duration of a key press in milliseconds.
|
||||
/// </summary>
|
||||
public abstract Replay Generate();
|
||||
|
||||
#region Parameters
|
||||
public const double KEY_UP_DELAY = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap we're making.
|
||||
/// The beatmap the autoplay is generated for.
|
||||
/// </summary>
|
||||
protected IBeatmap Beatmap;
|
||||
|
||||
#endregion
|
||||
protected IBeatmap Beatmap { get; }
|
||||
|
||||
protected AutoGenerator(IBeatmap beatmap)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
}
|
||||
|
||||
#region Constants
|
||||
|
||||
// Shared amongst all modes
|
||||
public const double KEY_UP_DELAY = 50;
|
||||
|
||||
#endregion
|
||||
/// <summary>
|
||||
/// Generate the replay of the autoplay.
|
||||
/// </summary>
|
||||
public abstract Replay Generate();
|
||||
|
||||
protected virtual HitObject GetNextObject(int currentIndex)
|
||||
{
|
||||
@ -44,4 +40,37 @@ namespace osu.Game.Rulesets.Replays
|
||||
return Beatmap.HitObjects[currentIndex + 1];
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class AutoGenerator<TFrame> : AutoGenerator
|
||||
where TFrame : ReplayFrame
|
||||
{
|
||||
/// <summary>
|
||||
/// The replay frames of the autoplay.
|
||||
/// </summary>
|
||||
protected readonly List<TFrame> Frames = new List<TFrame>();
|
||||
|
||||
[CanBeNull]
|
||||
protected TFrame LastFrame => Frames.Count == 0 ? null : Frames[^1];
|
||||
|
||||
protected AutoGenerator(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
}
|
||||
|
||||
public sealed override Replay Generate()
|
||||
{
|
||||
Frames.Clear();
|
||||
GenerateFrames();
|
||||
|
||||
return new Replay
|
||||
{
|
||||
Frames = Frames.OrderBy(frame => frame.Time).Cast<ReplayFrame>().ToList()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generate the replay frames of the autoplay and populate <see cref="Frames"/>.
|
||||
/// </summary>
|
||||
protected abstract void GenerateFrames();
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using JetBrains.Annotations;
|
||||
using System.Linq;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Replays;
|
||||
|
||||
@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Replays
|
||||
{
|
||||
// TODO: This replay frame ordering should be enforced on the Replay type.
|
||||
// Currently, the ordering can be broken if the frames are added after this construction.
|
||||
replay.Frames.Sort((x, y) => x.Time.CompareTo(y.Time));
|
||||
replay.Frames = replay.Frames.OrderBy(f => f.Time).ToList();
|
||||
|
||||
this.replay = replay;
|
||||
currentFrameIndex = -1;
|
||||
@ -116,7 +116,7 @@ namespace osu.Game.Rulesets.Replays
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual bool IsImportant([NotNull] TFrame frame) => false;
|
||||
protected virtual bool IsImportant(TFrame frame) => false;
|
||||
|
||||
/// <summary>
|
||||
/// Update the current frame based on an incoming time value.
|
||||
|
@ -1,12 +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 osu.Game.Replays;
|
||||
|
||||
namespace osu.Game.Rulesets.Replays
|
||||
{
|
||||
public interface IAutoGenerator
|
||||
{
|
||||
Replay Generate();
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ using JetBrains.Annotations;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.EnumExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Filter;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
|
||||
@ -126,7 +127,7 @@ namespace osu.Game.Rulesets
|
||||
[CanBeNull]
|
||||
public ModAutoplay GetAutoplayMod() => GetAllMods().OfType<ModAutoplay>().FirstOrDefault();
|
||||
|
||||
public virtual ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => null;
|
||||
public virtual ISkin CreateLegacySkinProvider([NotNull] ISkin skin, IBeatmap beatmap) => null;
|
||||
|
||||
protected Ruleset()
|
||||
{
|
||||
@ -135,7 +136,7 @@ namespace osu.Game.Rulesets
|
||||
Name = Description,
|
||||
ShortName = ShortName,
|
||||
ID = (this as ILegacyRuleset)?.LegacyID,
|
||||
InstantiationInfo = GetType().AssemblyQualifiedName,
|
||||
InstantiationInfo = GetType().GetInvariantInstantiationInfo(),
|
||||
Available = true,
|
||||
};
|
||||
}
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
|
||||
namespace osu.Game.Rulesets
|
||||
@ -18,20 +18,7 @@ namespace osu.Game.Rulesets
|
||||
|
||||
public string ShortName { get; set; }
|
||||
|
||||
private string instantiationInfo;
|
||||
|
||||
public string InstantiationInfo
|
||||
{
|
||||
get => instantiationInfo;
|
||||
set => instantiationInfo = abbreviateInstantiationInfo(value);
|
||||
}
|
||||
|
||||
private string abbreviateInstantiationInfo(string value)
|
||||
{
|
||||
// exclude version onwards, matching only on namespace and type.
|
||||
// this is mainly to allow for new versions of already loaded rulesets to "upgrade" from old.
|
||||
return string.Join(',', value.Split(',').Take(2));
|
||||
}
|
||||
public string InstantiationInfo { get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool Available { get; set; }
|
||||
@ -41,7 +28,7 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
if (!Available) return null;
|
||||
|
||||
var ruleset = (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo));
|
||||
var ruleset = (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo).AsNonNull());
|
||||
|
||||
// overwrite the pre-populated RulesetInfo with a potentially database attached copy.
|
||||
ruleset.RulesetInfo = this;
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Database;
|
||||
@ -95,13 +96,25 @@ namespace osu.Game.Rulesets
|
||||
|
||||
context.SaveChanges();
|
||||
|
||||
// add any other modes
|
||||
var existingRulesets = context.RulesetInfo.ToList();
|
||||
|
||||
// add any other rulesets which have assemblies present but are not yet in the database.
|
||||
foreach (var r in instances.Where(r => !(r is ILegacyRuleset)))
|
||||
{
|
||||
if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null)
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
{
|
||||
var existingSameShortName = existingRulesets.FirstOrDefault(ri => ri.ShortName == r.RulesetInfo.ShortName);
|
||||
|
||||
if (existingSameShortName != null)
|
||||
{
|
||||
// even if a matching InstantiationInfo was not found, there may be an existing ruleset with the same ShortName.
|
||||
// this generally means the user or ruleset provider has renamed their dll but the underlying ruleset is *likely* the same one.
|
||||
// in such cases, update the instantiation info of the existing entry to point to the new one.
|
||||
existingSameShortName.InstantiationInfo = r.RulesetInfo.InstantiationInfo;
|
||||
}
|
||||
else
|
||||
context.RulesetInfo.Add(r.RulesetInfo);
|
||||
}
|
||||
}
|
||||
|
||||
context.SaveChanges();
|
||||
@ -111,7 +124,7 @@ namespace osu.Game.Rulesets
|
||||
{
|
||||
try
|
||||
{
|
||||
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo))).RulesetInfo;
|
||||
var instanceInfo = ((Ruleset)Activator.CreateInstance(Type.GetType(r.InstantiationInfo).AsNonNull())).RulesetInfo;
|
||||
|
||||
r.Name = instanceInfo.Name;
|
||||
r.ShortName = instanceInfo.ShortName;
|
||||
|
@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore += result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1;
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) + 1;
|
||||
|
||||
hitEvents.Add(CreateHitEvent(result));
|
||||
lastHitObject = result.HitObject;
|
||||
@ -181,7 +180,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
|
||||
}
|
||||
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1;
|
||||
scoreResultCounts[result.Type] = scoreResultCounts.GetValueOrDefault(result.Type) - 1;
|
||||
|
||||
Debug.Assert(hitEvents.Count > 0);
|
||||
lastHitObject = hitEvents[^1].LastHitObject;
|
||||
@ -272,8 +271,8 @@ namespace osu.Game.Rulesets.Scoring
|
||||
private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1;
|
||||
|
||||
private double getBonusScore(Dictionary<HitResult, int> statistics)
|
||||
=> statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
||||
+ statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
||||
=> statistics.GetValueOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE
|
||||
+ statistics.GetValueOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE;
|
||||
|
||||
private ScoreRank rankFrom(double acc)
|
||||
{
|
||||
@ -291,7 +290,7 @@ namespace osu.Game.Rulesets.Scoring
|
||||
return ScoreRank.D;
|
||||
}
|
||||
|
||||
public int GetStatistic(HitResult result) => scoreResultCounts.GetOrDefault(result);
|
||||
public int GetStatistic(HitResult result) => scoreResultCounts.GetValueOrDefault(result);
|
||||
|
||||
public double GetStandardisedScore() => getScore(ScoringMode.Standardised);
|
||||
|
||||
@ -339,7 +338,6 @@ namespace osu.Game.Rulesets.Scoring
|
||||
score.MaxCombo = HighestCombo.Value;
|
||||
score.Accuracy = Accuracy.Value;
|
||||
score.Rank = Rank.Value;
|
||||
score.Date = DateTimeOffset.Now;
|
||||
|
||||
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r.IsScorable()))
|
||||
score.Statistics[result] = GetStatistic(result);
|
||||
|
@ -68,10 +68,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private bool frameStablePlayback = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable frame-stable playback.
|
||||
/// </summary>
|
||||
internal bool FrameStablePlayback
|
||||
internal override bool FrameStablePlayback
|
||||
{
|
||||
get => frameStablePlayback;
|
||||
set
|
||||
@ -182,18 +179,11 @@ namespace osu.Game.Rulesets.UI
|
||||
.WithChild(ResumeOverlay)));
|
||||
}
|
||||
|
||||
RegenerateAutoplay();
|
||||
applyRulesetMods(Mods, config);
|
||||
|
||||
loadObjects(cancellationToken ?? default);
|
||||
}
|
||||
|
||||
public void RegenerateAutoplay()
|
||||
{
|
||||
// for now this is applying mods which aren't just autoplay.
|
||||
// we'll need to reconsider this flow in the future.
|
||||
applyRulesetMods(Mods, config);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds drawable representations of hit objects to the play field.
|
||||
/// </summary>
|
||||
@ -209,8 +199,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
Playfield.PostProcess();
|
||||
|
||||
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
|
||||
mod.ApplyToDrawableHitObjects(Playfield.AllHitObjects);
|
||||
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObject>())
|
||||
{
|
||||
foreach (var drawableHitObject in Playfield.AllHitObjects)
|
||||
mod.ApplyToDrawableHitObject(drawableHitObject);
|
||||
}
|
||||
}
|
||||
|
||||
public override void RequestResume(Action continueResume)
|
||||
@ -274,6 +267,12 @@ namespace osu.Game.Rulesets.UI
|
||||
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
|
||||
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available");
|
||||
|
||||
if (score == null)
|
||||
{
|
||||
recordingInputManager.Recorder = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var recorder = CreateReplayRecorder(score);
|
||||
|
||||
if (recorder == null)
|
||||
@ -432,6 +431,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </summary>
|
||||
public abstract IFrameStableClock FrameStableClock { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to enable frame-stable playback.
|
||||
/// </summary>
|
||||
internal abstract bool FrameStablePlayback { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The mods which are to be applied.
|
||||
/// </summary>
|
||||
@ -485,15 +489,15 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
get
|
||||
{
|
||||
foreach (var h in Objects)
|
||||
foreach (var hitObject in Objects)
|
||||
{
|
||||
if (h.HitWindows.WindowFor(HitResult.Miss) > 0)
|
||||
return h.HitWindows;
|
||||
if (hitObject.HitWindows.WindowFor(HitResult.Miss) > 0)
|
||||
return hitObject.HitWindows;
|
||||
|
||||
foreach (var n in h.NestedHitObjects)
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
{
|
||||
if (h.HitWindows.WindowFor(HitResult.Miss) > 0)
|
||||
return n.HitWindows;
|
||||
if (nested.HitWindows.WindowFor(HitResult.Miss) > 0)
|
||||
return nested.HitWindows;
|
||||
}
|
||||
}
|
||||
|
||||
@ -518,7 +522,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// Sets a replay to be used to record gameplay.
|
||||
/// </summary>
|
||||
/// <param name="score">The target to be recorded to.</param>
|
||||
public abstract void SetRecordTarget(Score score);
|
||||
public abstract void SetRecordTarget([CanBeNull] Score score);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the interactive user requests resuming from a paused state.
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Shaders;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
@ -34,6 +35,11 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </remarks>
|
||||
public ISampleStore SampleStore { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The shader manager to be used for the ruleset.
|
||||
/// </summary>
|
||||
public ShaderManager ShaderManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The ruleset config manager.
|
||||
/// </summary>
|
||||
@ -52,6 +58,9 @@ namespace osu.Game.Rulesets.UI
|
||||
SampleStore = parent.Get<AudioManager>().GetSampleStore(new NamespacedResourceStore<byte[]>(resources, @"Samples"));
|
||||
SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY;
|
||||
CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get<ISampleStore>()));
|
||||
|
||||
ShaderManager = new ShaderManager(new NamespacedResourceStore<byte[]>(resources, @"Shaders"));
|
||||
CacheAs(ShaderManager = new FallbackShaderManager(ShaderManager, parent.Get<ShaderManager>()));
|
||||
}
|
||||
|
||||
RulesetConfigManager = parent.Get<RulesetConfigCache>().GetConfigFor(ruleset);
|
||||
@ -84,6 +93,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
SampleStore?.Dispose();
|
||||
TextureStore?.Dispose();
|
||||
ShaderManager?.Dispose();
|
||||
RulesetConfigManager = null;
|
||||
}
|
||||
|
||||
@ -172,5 +182,26 @@ namespace osu.Game.Rulesets.UI
|
||||
primary?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private class FallbackShaderManager : ShaderManager
|
||||
{
|
||||
private readonly ShaderManager primary;
|
||||
private readonly ShaderManager fallback;
|
||||
|
||||
public FallbackShaderManager(ShaderManager primary, ShaderManager fallback)
|
||||
: base(new ResourceStore<byte[]>())
|
||||
{
|
||||
this.primary = primary;
|
||||
this.fallback = fallback;
|
||||
}
|
||||
|
||||
public override byte[] LoadRaw(string name) => primary.LoadRaw(name) ?? fallback.LoadRaw(name);
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
primary?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,25 +3,23 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Pooling;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public class HitObjectContainer : LifetimeManagementContainer, IHitObjectContainer
|
||||
public class HitObjectContainer : PooledDrawableWithLifetimeContainer<HitObjectLifetimeEntry, DrawableHitObject>, IHitObjectContainer
|
||||
{
|
||||
public IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||
|
||||
public IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>().OrderBy(h => h.HitObject.StartTime);
|
||||
public IEnumerable<DrawableHitObject> AliveObjects => AliveEntries.Select(pair => pair.Drawable).OrderBy(h => h.HitObject.StartTime);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is judged.
|
||||
@ -49,19 +47,9 @@ namespace osu.Game.Rulesets.UI
|
||||
/// </remarks>
|
||||
internal event Action<HitObject> HitObjectUsageFinished;
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time prior to the current time within which <see cref="HitObject"/>s should be considered alive.
|
||||
/// </summary>
|
||||
internal double PastLifetimeExtension { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of time after the current time within which <see cref="HitObject"/>s should be considered alive.
|
||||
/// </summary>
|
||||
internal double FutureLifetimeExtension { get; set; }
|
||||
|
||||
private readonly Dictionary<DrawableHitObject, IBindable> startTimeMap = new Dictionary<DrawableHitObject, IBindable>();
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> drawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||
private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager();
|
||||
|
||||
private readonly Dictionary<HitObjectLifetimeEntry, DrawableHitObject> nonPooledDrawableMap = new Dictionary<HitObjectLifetimeEntry, DrawableHitObject>();
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private IPooledHitObjectProvider pooledObjectProvider { get; set; }
|
||||
@ -69,9 +57,6 @@ namespace osu.Game.Rulesets.UI
|
||||
public HitObjectContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
lifetimeManager.EntryBecameAlive += entryBecameAlive;
|
||||
lifetimeManager.EntryBecameDead += entryBecameDead;
|
||||
}
|
||||
|
||||
protected override void LoadAsyncComplete()
|
||||
@ -84,132 +69,88 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
#region Pooling support
|
||||
|
||||
public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry);
|
||||
|
||||
public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry);
|
||||
|
||||
private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry);
|
||||
|
||||
private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry);
|
||||
|
||||
private void addDrawable(HitObjectLifetimeEntry entry)
|
||||
public override bool Remove(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
Debug.Assert(!drawableMap.ContainsKey(entry));
|
||||
if (!base.Remove(entry)) return false;
|
||||
|
||||
var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null);
|
||||
if (drawable == null)
|
||||
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
|
||||
// This logic is not in `Remove(DrawableHitObject)` because a non-pooled drawable may be removed by specifying its entry.
|
||||
if (nonPooledDrawableMap.Remove(entry, out var drawable))
|
||||
removeDrawable(drawable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected sealed override DrawableHitObject GetDrawable(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
if (nonPooledDrawableMap.TryGetValue(entry, out var drawable))
|
||||
return drawable;
|
||||
|
||||
return pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null) ??
|
||||
throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}.");
|
||||
}
|
||||
|
||||
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
if (nonPooledDrawableMap.ContainsKey(entry)) return;
|
||||
|
||||
addDrawable(drawable);
|
||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||
}
|
||||
|
||||
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnKilled();
|
||||
if (nonPooledDrawableMap.ContainsKey(entry)) return;
|
||||
|
||||
removeDrawable(drawable);
|
||||
HitObjectUsageFinished?.Invoke(entry.HitObject);
|
||||
}
|
||||
|
||||
private void addDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
drawable.OnNewResult += onNewResult;
|
||||
drawable.OnRevertResult += onRevertResult;
|
||||
|
||||
bindStartTime(drawable);
|
||||
AddInternal(drawableMap[entry] = drawable, false);
|
||||
OnAdd(drawable);
|
||||
|
||||
HitObjectUsageBegan?.Invoke(entry.HitObject);
|
||||
AddInternal(drawable);
|
||||
}
|
||||
|
||||
private void removeDrawable(HitObjectLifetimeEntry entry)
|
||||
private void removeDrawable(DrawableHitObject drawable)
|
||||
{
|
||||
Debug.Assert(drawableMap.ContainsKey(entry));
|
||||
|
||||
var drawable = drawableMap[entry];
|
||||
|
||||
// OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding.
|
||||
drawable.OnKilled();
|
||||
drawable.OnNewResult -= onNewResult;
|
||||
drawable.OnRevertResult -= onRevertResult;
|
||||
|
||||
drawableMap.Remove(entry);
|
||||
|
||||
OnRemove(drawable);
|
||||
unbindStartTime(drawable);
|
||||
RemoveInternal(drawable);
|
||||
|
||||
HitObjectUsageFinished?.Invoke(entry.HitObject);
|
||||
RemoveInternal(drawable);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Non-pooling support
|
||||
|
||||
public virtual void Add(DrawableHitObject hitObject)
|
||||
public virtual void Add(DrawableHitObject drawable)
|
||||
{
|
||||
bindStartTime(hitObject);
|
||||
if (drawable.Entry == null)
|
||||
throw new InvalidOperationException($"May not add a {nameof(DrawableHitObject)} without {nameof(HitObject)} associated");
|
||||
|
||||
hitObject.OnNewResult += onNewResult;
|
||||
hitObject.OnRevertResult += onRevertResult;
|
||||
|
||||
AddInternal(hitObject);
|
||||
OnAdd(hitObject);
|
||||
nonPooledDrawableMap.Add(drawable.Entry, drawable);
|
||||
addDrawable(drawable);
|
||||
Add(drawable.Entry);
|
||||
}
|
||||
|
||||
public virtual bool Remove(DrawableHitObject hitObject)
|
||||
public virtual bool Remove(DrawableHitObject drawable)
|
||||
{
|
||||
OnRemove(hitObject);
|
||||
if (!RemoveInternal(hitObject))
|
||||
if (drawable.Entry == null)
|
||||
return false;
|
||||
|
||||
hitObject.OnNewResult -= onNewResult;
|
||||
hitObject.OnRevertResult -= onRevertResult;
|
||||
|
||||
unbindStartTime(hitObject);
|
||||
|
||||
return true;
|
||||
return Remove(drawable.Entry);
|
||||
}
|
||||
|
||||
public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject);
|
||||
|
||||
protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e)
|
||||
{
|
||||
if (!(e.Child is DrawableHitObject hitObject))
|
||||
return;
|
||||
|
||||
if ((e.Kind == LifetimeBoundaryKind.End && e.Direction == LifetimeBoundaryCrossingDirection.Forward)
|
||||
|| (e.Kind == LifetimeBoundaryKind.Start && e.Direction == LifetimeBoundaryCrossingDirection.Backward))
|
||||
{
|
||||
hitObject.OnKilled();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is added to this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||
/// </remarks>
|
||||
protected virtual void OnAdd(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when a <see cref="DrawableHitObject"/> is removed from this container.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is not invoked for nested <see cref="DrawableHitObject"/>s.
|
||||
/// </remarks>
|
||||
protected virtual void OnRemove(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Clear(bool disposeChildren = true)
|
||||
{
|
||||
lifetimeManager.ClearEntries();
|
||||
|
||||
ClearInternal(disposeChildren);
|
||||
unbindAllStartTimes();
|
||||
}
|
||||
|
||||
protected override bool CheckChildrenLife()
|
||||
{
|
||||
bool aliveChanged = base.CheckChildrenLife();
|
||||
aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension);
|
||||
return aliveChanged;
|
||||
}
|
||||
|
||||
private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r);
|
||||
private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r);
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private const float size = 80;
|
||||
|
||||
public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
|
||||
public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null;
|
||||
|
||||
private Mod mod;
|
||||
private readonly bool showTooltip;
|
||||
|
@ -354,8 +354,11 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
// If this is the first time this DHO is being used, then apply the DHO mods.
|
||||
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
|
||||
m.ApplyToDrawableHitObjects(dho.Yield());
|
||||
if (mods != null)
|
||||
{
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
|
||||
m.ApplyToDrawableHitObject(dho);
|
||||
}
|
||||
}
|
||||
|
||||
if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry))
|
||||
|
@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.UI
|
||||
public int RecordFrameRate = 60;
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private SpectatorStreamingClient spectatorStreaming { get; set; }
|
||||
private SpectatorClient spectatorClient { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private GameplayBeatmap gameplayBeatmap { get; set; }
|
||||
@ -49,13 +49,13 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
inputManager = GetContainingInputManager();
|
||||
|
||||
spectatorStreaming?.BeginPlaying(gameplayBeatmap, target);
|
||||
spectatorClient?.BeginPlaying(gameplayBeatmap, target);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
spectatorStreaming?.EndPlaying();
|
||||
spectatorClient?.EndPlaying();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
target.Replay.Frames.Add(frame);
|
||||
|
||||
spectatorStreaming?.HandleFrame(frame);
|
||||
spectatorClient?.HandleFrame(frame);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Framework.Input.StateChanges.Events;
|
||||
using osu.Framework.Input.States;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -29,12 +30,14 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
set
|
||||
{
|
||||
if (recorder != null)
|
||||
if (value != null && recorder != null)
|
||||
throw new InvalidOperationException("Cannot attach more than one recorder");
|
||||
|
||||
recorder?.Expire();
|
||||
recorder = value;
|
||||
|
||||
KeyBindingContainer.Add(recorder);
|
||||
if (recorder != null)
|
||||
KeyBindingContainer.Add(recorder);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,6 +172,13 @@ namespace osu.Game.Rulesets.UI
|
||||
: base(ruleset, variant, unique)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ReloadMappings()
|
||||
{
|
||||
base.ReloadMappings();
|
||||
|
||||
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
@ -18,12 +20,21 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// Hit objects which require lifetime computation in the next update call.
|
||||
/// Whether the scrolling direction is horizontal or vertical.
|
||||
/// </summary>
|
||||
private readonly HashSet<DrawableHitObject> toComputeLifetime = new HashSet<DrawableHitObject>();
|
||||
private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical;
|
||||
|
||||
/// <summary>
|
||||
/// A set containing all <see cref="HitObjectContainer.AliveObjects"/> which have an up-to-date layout.
|
||||
/// The scrolling axis is inverted if objects temporally farther in the future have a smaller position value across the scrolling axis.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// <see cref="ScrollingDirection.Down"/> is inverted, because given two objects, one of which is at the current time and one of which is 1000ms in the future,
|
||||
/// in the current time instant the future object is spatially above the current object, and therefore has a smaller value of the Y coordinate of its position.
|
||||
/// </example>
|
||||
private bool axisInverted => direction.Value == ScrollingDirection.Down || direction.Value == ScrollingDirection.Right;
|
||||
|
||||
/// <summary>
|
||||
/// A set of top-level <see cref="DrawableHitObject"/>s which have an up-to-date layout.
|
||||
/// </summary>
|
||||
private readonly HashSet<DrawableHitObject> layoutComputed = new HashSet<DrawableHitObject>();
|
||||
|
||||
@ -50,181 +61,107 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
timeRange.ValueChanged += _ => layoutCache.Invalidate();
|
||||
}
|
||||
|
||||
public override void Clear(bool disposeChildren = true)
|
||||
/// <summary>
|
||||
/// Given a position at <paramref name="currentTime"/>, return the time of the object corresponding to the position.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If there are multiple valid time values, one arbitrary time is returned.
|
||||
/// </remarks>
|
||||
public double TimeAtPosition(float localPosition, double currentTime)
|
||||
{
|
||||
base.Clear(disposeChildren);
|
||||
|
||||
toComputeLifetime.Clear();
|
||||
layoutComputed.Clear();
|
||||
float scrollPosition = axisInverted ? -localPosition : localPosition;
|
||||
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a position in screen space, return the time within this column.
|
||||
/// Given a position at the current time in screen space, return the time of the object corresponding the position.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If there are multiple valid time values, one arbitrary time is returned.
|
||||
/// </remarks>
|
||||
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
|
||||
{
|
||||
// convert to local space of column so we can snap and fetch correct location.
|
||||
Vector2 localPosition = ToLocalSpace(screenSpacePosition);
|
||||
|
||||
float position = 0;
|
||||
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
position = localPosition.Y;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Right:
|
||||
case ScrollingDirection.Left:
|
||||
position = localPosition.X;
|
||||
break;
|
||||
}
|
||||
|
||||
flipPositionIfRequired(ref position);
|
||||
|
||||
return scrollingInfo.Algorithm.TimeAt(position, Time.Current, scrollingInfo.TimeRange.Value, getLength());
|
||||
Vector2 pos = ToLocalSpace(screenSpacePosition);
|
||||
float localPosition = scrollingAxis == Direction.Horizontal ? pos.X : pos.Y;
|
||||
localPosition -= axisInverted ? scrollLength : 0;
|
||||
return TimeAtPosition(localPosition, Time.Current);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a time, return the screen space position within this column.
|
||||
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at time <paramref name="currentTime"/>.
|
||||
/// </summary>
|
||||
public float PositionAtTime(double time, double currentTime)
|
||||
{
|
||||
float scrollPosition = scrollingInfo.Algorithm.PositionAt(time, currentTime, timeRange.Value, scrollLength);
|
||||
return axisInverted ? -scrollPosition : scrollPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at the current time.
|
||||
/// </summary>
|
||||
public float PositionAtTime(double time) => PositionAtTime(time, Time.Current);
|
||||
|
||||
/// <summary>
|
||||
/// Given a time, return the screen space position within this <see cref="HitObjectContainer"/>.
|
||||
/// In the non-scrolling axis, the center of this <see cref="HitObjectContainer"/> is returned.
|
||||
/// </summary>
|
||||
public Vector2 ScreenSpacePositionAtTime(double time)
|
||||
{
|
||||
var pos = scrollingInfo.Algorithm.PositionAt(time, Time.Current, scrollingInfo.TimeRange.Value, getLength());
|
||||
|
||||
flipPositionIfRequired(ref pos);
|
||||
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
return ToScreenSpace(new Vector2(getBreadth() / 2, pos));
|
||||
|
||||
default:
|
||||
return ToScreenSpace(new Vector2(pos, getBreadth() / 2));
|
||||
}
|
||||
}
|
||||
|
||||
private float getLength()
|
||||
{
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
return DrawWidth;
|
||||
|
||||
default:
|
||||
return DrawHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private float getBreadth()
|
||||
{
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
return DrawWidth;
|
||||
|
||||
default:
|
||||
return DrawHeight;
|
||||
}
|
||||
}
|
||||
|
||||
private void flipPositionIfRequired(ref float position)
|
||||
{
|
||||
// We're dealing with screen coordinates in which the position decreases towards the centre of the screen resulting in an increase in start time.
|
||||
// The scrolling algorithm instead assumes a top anchor meaning an increase in time corresponds to an increase in position,
|
||||
// so when scrolling downwards the coordinates need to be flipped.
|
||||
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Down:
|
||||
position = DrawHeight - position;
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Right:
|
||||
position = DrawWidth - position;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject);
|
||||
|
||||
protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject);
|
||||
|
||||
private void onAddRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
invalidateHitObject(hitObject);
|
||||
|
||||
hitObject.DefaultsApplied += invalidateHitObject;
|
||||
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
onAddRecursive(nested);
|
||||
}
|
||||
|
||||
private void onRemoveRecursive(DrawableHitObject hitObject)
|
||||
{
|
||||
toComputeLifetime.Remove(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
|
||||
hitObject.DefaultsApplied -= invalidateHitObject;
|
||||
|
||||
foreach (var nested in hitObject.NestedHitObjects)
|
||||
onRemoveRecursive(nested);
|
||||
float localPosition = PositionAtTime(time, Time.Current);
|
||||
localPosition += axisInverted ? scrollLength : 0;
|
||||
return scrollingAxis == Direction.Horizontal
|
||||
? ToScreenSpace(new Vector2(localPosition, DrawHeight / 2))
|
||||
: ToScreenSpace(new Vector2(DrawWidth / 2, localPosition));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Make this <see cref="DrawableHitObject"/> lifetime and layout computed in next update.
|
||||
/// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis.
|
||||
/// </summary>
|
||||
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||
public float LengthAtTime(double startTime, double endTime)
|
||||
{
|
||||
// Lifetime computation is delayed until next update because
|
||||
// when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed.
|
||||
toComputeLifetime.Add(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
private float scrollLength;
|
||||
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
|
||||
|
||||
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
base.AddDrawable(entry, drawable);
|
||||
|
||||
invalidateHitObject(drawable);
|
||||
drawable.DefaultsApplied += invalidateHitObject;
|
||||
}
|
||||
|
||||
protected override void RemoveDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
base.RemoveDrawable(entry, drawable);
|
||||
|
||||
drawable.DefaultsApplied -= invalidateHitObject;
|
||||
layoutComputed.Remove(drawable);
|
||||
}
|
||||
|
||||
private void invalidateHitObject(DrawableHitObject hitObject)
|
||||
{
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
layoutComputed.Remove(hitObject);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!layoutCache.IsValid)
|
||||
{
|
||||
toComputeLifetime.Clear();
|
||||
if (layoutCache.IsValid) return;
|
||||
|
||||
foreach (var hitObject in Objects)
|
||||
{
|
||||
if (hitObject.HitObject != null)
|
||||
toComputeLifetime.Add(hitObject);
|
||||
}
|
||||
layoutComputed.Clear();
|
||||
|
||||
layoutComputed.Clear();
|
||||
// Reset lifetime to the conservative estimation.
|
||||
// If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
|
||||
foreach (var entry in Entries)
|
||||
entry.SetInitialLifetime();
|
||||
|
||||
scrollingInfo.Algorithm.Reset();
|
||||
scrollingInfo.Algorithm.Reset();
|
||||
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
scrollLength = DrawSize.Y;
|
||||
break;
|
||||
|
||||
default:
|
||||
scrollLength = DrawSize.X;
|
||||
break;
|
||||
}
|
||||
|
||||
layoutCache.Validate();
|
||||
}
|
||||
|
||||
foreach (var hitObject in toComputeLifetime)
|
||||
hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject);
|
||||
|
||||
toComputeLifetime.Clear();
|
||||
layoutCache.Validate();
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildrenLife()
|
||||
@ -249,6 +186,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject)
|
||||
{
|
||||
// Origin position may be relative to the parent size
|
||||
Debug.Assert(hitObject.Parent != null);
|
||||
|
||||
float originAdjustment = 0.0f;
|
||||
|
||||
// calculate the dimension of the part of the hitobject that should already be visible
|
||||
@ -279,18 +219,11 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
if (hitObject.HitObject is IHasDuration e)
|
||||
{
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
hitObject.Height = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
hitObject.Width = scrollingInfo.Algorithm.GetLength(hitObject.HitObject.StartTime, e.EndTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
}
|
||||
float length = LengthAtTime(hitObject.HitObject.StartTime, e.EndTime);
|
||||
if (scrollingAxis == Direction.Horizontal)
|
||||
hitObject.Width = length;
|
||||
else
|
||||
hitObject.Height = length;
|
||||
}
|
||||
|
||||
foreach (var obj in hitObject.NestedHitObjects)
|
||||
@ -304,24 +237,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
|
||||
private void updatePosition(DrawableHitObject hitObject, double currentTime)
|
||||
{
|
||||
switch (direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
hitObject.Y = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
float position = PositionAtTime(hitObject.HitObject.StartTime, currentTime);
|
||||
|
||||
case ScrollingDirection.Down:
|
||||
hitObject.Y = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Left:
|
||||
hitObject.X = scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
|
||||
case ScrollingDirection.Right:
|
||||
hitObject.X = -scrollingInfo.Algorithm.PositionAt(hitObject.HitObject.StartTime, currentTime, timeRange.Value, scrollLength);
|
||||
break;
|
||||
}
|
||||
if (scrollingAxis == Direction.Horizontal)
|
||||
hitObject.X = position;
|
||||
else
|
||||
hitObject.Y = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user