mirror of
https://github.com/osukey/osukey.git
synced 2025-08-05 07:33:55 +09:00
Merge branch 'master' of https://hub.fastgit.org/ppy/osu into auto-restart
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);
|
||||
|
||||
|
@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
||||
/// <summary>
|
||||
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
||||
/// </summary>
|
||||
public sealed override double DifficultyValue()
|
||||
public override double DifficultyValue()
|
||||
{
|
||||
double difficulty = 0;
|
||||
double weight = 1;
|
||||
|
@ -22,10 +22,13 @@ 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(BeatmapVerifierContext context)
|
||||
|
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);
|
||||
}
|
||||
}
|
||||
}
|
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.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </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 HitObjectSelectionBlueprint(HitObject hitObject)
|
||||
: base(hitObject)
|
||||
|
@ -1,6 +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 System.Diagnostics;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Judgements
|
||||
|
||||
private readonly Container aboveHitObjectsContent;
|
||||
|
||||
private readonly Lazy<Drawable> proxiedAboveHitObjectsContent;
|
||||
public Drawable ProxiedAboveHitObjectsContent => proxiedAboveHitObjectsContent.Value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
|
||||
/// </summary>
|
||||
@ -52,6 +56,8 @@ namespace osu.Game.Rulesets.Judgements
|
||||
Depth = float.MinValue,
|
||||
RelativeSizeAxes = Axes.Both
|
||||
});
|
||||
|
||||
proxiedAboveHitObjectsContent = new Lazy<Drawable>(() => aboveHitObjectsContent.CreateProxy());
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -60,8 +66,6 @@ namespace osu.Game.Rulesets.Judgements
|
||||
prepareDrawables();
|
||||
}
|
||||
|
||||
public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy();
|
||||
|
||||
/// <summary>
|
||||
/// Apply top-level animations to the current judgement when successfully hit.
|
||||
/// If displaying components which require lifetime extensions, manually adjusting <see cref="Drawable.LifetimeEnd"/> is required.
|
||||
@ -117,7 +121,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();
|
||||
|
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());
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public interface IApplicableToHealthProcessor : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide a <see cref="HealthProcessor"/> to a mod. Called once on initialisation of a play instance.
|
||||
/// Provides a loaded <see cref="HealthProcessor"/> to a mod. Called once on initialisation of a play instance.
|
||||
/// </summary>
|
||||
void ApplyToHealthProcessor(HealthProcessor healthProcessor);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public interface IApplicableToScoreProcessor : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Provide a <see cref="ScoreProcessor"/> to a mod. Called once on initialisation of a play instance.
|
||||
/// Provides a loaded <see cref="ScoreProcessor"/> to a mod. Called once on initialisation of a play instance.
|
||||
/// </summary>
|
||||
void ApplyToScoreProcessor(ScoreProcessor scoreProcessor);
|
||||
|
||||
|
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; }
|
||||
}
|
||||
}
|
91
osu.Game/Rulesets/Mods/Metronome.cs
Normal file
91
osu.Game/Rulesets/Mods/Metronome.cs
Normal file
@ -0,0 +1,91 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public class Metronome : BeatSyncedContainer, IAdjustableAudioComponent
|
||||
{
|
||||
private readonly double firstHitTime;
|
||||
|
||||
private readonly PausableSkinnableSound sample;
|
||||
|
||||
/// <param name="firstHitTime">Start time of the first hit object, used for providing a count down.</param>
|
||||
public Metronome(double firstHitTime)
|
||||
{
|
||||
this.firstHitTime = firstHitTime;
|
||||
AllowMistimedEventFiring = false;
|
||||
Divisor = 1;
|
||||
|
||||
InternalChild = sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana"));
|
||||
}
|
||||
|
||||
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
|
||||
{
|
||||
base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes);
|
||||
|
||||
if (!IsBeatSyncedWithTrack) return;
|
||||
|
||||
int timeSignature = (int)timingPoint.TimeSignature;
|
||||
|
||||
// play metronome from one measure before the first object.
|
||||
if (BeatSyncClock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature)
|
||||
return;
|
||||
|
||||
sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f;
|
||||
sample.Play();
|
||||
}
|
||||
|
||||
#region IAdjustableAudioComponent
|
||||
|
||||
public IBindable<double> AggregateVolume => sample.AggregateVolume;
|
||||
|
||||
public IBindable<double> AggregateBalance => sample.AggregateBalance;
|
||||
|
||||
public IBindable<double> AggregateFrequency => sample.AggregateFrequency;
|
||||
|
||||
public IBindable<double> AggregateTempo => sample.AggregateTempo;
|
||||
|
||||
public BindableNumber<double> Volume => sample.Volume;
|
||||
|
||||
public BindableNumber<double> Balance => sample.Balance;
|
||||
|
||||
public BindableNumber<double> Frequency => sample.Frequency;
|
||||
|
||||
public BindableNumber<double> Tempo => sample.Tempo;
|
||||
|
||||
public void BindAdjustments(IAggregateAudioAdjustment component)
|
||||
{
|
||||
sample.BindAdjustments(component);
|
||||
}
|
||||
|
||||
public void UnbindAdjustments(IAggregateAudioAdjustment component)
|
||||
{
|
||||
sample.UnbindAdjustments(component);
|
||||
}
|
||||
|
||||
public void AddAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
|
||||
{
|
||||
sample.AddAdjustment(type, adjustBindable);
|
||||
}
|
||||
|
||||
public void RemoveAdjustment(AdjustableProperty type, IBindable<double> adjustBindable)
|
||||
{
|
||||
sample.RemoveAdjustment(type, adjustBindable);
|
||||
}
|
||||
|
||||
public void RemoveAllAdjustments(AdjustableProperty type)
|
||||
{
|
||||
sample.RemoveAllAdjustments(type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -24,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();
|
||||
|
||||
|
@ -14,7 +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;
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
|
13
osu.Game/Rulesets/Mods/ModMirror.cs
Normal file
13
osu.Game/Rulesets/Mods/ModMirror.cs
Normal file
@ -0,0 +1,13 @@
|
||||
// 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.
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModMirror : Mod
|
||||
{
|
||||
public override string Name => "Mirror";
|
||||
public override string Acronym => "MR";
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
}
|
116
osu.Game/Rulesets/Mods/ModMuted.cs
Normal file
116
osu.Game/Rulesets/Mods/ModMuted.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// 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.Audio;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModMuted : Mod
|
||||
{
|
||||
public override string Name => "Muted";
|
||||
public override string Acronym => "MU";
|
||||
public override IconUsage? Icon => FontAwesome.Solid.VolumeMute;
|
||||
public override string Description => "Can you still feel the rhythm without music?";
|
||||
public override ModType Type => ModType.Fun;
|
||||
public override double ScoreMultiplier => 1;
|
||||
}
|
||||
|
||||
public abstract class ModMuted<TObject> : ModMuted, IApplicableToDrawableRuleset<TObject>, IApplicableToTrack, IApplicableToScoreProcessor
|
||||
where TObject : HitObject
|
||||
{
|
||||
private readonly BindableNumber<double> mainVolumeAdjust = new BindableDouble(0.5);
|
||||
private readonly BindableNumber<double> metronomeVolumeAdjust = new BindableDouble(0.5);
|
||||
|
||||
private BindableNumber<int> currentCombo;
|
||||
|
||||
[SettingSource("Enable metronome", "Add a metronome beat to help you keep track of the rhythm.")]
|
||||
public BindableBool EnableMetronome { get; } = new BindableBool
|
||||
{
|
||||
Default = true,
|
||||
Value = true
|
||||
};
|
||||
|
||||
[SettingSource("Final volume at combo", "The combo count at which point the track reaches its final volume.", SettingControlType = typeof(SettingsSlider<int, MuteComboSlider>))]
|
||||
public BindableInt MuteComboCount { get; } = new BindableInt
|
||||
{
|
||||
Default = 100,
|
||||
Value = 100,
|
||||
MinValue = 0,
|
||||
MaxValue = 500,
|
||||
};
|
||||
|
||||
[SettingSource("Start muted", "Increase volume as combo builds.")]
|
||||
public BindableBool InverseMuting { get; } = new BindableBool
|
||||
{
|
||||
Default = false,
|
||||
Value = false
|
||||
};
|
||||
|
||||
[SettingSource("Mute hit sounds", "Hit sounds are also muted alongside the track.")]
|
||||
public BindableBool AffectsHitSounds { get; } = new BindableBool
|
||||
{
|
||||
Default = true,
|
||||
Value = true
|
||||
};
|
||||
|
||||
protected ModMuted()
|
||||
{
|
||||
InverseMuting.BindValueChanged(i => MuteComboCount.MinValue = i.NewValue ? 1 : 0, true);
|
||||
}
|
||||
|
||||
public void ApplyToTrack(ITrack track)
|
||||
{
|
||||
track.AddAdjustment(AdjustableProperty.Volume, mainVolumeAdjust);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
|
||||
{
|
||||
if (EnableMetronome.Value)
|
||||
{
|
||||
Metronome metronome;
|
||||
|
||||
drawableRuleset.Overlays.Add(metronome = new Metronome(drawableRuleset.Beatmap.HitObjects.First().StartTime));
|
||||
|
||||
metronome.AddAdjustment(AdjustableProperty.Volume, metronomeVolumeAdjust);
|
||||
}
|
||||
|
||||
if (AffectsHitSounds.Value)
|
||||
drawableRuleset.Audio.AddAdjustment(AdjustableProperty.Volume, mainVolumeAdjust);
|
||||
}
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
{
|
||||
currentCombo = scoreProcessor.Combo.GetBoundCopy();
|
||||
currentCombo.BindValueChanged(combo =>
|
||||
{
|
||||
double dimFactor = MuteComboCount.Value == 0 ? 1 : (double)combo.NewValue / MuteComboCount.Value;
|
||||
|
||||
if (InverseMuting.Value)
|
||||
dimFactor = 1 - dimFactor;
|
||||
|
||||
scoreProcessor.TransformBindableTo(metronomeVolumeAdjust, dimFactor, 500, Easing.OutQuint);
|
||||
scoreProcessor.TransformBindableTo(mainVolumeAdjust, 1 - dimFactor, 500, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank;
|
||||
}
|
||||
|
||||
public class MuteComboSlider : OsuSliderBar<int>
|
||||
{
|
||||
public override LocalisableString TooltipText => Current.Value == 0 ? "always muted" : base.TooltipText;
|
||||
}
|
||||
}
|
@ -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.";
|
||||
|
||||
|
@ -2,18 +2,14 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
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";
|
||||
@ -21,88 +17,11 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.Dice;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))]
|
||||
[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
|
||||
};
|
||||
|
||||
private class ModRandomSettingsControl : SettingsItem<int?>
|
||||
{
|
||||
protected override Drawable CreateControl() => new SeedControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
};
|
||||
|
||||
private sealed class SeedControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||
{
|
||||
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
|
||||
|
||||
public Bindable<int?> Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
current.Current = value;
|
||||
seedNumberBox.Text = value.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly OsuNumberBox seedNumberBox;
|
||||
|
||||
public SeedControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 2),
|
||||
new Dimension(GridSizeMode.Relative, 0.25f)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
seedNumberBox = new OsuNumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
seedNumberBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
int? value = null;
|
||||
|
||||
if (int.TryParse(e.NewValue, out var intVal))
|
||||
value = intVal;
|
||||
|
||||
current.Value = value;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (current.Value == null)
|
||||
seedNumberBox.Text = current.Current.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,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();
|
||||
}
|
||||
|
@ -124,7 +124,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
public readonly Bindable<double> StartTimeBindable = new Bindable<double>();
|
||||
private readonly BindableList<HitSampleInfo> samplesBindable = new BindableList<HitSampleInfo>();
|
||||
private readonly Bindable<bool> userPositionalHitSounds = new Bindable<bool>();
|
||||
|
||||
private readonly Bindable<int> comboIndexBindable = new Bindable<int>();
|
||||
private readonly Bindable<int> comboIndexWithOffsetsBindable = new Bindable<int>();
|
||||
|
||||
protected override bool RequiresChildrenUpdate => true;
|
||||
|
||||
@ -156,10 +158,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]
|
||||
@ -184,9 +187,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true);
|
||||
comboIndexBindable.BindValueChanged(_ => UpdateComboColour());
|
||||
comboIndexWithOffsetsBindable.BindValueChanged(_ => UpdateComboColour(), true);
|
||||
|
||||
updateState(ArmedState.Idle, true);
|
||||
// Apply transforms
|
||||
updateState(State.Value, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -249,7 +254,10 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
StartTimeBindable.BindValueChanged(onStartTimeChanged);
|
||||
|
||||
if (HitObject is IHasComboInformation combo)
|
||||
{
|
||||
comboIndexBindable.BindTo(combo.ComboIndexBindable);
|
||||
comboIndexWithOffsetsBindable.BindTo(combo.ComboIndexWithOffsetsBindable);
|
||||
}
|
||||
|
||||
samplesBindable.BindTo(HitObject.SamplesBindable);
|
||||
samplesBindable.BindCollectionChanged(onSamplesChanged, true);
|
||||
@ -274,8 +282,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
protected sealed override void OnFree(HitObjectLifetimeEntry entry)
|
||||
{
|
||||
StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable);
|
||||
|
||||
if (HitObject is IHasComboInformation combo)
|
||||
{
|
||||
comboIndexBindable.UnbindFrom(combo.ComboIndexBindable);
|
||||
comboIndexWithOffsetsBindable.UnbindFrom(combo.ComboIndexWithOffsetsBindable);
|
||||
}
|
||||
|
||||
samplesBindable.UnbindFrom(HitObject.SamplesBindable);
|
||||
|
||||
// Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway.
|
||||
@ -403,13 +416,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
|
||||
clearExistingStateTransforms();
|
||||
|
||||
using (BeginAbsoluteSequence(transformTime, true))
|
||||
using (BeginAbsoluteSequence(transformTime))
|
||||
UpdateInitialTransforms();
|
||||
|
||||
using (BeginAbsoluteSequence(StateUpdateTime, true))
|
||||
using (BeginAbsoluteSequence(StateUpdateTime))
|
||||
UpdateStartTimeStateTransforms();
|
||||
|
||||
using (BeginAbsoluteSequence(HitStateUpdateTime, true))
|
||||
using (BeginAbsoluteSequence(HitStateUpdateTime))
|
||||
UpdateHitStateTransforms(newState);
|
||||
|
||||
state.Value = newState;
|
||||
@ -501,8 +514,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
{
|
||||
if (!(HitObject is IHasComboInformation combo)) return;
|
||||
|
||||
var comboColours = CurrentSkin.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty<Color4>();
|
||||
AccentColour.Value = combo.GetComboColour(comboColours);
|
||||
AccentColour.Value = combo.GetComboColour(CurrentSkin);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -716,7 +728,8 @@ namespace osu.Game.Rulesets.Objects.Drawables
|
||||
if (HitObject != null)
|
||||
HitObject.DefaultsApplied -= onDefaultsApplied;
|
||||
|
||||
CurrentSkin.SourceChanged -= skinSourceChanged;
|
||||
if (CurrentSkin != null)
|
||||
CurrentSkin.SourceChanged -= skinSourceChanged;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -118,6 +118,7 @@ namespace osu.Game.Rulesets.Objects
|
||||
foreach (var n in NestedHitObjects.OfType<IHasComboInformation>())
|
||||
{
|
||||
n.ComboIndexBindable.BindTo(hasCombo.ComboIndexBindable);
|
||||
n.ComboIndexWithOffsetsBindable.BindTo(hasCombo.ComboIndexWithOffsetsBindable);
|
||||
n.IndexInCurrentComboBindable.BindTo(hasCombo.IndexInCurrentComboBindable);
|
||||
}
|
||||
}
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
|
||||
return new ConvertHit
|
||||
{
|
||||
X = position.X,
|
||||
Position = position,
|
||||
NewCombo = newCombo,
|
||||
ComboOffset = comboOffset
|
||||
};
|
||||
@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
|
||||
|
||||
return new ConvertSlider
|
||||
{
|
||||
X = position.X,
|
||||
Position = position,
|
||||
NewCombo = FirstObject || newCombo,
|
||||
ComboOffset = comboOffset,
|
||||
Path = new SliderPath(controlPoints, length),
|
||||
|
@ -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; }
|
||||
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
|
||||
@ -16,14 +17,32 @@ 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>
|
||||
public 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; }
|
||||
|
||||
@ -65,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>
|
||||
@ -76,16 +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.");
|
||||
|
||||
Entry = entry;
|
||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||
setLifetimeFromEntry(entry);
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
apply(entry);
|
||||
}
|
||||
|
||||
protected sealed override void FreeAfterUse()
|
||||
@ -111,6 +124,20 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
{
|
||||
}
|
||||
|
||||
private void apply(TEntry entry)
|
||||
{
|
||||
if (HasEntryApplied)
|
||||
free();
|
||||
|
||||
this.entry = entry;
|
||||
entry.LifetimeChanged += setLifetimeFromEntry;
|
||||
setLifetimeFromEntry(entry);
|
||||
|
||||
OnApply(entry);
|
||||
|
||||
HasEntryApplied = true;
|
||||
}
|
||||
|
||||
private void free()
|
||||
{
|
||||
Debug.Assert(Entry != null && HasEntryApplied);
|
||||
@ -118,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Pooling
|
||||
OnFree(Entry);
|
||||
|
||||
Entry.LifetimeChanged -= setLifetimeFromEntry;
|
||||
Entry = null;
|
||||
entry = null;
|
||||
base.LifetimeStart = double.MinValue;
|
||||
base.LifetimeEnd = double.MaxValue;
|
||||
|
||||
|
47
osu.Game/Rulesets/Objects/SliderPathExtensions.cs
Normal file
47
osu.Game/Rulesets/Objects/SliderPathExtensions.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.Linq;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osuTK;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace osu.Game.Rulesets.Objects
|
||||
{
|
||||
public static class SliderPathExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Reverse the direction of this path.
|
||||
/// </summary>
|
||||
/// <param name="sliderPath">The <see cref="SliderPath"/>.</param>
|
||||
/// <param name="positionalOffset">The positional offset of the resulting path. It should be added to the start position of this path.</param>
|
||||
public static void Reverse(this SliderPath sliderPath, out Vector2 positionalOffset)
|
||||
{
|
||||
var points = sliderPath.ControlPoints.ToArray();
|
||||
positionalOffset = points.Last().Position.Value;
|
||||
|
||||
sliderPath.ControlPoints.Clear();
|
||||
|
||||
PathType? lastType = null;
|
||||
|
||||
for (var i = 0; i < points.Length; i++)
|
||||
{
|
||||
var p = points[i];
|
||||
p.Position.Value -= positionalOffset;
|
||||
|
||||
// propagate types forwards to last null type
|
||||
if (i == points.Length - 1)
|
||||
p.Type.Value = lastType;
|
||||
else if (p.Type.Value != null)
|
||||
{
|
||||
var newType = p.Type.Value;
|
||||
p.Type.Value = lastType;
|
||||
lastType = newType;
|
||||
}
|
||||
|
||||
sliderPath.ControlPoints.Insert(0, p);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
// 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 JetBrains.Annotations;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
@ -16,17 +15,25 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
Bindable<int> IndexInCurrentComboBindable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The offset of this hitobject in the current combo.
|
||||
/// The index of this hitobject in the current combo.
|
||||
/// </summary>
|
||||
int IndexInCurrentCombo { get; set; }
|
||||
|
||||
Bindable<int> ComboIndexBindable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The offset of this combo in relation to the beatmap.
|
||||
/// The index of this combo in relation to the beatmap.
|
||||
/// </summary>
|
||||
int ComboIndex { get; set; }
|
||||
|
||||
Bindable<int> ComboIndexWithOffsetsBindable { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The index of this combo in relation to the beatmap, with all aggregate <see cref="IHasCombo.ComboOffset"/>s applied.
|
||||
/// This should be used instead of <see cref="ComboIndex"/> only when retrieving combo colours from the beatmap's skin.
|
||||
/// </summary>
|
||||
int ComboIndexWithOffsets { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the HitObject starts a new combo.
|
||||
/// </summary>
|
||||
@ -40,11 +47,21 @@ namespace osu.Game.Rulesets.Objects.Types
|
||||
bool LastInCombo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object from a set of possible combo colours.
|
||||
/// Defaults to using <see cref="ComboIndex"/> to decide the colour.
|
||||
/// Retrieves the colour of the combo described by this <see cref="IHasComboInformation"/> object.
|
||||
/// </summary>
|
||||
/// <param name="comboColours">A list of possible combo colours provided by the beatmap or skin.</param>
|
||||
/// <returns>The colour of the combo described by this <see cref="IHasComboInformation"/> object.</returns>
|
||||
Color4 GetComboColour([NotNull] IReadOnlyList<Color4> comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White;
|
||||
/// <param name="skin">The skin to retrieve the combo colour from, if wanted.</param>
|
||||
Color4 GetComboColour(ISkin skin) => GetSkinComboColour(this, skin, ComboIndex);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves the colour of the combo described by a given <see cref="IHasComboInformation"/> object from a given skin.
|
||||
/// </summary>
|
||||
/// <param name="combo">The combo information, should be <c>this</c>.</param>
|
||||
/// <param name="skin">The skin to retrieve the combo colour from.</param>
|
||||
/// <param name="comboIndex">The index to retrieve the combo colour with.</param>
|
||||
/// <returns></returns>
|
||||
protected static Color4 GetSkinComboColour(IHasComboInformation combo, ISkin skin, int comboIndex)
|
||||
{
|
||||
return skin.GetConfig<SkinComboColourLookup, Color4>(new SkinComboColourLookup(comboIndex, combo))?.Value ?? Color4.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
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; }
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Replays;
|
||||
|
||||
@ -117,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.
|
||||
|
@ -127,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()
|
||||
{
|
||||
|
@ -96,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();
|
||||
|
@ -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);
|
||||
|
@ -1,29 +1,30 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Input.Handlers;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -98,6 +99,14 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private DrawableRulesetDependencies dependencies;
|
||||
|
||||
/// <summary>
|
||||
/// Audio adjustments which are applied to the playfield.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Does not affect <see cref="Overlays"/>.
|
||||
/// </remarks>
|
||||
public IAdjustableAudioComponent Audio { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a ruleset visualisation for the provided ruleset and beatmap.
|
||||
/// </summary>
|
||||
@ -155,23 +164,28 @@ namespace osu.Game.Rulesets.UI
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(CancellationToken? cancellationToken)
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
AudioContainer audioContainer;
|
||||
|
||||
InternalChild = frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
|
||||
{
|
||||
frameStabilityContainer = new FrameStabilityContainer(GameplayStartTime)
|
||||
FrameStablePlayback = FrameStablePlayback,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
FrameStablePlayback = FrameStablePlayback,
|
||||
Children = new Drawable[]
|
||||
FrameStableComponents,
|
||||
audioContainer = new AudioContainer
|
||||
{
|
||||
FrameStableComponents,
|
||||
KeyBindingInputManager
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = KeyBindingInputManager
|
||||
.WithChild(CreatePlayfieldAdjustmentContainer()
|
||||
.WithChild(Playfield)
|
||||
),
|
||||
Overlays,
|
||||
}
|
||||
},
|
||||
},
|
||||
Overlays,
|
||||
}
|
||||
};
|
||||
|
||||
Audio = audioContainer;
|
||||
|
||||
if ((ResumeOverlay = CreateResumeOverlay()) != null)
|
||||
{
|
||||
AddInternal(CreateInputManager()
|
||||
@ -199,8 +213,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)
|
||||
@ -486,15 +503,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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -356,8 +356,8 @@ namespace osu.Game.Rulesets.UI
|
||||
// This is done before Apply() so that the state is updated once when the hitobject is applied.
|
||||
if (mods != null)
|
||||
{
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObjects>())
|
||||
m.ApplyToDrawableHitObjects(dho.Yield());
|
||||
foreach (var m in mods.OfType<IApplicableToDrawableHitObject>())
|
||||
m.ApplyToDrawableHitObject(dho);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
base.ReloadMappings();
|
||||
|
||||
KeyBindings = KeyBindings.Where(b => KeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
KeyBindings = KeyBindings.Where(b => RealmKeyBindingStore.CheckValidForGameplay(b.KeyCombination)).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
/// <summary>
|
||||
/// The maximum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_max = 10000;
|
||||
private const double time_span_max = 20000;
|
||||
|
||||
/// <summary>
|
||||
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
|
||||
|
@ -19,6 +19,20 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
private readonly IBindable<double> timeRange = new BindableDouble();
|
||||
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether the scrolling direction is horizontal or vertical.
|
||||
/// </summary>
|
||||
private Direction scrollingAxis => direction.Value == ScrollingDirection.Left || direction.Value == ScrollingDirection.Right ? Direction.Horizontal : Direction.Vertical;
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
@ -48,99 +62,67 @@ namespace osu.Game.Rulesets.UI.Scrolling
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a position in screen space, return the time within this column.
|
||||
/// Given a position at <paramref name="currentTime"/>, return the time of the object corresponding to the position.
|
||||
/// </summary>
|
||||
public double TimeAtScreenSpacePosition(Vector2 screenSpacePosition)
|
||||
/// <remarks>
|
||||
/// If there are multiple valid time values, one arbitrary time is returned.
|
||||
/// </remarks>
|
||||
public double TimeAtPosition(float localPosition, double currentTime)
|
||||
{
|
||||
// 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, scrollLength);
|
||||
float scrollPosition = axisInverted ? -localPosition : localPosition;
|
||||
return scrollingInfo.Algorithm.TimeAt(scrollPosition, currentTime, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a time, return the screen space position 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)
|
||||
{
|
||||
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 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, scrollLength);
|
||||
|
||||
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));
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
private float scrollLength
|
||||
/// <summary>
|
||||
/// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis.
|
||||
/// </summary>
|
||||
public float LengthAtTime(double startTime, double endTime)
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (scrollingInfo.Direction.Value)
|
||||
{
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
return DrawWidth;
|
||||
|
||||
default:
|
||||
return DrawHeight;
|
||||
}
|
||||
}
|
||||
return scrollingInfo.Algorithm.GetLength(startTime, endTime, timeRange.Value, scrollLength);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
private float scrollLength => scrollingAxis == Direction.Horizontal ? DrawWidth : DrawHeight;
|
||||
|
||||
protected override void AddDrawable(HitObjectLifetimeEntry entry, DrawableHitObject drawable)
|
||||
{
|
||||
@ -237,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)
|
||||
@ -262,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