mirror of
https://github.com/ppy/osu.git
synced 2026-05-13 20:33:35 +08:00
Merge branch 'master' into mania-bring-back-touch-controls
This commit is contained in:
@@ -18,3 +18,6 @@ M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize(
|
||||
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.
|
||||
M:Humanizer.InflectorExtensions.Kebaberize(System.String);Humanizer's .Kebaberize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToKebabCase() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Int32,System.Int32,System.Int32)~System.Int32;Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Single,System.Single,System.Single)~System.Single;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
M:osuTK.MathHelper.Clamp(System.Double,System.Double,System.Double)~System.Double;This osuTK helper has unsafe semantics when one of the bounds provided is NaN. Use Math.Clamp() instead.
|
||||
|
||||
+2
-2
@@ -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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
@@ -9,7 +10,6 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Pippidon.Objects;
|
||||
using osu.Game.Rulesets.Pippidon.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Pippidon.Beatmaps
|
||||
};
|
||||
}
|
||||
|
||||
private int getLane(HitObject hitObject) => (int)MathHelper.Clamp(
|
||||
private int getLane(HitObject hitObject) => (int)Math.Clamp(
|
||||
(getUsablePosition(hitObject) - minPosition) / (maxPosition - minPosition) * PippidonPlayfield.LANE_COUNT, 0, PippidonPlayfield.LANE_COUNT - 1);
|
||||
|
||||
private float getUsablePosition(HitObject h) => (h as IHasYPosition)?.Y ?? ((IHasXPosition)h).X;
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.220.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.311.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Tests.Editor
|
||||
{
|
||||
public abstract partial class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new CatchRuleset();
|
||||
|
||||
protected const double TIME_SNAP = 100;
|
||||
|
||||
protected DrawableCatchHitObject LastObject;
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,15 +9,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
{
|
||||
public class CatchDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
@@ -26,7 +16,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
// Todo: osu!catch should not output star rating in the 'aim' attribute.
|
||||
yield return (ATTRIB_ID_AIM, StarRating);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -34,7 +23,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_AIM];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
|
||||
private float halfCatcherWidth;
|
||||
|
||||
public override int Version => 20220701;
|
||||
public override int Version => 20250306;
|
||||
|
||||
public CatchDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -35,14 +36,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new CatchDifficultyAttributes { Mods = mods };
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
CatchDifficultyAttributes attributes = new CatchDifficultyAttributes
|
||||
{
|
||||
StarRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier,
|
||||
StarRating = Math.Sqrt(skills.OfType<Movement>().Single().DifficultyValue()) * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
ApproachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
@@ -50,7 +53,19 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
if (catchAttributes.MaxCombo > 0)
|
||||
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
double approachRate = catchAttributes.ApproachRate;
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
double clockRate = track.Rate;
|
||||
|
||||
// this is the same as osu!, so there's potential to share the implementation... maybe
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
double approachRate = preempt > 1200.0 ? -(preempt - 1800.0) / 120.0 : -(preempt - 1200.0) / 150.0 + 5.0;
|
||||
|
||||
double approachRateFactor = 1.0;
|
||||
if (approachRate > 9.0)
|
||||
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
|
||||
|
||||
@@ -26,7 +26,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
private float? lastPlayerPosition;
|
||||
private float lastDistanceMoved;
|
||||
private float lastExactDistanceMoved;
|
||||
private double lastStrainTime;
|
||||
private bool isInBuzzSection;
|
||||
|
||||
/// <summary>
|
||||
/// The speed multiplier applied to the player's catcher.
|
||||
@@ -59,6 +61,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
|
||||
float distanceMoved = playerPosition - lastPlayerPosition.Value;
|
||||
|
||||
// For the exact position we consider that the catcher is in the correct position for both objects
|
||||
float exactDistanceMoved = catchCurrent.NormalizedPosition - lastPlayerPosition.Value;
|
||||
|
||||
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
|
||||
|
||||
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
|
||||
@@ -92,12 +97,30 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
|
||||
playerPosition = catchCurrent.NormalizedPosition;
|
||||
}
|
||||
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20)
|
||||
* Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
|
||||
}
|
||||
|
||||
// There is an edge case where horizontal back and forth sliders create "buzz" patterns which are repeated "movements" with a distance lower than
|
||||
// the platter's width but high enough to be considered a movement due to the absolute_player_positioning_error and normalized_hitobject_radius offsets
|
||||
// We are detecting this exact scenario. The first back and forth is counted but all subsequent ones are nullified.
|
||||
// To achieve that, we need to store the exact distances (distance ignoring absolute_player_positioning_error and normalized_hitobject_radius)
|
||||
if (Math.Abs(exactDistanceMoved) <= HalfCatcherWidth * 2 && exactDistanceMoved == -lastExactDistanceMoved && catchCurrent.StrainTime == lastStrainTime)
|
||||
{
|
||||
if (isInBuzzSection)
|
||||
distanceAddition = 0;
|
||||
else
|
||||
isInBuzzSection = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
isInBuzzSection = false;
|
||||
}
|
||||
|
||||
lastPlayerPosition = playerPosition;
|
||||
lastDistanceMoved = distanceMoved;
|
||||
lastStrainTime = catchCurrent.StrainTime;
|
||||
lastExactDistanceMoved = exactDistanceMoved;
|
||||
|
||||
return distanceAddition / weightedStrainTime;
|
||||
}
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints.Components
|
||||
{
|
||||
base.UpdateHitObjectFromPath(hitObject);
|
||||
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLength)
|
||||
if (hitObject.Path.ControlPoints.Count <= 1 || !hitObject.Path.HasValidLengthForPlacement)
|
||||
EditorBeatmap?.Remove(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 osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Argon
|
||||
|
||||
double duration = ObjectState.HitObject.StartTime - ObjectState.DisplayStartTime;
|
||||
|
||||
fadeContent.Alpha = MathHelper.Clamp(
|
||||
fadeContent.Alpha = Math.Clamp(
|
||||
Interpolation.ValueAt(
|
||||
Time.Current, 1f, 0f,
|
||||
ObjectState.DisplayStartTime + duration * lens_flare_start,
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public abstract partial class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new ManiaRuleset();
|
||||
|
||||
private readonly Column column;
|
||||
|
||||
[Cached(typeof(IReadOnlyList<Mod>))]
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().SingleOrDefault()?.CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("refuse", () => InputManager.Key(Key.Number2));
|
||||
AddAssert("key count is 5", () => keyCount.Current.Value, () => Is.EqualTo(5));
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
keyCount.Current.Value = 8;
|
||||
});
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<ReloadEditorDialog>);
|
||||
AddUntilStep("dialog visible", () => Game.ChildrenOfType<IDialogOverlay>().Single().CurrentDialog, Is.InstanceOf<SaveAndReloadEditorDialog>);
|
||||
AddStep("acquiesce", () => InputManager.Key(Key.Number1));
|
||||
AddUntilStep("beatmap became 8K", () => Game.Beatmap.Value.BeatmapInfo.Difficulty.CircleSize, () => Is.EqualTo(8));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
|
||||
@@ -10,22 +9,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
{
|
||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods do not affect the hit window at all in osu-stable.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
foreach (var v in base.ToDatabaseAttributes())
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -33,7 +22,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
private const double difficulty_multiplier = 0.018;
|
||||
|
||||
private readonly bool isForCurrentRuleset;
|
||||
private readonly double originalOverallDifficulty;
|
||||
|
||||
public override int Version => 20241007;
|
||||
|
||||
@@ -35,7 +34,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
: base(ruleset, beatmap)
|
||||
{
|
||||
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.MatchesOnlineID(ruleset);
|
||||
originalOverallDifficulty = beatmap.BeatmapInfo.Difficulty.OverallDifficulty;
|
||||
}
|
||||
|
||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||
@@ -48,11 +46,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
|
||||
ManiaDifficultyAttributes attributes = new ManiaDifficultyAttributes
|
||||
{
|
||||
StarRating = skills[0].DifficultyValue() * difficulty_multiplier,
|
||||
StarRating = skills.OfType<Strain>().Single().DifficultyValue() * difficulty_multiplier,
|
||||
Mods = mods,
|
||||
// In osu-stable mania, rate-adjustment mods don't affect the hit window.
|
||||
// This is done the way it is to introduce fractional differences in order to match osu-stable for the time being.
|
||||
GreatHitWindow = Math.Ceiling((int)(getHitWindow300(mods) * clockRate) / clockRate),
|
||||
MaxCombo = beatmap.HitObjects.Sum(maxComboForObject),
|
||||
};
|
||||
|
||||
@@ -124,29 +119,5 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
}).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private double getHitWindow300(Mod[] mods)
|
||||
{
|
||||
if (isForCurrentRuleset)
|
||||
{
|
||||
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
|
||||
return applyModAdjustments(34 + 3 * od, mods);
|
||||
}
|
||||
|
||||
if (Math.Round(originalOverallDifficulty) > 4)
|
||||
return applyModAdjustments(34, mods);
|
||||
|
||||
return applyModAdjustments(47, mods);
|
||||
|
||||
static double applyModAdjustments(double value, Mod[] mods)
|
||||
{
|
||||
if (mods.Any(m => m is ManiaModHardRock))
|
||||
value /= 1.4;
|
||||
else if (mods.Any(m => m is ManiaModEasy))
|
||||
value *= 1.4;
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
|
||||
updatingKeyCount = true;
|
||||
|
||||
editor.Reload().ContinueWith(t =>
|
||||
editor.SaveAndReload().ContinueWith(t =>
|
||||
{
|
||||
if (!t.GetResultSafely())
|
||||
{
|
||||
|
||||
@@ -26,9 +26,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
: base(barLine)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 1;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.BarLine), _ => new DefaultBarLine())
|
||||
@@ -36,8 +37,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
});
|
||||
|
||||
Major.BindValueChanged(major => Height = major.NewValue ? 1.7f : 1.2f, true);
|
||||
}
|
||||
|
||||
protected override void OnApply()
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(DrawableHitObject drawableHitObject)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
|
||||
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||
var edgeSmoothness = new Vector2(0.3f);
|
||||
@@ -75,6 +75,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Default
|
||||
|
||||
private void updateMajor(ValueChangedEvent<bool> major)
|
||||
{
|
||||
Height = major.NewValue ? 1.7f : 1.2f;
|
||||
|
||||
mainLine.Alpha = major.NewValue ? 0.5f : 0.2f;
|
||||
leftAnchor.Alpha = rightAnchor.Alpha = major.NewValue ? mainLine.Alpha * 0.3f : 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyBarLine : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
float skinHeight = skin.GetManiaSkinConfig<float>(LegacyManiaSkinConfigurationLookups.BarLineHeight)?.Value ?? 1;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 1.2f * skinHeight;
|
||||
Colour = skin.GetManiaSkinConfig<Color4>(LegacyManiaSkinConfigurationLookups.BarLineColour)?.Value ?? Color4.White;
|
||||
|
||||
// Avoid flickering due to no anti-aliasing of boxes by default.
|
||||
var edgeSmoothness = new Vector2(0.3f);
|
||||
|
||||
AddInternal(new Box
|
||||
{
|
||||
Name = "Bar line",
|
||||
EdgeSmoothness = edgeSmoothness,
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
|
||||
return new LegacyStageForeground();
|
||||
|
||||
case ManiaSkinComponents.BarLine:
|
||||
return null; // Not yet implemented.
|
||||
return new LegacyBarLine();
|
||||
|
||||
default:
|
||||
throw new UnsupportedSkinComponentException(lookup);
|
||||
|
||||
@@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableHitCircle((HitCircle)hitObject);
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new HitCirclePlacementBlueprint();
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
{
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene
|
||||
{
|
||||
protected sealed override Ruleset CreateRuleset() => new OsuRuleset();
|
||||
|
||||
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSpinner((Spinner)hitObject);
|
||||
|
||||
protected override HitObjectPlacementBlueprint CreateBlueprint() => new SpinnerPlacementBlueprint();
|
||||
|
||||
@@ -15,22 +15,22 @@ namespace osu.Game.Rulesets.Osu.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
[TestCase(0.14143808967817237d, 2, "nan-slider")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(8.9825709931204205d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55231632896800109d, 4, "very-fast-slider")]
|
||||
[TestCase(9.6779746353001634d, 239, "diffcalc-test")]
|
||||
[TestCase(1.7691451263718989d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.55785578988249407d, 4, "very-fast-slider")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
|
||||
|
||||
[TestCase(6.7171144000821119d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.42630400627180914d, 4, "very-fast-slider")]
|
||||
[TestCase(6.7331304290522747d, 239, "diffcalc-test")]
|
||||
[TestCase(1.4602604078137214d, 54, "zero-length-sliders")]
|
||||
[TestCase(0.43052813047866129d, 4, "very-fast-slider")]
|
||||
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
|
||||
|
||||
|
||||
@@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
public static class AimEvaluator
|
||||
{
|
||||
private const double wide_angle_multiplier = 1.5;
|
||||
private const double acute_angle_multiplier = 1.95;
|
||||
private const double acute_angle_multiplier = 2.6;
|
||||
private const double slider_multiplier = 1.35;
|
||||
private const double velocity_change_multiplier = 0.75;
|
||||
private const double wiggle_multiplier = 1.02;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of aiming the current object, based on:
|
||||
@@ -64,16 +65,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
double acuteAngleBonus = 0;
|
||||
double sliderBonus = 0;
|
||||
double velocityChangeBonus = 0;
|
||||
double wiggleBonus = 0;
|
||||
|
||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||
|
||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
||||
{
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
|
||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null)
|
||||
{
|
||||
double currAngle = osuCurrObj.Angle.Value;
|
||||
double lastAngle = osuLastObj.Angle.Value;
|
||||
double lastLastAngle = osuLastLastObj.Angle.Value;
|
||||
|
||||
// Rewarding angles, take the smaller velocity as base.
|
||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||
@@ -81,20 +82,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||
|
||||
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||
acuteAngleBonus = 0;
|
||||
else
|
||||
{
|
||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
|
||||
}
|
||||
// Penalize angle repetition.
|
||||
wideAngleBonus *= 1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3));
|
||||
acuteAngleBonus *= 0.08 + 0.92 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastAngle), 3)));
|
||||
|
||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
||||
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
|
||||
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
|
||||
// Apply full wide angle bonus for distance more than one diameter
|
||||
wideAngleBonus *= angleBonus * DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, 0, diameter);
|
||||
|
||||
// Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
|
||||
acuteAngleBonus *= angleBonus *
|
||||
DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
|
||||
DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
|
||||
|
||||
// Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle
|
||||
// https://www.desmos.com/calculator/dp0v0nvowc
|
||||
wiggleBonus = angleBonus
|
||||
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
|
||||
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
|
||||
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
|
||||
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +130,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||
}
|
||||
|
||||
aimStrain += wiggleBonus * wiggle_multiplier;
|
||||
|
||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||
|
||||
@@ -132,8 +142,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
return aimStrain;
|
||||
}
|
||||
|
||||
private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
||||
private static double calcWideAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(40), double.DegreesToRadians(140));
|
||||
|
||||
private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
||||
private static double calcAcuteAngleBonus(double angle) => DifficultyCalculationUtils.Smoothstep(angle, double.DegreesToRadians(140), double.DegreesToRadians(40));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
if (!(currentObj.BaseObject is Spinner))
|
||||
{
|
||||
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.StackedEndPosition).Length;
|
||||
|
||||
cumulativeStrainTime += lastObj.StrainTime;
|
||||
|
||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||
if (i == 0)
|
||||
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
@@ -14,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
private const double single_spacing_threshold = OsuDifficultyHitObject.NORMALISED_DIAMETER * 1.25; // 1.25 circles distance between centers
|
||||
private const double min_speed_bonus = 200; // 200 BPM 1/4th
|
||||
private const double speed_balancing_factor = 40;
|
||||
private const double distance_multiplier = 0.94;
|
||||
private const double distance_multiplier = 0.9;
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the difficulty of tapping the current object, based on:
|
||||
@@ -24,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current)
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject current, IReadOnlyList<Mod> mods)
|
||||
{
|
||||
if (current.BaseObject is Spinner)
|
||||
return 0;
|
||||
@@ -56,6 +60,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||
// Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold
|
||||
double distanceBonus = Math.Pow(distance / single_spacing_threshold, 3.95) * distance_multiplier;
|
||||
|
||||
if (mods.OfType<OsuModAutopilot>().Any())
|
||||
distanceBonus = 0;
|
||||
|
||||
// Base difficulty with all bonuses
|
||||
double difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ using Newtonsoft.Json;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("aim_difficulty")]
|
||||
public double AimDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of <see cref="Slider"/>s weighted by difficulty.
|
||||
/// </summary>
|
||||
[JsonProperty("aim_difficult_slider_count")]
|
||||
public double AimDifficultSliderCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the speed skill.
|
||||
/// </summary>
|
||||
@@ -52,24 +59,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("speed_difficult_strain_count")]
|
||||
public double SpeedDifficultStrainCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived approach rate inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the approach rate difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("approach_rate")]
|
||||
public double ApproachRate { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived overall difficulty inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the overall difficulty value, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("overall_difficulty")]
|
||||
public double OverallDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The beatmap's drain rate. This doesn't scale with rate-adjusting mods.
|
||||
/// </summary>
|
||||
@@ -97,8 +86,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
yield return (ATTRIB_ID_AIM, AimDifficulty);
|
||||
yield return (ATTRIB_ID_SPEED, SpeedDifficulty);
|
||||
yield return (ATTRIB_ID_OVERALL_DIFFICULTY, OverallDifficulty);
|
||||
yield return (ATTRIB_ID_APPROACH_RATE, ApproachRate);
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
|
||||
if (ShouldSerializeFlashlightDifficulty())
|
||||
@@ -109,6 +96,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
|
||||
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
|
||||
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
|
||||
@@ -117,14 +105,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
AimDifficulty = values[ATTRIB_ID_AIM];
|
||||
SpeedDifficulty = values[ATTRIB_ID_SPEED];
|
||||
OverallDifficulty = values[ATTRIB_ID_OVERALL_DIFFICULTY];
|
||||
ApproachRate = values[ATTRIB_ID_APPROACH_RATE];
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
FlashlightDifficulty = values.GetValueOrDefault(ATTRIB_ID_FLASHLIGHT);
|
||||
SliderFactor = values[ATTRIB_ID_SLIDER_FACTOR];
|
||||
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
|
||||
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
|
||||
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
|
||||
DrainRate = onlineInfo.DrainRate;
|
||||
HitCircleCount = onlineInfo.CircleCount;
|
||||
SliderCount = onlineInfo.SliderCount;
|
||||
|
||||
@@ -15,8 +15,6 @@ using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
@@ -24,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
{
|
||||
private const double difficulty_multiplier = 0.0675;
|
||||
|
||||
public override int Version => 20241007;
|
||||
public override int Version => 20250306;
|
||||
|
||||
public OsuDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -36,20 +34,22 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new OsuDifficultyAttributes { Mods = mods };
|
||||
|
||||
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
|
||||
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
|
||||
|
||||
double flashlightRating = 0.0;
|
||||
|
||||
if (mods.Any(h => h is OsuModFlashlight))
|
||||
flashlightRating = Math.Sqrt(skills[3].DifficultyValue()) * difficulty_multiplier;
|
||||
var aim = skills.OfType<Aim>().Single(a => a.IncludeSliders);
|
||||
double aimRating = Math.Sqrt(aim.DifficultyValue()) * difficulty_multiplier;
|
||||
double aimDifficultyStrainCount = aim.CountTopWeightedStrains();
|
||||
double difficultSliders = aim.GetDifficultSliders();
|
||||
|
||||
var aimWithoutSliders = skills.OfType<Aim>().Single(a => !a.IncludeSliders);
|
||||
double aimRatingNoSliders = Math.Sqrt(aimWithoutSliders.DifficultyValue()) * difficulty_multiplier;
|
||||
double sliderFactor = aimRating > 0 ? aimRatingNoSliders / aimRating : 1;
|
||||
|
||||
double aimDifficultyStrainCount = ((OsuStrainSkill)skills[0]).CountTopWeightedStrains();
|
||||
double speedDifficultyStrainCount = ((OsuStrainSkill)skills[2]).CountTopWeightedStrains();
|
||||
var speed = skills.OfType<Speed>().Single();
|
||||
double speedRating = Math.Sqrt(speed.DifficultyValue()) * difficulty_multiplier;
|
||||
double speedNotes = speed.RelevantNoteCount();
|
||||
double speedDifficultyStrainCount = speed.CountTopWeightedStrains();
|
||||
|
||||
var flashlight = skills.OfType<Flashlight>().SingleOrDefault();
|
||||
double flashlightRating = flashlight == null ? 0.0 : Math.Sqrt(flashlight.DifficultyValue()) * difficulty_multiplier;
|
||||
|
||||
if (mods.Any(m => m is OsuModTouchDevice))
|
||||
{
|
||||
@@ -63,6 +63,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedRating = 0.0;
|
||||
flashlightRating *= 0.7;
|
||||
}
|
||||
else if (mods.Any(h => h is OsuModAutopilot))
|
||||
{
|
||||
speedRating *= 0.5;
|
||||
aimRating = 0.0;
|
||||
flashlightRating *= 0.4;
|
||||
}
|
||||
|
||||
double baseAimPerformance = OsuStrainSkill.DifficultyToPerformance(aimRating);
|
||||
double baseSpeedPerformance = OsuStrainSkill.DifficultyToPerformance(speedRating);
|
||||
@@ -82,31 +88,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
? Math.Cbrt(OsuPerformanceCalculator.PERFORMANCE_BASE_MULTIPLIER) * 0.027 * (Math.Cbrt(100000 / Math.Pow(2, 1 / 1.1) * basePerformance) + 4)
|
||||
: 0;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
double drainRate = beatmap.Difficulty.DrainRate;
|
||||
|
||||
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
|
||||
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
|
||||
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
double hitWindowGreat = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
OsuDifficultyAttributes attributes = new OsuDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
AimDifficulty = aimRating,
|
||||
AimDifficultSliderCount = difficultSliders,
|
||||
SpeedDifficulty = speedRating,
|
||||
SpeedNoteCount = speedNotes,
|
||||
FlashlightDifficulty = flashlightRating,
|
||||
SliderFactor = sliderFactor,
|
||||
AimDifficultStrainCount = aimDifficultyStrainCount,
|
||||
SpeedDifficultStrainCount = speedDifficultyStrainCount,
|
||||
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
|
||||
OverallDifficulty = (80 - hitWindowGreat) / 6,
|
||||
DrainRate = drainRate,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
HitCircleCount = hitCirclesCount,
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
[JsonProperty("effective_miss_count")]
|
||||
public double EffectiveMissCount { get; set; }
|
||||
|
||||
[JsonProperty("speed_deviation")]
|
||||
public double? SpeedDeviation { get; set; }
|
||||
|
||||
public override IEnumerable<PerformanceDisplayAttribute> GetAttributesForDisplay()
|
||||
{
|
||||
foreach (var attribute in base.GetAttributesForDisplay())
|
||||
|
||||
@@ -4,9 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Scoring;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
@@ -40,6 +46,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
/// </summary>
|
||||
private double effectiveMissCount;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
private double okHitWindow;
|
||||
private double mehHitWindow;
|
||||
private double overallDifficulty;
|
||||
private double approachRate;
|
||||
|
||||
private double? speedDeviation;
|
||||
|
||||
public OsuPerformanceCalculator()
|
||||
: base(new OsuRuleset())
|
||||
{
|
||||
@@ -61,6 +76,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
countSliderTickMiss = score.Statistics.GetValueOrDefault(HitResult.LargeTickMiss);
|
||||
effectiveMissCount = countMiss;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
HitWindows hitWindows = new OsuHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
okHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate;
|
||||
mehHitWindow = hitWindows.WindowFor(HitResult.Meh) / clockRate;
|
||||
|
||||
double preempt = IBeatmapDifficultyInfo.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
|
||||
|
||||
overallDifficulty = (80 - greatHitWindow) / 6;
|
||||
approachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5;
|
||||
|
||||
if (osuAttributes.SliderCount > 0)
|
||||
{
|
||||
if (usingClassicSliderAccuracy)
|
||||
@@ -103,17 +138,20 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// https://www.desmos.com/calculator/bc9eybdthb
|
||||
// we use OD13.3 as maximum since it's the value at which great hitwidow becomes 0
|
||||
// this is well beyond currently maximum achievable OD which is 12.17 (DTx2 + DA with OD11)
|
||||
double okMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, osuAttributes.OverallDifficulty > 0.0 ? 1 - Math.Pow(osuAttributes.OverallDifficulty / 13.33, 5) : 1.0);
|
||||
double okMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 1.8) : 1.0);
|
||||
double mehMultiplier = Math.Max(0.0, overallDifficulty > 0.0 ? 1 - Math.Pow(overallDifficulty / 13.33, 5) : 1.0);
|
||||
|
||||
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
|
||||
effectiveMissCount = Math.Min(effectiveMissCount + countOk * okMultiplier + countMeh * mehMultiplier, totalHits);
|
||||
}
|
||||
|
||||
speedDeviation = calculateSpeedDeviation(osuAttributes);
|
||||
|
||||
double aimValue = computeAimValue(score, osuAttributes);
|
||||
double speedValue = computeSpeedValue(score, osuAttributes);
|
||||
double accuracyValue = computeAccuracyValue(score, osuAttributes);
|
||||
double flashlightValue = computeFlashlightValue(score, osuAttributes);
|
||||
|
||||
double totalValue =
|
||||
Math.Pow(
|
||||
Math.Pow(aimValue, 1.1) +
|
||||
@@ -129,13 +167,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
Accuracy = accuracyValue,
|
||||
Flashlight = flashlightValue,
|
||||
EffectiveMissCount = effectiveMissCount,
|
||||
SpeedDeviation = speedDeviation,
|
||||
Total = totalValue
|
||||
};
|
||||
}
|
||||
|
||||
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty);
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
return 0.0;
|
||||
|
||||
double aimDifficulty = attributes.AimDifficulty;
|
||||
|
||||
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, attributes.AimDifficultSliderCount);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
|
||||
aimDifficulty *= sliderNerfFactor;
|
||||
}
|
||||
|
||||
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
|
||||
|
||||
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
|
||||
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
|
||||
@@ -145,10 +210,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
aimValue *= calculateMissPenalty(effectiveMissCount, attributes.AimDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
else if (attributes.ApproachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - attributes.ApproachRate);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
else if (approachRate < 8.0)
|
||||
approachRateFactor = 0.05 * (8.0 - approachRate);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
approachRateFactor = 0.0;
|
||||
@@ -160,43 +225,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
}
|
||||
|
||||
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
|
||||
double estimateDifficultSliders = attributes.SliderCount * 0.15;
|
||||
|
||||
if (attributes.SliderCount > 0)
|
||||
{
|
||||
double estimateImproperlyFollowedDifficultSliders;
|
||||
|
||||
if (usingClassicSliderAccuracy)
|
||||
{
|
||||
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
|
||||
int maximumPossibleDroppedSliders = totalImperfectHits;
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We add tick misses here since they too mean that the player didn't follow the slider properly
|
||||
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
|
||||
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
|
||||
}
|
||||
|
||||
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
|
||||
aimValue *= sliderNerfFactor;
|
||||
aimValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
aimValue *= accuracy;
|
||||
// It is important to consider accuracy difficulty when scaling with accuracy.
|
||||
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
aimValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return aimValue;
|
||||
}
|
||||
|
||||
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (score.Mods.Any(h => h is OsuModRelax))
|
||||
if (score.Mods.Any(h => h is OsuModRelax) || speedDeviation == null)
|
||||
return 0.0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
@@ -209,8 +250,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
speedValue *= calculateMissPenalty(effectiveMissCount, attributes.SpeedDifficultStrainCount);
|
||||
|
||||
double approachRateFactor = 0.0;
|
||||
if (attributes.ApproachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
|
||||
if (approachRate > 10.33)
|
||||
approachRateFactor = 0.3 * (approachRate - 10.33);
|
||||
|
||||
if (score.Mods.Any(h => h is OsuModAutopilot))
|
||||
approachRateFactor = 0.0;
|
||||
|
||||
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
|
||||
|
||||
@@ -222,21 +266,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
else if (score.Mods.Any(m => m is OsuModHidden || m is OsuModTraceable))
|
||||
{
|
||||
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
|
||||
speedValue *= 1.0 + 0.04 * (12.0 - approachRate);
|
||||
}
|
||||
|
||||
double speedHighDeviationMultiplier = calculateSpeedHighDeviationNerf(attributes);
|
||||
speedValue *= speedHighDeviationMultiplier;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double relevantTotalDiff = totalHits - attributes.SpeedNoteCount;
|
||||
double relevantTotalDiff = Math.Max(0, totalHits - attributes.SpeedNoteCount);
|
||||
double relevantCountGreat = Math.Max(0, countGreat - relevantTotalDiff);
|
||||
double relevantCountOk = Math.Max(0, countOk - Math.Max(0, relevantTotalDiff - countGreat));
|
||||
double relevantCountMeh = Math.Max(0, countMeh - Math.Max(0, relevantTotalDiff - countGreat - countOk));
|
||||
double relevantAccuracy = attributes.SpeedNoteCount == 0 ? 0 : (relevantCountGreat * 6.0 + relevantCountOk * 2.0 + relevantCountMeh) / (attributes.SpeedNoteCount * 6.0);
|
||||
|
||||
// Scale the speed value with accuracy and OD.
|
||||
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - attributes.OverallDifficulty) / 2);
|
||||
|
||||
// Scale the speed value with # of 50s to punish doubletapping.
|
||||
speedValue *= Math.Pow(0.99, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
|
||||
speedValue *= (0.95 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 750) * Math.Pow((accuracy + relevantAccuracy) / 2.0, (14.5 - overallDifficulty) / 2);
|
||||
|
||||
return speedValue;
|
||||
}
|
||||
@@ -253,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
amountHitObjectsWithAccuracy += attributes.SliderCount;
|
||||
|
||||
if (amountHitObjectsWithAccuracy > 0)
|
||||
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
betterAccuracyPercentage = ((countGreat - Math.Max(totalHits - amountHitObjectsWithAccuracy, 0)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
|
||||
else
|
||||
betterAccuracyPercentage = 0;
|
||||
|
||||
@@ -263,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
|
||||
// Lots of arbitrary values from testing.
|
||||
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
|
||||
double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
double accuracyValue = Math.Pow(1.52163, overallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
|
||||
|
||||
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
|
||||
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
|
||||
@@ -300,17 +344,117 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
// Scale the flashlight value with accuracy _slightly_.
|
||||
flashlightValue *= 0.5 + accuracy / 2.0;
|
||||
// It is important to also consider accuracy difficulty when doing that.
|
||||
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
|
||||
flashlightValue *= 0.98 + Math.Pow(Math.Max(0, overallDifficulty), 2) / 2500;
|
||||
|
||||
return flashlightValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates player's deviation on speed notes using <see cref="calculateDeviation"/>, assuming worst-case.
|
||||
/// Treats all speed notes as hit circles.
|
||||
/// </summary>
|
||||
private double? calculateSpeedDeviation(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (totalSuccessfulHits == 0)
|
||||
return null;
|
||||
|
||||
// Calculate accuracy assuming the worst case scenario
|
||||
double speedNoteCount = attributes.SpeedNoteCount;
|
||||
speedNoteCount += (totalHits - attributes.SpeedNoteCount) * 0.1;
|
||||
|
||||
// Assume worst case: all mistakes were on speed notes
|
||||
double relevantCountMiss = Math.Min(countMiss, speedNoteCount);
|
||||
double relevantCountMeh = Math.Min(countMeh, speedNoteCount - relevantCountMiss);
|
||||
double relevantCountOk = Math.Min(countOk, speedNoteCount - relevantCountMiss - relevantCountMeh);
|
||||
double relevantCountGreat = Math.Max(0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk);
|
||||
|
||||
return calculateDeviation(attributes, relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses,
|
||||
/// assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the same map with the same settings
|
||||
/// will always return the same deviation. Misses are ignored because they are usually due to misaiming.
|
||||
/// Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution.
|
||||
/// </summary>
|
||||
private double? calculateDeviation(OsuDifficultyAttributes attributes, double relevantCountGreat, double relevantCountOk, double relevantCountMeh, double relevantCountMiss)
|
||||
{
|
||||
if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0)
|
||||
return null;
|
||||
|
||||
double objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss;
|
||||
|
||||
// The probability that a player hits a circle is unknown, but we can estimate it to be
|
||||
// the number of greats on circles divided by the number of circles, and then add one
|
||||
// to the number of circles as a bias correction.
|
||||
double n = Math.Max(1, objectCount - relevantCountMiss - relevantCountMeh);
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// Proportion of greats hit on circles, ignoring misses and 50s.
|
||||
double p = relevantCountGreat / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed.
|
||||
// Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than:
|
||||
double deviation = greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
|
||||
double randomValue = Math.Sqrt(2 / Math.PI) * okHitWindow * Math.Exp(-0.5 * Math.Pow(okHitWindow / deviation, 2))
|
||||
/ (deviation * DifficultyCalculationUtils.Erf(okHitWindow / (Math.Sqrt(2) * deviation)));
|
||||
|
||||
deviation *= Math.Sqrt(1 - randomValue);
|
||||
|
||||
// Value deviation approach as greatCount approaches 0
|
||||
double limitValue = okHitWindow / Math.Sqrt(3);
|
||||
|
||||
// If precision is not enough to compute true deviation - use limit value
|
||||
if (pLowerBound == 0 || randomValue >= 1 || deviation > limitValue)
|
||||
deviation = limitValue;
|
||||
|
||||
// Then compute the variance for mehs.
|
||||
double mehVariance = (mehHitWindow * mehHitWindow + okHitWindow * mehHitWindow + okHitWindow * okHitWindow) / 3;
|
||||
|
||||
// Find the total deviation.
|
||||
deviation = Math.Sqrt(((relevantCountGreat + relevantCountOk) * Math.Pow(deviation, 2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh));
|
||||
|
||||
return deviation;
|
||||
}
|
||||
|
||||
// Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty
|
||||
// https://www.desmos.com/calculator/dmogdhzofn
|
||||
private double calculateSpeedHighDeviationNerf(OsuDifficultyAttributes attributes)
|
||||
{
|
||||
if (speedDeviation == null)
|
||||
return 0;
|
||||
|
||||
double speedValue = OsuStrainSkill.DifficultyToPerformance(attributes.SpeedDifficulty);
|
||||
|
||||
// Decides a point where the PP value achieved compared to the speed deviation is assumed to be tapped improperly. Any PP above this point is considered "excess" speed difficulty.
|
||||
// This is used to cause PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value.
|
||||
double excessSpeedDifficultyCutoff = 100 + 220 * Math.Pow(22 / speedDeviation.Value, 6.5);
|
||||
|
||||
if (speedValue <= excessSpeedDifficultyCutoff)
|
||||
return 1.0;
|
||||
|
||||
const double scale = 50;
|
||||
double adjustedSpeedValue = scale * (Math.Log((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale);
|
||||
|
||||
// 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible
|
||||
double lerp = 1 - DifficultyCalculationUtils.ReverseLerp(speedDeviation.Value, 22.0, 27.0);
|
||||
adjustedSpeedValue = double.Lerp(adjustedSpeedValue, speedValue, lerp);
|
||||
|
||||
return adjustedSpeedValue / speedValue;
|
||||
}
|
||||
|
||||
// Miss penalty assumes that a player will miss on the hardest parts of a map,
|
||||
// so we use the amount of relatively difficult sections to adjust miss penalty
|
||||
// to make it more punishing on maps with lower amount of hard sections.
|
||||
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.96 / ((missCount / (4 * Math.Pow(Math.Log(difficultStrainCount), 0.94))) + 1);
|
||||
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
private int totalSuccessfulHits => countGreat + countOk + countMeh;
|
||||
private int totalImperfectHits => countOk + countMeh + countMiss;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,12 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
{
|
||||
@@ -13,19 +16,21 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Aim : OsuStrainSkill
|
||||
{
|
||||
public Aim(Mod[] mods, bool withSliders)
|
||||
public readonly bool IncludeSliders;
|
||||
|
||||
public Aim(Mod[] mods, bool includeSliders)
|
||||
: base(mods)
|
||||
{
|
||||
this.withSliders = withSliders;
|
||||
IncludeSliders = includeSliders;
|
||||
}
|
||||
|
||||
private readonly bool withSliders;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
private double skillMultiplier => 25.18;
|
||||
private double skillMultiplier => 25.6;
|
||||
private double strainDecayBase => 0.15;
|
||||
|
||||
private readonly List<double> sliderStrains = new List<double>();
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
@@ -33,9 +38,26 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(current.DeltaTime);
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, IncludeSliders) * skillMultiplier;
|
||||
|
||||
if (current.BaseObject is Slider)
|
||||
{
|
||||
sliderStrains.Add(currentStrain);
|
||||
}
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
public double GetDifficultSliders()
|
||||
{
|
||||
if (sliderStrains.Count == 0)
|
||||
return 0;
|
||||
|
||||
double maxSliderStrain = sliderStrains.Max();
|
||||
if (maxSliderStrain == 0)
|
||||
return 0;
|
||||
|
||||
return sliderStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Speed : OsuStrainSkill
|
||||
{
|
||||
private double skillMultiplier => 1.430;
|
||||
private double skillMultiplier => 1.46;
|
||||
private double strainDecayBase => 0.3;
|
||||
|
||||
private double currentStrain;
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||
protected override double StrainValueAt(DifficultyHitObject current)
|
||||
{
|
||||
currentStrain *= strainDecay(((OsuDifficultyHitObject)current).StrainTime);
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current) * skillMultiplier;
|
||||
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, Mods) * skillMultiplier;
|
||||
|
||||
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current);
|
||||
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components
|
||||
if (hasReachedObject && showHitMarkers.Value)
|
||||
{
|
||||
float alpha = Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION, Easing.In);
|
||||
float ringScale = MathHelper.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
|
||||
float ringScale = Math.Clamp(Interpolation.ValueAt(editorTime, 0, 1f, hitObjectTime, hitObjectTime + FADE_OUT_EXTENSION / 2, Easing.OutQuint), 0, 1);
|
||||
|
||||
ring.Scale = new Vector2(1 + 0.1f * ringScale);
|
||||
content.Alpha = 0.9f * (1 - alpha);
|
||||
|
||||
+1
-1
@@ -484,7 +484,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Snap the path to the current beat divisor before checking length validity.
|
||||
hitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
if (!hitObject.Path.HasValidLength)
|
||||
if (!hitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Position = oldControlPoints[i];
|
||||
|
||||
@@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
private readonly IncrementalBSplineBuilder bSplineBuilder = new IncrementalBSplineBuilder { Degree = 4 };
|
||||
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLength;
|
||||
protected override bool IsValidForPlacement => HitObject.Path.HasValidLengthForPlacement;
|
||||
|
||||
public SliderPlacementBlueprint()
|
||||
: base(new Slider())
|
||||
|
||||
@@ -270,14 +270,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
if (adjustVelocity)
|
||||
{
|
||||
proposedVelocity = proposedDistance / oldDuration;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
proposedDistance = Math.Clamp(proposedDistance, 0.1 * oldDuration, 10 * oldDuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
double minDistance = distanceSnapProvider?.GetBeatSnapDistance() * oldVelocityMultiplier ?? 1;
|
||||
// Add a small amount to the proposed distance to make it easier to snap to the full length of the slider.
|
||||
proposedDistance = distanceSnapProvider?.FindSnappedDistance((float)proposedDistance + 1, HitObject.StartTime, HitObject) ?? proposedDistance;
|
||||
proposedDistance = MathHelper.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
proposedDistance = Math.Clamp(proposedDistance, minDistance, HitObject.Path.CalculatedDistance);
|
||||
}
|
||||
|
||||
if (Precision.AlmostEquals(proposedDistance, HitObject.Path.Distance) && Precision.AlmostEquals(proposedVelocity, HitObject.SliderVelocityMultiplier))
|
||||
@@ -476,7 +476,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
// If there are 0 or 1 remaining control points, or the slider has an invalid length, it is in a degenerate form and should be deleted
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLength)
|
||||
if (controlPoints.Count <= 1 || !HitObject.Path.HasValidLengthForPlacement)
|
||||
{
|
||||
placementHandler?.Delete(HitObject);
|
||||
return;
|
||||
@@ -626,7 +626,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos)
|
||||
{
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0))
|
||||
if (BodyPiece.ReceivePositionalInputAt(screenSpacePos) && (IsSelected || DrawableObject.Body.Alpha > 0 || DrawableObject.HeadCircle.Alpha > 0))
|
||||
return true;
|
||||
|
||||
if (ControlPointVisualiser == null)
|
||||
|
||||
@@ -1,10 +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 System.Linq;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -14,7 +16,9 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public override double ReadCurrentDistanceSnap(HitObject before, HitObject after)
|
||||
{
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime);
|
||||
var lastObjectWithVelocity = EditorBeatmap.HitObjects.TakeWhile(ho => ho != after).OfType<IHasSliderVelocity>().LastOrDefault();
|
||||
|
||||
float expectedDistance = DurationToDistance(after.StartTime - before.GetEndTime(), before.StartTime, lastObjectWithVelocity);
|
||||
float actualDistance = Vector2.Distance(((OsuHitObject)before).EndPosition, ((OsuHitObject)after).Position);
|
||||
|
||||
return actualDistance / expectedDistance;
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public BindableFloat Spacing { get; } = new BindableFloat(4f)
|
||||
{
|
||||
MinValue = 4f,
|
||||
MaxValue = 128f,
|
||||
MaxValue = 256f,
|
||||
Precision = 0.01f,
|
||||
};
|
||||
|
||||
|
||||
@@ -180,7 +180,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
Quad scaledQuad = GeometryUtils.GetSurroundingQuad(new OsuHitObject[] { slider });
|
||||
(bool xInBounds, bool yInBounds) = isQuadInBounds(scaledQuad);
|
||||
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLength)
|
||||
if (xInBounds && yInBounds && slider.Path.HasValidLengthForPlacement)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < slider.Path.ControlPoints.Count; i++)
|
||||
@@ -263,12 +263,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
case Axes.X:
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - b, upperBounds - b, a);
|
||||
s.X = MathHelper.Clamp(s.X, sLowerBound, sUpperBound);
|
||||
s.X = Math.Clamp(s.X, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Y:
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds - a, upperBounds - a, b);
|
||||
s.Y = MathHelper.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||
s.Y = Math.Clamp(s.Y, sLowerBound, sUpperBound);
|
||||
break;
|
||||
|
||||
case Axes.Both:
|
||||
@@ -276,11 +276,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
// Therefore the ratio s.X / s.Y will be maintained
|
||||
(sLowerBound, sUpperBound) = computeBounds(lowerBounds, upperBounds, a * s.X + b * s.Y);
|
||||
s.X = s.X < 0
|
||||
? MathHelper.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||
: MathHelper.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||
? Math.Clamp(s.X, s.X * sUpperBound, s.X * sLowerBound)
|
||||
: Math.Clamp(s.X, s.X * sLowerBound, s.X * sUpperBound);
|
||||
s.Y = s.Y < 0
|
||||
? MathHelper.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||
: MathHelper.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||
? Math.Clamp(s.Y, s.Y * sUpperBound, s.Y * sLowerBound)
|
||||
: Math.Clamp(s.Y, s.Y * sLowerBound, s.Y * sUpperBound);
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -127,8 +128,11 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
if (relativeCheckbox.Current.Value)
|
||||
{
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.X, OsuPlayfield.BASE_SIZE.X - initialSurroundingQuad.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - initialSurroundingQuad.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - initialSurroundingQuad.BottomRight.Y);
|
||||
xBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.X, 0);
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - Math.Min(initialSurroundingQuad.BottomRight.X, OsuPlayfield.BASE_SIZE.X);
|
||||
|
||||
yBindable.MinValue = 0 - Math.Max(initialSurroundingQuad.TopLeft.Y, 0);
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - Math.Min(initialSurroundingQuad.BottomRight.Y, OsuPlayfield.BASE_SIZE.Y);
|
||||
|
||||
xBindable.Default = yBindable.Default = 0;
|
||||
|
||||
@@ -146,8 +150,21 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
|
||||
var quadRelativeToPosition = new RectangleF(initialSurroundingQuad.Location - initialPosition, initialSurroundingQuad.Size);
|
||||
|
||||
(xBindable.MinValue, xBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.X, OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X);
|
||||
(yBindable.MinValue, yBindable.MaxValue) = (0 - quadRelativeToPosition.TopLeft.Y, OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y);
|
||||
if (initialSurroundingQuad.Width < OsuPlayfield.BASE_SIZE.X)
|
||||
{
|
||||
xBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.X;
|
||||
xBindable.MaxValue = OsuPlayfield.BASE_SIZE.X - quadRelativeToPosition.BottomRight.X;
|
||||
}
|
||||
else
|
||||
xBindable.MinValue = xBindable.MaxValue = initialPosition.X;
|
||||
|
||||
if (initialSurroundingQuad.Height < OsuPlayfield.BASE_SIZE.Y)
|
||||
{
|
||||
yBindable.MinValue = 0 - quadRelativeToPosition.TopLeft.Y;
|
||||
yBindable.MaxValue = OsuPlayfield.BASE_SIZE.Y - quadRelativeToPosition.BottomRight.Y;
|
||||
}
|
||||
else
|
||||
yBindable.MinValue = yBindable.MaxValue = initialPosition.Y;
|
||||
|
||||
xBindable.Default = initialPosition.X;
|
||||
yBindable.Default = initialPosition.Y;
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
@@ -63,13 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
|
||||
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
|
||||
{
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
|
||||
new ApproachRateSlider
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Current = current,
|
||||
KeyboardStep = 0.1f,
|
||||
};
|
||||
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new ApproachRateSlider();
|
||||
|
||||
/// <summary>
|
||||
/// A slider bar with more detailed approach rate info for its given value
|
||||
|
||||
@@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test")]
|
||||
[TestCase(3.0920212594351191d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test")]
|
||||
[TestCase(3.305554470092722d, 200, "diffcalc-test-strong")]
|
||||
public void Test(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> base.Test(expectedStarRating, expectedMaxCombo, name);
|
||||
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test")]
|
||||
[TestCase(4.0789820318081444d, 200, "diffcalc-test-strong")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test")]
|
||||
[TestCase(4.4472572672057815d, 200, "diffcalc-test-strong")]
|
||||
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
|
||||
=> Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
|
||||
|
||||
|
||||
@@ -10,45 +10,82 @@ using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class ColourEvaluator
|
||||
public static class ColourEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="MonoStreak"/>.
|
||||
/// Calculates a consistency penalty based on the number of consecutive consistent intervals,
|
||||
/// considering the delta time between each colour sequence.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(MonoStreak monoStreak)
|
||||
/// <param name="hitObject">The current hitObject to consider.</param>
|
||||
/// <param name="threshold"> The allowable margin of error for determining whether ratios are consistent.</param>
|
||||
/// <param name="maxObjectsToCheck">The maximum objects to check per count of consistent ratio.</param>
|
||||
private static double consistentRatioPenalty(TaikoDifficultyHitObject hitObject, double threshold = 0.01, int maxObjectsToCheck = 64)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * EvaluateDifficultyOf(monoStreak.Parent) * 0.5;
|
||||
int consistentRatioCount = 0;
|
||||
double totalRatioCount = 0.0;
|
||||
|
||||
TaikoDifficultyHitObject current = hitObject;
|
||||
|
||||
for (int i = 0; i < maxObjectsToCheck; i++)
|
||||
{
|
||||
// Break if there is no valid previous object
|
||||
if (current.Index <= 1)
|
||||
break;
|
||||
|
||||
var previousHitObject = (TaikoDifficultyHitObject)current.Previous(1);
|
||||
|
||||
double currentRatio = current.RhythmData.Ratio;
|
||||
double previousRatio = previousHitObject.RhythmData.Ratio;
|
||||
|
||||
// A consistent interval is defined as the percentage difference between the two rhythmic ratios with the margin of error.
|
||||
if (Math.Abs(1 - currentRatio / previousRatio) <= threshold)
|
||||
{
|
||||
consistentRatioCount++;
|
||||
totalRatioCount += currentRatio;
|
||||
break;
|
||||
}
|
||||
|
||||
// Move to the previous object
|
||||
current = previousHitObject;
|
||||
}
|
||||
|
||||
// Ensure no division by zero
|
||||
double ratioPenalty = 1 - totalRatioCount / (consistentRatioCount + 1) * 0.80;
|
||||
|
||||
return ratioPenalty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="AlternatingMonoPattern"/>.
|
||||
/// Evaluate the difficulty of the first hitobject within a colour streak.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(AlternatingMonoPattern alternatingMonoPattern)
|
||||
{
|
||||
return DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * EvaluateDifficultyOf(alternatingMonoPattern.Parent);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of the first note of a <see cref="RepeatingHitPatterns"/>.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(RepeatingHitPatterns repeatingHitPattern)
|
||||
{
|
||||
return 2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject)
|
||||
{
|
||||
TaikoDifficultyHitObjectColour colour = ((TaikoDifficultyHitObject)hitObject).Colour;
|
||||
var taikoObject = (TaikoDifficultyHitObject)hitObject;
|
||||
TaikoColourData colourData = taikoObject.ColourData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
if (colour.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += EvaluateDifficultyOf(colour.MonoStreak);
|
||||
if (colour.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.AlternatingMonoPattern);
|
||||
if (colour.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += EvaluateDifficultyOf(colour.RepeatingHitPattern);
|
||||
if (colourData.MonoStreak?.FirstHitObject == hitObject) // Difficulty for MonoStreak
|
||||
difficulty += evaluateMonoStreakDifficulty(colourData.MonoStreak);
|
||||
|
||||
if (colourData.AlternatingMonoPattern?.FirstHitObject == hitObject) // Difficulty for AlternatingMonoPattern
|
||||
difficulty += evaluateAlternatingMonoPatternDifficulty(colourData.AlternatingMonoPattern);
|
||||
|
||||
if (colourData.RepeatingHitPattern?.FirstHitObject == hitObject) // Difficulty for RepeatingHitPattern
|
||||
difficulty += evaluateRepeatingHitPatternsDifficulty(colourData.RepeatingHitPattern);
|
||||
|
||||
double consistencyPenalty = consistentRatioPenalty(taikoObject);
|
||||
difficulty *= consistencyPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateMonoStreakDifficulty(MonoStreak monoStreak) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * monoStreak.Index - 2 * Math.E) * evaluateAlternatingMonoPatternDifficulty(monoStreak.Parent) * 0.5;
|
||||
|
||||
private static double evaluateAlternatingMonoPatternDifficulty(AlternatingMonoPattern alternatingMonoPattern) =>
|
||||
DifficultyCalculationUtils.Logistic(exponent: Math.E * alternatingMonoPattern.Index - 2 * Math.E) * evaluateRepeatingHitPatternsDifficulty(alternatingMonoPattern.Parent);
|
||||
|
||||
private static double evaluateRepeatingHitPatternsDifficulty(RepeatingHitPatterns repeatingHitPattern) =>
|
||||
2 * (1 - DifficultyCalculationUtils.Logistic(exponent: Math.E * repeatingHitPattern.RepetitionInterval - 2 * Math.E));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public static class ReadingEvaluator
|
||||
{
|
||||
private readonly struct VelocityRange
|
||||
{
|
||||
public double Min { get; }
|
||||
public double Max { get; }
|
||||
public double Center => (Max + Min) / 2;
|
||||
public double Range => Max - Min;
|
||||
|
||||
public VelocityRange(double min, double max)
|
||||
{
|
||||
Min = min;
|
||||
Max = max;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the influence of higher slider velocities on hitobject difficulty.
|
||||
/// The bonus is determined based on the EffectiveBPM, shifting within a defined range
|
||||
/// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty.
|
||||
/// </summary>
|
||||
/// <param name="noteObject">The hit object to evaluate.</param>
|
||||
/// <returns>The reading difficulty value for the given hit object.</returns>
|
||||
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
|
||||
{
|
||||
var highVelocity = new VelocityRange(480, 640);
|
||||
var midVelocity = new VelocityRange(360, 480);
|
||||
|
||||
// Apply a cap to prevent outlier values on maps that exceed the editor's parameters.
|
||||
double effectiveBPM = Math.Max(1.0, noteObject.EffectiveBPM);
|
||||
|
||||
double midVelocityDifficulty = 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
|
||||
|
||||
// Expected DeltaTime is the DeltaTime this note would need to be spaced equally to a base slider velocity 1/4 note.
|
||||
double expectedDeltaTime = 21000.0 / effectiveBPM;
|
||||
double objectDensity = expectedDeltaTime / Math.Max(1.0, noteObject.DeltaTime);
|
||||
|
||||
// High density is penalised at high velocity as it is generally considered easier to read. See https://www.desmos.com/calculator/u63f3ntdsi
|
||||
double densityPenalty = DifficultyCalculationUtils.Logistic(objectDensity, 0.925, 15);
|
||||
|
||||
double highVelocityDifficulty = (1.0 - 0.33 * densityPenalty)
|
||||
* DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center + 8 * densityPenalty, (1.0 + 0.5 * densityPenalty) / (highVelocity.Range / 10));
|
||||
|
||||
return midVelocityDifficulty + highVelocityDifficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class RhythmEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Evaluate the difficulty of a hitobject considering its interval change.
|
||||
/// </summary>
|
||||
public static double EvaluateDifficultyOf(DifficultyHitObject hitObject, double hitWindow)
|
||||
{
|
||||
TaikoRhythmData rhythmData = ((TaikoDifficultyHitObject)hitObject).RhythmData;
|
||||
double difficulty = 0.0d;
|
||||
|
||||
double sameRhythm = 0;
|
||||
double samePattern = 0;
|
||||
double intervalPenalty = 0;
|
||||
|
||||
if (rhythmData.SameRhythmGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SameRhythmGroupedHitObjects
|
||||
{
|
||||
sameRhythm += 10.0 * evaluateDifficultyOf(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
intervalPenalty = repeatedIntervalPenalty(rhythmData.SameRhythmGroupedHitObjects, hitWindow);
|
||||
}
|
||||
|
||||
if (rhythmData.SamePatternsGroupedHitObjects?.FirstHitObject == hitObject) // Difficulty for SamePatternsGroupedHitObjects
|
||||
samePattern += 1.15 * ratioDifficulty(rhythmData.SamePatternsGroupedHitObjects.IntervalRatio);
|
||||
|
||||
difficulty += Math.Max(sameRhythm, samePattern) * intervalPenalty;
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
private static double evaluateDifficultyOf(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow)
|
||||
{
|
||||
double intervalDifficulty = ratioDifficulty(sameRhythmGroupedHitObjects.HitObjectIntervalRatio);
|
||||
double? previousInterval = sameRhythmGroupedHitObjects.Previous?.HitObjectInterval;
|
||||
|
||||
intervalDifficulty *= repeatedIntervalPenalty(sameRhythmGroupedHitObjects, hitWindow);
|
||||
|
||||
// If a previous interval exists and there are multiple hit objects in the sequence:
|
||||
if (previousInterval != null && sameRhythmGroupedHitObjects.HitObjects.Count > 1)
|
||||
{
|
||||
double expectedDurationFromPrevious = (double)previousInterval * sameRhythmGroupedHitObjects.HitObjects.Count;
|
||||
double durationDifference = sameRhythmGroupedHitObjects.Duration - expectedDurationFromPrevious;
|
||||
|
||||
if (durationDifference > 0)
|
||||
{
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
durationDifference / hitWindow,
|
||||
midpointOffset: 0.7,
|
||||
multiplier: 1.0,
|
||||
maxValue: 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Penalise patterns that can be hit within a single hit window.
|
||||
intervalDifficulty *= DifficultyCalculationUtils.Logistic(
|
||||
sameRhythmGroupedHitObjects.Duration / hitWindow,
|
||||
midpointOffset: 0.6,
|
||||
multiplier: 1,
|
||||
maxValue: 1);
|
||||
|
||||
return Math.Pow(intervalDifficulty, 0.75);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if the changes in hit object intervals is consistent based on a given threshold.
|
||||
/// </summary>
|
||||
private static double repeatedIntervalPenalty(SameRhythmHitObjectGrouping sameRhythmGroupedHitObjects, double hitWindow, double threshold = 0.1)
|
||||
{
|
||||
double longIntervalPenalty = sameInterval(sameRhythmGroupedHitObjects, 3);
|
||||
|
||||
double shortIntervalPenalty = sameRhythmGroupedHitObjects.HitObjects.Count < 6
|
||||
? sameInterval(sameRhythmGroupedHitObjects, 4)
|
||||
: 1.0; // Returns a non-penalty if there are 6 or more notes within an interval.
|
||||
|
||||
// The duration penalty is based on hit object duration relative to hitWindow.
|
||||
double durationPenalty = Math.Max(1 - sameRhythmGroupedHitObjects.Duration * 2 / hitWindow, 0.5);
|
||||
|
||||
return Math.Min(longIntervalPenalty, shortIntervalPenalty) * durationPenalty;
|
||||
|
||||
double sameInterval(SameRhythmHitObjectGrouping startObject, int intervalCount)
|
||||
{
|
||||
List<double?> intervals = new List<double?>();
|
||||
var currentObject = startObject;
|
||||
|
||||
for (int i = 0; i < intervalCount && currentObject != null; i++)
|
||||
{
|
||||
intervals.Add(currentObject.HitObjectInterval);
|
||||
currentObject = currentObject.Previous;
|
||||
}
|
||||
|
||||
intervals.RemoveAll(interval => interval == null);
|
||||
|
||||
if (intervals.Count < intervalCount)
|
||||
return 1.0; // No penalty if there aren't enough valid intervals.
|
||||
|
||||
for (int i = 0; i < intervals.Count; i++)
|
||||
{
|
||||
for (int j = i + 1; j < intervals.Count; j++)
|
||||
{
|
||||
double ratio = intervals[i]!.Value / intervals[j]!.Value;
|
||||
if (Math.Abs(1 - ratio) <= threshold) // If any two intervals are similar, apply a penalty.
|
||||
return 0.80;
|
||||
}
|
||||
}
|
||||
|
||||
return 1.0; // No penalty if all intervals are different.
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates the difficulty of a given ratio using a combination of periodic penalties and bonuses.
|
||||
/// </summary>
|
||||
private static double ratioDifficulty(double ratio, int terms = 8)
|
||||
{
|
||||
double difficulty = 0;
|
||||
|
||||
// Validate the ratio by ensuring it is a normal number in cases where maps breach regular mapping conditions.
|
||||
ratio = double.IsNormal(ratio) ? ratio : 0;
|
||||
|
||||
for (int i = 1; i <= terms; ++i)
|
||||
{
|
||||
difficulty += termPenalty(ratio, i, 4, 1);
|
||||
}
|
||||
|
||||
difficulty += terms / (1 + ratio);
|
||||
|
||||
// Give bonus to near-1 ratios
|
||||
difficulty += DifficultyCalculationUtils.BellCurve(ratio, 1, 0.5);
|
||||
|
||||
// Penalize ratios that are VERY near 1
|
||||
difficulty -= DifficultyCalculationUtils.BellCurve(ratio, 1, 0.3);
|
||||
|
||||
difficulty = Math.Max(difficulty, 0);
|
||||
difficulty /= Math.Sqrt(8);
|
||||
|
||||
return difficulty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Multiplier for a given denominator term.
|
||||
/// </summary>
|
||||
private static double termPenalty(double ratio, int denominator, double power, double multiplier) =>
|
||||
-multiplier * Math.Pow(Math.Cos(denominator * Math.PI * ratio), power);
|
||||
}
|
||||
}
|
||||
@@ -8,43 +8,8 @@ using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
{
|
||||
public class StaminaEvaluator
|
||||
public static class StaminaEvaluator
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 30 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.Colour.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.Colour.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates the minimum mechanical stamina required to play the current object. This is calculated using the
|
||||
/// maximum possible interval between two hits using the same key, by alternating available fingers for each colour.
|
||||
@@ -59,17 +24,51 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
|
||||
// Find the previous hit object hit by the current finger, which is n notes prior, n being the number of
|
||||
// available fingers.
|
||||
TaikoDifficultyHitObject taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||
TaikoDifficultyHitObject? keyPrevious = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
if (keyPrevious == null)
|
||||
{
|
||||
// There is no previous hit object hit by the current finger
|
||||
return 0.0;
|
||||
}
|
||||
TaikoDifficultyHitObject? taikoPrevious = current.Previous(1) as TaikoDifficultyHitObject;
|
||||
TaikoDifficultyHitObject? previousMono = taikoCurrent.PreviousMono(availableFingersFor(taikoCurrent) - 1);
|
||||
|
||||
double objectStrain = 0.5; // Add a base strain to all objects
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - keyPrevious.StartTime);
|
||||
if (taikoPrevious == null) return objectStrain;
|
||||
|
||||
if (previousMono != null)
|
||||
objectStrain += speedBonus(taikoCurrent.StartTime - previousMono.StartTime) + 0.5 * speedBonus(taikoCurrent.StartTime - taikoPrevious.StartTime);
|
||||
|
||||
return objectStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a speed bonus dependent on the time since the last hit performed using this finger.
|
||||
/// </summary>
|
||||
/// <param name="interval">The interval between the current and previous note hit using the same finger.</param>
|
||||
private static double speedBonus(double interval)
|
||||
{
|
||||
// Interval is capped at a very small value to prevent infinite values.
|
||||
interval = Math.Max(interval, 1);
|
||||
|
||||
return 20 / interval;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the number of fingers available to hit the current <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Any mono notes that is more than 300ms apart from a colour change will be considered to have more than 2
|
||||
/// fingers available, since players can hit the same key with multiple fingers.
|
||||
/// </summary>
|
||||
private static int availableFingersFor(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
DifficultyHitObject? previousColourChange = hitObject.ColourData.PreviousColourChange;
|
||||
DifficultyHitObject? nextColourChange = hitObject.ColourData.NextColourChange;
|
||||
|
||||
if (previousColourChange != null && hitObject.StartTime - previousColourChange.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
if (nextColourChange != null && nextColourChange.StartTime - hitObject.StartTime < 300)
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
/// <summary>
|
||||
/// Stores colour compression information for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObjectColour
|
||||
public class TaikoColourData
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="MonoStreak"/> that encodes this note.
|
||||
+5
-5
@@ -14,8 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
public static class TaikoColourDifficultyPreprocessor
|
||||
{
|
||||
/// <summary>
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoDifficultyHitObjectColour"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoDifficultyHitObjectColour"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// Processes and encodes a list of <see cref="TaikoDifficultyHitObject"/>s into a list of <see cref="TaikoColourData"/>s,
|
||||
/// assigning the appropriate <see cref="TaikoColourData"/>s to each <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public static void ProcessAndAssign(List<DifficultyHitObject> hitObjects)
|
||||
{
|
||||
@@ -41,9 +41,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour
|
||||
|
||||
foreach (var hitObject in monoStreak.HitObjects)
|
||||
{
|
||||
hitObject.Colour.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.Colour.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.Colour.MonoStreak = monoStreak;
|
||||
hitObject.ColourData.RepeatingHitPattern = repeatingHitPattern;
|
||||
hitObject.ColourData.AlternatingMonoPattern = monoPattern;
|
||||
hitObject.ColourData.MonoStreak = monoStreak;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents <see cref="SameRhythmHitObjectGrouping"/> grouped by their <see cref="SameRhythmHitObjectGrouping.StartTime"/>'s interval.
|
||||
/// </summary>
|
||||
public class SamePatternsGroupedHitObjects
|
||||
{
|
||||
public IReadOnlyList<SameRhythmHitObjectGrouping> Groups { get; }
|
||||
|
||||
public SamePatternsGroupedHitObjects? Previous { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="SameRhythmHitObjectGrouping.Interval"/> between groups <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// If there is only one group, this will have the value of the first group's <see cref="SameRhythmHitObjectGrouping.Interval"/>.
|
||||
/// </summary>
|
||||
public double GroupInterval => Groups.Count > 1 ? Groups[1].Interval : Groups[0].Interval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="GroupInterval"/> between this and the previous <see cref="SamePatternsGroupedHitObjects"/>. In the
|
||||
/// case where there is no previous <see cref="SamePatternsGroupedHitObjects"/>, this will have a value of 1.
|
||||
/// </summary>
|
||||
public double IntervalRatio => GroupInterval / Previous?.GroupInterval ?? 1.0d;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => Groups[0].FirstHitObject;
|
||||
|
||||
public IEnumerable<TaikoDifficultyHitObject> AllHitObjects => Groups.SelectMany(hitObject => hitObject.HitObjects);
|
||||
|
||||
public SamePatternsGroupedHitObjects(SamePatternsGroupedHitObjects? previous, List<SameRhythmHitObjectGrouping> groups)
|
||||
{
|
||||
Previous = previous;
|
||||
Groups = groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
// 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.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a group of <see cref="TaikoDifficultyHitObject"/>s with no rhythm variation.
|
||||
/// </summary>
|
||||
public class SameRhythmHitObjectGrouping : IHasInterval
|
||||
{
|
||||
public readonly List<TaikoDifficultyHitObject> HitObjects;
|
||||
|
||||
public TaikoDifficultyHitObject FirstHitObject => HitObjects[0];
|
||||
|
||||
public readonly SameRhythmHitObjectGrouping? Previous;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="DifficultyHitObject.StartTime"/> of the first hit object.
|
||||
/// </summary>
|
||||
public double StartTime => HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval between the first and final hit object within this group.
|
||||
/// </summary>
|
||||
public double Duration => HitObjects[^1].StartTime - HitObjects[0].StartTime;
|
||||
|
||||
/// <summary>
|
||||
/// The interval in ms of each hit object in this <see cref="SameRhythmHitObjectGrouping"/>. This is only defined if there is
|
||||
/// more than two hit objects in this <see cref="SameRhythmHitObjectGrouping"/>.
|
||||
/// </summary>
|
||||
public readonly double? HitObjectInterval;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of <see cref="HitObjectInterval"/> between this and the previous <see cref="SameRhythmHitObjectGrouping"/>. In the
|
||||
/// case where one or both of the <see cref="HitObjectInterval"/> is undefined, this will have a value of 1.
|
||||
/// </summary>
|
||||
public readonly double HitObjectIntervalRatio;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public double Interval { get; }
|
||||
|
||||
public SameRhythmHitObjectGrouping(SameRhythmHitObjectGrouping? previous, List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
Previous = previous;
|
||||
HitObjects = hitObjects;
|
||||
|
||||
// Calculate the average interval between hitobjects, or null if there are fewer than two
|
||||
HitObjectInterval = HitObjects.Count < 2 ? null : Duration / (HitObjects.Count - 1);
|
||||
|
||||
// Calculate the ratio between this group's interval and the previous group's interval
|
||||
HitObjectIntervalRatio = Previous?.HitObjectInterval != null && HitObjectInterval != null
|
||||
? HitObjectInterval.Value / Previous.HitObjectInterval.Value
|
||||
: 1;
|
||||
|
||||
// Calculate the interval from the previous group's start time
|
||||
Interval = Previous != null ? StartTime - Previous.StartTime : double.PositiveInfinity;
|
||||
}
|
||||
}
|
||||
}
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a rhythm change in a taiko map.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObjectRhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// The difficulty multiplier associated with this rhythm change.
|
||||
/// </summary>
|
||||
public readonly double Difficulty;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||
/// </summary>
|
||||
public readonly double Ratio;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an object representing a rhythm change.
|
||||
/// </summary>
|
||||
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
|
||||
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
|
||||
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
|
||||
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
|
||||
{
|
||||
Ratio = numerator / (double)denominator;
|
||||
Difficulty = difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores rhythm data for a <see cref="TaikoDifficultyHitObject"/>.
|
||||
/// </summary>
|
||||
public class TaikoRhythmData
|
||||
{
|
||||
/// <summary>
|
||||
/// The group of hit objects with consistent rhythm that this object belongs to.
|
||||
/// </summary>
|
||||
public SameRhythmHitObjectGrouping? SameRhythmGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The larger pattern of rhythm groups that this object is part of.
|
||||
/// </summary>
|
||||
public SamePatternsGroupedHitObjects? SamePatternsGroupedHitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The ratio of current <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||
/// to previous <see cref="Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is snapped to the closest matching <see cref="common_ratios"/>.
|
||||
/// </remarks>
|
||||
public readonly double Ratio;
|
||||
|
||||
/// <summary>
|
||||
/// Initialises a new instance of <see cref="TaikoRhythmData"/>s,
|
||||
/// calculating the closest rhythm change and its associated difficulty for the current hit object.
|
||||
/// </summary>
|
||||
/// <param name="current">The current <see cref="TaikoDifficultyHitObject"/> being processed.</param>
|
||||
public TaikoRhythmData(TaikoDifficultyHitObject current)
|
||||
{
|
||||
var previous = current.Previous(0);
|
||||
|
||||
if (previous == null)
|
||||
{
|
||||
Ratio = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
double actualRatio = current.DeltaTime / previous.DeltaTime;
|
||||
double closestRatio = common_ratios.MinBy(r => Math.Abs(r - actualRatio));
|
||||
|
||||
Ratio = closestRatio;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps. Based on how each object's interval compares to the previous object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The general guidelines for the values are:
|
||||
/// <list type="bullet">
|
||||
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static readonly double[] common_ratios =
|
||||
[
|
||||
1.0 / 1,
|
||||
2.0 / 1,
|
||||
1.0 / 2,
|
||||
3.0 / 1,
|
||||
1.0 / 3,
|
||||
3.0 / 2,
|
||||
2.0 / 3,
|
||||
5.0 / 4,
|
||||
4.0 / 5
|
||||
];
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
// 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.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm.Data;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm
|
||||
{
|
||||
public static class TaikoRhythmDifficultyPreprocessor
|
||||
{
|
||||
public static void ProcessAndAssign(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = createSameRhythmGroupedHitObjects(hitObjects);
|
||||
|
||||
foreach (var rhythmGroup in rhythmGroups)
|
||||
{
|
||||
foreach (var hitObject in rhythmGroup.HitObjects)
|
||||
hitObject.RhythmData.SameRhythmGroupedHitObjects = rhythmGroup;
|
||||
}
|
||||
|
||||
var patternGroups = createSamePatternGroupedHitObjects(rhythmGroups);
|
||||
|
||||
foreach (var patternGroup in patternGroups)
|
||||
{
|
||||
foreach (var hitObject in patternGroup.AllHitObjects)
|
||||
hitObject.RhythmData.SamePatternsGroupedHitObjects = patternGroup;
|
||||
}
|
||||
}
|
||||
|
||||
private static List<SameRhythmHitObjectGrouping> createSameRhythmGroupedHitObjects(List<TaikoDifficultyHitObject> hitObjects)
|
||||
{
|
||||
var rhythmGroups = new List<SameRhythmHitObjectGrouping>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(hitObjects))
|
||||
rhythmGroups.Add(new SameRhythmHitObjectGrouping(rhythmGroups.LastOrDefault(), grouped));
|
||||
|
||||
return rhythmGroups;
|
||||
}
|
||||
|
||||
private static List<SamePatternsGroupedHitObjects> createSamePatternGroupedHitObjects(List<SameRhythmHitObjectGrouping> rhythmGroups)
|
||||
{
|
||||
var patternGroups = new List<SamePatternsGroupedHitObjects>();
|
||||
|
||||
foreach (var grouped in IntervalGroupingUtils.GroupByInterval(rhythmGroups))
|
||||
patternGroups.Add(new SamePatternsGroupedHitObjects(patternGroups.LastOrDefault(), grouped));
|
||||
|
||||
return patternGroups;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,23 @@
|
||||
// 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.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single hit object in taiko difficulty calculation.
|
||||
/// </summary>
|
||||
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||
public class TaikoDifficultyHitObject : DifficultyHitObject, IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The list of all <see cref="TaikoDifficultyHitObject"/> of the same colour as this <see cref="TaikoDifficultyHitObject"/> in the beatmap.
|
||||
@@ -38,98 +40,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public readonly int NoteIndex;
|
||||
|
||||
/// <summary>
|
||||
/// The rhythm required to hit this hit object.
|
||||
/// Rhythm data used by <see cref="RhythmEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoRhythmDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
|
||||
public readonly TaikoRhythmData RhythmData;
|
||||
|
||||
/// <summary>
|
||||
/// Colour data for this hit object. This is used by colour evaluator to calculate colour difficulty, but can be used
|
||||
/// by other skills in the future.
|
||||
/// Colour data used by <see cref="ColourEvaluator"/> and <see cref="StaminaEvaluator"/>.
|
||||
/// This is populated via <see cref="TaikoColourDifficultyPreprocessor"/>.
|
||||
/// </summary>
|
||||
public readonly TaikoDifficultyHitObjectColour Colour;
|
||||
public readonly TaikoColourData ColourData;
|
||||
|
||||
/// <summary>
|
||||
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
|
||||
/// </summary>
|
||||
public double EffectiveBPM;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new difficulty hit object.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
|
||||
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
|
||||
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
|
||||
/// <param name="objects">The list of all <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="centreHitObjects">The list of centre (don) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="rimHitObjects">The list of rim (kat) <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||
/// <param name="noteObjects">The list of <see cref="DifficultyHitObject"/>s that is a hit (i.e. not a drumroll or swell) in the current beatmap.</param>
|
||||
/// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate,
|
||||
/// <param name="controlPointInfo">The control point info of the beatmap.</param>
|
||||
/// <param name="globalSliderVelocity">The global slider velocity of the beatmap.</param>
|
||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate,
|
||||
List<DifficultyHitObject> objects,
|
||||
List<TaikoDifficultyHitObject> centreHitObjects,
|
||||
List<TaikoDifficultyHitObject> rimHitObjects,
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index)
|
||||
List<TaikoDifficultyHitObject> noteObjects, int index,
|
||||
ControlPointInfo controlPointInfo,
|
||||
double globalSliderVelocity)
|
||||
: base(hitObject, lastObject, clockRate, objects, index)
|
||||
{
|
||||
noteDifficultyHitObjects = noteObjects;
|
||||
|
||||
// Create the Colour object, its properties should be filled in by TaikoDifficultyPreprocessor
|
||||
Colour = new TaikoDifficultyHitObjectColour();
|
||||
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||
ColourData = new TaikoColourData();
|
||||
RhythmData = new TaikoRhythmData(this);
|
||||
|
||||
switch ((hitObject as Hit)?.Type)
|
||||
if (hitObject is Hit hit)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
switch (hit.Type)
|
||||
{
|
||||
case HitType.Centre:
|
||||
MonoIndex = centreHitObjects.Count;
|
||||
centreHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = centreHitObjects;
|
||||
break;
|
||||
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
case HitType.Rim:
|
||||
MonoIndex = rimHitObjects.Count;
|
||||
rimHitObjects.Add(this);
|
||||
monoDifficultyHitObjects = rimHitObjects;
|
||||
break;
|
||||
}
|
||||
|
||||
if (hitObject is Hit)
|
||||
{
|
||||
NoteIndex = noteObjects.Count;
|
||||
noteObjects.Add(this);
|
||||
}
|
||||
|
||||
// Using `hitObject.StartTime` causes floating point error differences
|
||||
double normalisedStartTime = StartTime * clockRate;
|
||||
|
||||
// Retrieve the timing point at the note's start time
|
||||
TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(normalisedStartTime);
|
||||
|
||||
// Calculate the slider velocity at the note's start time.
|
||||
double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, globalSliderVelocity, normalisedStartTime, clockRate);
|
||||
|
||||
EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// List of most common rhythm changes in taiko maps.
|
||||
/// Calculates the slider velocity based on control point info and clock rate.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The general guidelines for the values are:
|
||||
/// <list type="bullet">
|
||||
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
|
||||
private static double calculateSliderVelocity(ControlPointInfo controlPointInfo, double globalSliderVelocity, double startTime, double clockRate)
|
||||
{
|
||||
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
|
||||
new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
|
||||
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
|
||||
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
|
||||
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
|
||||
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
|
||||
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
|
||||
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
|
||||
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
|
||||
/// </summary>
|
||||
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
|
||||
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||
/// <param name="clockRate">The rate of the gameplay clock.</param>
|
||||
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
|
||||
{
|
||||
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
|
||||
double ratio = DeltaTime / prevLength;
|
||||
|
||||
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
|
||||
var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
|
||||
return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
|
||||
}
|
||||
|
||||
public TaikoDifficultyHitObject? PreviousMono(int backwardsIndex) => monoDifficultyHitObjects?.ElementAtOrDefault(MonoIndex - (backwardsIndex + 1));
|
||||
@@ -139,5 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||
public TaikoDifficultyHitObject? PreviousNote(int backwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex - (backwardsIndex + 1));
|
||||
|
||||
public TaikoDifficultyHitObject? NextNote(int forwardsIndex) => noteDifficultyHitObjects.ElementAtOrDefault(NoteIndex + (forwardsIndex + 1));
|
||||
|
||||
public double Interval => DeltaTime;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// 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.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
/// <summary>
|
||||
/// Calculates the reading coefficient of taiko difficulty.
|
||||
/// </summary>
|
||||
public class Reading : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
public Reading(Mod[] mods)
|
||||
: base(mods)
|
||||
{
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// Drum Rolls and Swells are exempt.
|
||||
if (current.BaseObject is not Hit)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
var taikoObject = (TaikoDifficultyHitObject)current;
|
||||
int index = taikoObject.ColourData.MonoStreak?.HitObjects.IndexOf(taikoObject) ?? 0;
|
||||
|
||||
currentStrain *= DifficultyCalculationUtils.Logistic(index, 4, -1 / 25.0, 0.5) + 0.5;
|
||||
|
||||
currentStrain *= StrainDecayBase;
|
||||
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
|
||||
|
||||
return currentStrain;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Utils;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
{
|
||||
@@ -16,158 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
public class Rhythm : StrainDecaySkill
|
||||
{
|
||||
protected override double SkillMultiplier => 10;
|
||||
protected override double StrainDecayBase => 0;
|
||||
protected override double SkillMultiplier => 1.0;
|
||||
protected override double StrainDecayBase => 0.4;
|
||||
|
||||
/// <summary>
|
||||
/// The note-based decay for rhythm strain.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
|
||||
/// </remarks>
|
||||
private const double strain_decay = 0.96;
|
||||
private readonly double greatHitWindow;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of entries in <see cref="rhythmHistory"/>.
|
||||
/// </summary>
|
||||
private const int rhythm_history_max_length = 8;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
|
||||
/// </summary>
|
||||
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
|
||||
|
||||
/// <summary>
|
||||
/// Contains the rolling rhythm strain.
|
||||
/// Used to apply per-note decay.
|
||||
/// </summary>
|
||||
private double currentStrain;
|
||||
|
||||
/// <summary>
|
||||
/// Number of notes since the last rhythm change has taken place.
|
||||
/// </summary>
|
||||
private int notesSinceRhythmChange;
|
||||
|
||||
public Rhythm(Mod[] mods)
|
||||
public Rhythm(Mod[] mods, double greatHitWindow)
|
||||
: base(mods)
|
||||
{
|
||||
this.greatHitWindow = greatHitWindow;
|
||||
}
|
||||
|
||||
protected override double StrainValueOf(DifficultyHitObject current)
|
||||
{
|
||||
// drum rolls and swells are exempt.
|
||||
if (!(current.BaseObject is Hit))
|
||||
{
|
||||
resetRhythmAndStrain();
|
||||
return 0.0;
|
||||
}
|
||||
double difficulty = RhythmEvaluator.EvaluateDifficultyOf(current, greatHitWindow);
|
||||
|
||||
currentStrain *= strain_decay;
|
||||
// To prevent abuse of exceedingly long intervals between awkward rhythms, we penalise its difficulty.
|
||||
double staminaDifficulty = StaminaEvaluator.EvaluateDifficultyOf(current) - 0.5; // Remove base strain
|
||||
difficulty *= DifficultyCalculationUtils.Logistic(staminaDifficulty, 1 / 15.0, 50.0);
|
||||
|
||||
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||
notesSinceRhythmChange += 1;
|
||||
|
||||
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
|
||||
if (hitObject.Rhythm.Difficulty == 0.0)
|
||||
{
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
double objectStrain = hitObject.Rhythm.Difficulty;
|
||||
|
||||
objectStrain *= repetitionPenalties(hitObject);
|
||||
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
|
||||
objectStrain *= speedPenalty(hitObject.DeltaTime);
|
||||
|
||||
// careful - needs to be done here since calls above read this value
|
||||
notesSinceRhythmChange = 0;
|
||||
|
||||
currentStrain += objectStrain;
|
||||
return currentStrain;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Repetitions of more recent patterns are associated with a higher penalty.
|
||||
/// </remarks>
|
||||
/// <param name="hitObject">The current hit object being considered.</param>
|
||||
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
|
||||
{
|
||||
double penalty = 1;
|
||||
|
||||
rhythmHistory.Enqueue(hitObject);
|
||||
|
||||
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
|
||||
{
|
||||
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
|
||||
{
|
||||
if (!samePattern(start, mostRecentPatternsToCompare))
|
||||
continue;
|
||||
|
||||
int notesSince = hitObject.Index - rhythmHistory[start].Index;
|
||||
penalty *= repetitionPenalty(notesSince);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return penalty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
|
||||
/// <paramref name="mostRecentPatternsToCompare"/>.
|
||||
/// </summary>
|
||||
private bool samePattern(int start, int mostRecentPatternsToCompare)
|
||||
{
|
||||
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||
{
|
||||
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a single rhythm repetition penalty.
|
||||
/// </summary>
|
||||
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
|
||||
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a penalty based on the number of notes since the last rhythm change.
|
||||
/// Both rare and frequent rhythm changes are penalised.
|
||||
/// </summary>
|
||||
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
|
||||
private static double patternLengthPenalty(int patternLength)
|
||||
{
|
||||
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
|
||||
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
|
||||
return Math.Min(shortPatternPenalty, longPatternPenalty);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calculates a penalty for objects that do not require alternating hands.
|
||||
/// </summary>
|
||||
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
|
||||
private double speedPenalty(double deltaTime)
|
||||
{
|
||||
if (deltaTime < 80) return 1;
|
||||
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
|
||||
|
||||
resetRhythmAndStrain();
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
|
||||
/// </summary>
|
||||
private void resetRhythmAndStrain()
|
||||
{
|
||||
currentStrain = 0.0;
|
||||
notesSinceRhythmChange = 0;
|
||||
return difficulty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
@@ -18,7 +19,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
private double skillMultiplier => 1.1;
|
||||
private double strainDecayBase => 0.4;
|
||||
|
||||
private readonly bool singleColourStamina;
|
||||
public readonly bool SingleColourStamina;
|
||||
private readonly bool isConvert;
|
||||
|
||||
private double currentStrain;
|
||||
|
||||
@@ -27,10 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
/// </summary>
|
||||
/// <param name="mods">Mods for use in skill calculations.</param>
|
||||
/// <param name="singleColourStamina">Reads when Stamina is from a single coloured pattern.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina)
|
||||
/// <param name="isConvert">Determines if the currently evaluated beatmap is converted.</param>
|
||||
public Stamina(Mod[] mods, bool singleColourStamina, bool isConvert)
|
||||
: base(mods)
|
||||
{
|
||||
this.singleColourStamina = singleColourStamina;
|
||||
SingleColourStamina = singleColourStamina;
|
||||
this.isConvert = isConvert;
|
||||
}
|
||||
|
||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||
@@ -42,14 +46,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||
|
||||
// Safely prevents previous strains from shifting as new notes are added.
|
||||
var currentObject = current as TaikoDifficultyHitObject;
|
||||
int index = currentObject?.Colour.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
int index = currentObject?.ColourData.MonoStreak?.HitObjects.IndexOf(currentObject) ?? 0;
|
||||
|
||||
if (singleColourStamina)
|
||||
return currentStrain / (1 + Math.Exp(-(index - 10) / 2.0));
|
||||
double monolengthBonus = isConvert ? 1 : 1 + Math.Min(Math.Max((index - 5) / 50.0, 0), 0.30);
|
||||
|
||||
return currentStrain;
|
||||
if (SingleColourStamina)
|
||||
return DifficultyCalculationUtils.Logistic(-(index - 10) / 2.0, currentStrain);
|
||||
|
||||
return currentStrain * monolengthBonus;
|
||||
}
|
||||
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => singleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => SingleColourStamina ? 0 : currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the reading skill.
|
||||
/// </summary>
|
||||
[JsonProperty("reading_difficulty")]
|
||||
public double ReadingDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the stamina skill.
|
||||
/// </summary>
|
||||
@@ -22,41 +40,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
[JsonProperty("mono_stamina_factor")]
|
||||
public double MonoStaminaFactor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the rhythm skill.
|
||||
/// </summary>
|
||||
[JsonProperty("rhythm_difficulty")]
|
||||
public double RhythmDifficulty { get; set; }
|
||||
[JsonProperty("rhythm_difficult_strains")]
|
||||
public double RhythmTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the colour skill.
|
||||
/// </summary>
|
||||
[JsonProperty("colour_difficulty")]
|
||||
public double ColourDifficulty { get; set; }
|
||||
[JsonProperty("colour_difficult_strains")]
|
||||
public double ColourTopStrains { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The difficulty corresponding to the hardest parts of the map.
|
||||
/// </summary>
|
||||
[JsonProperty("peak_difficulty")]
|
||||
public double PeakDifficulty { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("great_hit_window")]
|
||||
public double GreatHitWindow { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The perceived hit window for an OK hit inclusive of rate-adjusting mods (DT/HT/etc).
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Rate-adjusting mods don't directly affect the hit window, but have a perceived effect as a result of adjusting audio timing.
|
||||
/// </remarks>
|
||||
[JsonProperty("ok_hit_window")]
|
||||
public double OkHitWindow { get; set; }
|
||||
[JsonProperty("stamina_difficult_strains")]
|
||||
public double StaminaTopStrains { get; set; }
|
||||
|
||||
public override IEnumerable<(int attributeId, object value)> ToDatabaseAttributes()
|
||||
{
|
||||
@@ -64,8 +55,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
yield return v;
|
||||
|
||||
yield return (ATTRIB_ID_DIFFICULTY, StarRating);
|
||||
yield return (ATTRIB_ID_GREAT_HIT_WINDOW, GreatHitWindow);
|
||||
yield return (ATTRIB_ID_OK_HIT_WINDOW, OkHitWindow);
|
||||
yield return (ATTRIB_ID_MONO_STAMINA_FACTOR, MonoStaminaFactor);
|
||||
}
|
||||
|
||||
@@ -74,8 +63,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
base.FromDatabaseAttributes(values, onlineInfo);
|
||||
|
||||
StarRating = values[ATTRIB_ID_DIFFICULTY];
|
||||
GreatHitWindow = values[ATTRIB_ID_GREAT_HIT_WINDOW];
|
||||
OkHitWindow = values[ATTRIB_ID_OK_HIT_WINDOW];
|
||||
MonoStaminaFactor = values[ATTRIB_ID_MONO_STAMINA_FACTOR];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,10 +8,12 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Rhythm;
|
||||
using osu.Game.Rulesets.Taiko.Difficulty.Skills;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
@@ -21,11 +23,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||
{
|
||||
private const double difficulty_multiplier = 0.084375;
|
||||
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier;
|
||||
private const double rhythm_skill_multiplier = 0.65 * difficulty_multiplier;
|
||||
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
|
||||
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
|
||||
private const double stamina_skill_multiplier = 0.445 * difficulty_multiplier;
|
||||
|
||||
public override int Version => 20241007;
|
||||
private double strainLengthBonus;
|
||||
private double patternMultiplier;
|
||||
|
||||
private bool isConvert;
|
||||
|
||||
public override int Version => 20250306;
|
||||
|
||||
public TaikoDifficultyCalculator(IRulesetInfo ruleset, IWorkingBeatmap beatmap)
|
||||
: base(ruleset, beatmap)
|
||||
@@ -34,12 +42,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||
{
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
isConvert = beatmap.BeatmapInfo.Ruleset.OnlineID == 0;
|
||||
|
||||
return new Skill[]
|
||||
{
|
||||
new Rhythm(mods),
|
||||
new Rhythm(mods, hitWindows.WindowFor(HitResult.Great) / clockRate),
|
||||
new Reading(mods),
|
||||
new Colour(mods),
|
||||
new Stamina(mods, false),
|
||||
new Stamina(mods, true)
|
||||
new Stamina(mods, false, isConvert),
|
||||
new Stamina(mods, true, isConvert)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,21 +67,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||
{
|
||||
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
var difficultyHitObjects = new List<DifficultyHitObject>();
|
||||
var centreObjects = new List<TaikoDifficultyHitObject>();
|
||||
var rimObjects = new List<TaikoDifficultyHitObject>();
|
||||
var noteObjects = new List<TaikoDifficultyHitObject>();
|
||||
|
||||
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
|
||||
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||
{
|
||||
difficultyHitObjects.Add(
|
||||
new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects,
|
||||
centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count)
|
||||
);
|
||||
difficultyHitObjects.Add(new TaikoDifficultyHitObject(
|
||||
beatmap.HitObjects[i],
|
||||
beatmap.HitObjects[i - 1],
|
||||
clockRate,
|
||||
difficultyHitObjects,
|
||||
centreObjects,
|
||||
rimObjects,
|
||||
noteObjects,
|
||||
difficultyHitObjects.Count,
|
||||
beatmap.ControlPointInfo,
|
||||
beatmap.Difficulty.SliderMultiplier
|
||||
));
|
||||
}
|
||||
|
||||
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
|
||||
TaikoRhythmDifficultyPreprocessor.ProcessAndAssign(noteObjects);
|
||||
|
||||
return difficultyHitObjects;
|
||||
}
|
||||
@@ -77,60 +100,53 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (beatmap.HitObjects.Count == 0)
|
||||
return new TaikoDifficultyAttributes { Mods = mods };
|
||||
|
||||
Colour colour = (Colour)skills.First(x => x is Colour);
|
||||
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
|
||||
Stamina stamina = (Stamina)skills.First(x => x is Stamina);
|
||||
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
|
||||
bool isRelax = mods.Any(h => h is TaikoModRelax);
|
||||
|
||||
var rhythm = skills.OfType<Rhythm>().Single();
|
||||
var reading = skills.OfType<Reading>().Single();
|
||||
var colour = skills.OfType<Colour>().Single();
|
||||
var stamina = skills.OfType<Stamina>().Single(s => !s.SingleColourStamina);
|
||||
var singleColourStamina = skills.OfType<Stamina>().Single(s => s.SingleColourStamina);
|
||||
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||
double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
|
||||
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
|
||||
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina);
|
||||
double colourDifficultStrains = colour.CountTopWeightedStrains();
|
||||
double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
|
||||
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
|
||||
|
||||
// As we don't have pattern integration in osu!taiko, we apply the other two skills relative to rhythm.
|
||||
patternMultiplier = Math.Pow(staminaRating * colourRating, 0.10);
|
||||
|
||||
strainLengthBonus = 1
|
||||
+ Math.Min(Math.Max((staminaDifficultStrains - 1000) / 3700, 0), 0.15)
|
||||
+ Math.Min(Math.Max((staminaRating - 7.0) / 1.0, 0), 0.05);
|
||||
|
||||
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax, isConvert);
|
||||
double starRating = rescale(combinedRating * 1.4);
|
||||
|
||||
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system.
|
||||
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
|
||||
{
|
||||
starRating *= 0.925;
|
||||
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
|
||||
if (colourRating < 2 && staminaRating > 8)
|
||||
starRating *= 0.80;
|
||||
}
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(beatmap.Difficulty.OverallDifficulty);
|
||||
|
||||
TaikoDifficultyAttributes attributes = new TaikoDifficultyAttributes
|
||||
{
|
||||
StarRating = starRating,
|
||||
Mods = mods,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ReadingDifficulty = readingRating,
|
||||
ColourDifficulty = colourRating,
|
||||
StaminaDifficulty = staminaRating,
|
||||
MonoStaminaFactor = monoStaminaFactor,
|
||||
RhythmDifficulty = rhythmRating,
|
||||
ColourDifficulty = colourRating,
|
||||
PeakDifficulty = combinedRating,
|
||||
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
|
||||
RhythmTopStrains = rhythmDifficultStrains,
|
||||
ColourTopStrains = colourDifficultStrains,
|
||||
StaminaTopStrains = staminaDifficultStrains,
|
||||
MaxCombo = beatmap.GetMaxCombo(),
|
||||
};
|
||||
|
||||
return attributes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private double rescale(double sr)
|
||||
{
|
||||
if (sr < 0) return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||
/// </summary>
|
||||
@@ -138,22 +154,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||
/// </remarks>
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina)
|
||||
private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax, bool isConvert)
|
||||
{
|
||||
List<double> peaks = new List<double>();
|
||||
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
|
||||
var readingPeaks = reading.GetCurrentStrainPeaks().ToList();
|
||||
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
|
||||
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
|
||||
|
||||
for (int i = 0; i < colourPeaks.Count; i++)
|
||||
{
|
||||
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
|
||||
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier * patternMultiplier;
|
||||
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
|
||||
double colourPeak = isRelax ? 0 : colourPeaks[i] * colour_skill_multiplier; // There is no colour difficulty in relax.
|
||||
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier * strainLengthBonus;
|
||||
staminaPeak /= isConvert || isRelax ? 1.5 : 1.0; // Available finger count is increased by 150%, thus we adjust accordingly.
|
||||
|
||||
double peak = norm(1.5, colourPeak, staminaPeak);
|
||||
peak = norm(2, peak, rhythmPeak);
|
||||
double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
|
||||
|
||||
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
|
||||
// These sections will not contribute to the difficulty.
|
||||
@@ -174,10 +192,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||
/// Applies a final re-scaling of the star rating.
|
||||
/// </summary>
|
||||
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||
/// <param name="values">The coefficients of the vector.</param>
|
||||
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||
private static double rescale(double sr)
|
||||
{
|
||||
if (sr < 0)
|
||||
return sr;
|
||||
|
||||
return 10.43 * Math.Log(sr / 8 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Difficulty.Utils;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
{
|
||||
@@ -21,6 +24,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
private int countMiss;
|
||||
private double? estimatedUnstableRate;
|
||||
|
||||
private double clockRate;
|
||||
private double greatHitWindow;
|
||||
|
||||
private double effectiveMissCount;
|
||||
|
||||
public TaikoPerformanceCalculator()
|
||||
@@ -36,7 +42,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
|
||||
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
|
||||
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
|
||||
estimatedUnstableRate = computeDeviationUpperBound(taikoAttributes) * 10;
|
||||
|
||||
var track = new TrackVirtual(10000);
|
||||
score.Mods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));
|
||||
clockRate = track.Rate;
|
||||
|
||||
var difficulty = score.BeatmapInfo!.Difficulty.Clone();
|
||||
|
||||
score.Mods.OfType<IApplicableToDifficulty>().ForEach(m => m.ApplyToDifficulty(difficulty));
|
||||
|
||||
HitWindows hitWindows = new TaikoHitWindows();
|
||||
hitWindows.SetDifficulty(difficulty.OverallDifficulty);
|
||||
|
||||
greatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate;
|
||||
|
||||
estimatedUnstableRate = computeDeviationUpperBound() * 10;
|
||||
|
||||
// The effectiveMissCount is calculated by gaining a ratio for totalSuccessfulHits and increasing the miss penalty for shorter object counts lower than 1000.
|
||||
if (totalSuccessfulHits > 0)
|
||||
@@ -73,7 +93,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
|
||||
{
|
||||
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0;
|
||||
double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.110) - 4.0;
|
||||
double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1250.0);
|
||||
|
||||
difficultyValue *= 1 + 0.10 * Math.Max(0, attributes.StarRating - 10);
|
||||
|
||||
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
|
||||
difficultyValue *= lengthBonus;
|
||||
@@ -86,9 +109,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
if (score.Mods.Any(m => m is ModHidden))
|
||||
difficultyValue *= 1.025;
|
||||
|
||||
if (score.Mods.Any(m => m is ModHardRock))
|
||||
difficultyValue *= 1.10;
|
||||
|
||||
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
|
||||
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
|
||||
|
||||
@@ -97,14 +117,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
|
||||
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
|
||||
double accScalingExponent = 2 + attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor;
|
||||
double accScalingShift = 500 - 100 * (attributes.MonoStaminaFactor * 3);
|
||||
|
||||
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
return difficultyValue * Math.Pow(DifficultyCalculationUtils.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
|
||||
}
|
||||
|
||||
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes, bool isConvert)
|
||||
{
|
||||
if (attributes.GreatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
if (greatHitWindow <= 0 || estimatedUnstableRate == null)
|
||||
return 0;
|
||||
|
||||
double accuracyValue = Math.Pow(70 / estimatedUnstableRate.Value, 1.1) * Math.Pow(attributes.StarRating, 0.4) * 100.0;
|
||||
@@ -123,58 +143,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
/// and the hit judgements, assuming the player's mean hit error is 0. The estimation is consistent in that
|
||||
/// two SS scores on the same map with the same settings will always return the same deviation.
|
||||
/// </summary>
|
||||
private double? computeDeviationUpperBound(TaikoDifficultyAttributes attributes)
|
||||
private double? computeDeviationUpperBound()
|
||||
{
|
||||
if (totalSuccessfulHits == 0 || attributes.GreatHitWindow <= 0)
|
||||
if (countGreat == 0 || greatHitWindow <= 0)
|
||||
return null;
|
||||
|
||||
double h300 = attributes.GreatHitWindow;
|
||||
double h100 = attributes.OkHitWindow;
|
||||
|
||||
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
|
||||
double? calcDeviationGreatWindow()
|
||||
{
|
||||
if (countGreat == 0) return null;
|
||||
double n = totalHits;
|
||||
|
||||
double n = totalHits;
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
|
||||
// Proportion of greats hit.
|
||||
double p = countGreat / n;
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h300 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
// The upper bound on deviation, calculated with the ratio of 300s + 100s to objects, and the good hit window.
|
||||
// This will return a lower value than the first method when the number of 100s is high, but the miss count is low.
|
||||
double? calcDeviationGoodWindow()
|
||||
{
|
||||
if (totalSuccessfulHits == 0) return null;
|
||||
|
||||
double n = totalHits;
|
||||
|
||||
// Proportion of greats + goods hit.
|
||||
double p = totalSuccessfulHits / n;
|
||||
|
||||
// We can be 99% confident that p is at least this value.
|
||||
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
|
||||
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
double? deviationGreatWindow = calcDeviationGreatWindow();
|
||||
double? deviationGoodWindow = calcDeviationGoodWindow();
|
||||
|
||||
if (deviationGreatWindow is null)
|
||||
return deviationGoodWindow;
|
||||
|
||||
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
|
||||
// We can be 99% confident that the deviation is not higher than:
|
||||
return greatHitWindow / (Math.Sqrt(2) * DifficultyCalculationUtils.ErfInv(pLowerBound));
|
||||
}
|
||||
|
||||
private int totalHits => countGreat + countOk + countMeh + countMiss;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// 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.Taiko.Difficulty.Utils
|
||||
{
|
||||
/// <summary>
|
||||
/// The interface for objects that provide an interval value.
|
||||
/// </summary>
|
||||
public interface IHasInterval
|
||||
{
|
||||
/// <summary>
|
||||
/// The interval – ie delta time – between this object and a known previous object.
|
||||
/// </summary>
|
||||
double Interval { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.Framework.Utils;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Utils
|
||||
{
|
||||
public static class IntervalGroupingUtils
|
||||
{
|
||||
public static List<List<T>> GroupByInterval<T>(IReadOnlyList<T> objects) where T : IHasInterval
|
||||
{
|
||||
var groups = new List<List<T>>();
|
||||
|
||||
int i = 0;
|
||||
while (i < objects.Count)
|
||||
groups.Add(createNextGroup(objects, ref i));
|
||||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private static List<T> createNextGroup<T>(IReadOnlyList<T> objects, ref int i) where T : IHasInterval
|
||||
{
|
||||
const double margin_of_error = 5;
|
||||
|
||||
// This never compares the first two elements in the group.
|
||||
// This sounds wrong but is apparently "as intended" (https://github.com/ppy/osu/pull/31636#discussion_r1942673329)
|
||||
var groupedObjects = new List<T> { objects[i] };
|
||||
i++;
|
||||
|
||||
for (; i < objects.Count - 1; i++)
|
||||
{
|
||||
if (!Precision.AlmostEquals(objects[i].Interval, objects[i + 1].Interval, margin_of_error))
|
||||
{
|
||||
// When an interval change occurs, include the object with the differing interval in the case it increased
|
||||
// See https://github.com/ppy/osu/pull/31636#discussion_r1942368372 for rationale.
|
||||
if (objects[i + 1].Interval > objects[i].Interval + margin_of_error)
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
|
||||
// No interval change occurred
|
||||
groupedObjects.Add(objects[i]);
|
||||
}
|
||||
|
||||
// Check if the last two objects in the object form a "flat" rhythm pattern within the specified margin of error.
|
||||
// If true, add the current object to the group and increment the index to process the next object.
|
||||
if (objects.Count > 2 && i < objects.Count && Precision.AlmostEquals(objects[^1].Interval, objects[^2].Interval, margin_of_error))
|
||||
{
|
||||
groupedObjects.Add(objects[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
return groupedObjects;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,7 @@
|
||||
#nullable disable
|
||||
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Rulesets.Taiko.Skinning.Default;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
@@ -25,8 +23,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
{
|
||||
}
|
||||
|
||||
protected override void UpdateInitialTransforms() => this.FadeOut();
|
||||
|
||||
public void TriggerResult(bool hit)
|
||||
{
|
||||
HitObject.StartTime = Time.Current;
|
||||
@@ -43,7 +39,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
|
||||
public override bool OnPressed(KeyBindingPressEvent<TaikoAction> e) => false;
|
||||
|
||||
protected override SkinnableDrawable CreateMainPiece() => new SkinnableDrawable(new TaikoSkinComponentLookup(TaikoSkinComponents.DrumRollTick),
|
||||
_ => new TickPiece());
|
||||
protected override SkinnableDrawable CreateMainPiece() => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,9 +154,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
|
||||
if (MainPiece != null)
|
||||
Content.Remove(MainPiece, true);
|
||||
|
||||
Content.Add(MainPiece = CreateMainPiece());
|
||||
MainPiece = CreateMainPiece();
|
||||
|
||||
if (MainPiece != null)
|
||||
Content.Add(MainPiece);
|
||||
}
|
||||
|
||||
[CanBeNull]
|
||||
protected abstract SkinnableDrawable CreateMainPiece();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
@@ -21,9 +19,6 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
|
||||
|
||||
[Resolved]
|
||||
private OsuGame? osuGame { get; set; }
|
||||
|
||||
public TaikoPlayfieldAdjustmentContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
@@ -60,19 +55,7 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
|
||||
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
|
||||
|
||||
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
|
||||
|
||||
// on mobile platforms where the base aspect ratio is wider, the taiko playfield
|
||||
// needs to be scaled down to remain playable.
|
||||
if (RuntimeInfo.IsMobile && osuGame != null)
|
||||
{
|
||||
const float base_aspect_ratio = 1024f / 768f;
|
||||
float gameAspectRatio = osuGame.ScalingContainerTargetDrawSize.X / osuGame.ScalingContainerTargetDrawSize.Y;
|
||||
// this magic scale is unexplainable, but required so the playfield doesn't become too zoomed out as the aspect ratio increases.
|
||||
const float magic_scale = 1.25f;
|
||||
Scale *= magic_scale * new Vector2(base_aspect_ratio / gameAspectRatio);
|
||||
}
|
||||
|
||||
Scale = new Vector2(Parent!.ChildSize.Y / 768f * (relativeHeight / base_relative_height));
|
||||
Width = 1 / Scale.X;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps
|
||||
{
|
||||
public class BeatmapExtensionsTest
|
||||
{
|
||||
[Test]
|
||||
public void TestLengthCalculations()
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 5_000 },
|
||||
new HitCircle { StartTime = 300_000 },
|
||||
new Spinner { StartTime = 280_000, Duration = 40_000 }
|
||||
},
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(50_000, 75_000),
|
||||
new BreakPeriod(100_000, 150_000),
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000)));
|
||||
Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000
|
||||
Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(240_000)); // 315_000 - (25_000 + 50_000) = 315_000 - 75_000
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrainLengthCannotGoNegative()
|
||||
{
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new HitCircle { StartTime = 5_000 },
|
||||
new HitCircle { StartTime = 300_000 },
|
||||
new Spinner { StartTime = 280_000, Duration = 40_000 }
|
||||
},
|
||||
Breaks =
|
||||
{
|
||||
new BreakPeriod(0, 350_000),
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(beatmap.CalculatePlayableBounds(), Is.EqualTo((5_000, 320_000)));
|
||||
Assert.That(beatmap.CalculatePlayableLength(), Is.EqualTo(315_000)); // 320_000 - 5_000
|
||||
Assert.That(beatmap.CalculateDrainLength(), Is.EqualTo(0)); // break period encompasses entire beatmap
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -404,6 +404,35 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestComboColourCountIsLimitedToEight()
|
||||
{
|
||||
var decoder = new LegacySkinDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("too-many-combo-colours.osu"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var comboColors = decoder.Decode(stream).ComboColours;
|
||||
|
||||
Debug.Assert(comboColors != null);
|
||||
|
||||
Color4[] expectedColors =
|
||||
{
|
||||
new Color4(142, 199, 255, 255),
|
||||
new Color4(255, 128, 128, 255),
|
||||
new Color4(128, 255, 255, 255),
|
||||
new Color4(128, 255, 128, 255),
|
||||
new Color4(255, 187, 255, 255),
|
||||
new Color4(255, 177, 140, 255),
|
||||
new Color4(100, 100, 100, 255),
|
||||
new Color4(142, 199, 255, 255),
|
||||
};
|
||||
Assert.AreEqual(expectedColors.Length, comboColors.Count);
|
||||
for (int i = 0; i < expectedColors.Length; i++)
|
||||
Assert.AreEqual(expectedColors[i], comboColors[i]);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGetLastObjectTime()
|
||||
{
|
||||
|
||||
@@ -28,6 +28,7 @@ using osu.Game.Skinning;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
@@ -184,6 +185,32 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
Assert.That(decodedSlider.Path.ControlPoints.Count, Is.EqualTo(5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestOnlyEightComboColoursEncoded()
|
||||
{
|
||||
var beatmapSkin = new LegacyBeatmapSkin(new BeatmapInfo(), null)
|
||||
{
|
||||
Configuration =
|
||||
{
|
||||
CustomComboColours =
|
||||
{
|
||||
new Color4(1, 1, 1, 255),
|
||||
new Color4(2, 2, 2, 255),
|
||||
new Color4(3, 3, 3, 255),
|
||||
new Color4(4, 4, 4, 255),
|
||||
new Color4(5, 5, 5, 255),
|
||||
new Color4(6, 6, 6, 255),
|
||||
new Color4(7, 7, 7, 255),
|
||||
new Color4(8, 8, 8, 255),
|
||||
new Color4(9, 9, 9, 255),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy((new Beatmap(), beatmapSkin)), string.Empty);
|
||||
Assert.That(decodedAfterEncode.skin.Configuration.CustomComboColours, Has.Count.EqualTo(8));
|
||||
}
|
||||
|
||||
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
|
||||
{
|
||||
// equal to null, no need to SequenceEqual
|
||||
@@ -212,6 +239,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
{
|
||||
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
|
||||
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
|
||||
stream.Seek(0, SeekOrigin.Begin);
|
||||
beatmapSkin.Configuration = new LegacySkinDecoder().Decode(reader);
|
||||
return (convert(beatmap), beatmapSkin);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ using Humanizer;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Tests.Visual.Multiplayer;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
@@ -16,6 +16,13 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
[HeadlessTest]
|
||||
public partial class StatefulMultiplayerClientTest : MultiplayerTestScene
|
||||
{
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserAddedOnJoin()
|
||||
{
|
||||
@@ -72,10 +79,6 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
|
||||
AddStep("create room initially in gameplay", () =>
|
||||
{
|
||||
var newRoom = new Room();
|
||||
newRoom.CopyFrom(SelectedRoom.Value!);
|
||||
|
||||
newRoom.RoomID = null;
|
||||
MultiplayerClient.RoomSetupAction = room =>
|
||||
{
|
||||
room.State = MultiplayerRoomState.Playing;
|
||||
@@ -86,13 +89,32 @@ namespace osu.Game.Tests.NonVisual.Multiplayer
|
||||
});
|
||||
};
|
||||
|
||||
RoomManager.CreateRoom(newRoom);
|
||||
MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
checkPlayingUserCount(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJoinRoomWithManyUsers()
|
||||
{
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddStep("create room with many users", () =>
|
||||
{
|
||||
MultiplayerClient.RoomSetupAction = room =>
|
||||
{
|
||||
room.Users.AddRange(Enumerable.Range(PLAYER_1_ID, 100).Select(id => new MultiplayerRoomUser(id)));
|
||||
};
|
||||
|
||||
MultiplayerClient.JoinRoom(MultiplayerClient.ServerSideRooms.Single()).ConfigureAwait(false);
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
}
|
||||
|
||||
private void checkPlayingUserCount(int expectedCount)
|
||||
=> AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => MultiplayerClient.CurrentMatchPlayingUserIds.Count == expectedCount);
|
||||
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
osu file format v14
|
||||
|
||||
[General]
|
||||
AudioFilename: 03. Renatus - Soleily 192kbps.mp3
|
||||
AudioLeadIn: 0
|
||||
PreviewTime: 164471
|
||||
Countdown: 0
|
||||
SampleSet: Soft
|
||||
StackLeniency: 0.7
|
||||
Mode: 0
|
||||
LetterboxInBreaks: 0
|
||||
WidescreenStoryboard: 0
|
||||
|
||||
[Editor]
|
||||
Bookmarks: 11505,22054,32604,43153,53703,64252,74802,85351,95901,106450,116999,119637,130186,140735,151285,161834,164471,175020,185570,196119,206669,209306
|
||||
DistanceSpacing: 1.8
|
||||
BeatDivisor: 4
|
||||
GridSize: 4
|
||||
TimelineZoom: 2
|
||||
|
||||
[Metadata]
|
||||
Title:Renatus
|
||||
TitleUnicode:Renatus
|
||||
Artist:Soleily
|
||||
ArtistUnicode:Soleily
|
||||
Creator:Gamu
|
||||
Version:Insane
|
||||
Source:
|
||||
Tags:MBC7 Unisphere 地球ヤバイEP Chikyu Yabai
|
||||
BeatmapID:557821
|
||||
BeatmapSetID:241526
|
||||
|
||||
[Difficulty]
|
||||
HPDrainRate:6.5
|
||||
CircleSize:4
|
||||
OverallDifficulty:8
|
||||
ApproachRate:9
|
||||
SliderMultiplier:1.8
|
||||
SliderTickRate:2
|
||||
|
||||
[Events]
|
||||
//Background and Video events
|
||||
0,0,"machinetop_background.jpg",0,0
|
||||
//Break Periods
|
||||
2,122474,140135
|
||||
//Storyboard Layer 0 (Background)
|
||||
//Storyboard Layer 1 (Fail)
|
||||
//Storyboard Layer 2 (Pass)
|
||||
//Storyboard Layer 3 (Foreground)
|
||||
//Storyboard Sound Samples
|
||||
|
||||
[TimingPoints]
|
||||
956,329.67032967033,4,2,0,60,1,0
|
||||
|
||||
|
||||
[Colours]
|
||||
Combo1:142,199,255
|
||||
Combo2:255,128,128
|
||||
Combo3:128,255,255
|
||||
Combo4:128,255,128
|
||||
Combo5:255,187,255
|
||||
Combo6:255,177,140
|
||||
Combo7:100,100,100
|
||||
Combo8:142,199,255
|
||||
Combo9:255,128,128
|
||||
Combo10:128,255,255
|
||||
Combo11:128,255,128
|
||||
Combo12:255,187,255
|
||||
Combo13:255,177,140
|
||||
Combo14:100,100,100
|
||||
|
||||
[HitObjects]
|
||||
192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0:
|
||||
@@ -6,6 +6,8 @@ using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Metadata;
|
||||
@@ -13,9 +15,11 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Screens.SelectV2.Leaderboards;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osu.Game.Tests.Visual.Metadata;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
@@ -57,6 +61,39 @@ namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUseTheseModsUnavailableIfNoFreeMods()
|
||||
{
|
||||
var room = new Room
|
||||
{
|
||||
RoomID = 1234,
|
||||
Name = "Daily Challenge: June 4, 2024",
|
||||
Playlist =
|
||||
[
|
||||
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
|
||||
{
|
||||
RequiredMods = [new APIMod(new OsuModTraceable())],
|
||||
AllowedMods = []
|
||||
}
|
||||
],
|
||||
EndDate = DateTimeOffset.Now.AddHours(12),
|
||||
Category = RoomCategory.DailyChallenge
|
||||
};
|
||||
|
||||
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
|
||||
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
|
||||
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
|
||||
AddUntilStep("wait for pushed", () => screen.IsCurrentScreen());
|
||||
AddStep("force transforms to finish", () => FinishTransforms(true));
|
||||
AddStep("right click second score", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<LeaderboardScoreV2>().ElementAt(1));
|
||||
InputManager.Click(MouseButton.Right);
|
||||
});
|
||||
AddAssert("use these mods not present",
|
||||
() => this.ChildrenOfType<OsuContextMenu>().All(m => m.Items.All(item => item.Text.Value != "Use these mods")));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotifications()
|
||||
{
|
||||
|
||||
@@ -94,6 +94,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("release", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == editorBeatmap.BeatmapInfo.BeatmapSet.AsNonNull().ID)?.Value.DeletePending == true);
|
||||
|
||||
AddUntilStep("wait for default beatmap", () => Editor.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -171,6 +173,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -215,6 +219,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != previousDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
|
||||
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
|
||||
AddStep("add effect points", () =>
|
||||
@@ -239,6 +245,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != previousDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -287,6 +295,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != firstDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
|
||||
@@ -367,6 +377,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("created difficulty has copy suffix in name", () => EditorBeatmap.BeatmapInfo.DifficultyName == copyDifficultyName);
|
||||
AddAssert("created difficulty has timing point", () =>
|
||||
{
|
||||
@@ -377,7 +389,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("approach rate correctly copied", () => EditorBeatmap.Difficulty.ApproachRate == 4);
|
||||
AddAssert("combo colours correctly copied", () => EditorBeatmap.BeatmapSkin.AsNonNull().ComboColours.Count == 2);
|
||||
|
||||
ensureEditorLoaded();
|
||||
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
|
||||
|
||||
AddAssert("online ID not copied", () => EditorBeatmap.BeatmapInfo.OnlineID == -1);
|
||||
|
||||
AddStep("save beatmap", () => Editor.Save());
|
||||
@@ -440,6 +454,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != originalDifficultyName;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("save without changes", () => Editor.Save());
|
||||
|
||||
AddAssert("collection still points to old beatmap", () => !collection.BeatmapMD5Hashes.Contains(EditorBeatmap.BeatmapInfo.MD5Hash)
|
||||
@@ -477,6 +493,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != "New Difficulty";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty has correct name", () => EditorBeatmap.BeatmapInfo.DifficultyName == "New Difficulty (1)");
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
@@ -514,6 +533,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("set difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = duplicate_difficulty_name);
|
||||
AddStep("try to save beatmap", () => Editor.Save());
|
||||
AddAssert("beatmap set not corrupted", () =>
|
||||
@@ -540,6 +561,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
return set != null && set.PerformRead(s => s.Beatmaps.Count == 1 && s.Files.Count == 1);
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new CatchRuleset().RulesetInfo));
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
@@ -547,7 +570,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != duplicate_difficulty_name;
|
||||
});
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
|
||||
{
|
||||
@@ -584,6 +608,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName == "New Difficulty";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||
@@ -610,6 +637,9 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName == "New Difficulty (1)";
|
||||
});
|
||||
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddAssert("new difficulty persisted", () =>
|
||||
{
|
||||
var set = beatmapManager.QueryBeatmapSet(s => s.ID == setId);
|
||||
@@ -735,6 +765,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
|
||||
}
|
||||
|
||||
private void ensureEditorLoaded() => AddUntilStep("wait for editor load", () => Editor.IsLoaded && DialogOverlay.IsLoaded);
|
||||
|
||||
private void createNewDifficulty()
|
||||
{
|
||||
string? currentDifficulty = null;
|
||||
@@ -748,13 +780,14 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
|
||||
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
|
||||
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
|
||||
AddUntilStep("wait for created", () =>
|
||||
{
|
||||
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
|
||||
return difficultyName != null && difficultyName != currentDifficulty;
|
||||
});
|
||||
ensureEditorLoaded();
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
@@ -765,7 +798,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep($"switch to difficulty #{index + 1}", () =>
|
||||
Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index)));
|
||||
|
||||
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
|
||||
ensureEditorLoaded();
|
||||
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
|
||||
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
|
||||
}
|
||||
|
||||
@@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft));
|
||||
AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3)));
|
||||
AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
|
||||
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
|
||||
AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X));
|
||||
|
||||
// Scroll out at 0.25
|
||||
AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3)));
|
||||
AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft));
|
||||
AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X));
|
||||
AddAssert("Box 1/2 at 1/2", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.5f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.5f * scrollQuad.Size.X));
|
||||
AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft));
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Bindable<LocalUserPlayingState> playingState = new Bindable<LocalUserPlayingState>();
|
||||
GameplayState gameplayState = new GameplayState(new Beatmap(), new OsuRuleset(), healthProcessor: new OsuHealthProcessor(0), localUserPlayingState: playingState);
|
||||
TestSpectatorClient spectatorClient = new TestSpectatorClient();
|
||||
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestMultiplayerRoomManager(new TestRoomRequestsHandler()));
|
||||
TestMultiplayerClient multiplayerClient = new TestMultiplayerClient(new TestRoomRequestsHandler());
|
||||
|
||||
AddStep("create spectator list", () =>
|
||||
{
|
||||
@@ -75,8 +75,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddRepeatStep("remove random user", () => ((ISpectatorClient)spectatorClient).UserEndedWatching(
|
||||
spectatorClient.WatchingUsers[RNG.Next(spectatorClient.WatchingUsers.Count)].OnlineID), 5);
|
||||
|
||||
AddStep("change font to venera", () => list.Font.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.Font.Value = Typeface.Torus);
|
||||
AddStep("change font to venera", () => list.HeaderFont.Value = Typeface.Venera);
|
||||
AddStep("change font to torus", () => list.HeaderFont.Value = Typeface.Torus);
|
||||
AddStep("change header colour", () => list.HeaderColour.Value = new Colour4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1));
|
||||
|
||||
AddStep("enter break", () => playingState.Value = LocalUserPlayingState.Break);
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
var mockLounge = new Mock<LoungeSubScreen>();
|
||||
var mockLounge = new Mock<IOnlinePlayLounge>();
|
||||
mockLounge
|
||||
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
|
||||
.Callback<Room, string, Action<Room>, Action<string>>((_, _, _, d) =>
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneDrawableRoomParticipantsList : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
private DrawableRoomParticipantsList list = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
@@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create list", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room
|
||||
room = new Room
|
||||
{
|
||||
Name = "test room",
|
||||
Host = new APIUser
|
||||
@@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
};
|
||||
|
||||
Child = list = new DrawableRoomParticipantsList(SelectedRoom.Value)
|
||||
Child = list = new DrawableRoomParticipantsList(room)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -119,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("4 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 4);
|
||||
AddAssert("46 hidden users", () => list.ChildrenOfType<DrawableRoomParticipantsList.HiddenUserCount>().Single().Count == 46);
|
||||
|
||||
AddStep("remove from end", () => removeUserAt(SelectedRoom.Value!.RecentParticipants.Count - 1));
|
||||
AddStep("remove from end", () => removeUserAt(room.RecentParticipants.Count - 1));
|
||||
AddAssert("4 circles displayed", () => list.ChildrenOfType<UpdateableAvatar>().Count() == 4);
|
||||
AddAssert("45 hidden users", () => list.ChildrenOfType<DrawableRoomParticipantsList.HiddenUserCount>().Single().Count == 45);
|
||||
|
||||
@@ -138,18 +139,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void addUser(int id)
|
||||
{
|
||||
SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Append(new APIUser
|
||||
room.RecentParticipants = room.RecentParticipants.Append(new APIUser
|
||||
{
|
||||
Id = id,
|
||||
Username = $"User {id}"
|
||||
}).ToArray();
|
||||
SelectedRoom.Value!.ParticipantCount++;
|
||||
room.ParticipantCount++;
|
||||
}
|
||||
|
||||
private void removeUserAt(int index)
|
||||
{
|
||||
SelectedRoom.Value!.RecentParticipants = SelectedRoom.Value!.RecentParticipants.Where(u => !u.Equals(SelectedRoom.Value!.RecentParticipants[index])).ToArray();
|
||||
SelectedRoom.Value!.ParticipantCount--;
|
||||
room.RecentParticipants = room.RecentParticipants.Where(u => !u.Equals(room.RecentParticipants[index])).ToArray();
|
||||
room.ParticipantCount--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMatchBeatmapDetailArea : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create area", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room();
|
||||
|
||||
Child = new MatchBeatmapDetailArea(SelectedRoom.Value)
|
||||
Child = new MatchBeatmapDetailArea(room = new Room())
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -36,9 +36,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private void createNewItem()
|
||||
{
|
||||
SelectedRoom.Value!.Playlist = SelectedRoom.Value.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
room.Playlist = room.Playlist.Append(new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
ID = SelectedRoom.Value.Playlist.Count,
|
||||
ID = room.Playlist.Count,
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
RequiredMods = new[]
|
||||
{
|
||||
|
||||
@@ -61,9 +61,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create leaderboard", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room { RoomID = 3 };
|
||||
|
||||
Child = new MatchLeaderboard(SelectedRoom.Value)
|
||||
Child = new MatchLeaderboard(new Room { RoomID = 3 })
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
|
||||
@@ -24,6 +24,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
leaderboard?.RemoveAndDisposeImmediately();
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Online.Multiplayer.MatchTypes.TeamVersus;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer.Spectate;
|
||||
@@ -42,6 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
private MultiSpectatorScreen spectatorScreen = null!;
|
||||
private Room room = null!;
|
||||
|
||||
private readonly List<MultiplayerRoomUser> playingUsers = new List<MultiplayerRoomUser>();
|
||||
|
||||
@@ -63,6 +65,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("clear playing users", () => playingUsers.Clear());
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
[TestCase(1)]
|
||||
@@ -456,7 +462,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
applyToBeatmap?.Invoke(Beatmap.Value);
|
||||
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(SelectedRoom.Value!, playingUsers.ToArray()));
|
||||
LoadScreen(spectatorScreen = new MultiSpectatorScreen(room, playingUsers.ToArray()));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded && (!waitForPlayerLoad || spectatorScreen.AllPlayersLoaded));
|
||||
|
||||
@@ -33,7 +33,6 @@ using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Match;
|
||||
@@ -58,7 +57,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private TestMultiplayerComponents multiplayerComponents = null!;
|
||||
|
||||
private TestMultiplayerClient multiplayerClient => multiplayerComponents.MultiplayerClient;
|
||||
private TestMultiplayerRoomManager roomManager => multiplayerComponents.RoomManager;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; } = null!;
|
||||
@@ -257,7 +255,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Playlist =
|
||||
@@ -286,7 +284,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Playlist =
|
||||
@@ -336,7 +334,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
Password = "password",
|
||||
@@ -789,7 +787,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
roomManager.AddServerSideRoom(new Room
|
||||
multiplayerClient.AddServerSideRoom(new Room
|
||||
{
|
||||
Name = "Test Room",
|
||||
QueueMode = QueueMode.AllPlayers,
|
||||
@@ -807,11 +805,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
|
||||
AddStep("disable polling", () => this.ChildrenOfType<ListingPollingComponent>().Single().TimeBetweenPolls.Value = 0);
|
||||
AddStep("disable polling", () => this.ChildrenOfType<LoungeListingPoller>().Single().TimeBetweenPolls.Value = 0);
|
||||
AddStep("change server-side settings", () =>
|
||||
{
|
||||
roomManager.ServerSideRooms[0].Name = "New name";
|
||||
roomManager.ServerSideRooms[0].Playlist =
|
||||
multiplayerClient.ServerSideRooms[0].Name = "New name";
|
||||
multiplayerClient.ServerSideRooms[0].Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
@@ -828,7 +826,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("local room has correct settings", () =>
|
||||
{
|
||||
var localRoom = this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().Room;
|
||||
return localRoom.Name == roomManager.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
|
||||
return localRoom.Name == multiplayerClient.ServerSideRooms[0].Name && localRoom.Playlist.Single().ID == 2;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -16,45 +16,32 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerLoungeSubScreen : OnlinePlayTestScene
|
||||
public partial class TestSceneMultiplayerLoungeSubScreen : MultiplayerTestScene
|
||||
{
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private LoungeSubScreen loungeScreen = null!;
|
||||
private Room? lastJoinedRoom;
|
||||
private string? lastJoinedPassword;
|
||||
private MultiplayerLoungeSubScreen loungeScreen = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("push screen", () => LoadScreen(loungeScreen = new MultiplayerLoungeSubScreen()));
|
||||
|
||||
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
|
||||
|
||||
AddStep("bind to event", () =>
|
||||
{
|
||||
lastJoinedRoom = null;
|
||||
lastJoinedPassword = null;
|
||||
RoomManager.JoinRoomRequested = onRoomJoined;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestJoinRoomWithoutPassword()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: false));
|
||||
createRooms(GenerateRooms(1, withPassword: false));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == null);
|
||||
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHidesOnBackButton()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
@@ -67,18 +54,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("hit escape", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPopoverHidesOnLeavingScreen()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
AddStep("exit screen", () => Stack.Exit());
|
||||
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -86,16 +77,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
|
||||
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
||||
|
||||
AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
|
||||
AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
|
||||
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
|
||||
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -103,16 +96,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room not joined", () => loungeScreen.IsCurrentScreen());
|
||||
AddAssert("still at lounge", () => loungeScreen.IsCurrentScreen());
|
||||
AddUntilStep("password prompt still visible", () => passwordEntryPopover!.State.Value == Visibility.Visible);
|
||||
AddAssert("textbox still focused", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
|
||||
|
||||
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -120,15 +115,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
||||
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == "password");
|
||||
AddUntilStep("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -136,21 +130,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
|
||||
|
||||
AddStep("add room", () => RoomManager.AddRooms(1, withPassword: true));
|
||||
createRooms(GenerateRooms(1, withPassword: true));
|
||||
AddStep("select room", () => InputManager.Key(Key.Down));
|
||||
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
|
||||
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
|
||||
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
|
||||
AddAssert("room join requested", () => lastJoinedRoom == RoomManager.Rooms.First());
|
||||
AddAssert("room join password correct", () => lastJoinedPassword == "password");
|
||||
AddAssert("room joined", () => MultiplayerClient.RoomJoined);
|
||||
}
|
||||
|
||||
private void onRoomJoined(Room room, string? password)
|
||||
private void createRooms(params Room[] rooms)
|
||||
{
|
||||
lastJoinedRoom = room;
|
||||
lastJoinedPassword = password;
|
||||
AddStep("create rooms", () =>
|
||||
{
|
||||
foreach (var room in rooms)
|
||||
API.Queue(new CreateRoomRequest(room));
|
||||
});
|
||||
|
||||
AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
|
||||
}
|
||||
|
||||
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new MultiplayerTestSceneDependencies();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
private TestMultiplayerMatchSongSelect songSelect = null!;
|
||||
private Live<BeatmapSetInfo> importedBeatmapSet = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager configManager { get; set; } = null!;
|
||||
@@ -58,6 +59,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Add(beatmapStore);
|
||||
}
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
private void setUp()
|
||||
{
|
||||
AddStep("create song select", () =>
|
||||
@@ -66,7 +76,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.SetDefault();
|
||||
|
||||
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value!));
|
||||
LoadScreen(songSelect = new TestMultiplayerMatchSongSelect(room));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||
@@ -138,8 +148,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create song select", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist.Single().RulesetID = 2;
|
||||
songSelect = new TestMultiplayerMatchSongSelect(SelectedRoom.Value, SelectedRoom.Value.Playlist.Single());
|
||||
room.Playlist.Single().RulesetID = 2;
|
||||
songSelect = new TestMultiplayerMatchSongSelect(room, room.Playlist.Single());
|
||||
songSelect.OnLoadComplete += _ => Ruleset.Value = new TaikoRuleset().RulesetInfo;
|
||||
LoadScreen(songSelect);
|
||||
});
|
||||
|
||||
@@ -43,11 +43,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private MultiplayerMatchSubScreen screen = null!;
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
|
||||
public TestSceneMultiplayerMatchSubScreen()
|
||||
: base(false)
|
||||
{
|
||||
}
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -66,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("load match", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room { Name = "Test Room" };
|
||||
LoadScreen(screen = new TestMultiplayerMatchSubScreen(SelectedRoom.Value!));
|
||||
room = new Room { Name = "Test Room" };
|
||||
LoadScreen(screen = new TestMultiplayerMatchSubScreen(room));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => screen.IsCurrentScreen());
|
||||
@@ -78,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -97,7 +93,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -122,7 +118,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -139,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("set playlist", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
@@ -170,7 +166,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -199,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with allowed mod", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -223,7 +219,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item with no allowed mods", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -246,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add two playlist items", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()).BeatmapInfo)
|
||||
{
|
||||
@@ -285,7 +281,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Playlist =
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
@@ -317,6 +313,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("score multiplier = 1.20", () => this.ChildrenOfType<RankingInformationDisplay>().Single().ModMultiplier.Value, () => Is.EqualTo(1.2).Within(0.01));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeSettingsButtonVisibleForHost()
|
||||
{
|
||||
AddStep("add playlist item", () =>
|
||||
{
|
||||
room.Playlist =
|
||||
[
|
||||
new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
|
||||
}
|
||||
];
|
||||
});
|
||||
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
|
||||
|
||||
AddUntilStep("wait for join", () => RoomJoined);
|
||||
|
||||
AddUntilStep("button visible", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0));
|
||||
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
|
||||
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
|
||||
AddAssert("button hidden", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0));
|
||||
}
|
||||
|
||||
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
|
||||
{
|
||||
[Resolved(canBeNull: true)]
|
||||
|
||||
@@ -28,9 +28,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
|
||||
{
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
createNewParticipantsList();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
private MultiplayerPlayer player = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
|
||||
WaitForJoined();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplay()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -46,9 +47,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create list", () =>
|
||||
{
|
||||
Child = list = new MultiplayerPlaylist(SelectedRoom.Value!)
|
||||
Child = list = new MultiplayerPlaylist(room)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -127,7 +132,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
addItemStep();
|
||||
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
|
||||
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddUntilStep("item 0 not in lists", () => !inHistoryList(0) && !inQueueList(0));
|
||||
@@ -148,7 +153,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("finish current item", () => MultiplayerClient.FinishCurrentItem().WaitSafely());
|
||||
assertQueueTabCount(2);
|
||||
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
assertQueueTabCount(0);
|
||||
}
|
||||
@@ -157,12 +162,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
|
||||
{
|
||||
AddStep("leave room", () => RoomManager.PartRoom());
|
||||
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
|
||||
AddUntilStep("wait for room part", () => !RoomJoined);
|
||||
|
||||
AddStep("join room with items", () =>
|
||||
{
|
||||
RoomManager.CreateRoom(new Room
|
||||
API.Queue(new CreateRoomRequest(new Room
|
||||
{
|
||||
Name = "test name",
|
||||
Playlist =
|
||||
@@ -177,7 +182,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Expired = true
|
||||
}
|
||||
]
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
AddUntilStep("wait for room join", () => RoomJoined);
|
||||
|
||||
@@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
private BeatmapManager beatmaps = null!;
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapInfo importedBeatmap = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -42,9 +43,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create playlist", () =>
|
||||
{
|
||||
Child = playlist = new MultiplayerQueueList(SelectedRoom.Value!)
|
||||
Child = playlist = new MultiplayerQueueList(room)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
||||
@@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
private MultiplayerSpectateButton spectateButton = null!;
|
||||
private MatchStartControl startControl = null!;
|
||||
private Room room = null!;
|
||||
|
||||
private BeatmapSetInfo importedSet = null!;
|
||||
private BeatmapManager beatmaps = null!;
|
||||
@@ -46,11 +47,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create room", () => room = CreateDefaultRoom());
|
||||
AddStep("join room", () => JoinRoom(room));
|
||||
WaitForJoined();
|
||||
|
||||
AddStep("create button", () =>
|
||||
{
|
||||
PlaylistItem item = SelectedRoom.Value!.Playlist.First();
|
||||
|
||||
AvailabilityTracker.SelectedItem.Value = item;
|
||||
AvailabilityTracker.SelectedItem.Value = room.Playlist.First();
|
||||
|
||||
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
|
||||
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
|
||||
@@ -69,14 +72,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(200, 50),
|
||||
SelectedItem = new Bindable<PlaylistItem?>(item)
|
||||
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
|
||||
},
|
||||
startControl = new MatchStartControl
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(200, 50),
|
||||
SelectedItem = new Bindable<PlaylistItem?>(item)
|
||||
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
private BeatmapManager manager = null!;
|
||||
private TestPlaylistsSongSelect songSelect = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -51,13 +52,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("reset", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room();
|
||||
room = new Room();
|
||||
Ruleset.Value = new OsuRuleset().RulesetInfo;
|
||||
Beatmap.SetDefault();
|
||||
SelectedMods.Value = Array.Empty<Mod>();
|
||||
});
|
||||
|
||||
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(SelectedRoom.Value!)));
|
||||
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect(room)));
|
||||
AddUntilStep("wait for present", () => songSelect.IsCurrentScreen() && songSelect.BeatmapSetsLoaded);
|
||||
}
|
||||
|
||||
@@ -65,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public void TestItemAddedIfEmptyOnStart()
|
||||
{
|
||||
AddStep("finalise selection", () => songSelect.FinaliseSelection());
|
||||
AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
|
||||
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestItemAddedWhenCreateNewItemClicked()
|
||||
{
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
|
||||
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -80,7 +81,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddStep("finalise selection", () => songSelect.FinaliseSelection());
|
||||
AddAssert("playlist has 1 item", () => SelectedRoom.Value!.Playlist.Count == 1);
|
||||
AddAssert("playlist has 1 item", () => room.Playlist.Count == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -88,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddAssert("playlist has 2 items", () => SelectedRoom.Value!.Playlist.Count == 2);
|
||||
AddAssert("playlist has 2 items", () => room.Playlist.Count == 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -96,10 +97,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddStep("rearrange", () => SelectedRoom.Value!.Playlist = SelectedRoom.Value!.Playlist.Skip(1).Append(SelectedRoom.Value!.Playlist[0]).ToArray());
|
||||
AddStep("rearrange", () => room.Playlist = room.Playlist.Skip(1).Append(room.Playlist[0]).ToArray());
|
||||
|
||||
AddStep("create new item", () => songSelect.BeatmapDetails.CreateNewItem!());
|
||||
AddAssert("new item has id 2", () => SelectedRoom.Value!.Playlist.Last().ID == 2);
|
||||
AddAssert("new item has id 2", () => room.Playlist.Last().ID == 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -115,13 +116,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddAssert("item 1 has rate 1.5", () =>
|
||||
{
|
||||
var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
var mod = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
return Precision.AlmostEquals(1.5, mod.SpeedChange.Value);
|
||||
});
|
||||
|
||||
AddAssert("item 2 has rate 2", () =>
|
||||
{
|
||||
var mod = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
var mod = (OsuModDoubleTime)room.Playlist.Last().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
return Precision.AlmostEquals(2, mod.SpeedChange.Value);
|
||||
});
|
||||
}
|
||||
@@ -147,7 +148,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
|
||||
AddAssert("item has rate 1.5", () =>
|
||||
{
|
||||
var m = (OsuModDoubleTime)SelectedRoom.Value!.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
var m = (OsuModDoubleTime)room.Playlist.First().RequiredMods[0].ToMod(new OsuRuleset());
|
||||
return Precision.AlmostEquals(1.5, m.SpeedChange.Value);
|
||||
});
|
||||
}
|
||||
|
||||
+52
-47
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
@@ -17,11 +18,11 @@ using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
public partial class TestSceneLoungeRoomsContainer : OnlinePlayTestScene
|
||||
public partial class TestSceneRoomListing : OnlinePlayTestScene
|
||||
{
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private RoomsContainer container = null!;
|
||||
private BindableList<Room> rooms = null!;
|
||||
private IBindable<Room?> selectedRoom = null!;
|
||||
private RoomListing container = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
@@ -29,17 +30,20 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
|
||||
AddStep("create container", () =>
|
||||
{
|
||||
rooms = new BindableList<Room>();
|
||||
selectedRoom = new Bindable<Room?>();
|
||||
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Width = 0.5f,
|
||||
|
||||
Child = container = new RoomsContainer
|
||||
Child = container = new RoomListing
|
||||
{
|
||||
SelectedRoom = { BindTarget = SelectedRoom }
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Rooms = { BindTarget = rooms },
|
||||
SelectedRoom = { BindTarget = selectedRoom }
|
||||
}
|
||||
};
|
||||
});
|
||||
@@ -48,57 +52,58 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestBasicListChanges()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(5, withSpotlightRooms: true));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(5, withSpotlightRooms: true)));
|
||||
|
||||
AddAssert("has 5 rooms", () => container.Rooms.Count == 5);
|
||||
AddAssert("has 5 rooms", () => container.DrawableRooms.Count == 5);
|
||||
|
||||
AddAssert("all spotlights at top", () => container.Rooms
|
||||
AddAssert("all spotlights at top", () => container.DrawableRooms
|
||||
.SkipWhile(r => r.Room.Category == RoomCategory.Spotlight)
|
||||
.All(r => r.Room.Category == RoomCategory.Normal));
|
||||
|
||||
AddStep("remove first room", () => RoomManager.RemoveRoom(RoomManager.Rooms.First(r => r.RoomID == 0)));
|
||||
AddAssert("has 4 rooms", () => container.Rooms.Count == 4);
|
||||
AddAssert("first room removed", () => container.Rooms.All(r => r.Room.RoomID != 0));
|
||||
AddStep("remove first room", () => rooms.RemoveAt(0));
|
||||
AddAssert("has 4 rooms", () => container.DrawableRooms.Count == 4);
|
||||
AddAssert("first room removed", () => container.DrawableRooms.All(r => r.Room.RoomID != 0));
|
||||
|
||||
AddStep("select first room", () => container.Rooms.First().TriggerClick());
|
||||
AddAssert("first spotlight selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight)));
|
||||
AddStep("select first room", () => container.DrawableRooms.First().TriggerClick());
|
||||
AddAssert("first spotlight selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight)));
|
||||
|
||||
AddStep("remove last room", () => RoomManager.RemoveRoom(RoomManager.Rooms.MinBy(r => r.RoomID)!));
|
||||
AddAssert("first spotlight still selected", () => checkRoomSelected(RoomManager.Rooms.First(r => r.Category == RoomCategory.Spotlight)));
|
||||
AddStep("remove last room", () => rooms.RemoveAt(rooms.Count - 1));
|
||||
AddAssert("first spotlight still selected", () => checkRoomSelected(rooms.First(r => r.Category == RoomCategory.Spotlight)));
|
||||
|
||||
AddStep("remove spotlight room", () => RoomManager.RemoveRoom(RoomManager.Rooms.Single(r => r.Category == RoomCategory.Spotlight)));
|
||||
AddStep("remove spotlight room", () => rooms.RemoveAll(r => r.Category == RoomCategory.Spotlight));
|
||||
AddAssert("selection vacated", () => checkRoomSelected(null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardNavigation()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(3));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3)));
|
||||
|
||||
AddAssert("no selection", () => checkRoomSelected(null));
|
||||
|
||||
press(Key.Down);
|
||||
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
|
||||
AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
|
||||
|
||||
press(Key.Up);
|
||||
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
|
||||
AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
|
||||
|
||||
press(Key.Down);
|
||||
press(Key.Down);
|
||||
AddAssert("last room selected", () => checkRoomSelected(RoomManager.Rooms.Last()));
|
||||
AddAssert("last room selected", () => checkRoomSelected(container.DrawableRooms.Last().Room));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardNavigationAfterOrderChange()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(3));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3)));
|
||||
|
||||
AddStep("reorder rooms", () =>
|
||||
{
|
||||
var room = RoomManager.Rooms[1];
|
||||
var room = rooms[1];
|
||||
rooms.Remove(room);
|
||||
|
||||
RoomManager.RemoveRoom(room);
|
||||
RoomManager.AddOrUpdateRoom(room);
|
||||
room.RoomID += 3;
|
||||
rooms.Add(room);
|
||||
});
|
||||
|
||||
AddAssert("no selection", () => checkRoomSelected(null));
|
||||
@@ -116,12 +121,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestClickDeselection()
|
||||
{
|
||||
AddStep("add room", () => RoomManager.AddRooms(1));
|
||||
AddStep("add room", () => rooms.AddRange(GenerateRooms(1)));
|
||||
|
||||
AddAssert("no selection", () => checkRoomSelected(null));
|
||||
|
||||
press(Key.Down);
|
||||
AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First()));
|
||||
AddAssert("first room selected", () => checkRoomSelected(container.DrawableRooms.First().Room));
|
||||
|
||||
AddStep("click away", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("no selection", () => checkRoomSelected(null));
|
||||
@@ -135,34 +140,34 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
[Test]
|
||||
public void TestStringFiltering()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(4));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(4)));
|
||||
|
||||
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
|
||||
AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
|
||||
|
||||
AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = "1" });
|
||||
AddStep("filter one room", () => container.Filter.Value = new FilterCriteria { SearchString = rooms.First().Name });
|
||||
|
||||
AddUntilStep("1 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 1);
|
||||
AddUntilStep("1 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 1);
|
||||
|
||||
AddStep("remove filter", () => container.Filter.Value = null);
|
||||
|
||||
AddUntilStep("4 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 4);
|
||||
AddUntilStep("4 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 4);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetFiltering()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(2, new OsuRuleset().RulesetInfo));
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(3, new CatchRuleset().RulesetInfo));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(2, new OsuRuleset().RulesetInfo)));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, new CatchRuleset().RulesetInfo)));
|
||||
|
||||
// Todo: What even is this case...?
|
||||
AddStep("set empty filter criteria", () => container.Filter.Value = new FilterCriteria());
|
||||
AddUntilStep("5 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 5);
|
||||
AddUntilStep("5 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 5);
|
||||
|
||||
AddStep("filter osu! rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new OsuRuleset().RulesetInfo });
|
||||
AddUntilStep("2 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
|
||||
AddUntilStep("2 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter catch rooms", () => container.Filter.Value = new FilterCriteria { Ruleset = new CatchRuleset().RulesetInfo });
|
||||
AddUntilStep("3 rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 3);
|
||||
AddUntilStep("3 rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -170,30 +175,30 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
AddStep("add rooms", () =>
|
||||
{
|
||||
RoomManager.AddRooms(1, withPassword: true);
|
||||
RoomManager.AddRooms(1, withPassword: false);
|
||||
rooms.AddRange(GenerateRooms(1, withPassword: true));
|
||||
rooms.AddRange(GenerateRooms(1, withPassword: false));
|
||||
});
|
||||
|
||||
AddStep("apply default filter", () => container.Filter.SetDefault());
|
||||
|
||||
AddUntilStep("both rooms visible", () => container.Rooms.Count(r => r.IsPresent) == 2);
|
||||
AddUntilStep("both rooms visible", () => container.DrawableRooms.Count(r => r.IsPresent) == 2);
|
||||
|
||||
AddStep("filter public rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Public });
|
||||
|
||||
AddUntilStep("private room hidden", () => container.Rooms.All(r => !r.Room.HasPassword));
|
||||
AddUntilStep("private room hidden", () => container.DrawableRooms.All(r => !r.Room.HasPassword));
|
||||
|
||||
AddStep("filter private rooms", () => container.Filter.Value = new FilterCriteria { Permissions = RoomPermissionsFilter.Private });
|
||||
|
||||
AddUntilStep("public room hidden", () => container.Rooms.All(r => r.Room.HasPassword));
|
||||
AddUntilStep("public room hidden", () => container.DrawableRooms.All(r => r.Room.HasPassword));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPasswordProtectedRooms()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(3, withPassword: true));
|
||||
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true)));
|
||||
}
|
||||
|
||||
private bool checkRoomSelected(Room? room) => SelectedRoom.Value == room;
|
||||
private bool checkRoomSelected(Room? room) => selectedRoom.Value == room;
|
||||
|
||||
private Room? getRoomInFlow(int index) =>
|
||||
(container.ChildrenOfType<FillFlowContainer<DrawableLoungeRoom>>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
|
||||
@@ -115,6 +115,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
channelList.AddChannel(createRandomPrivateChannel());
|
||||
});
|
||||
|
||||
AddStep("Add Team Channel", () => channelList.AddChannel(createRandomTeamChannel()));
|
||||
|
||||
AddStep("Add Announce Channels", () =>
|
||||
{
|
||||
for (int i = 0; i < 2; i++)
|
||||
@@ -189,5 +191,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
|
||||
private Channel createRandomTeamChannel()
|
||||
{
|
||||
int id = TestResources.GetNextTestID();
|
||||
return new Channel
|
||||
{
|
||||
Name = $"Team {id}",
|
||||
Type = ChannelType.Team,
|
||||
Id = id,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -17,25 +16,22 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
public partial class TestScenePlaylistsLoungeSubScreen : OnlinePlayTestScene
|
||||
{
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private TestLoungeSubScreen loungeScreen = null!;
|
||||
private PlaylistsLoungeSubScreen loungeScreen = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("push screen", () => LoadScreen(loungeScreen = new TestLoungeSubScreen()));
|
||||
|
||||
AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen()));
|
||||
AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen());
|
||||
}
|
||||
|
||||
private RoomsContainer roomsContainer => loungeScreen.ChildrenOfType<RoomsContainer>().First();
|
||||
private RoomListing roomListing => loungeScreen.ChildrenOfType<RoomListing>().First();
|
||||
|
||||
[Test]
|
||||
public void TestManyRooms()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(500));
|
||||
createRooms(GenerateRooms(500));
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -43,59 +39,41 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
AddStep("reset mouse", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(30));
|
||||
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
|
||||
createRooms(GenerateRooms(30));
|
||||
|
||||
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
|
||||
|
||||
AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomsContainer.Rooms[2]));
|
||||
AddStep("move mouse to third room", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[2]));
|
||||
AddStep("hold down", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("drag to top", () => InputManager.MoveMouseTo(roomsContainer.Rooms[0]));
|
||||
AddStep("drag to top", () => InputManager.MoveMouseTo(roomListing.DrawableRooms[0]));
|
||||
|
||||
AddAssert("first and second room masked", ()
|
||||
=> !checkRoomVisible(roomsContainer.Rooms[0]) &&
|
||||
!checkRoomVisible(roomsContainer.Rooms[1]));
|
||||
=> !checkRoomVisible(roomListing.DrawableRooms[0]) &&
|
||||
!checkRoomVisible(roomListing.DrawableRooms[1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScrollSelectedIntoView()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(30));
|
||||
AddUntilStep("wait for rooms", () => roomsContainer.Rooms.Count == 30);
|
||||
createRooms(GenerateRooms(30));
|
||||
|
||||
AddUntilStep("first room is not masked", () => checkRoomVisible(roomsContainer.Rooms[0]));
|
||||
AddStep("select last room", () => roomListing.DrawableRooms[^1].TriggerClick());
|
||||
|
||||
AddStep("select last room", () => roomsContainer.Rooms[^1].TriggerClick());
|
||||
|
||||
AddUntilStep("first room is masked", () => !checkRoomVisible(roomsContainer.Rooms[0]));
|
||||
AddUntilStep("last room is not masked", () => checkRoomVisible(roomsContainer.Rooms[^1]));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEnteringRoomTakesLeaseOnSelection()
|
||||
{
|
||||
AddStep("add rooms", () => RoomManager.AddRooms(1));
|
||||
|
||||
AddAssert("selected room is not disabled", () => !loungeScreen.SelectedRoom.Disabled);
|
||||
|
||||
AddStep("select room", () => roomsContainer.Rooms[0].TriggerClick());
|
||||
AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null);
|
||||
|
||||
AddStep("enter room", () => roomsContainer.Rooms[0].TriggerClick());
|
||||
|
||||
AddUntilStep("wait for match load", () => Stack.CurrentScreen is PlaylistsRoomSubScreen);
|
||||
|
||||
AddAssert("selected room is non-null", () => loungeScreen.SelectedRoom.Value != null);
|
||||
AddAssert("selected room is disabled", () => loungeScreen.SelectedRoom.Disabled);
|
||||
AddUntilStep("first room is masked", () => !checkRoomVisible(roomListing.DrawableRooms[0]));
|
||||
AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1]));
|
||||
}
|
||||
|
||||
private bool checkRoomVisible(DrawableRoom room) =>
|
||||
loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
|
||||
.Contains(room.ScreenSpaceDrawQuad.Centre);
|
||||
|
||||
private partial class TestLoungeSubScreen : PlaylistsLoungeSubScreen
|
||||
private void createRooms(params Room[] rooms)
|
||||
{
|
||||
public new Bindable<Room?> SelectedRoom => base.SelectedRoom;
|
||||
AddStep("create rooms", () =>
|
||||
{
|
||||
foreach (var room in rooms)
|
||||
API.Queue(new CreateRoomRequest(room));
|
||||
});
|
||||
|
||||
AddStep("refresh lounge", () => loungeScreen.RefreshRooms());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
|
||||
using System;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Tests.Visual.OnlinePlay;
|
||||
|
||||
@@ -18,21 +17,38 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
public partial class TestScenePlaylistsMatchSettingsOverlay : OnlinePlayTestScene
|
||||
{
|
||||
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
|
||||
|
||||
private TestRoomSettings settings = null!;
|
||||
|
||||
protected override OnlinePlayTestSceneDependencies CreateOnlinePlayDependencies() => new TestDependencies();
|
||||
private Room room = null!;
|
||||
private Func<Room, string?>? handleRequest;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("setup api", () =>
|
||||
{
|
||||
handleRequest = null;
|
||||
((DummyAPIAccess)API).HandleRequest = req =>
|
||||
{
|
||||
if (req is not CreateRoomRequest createReq || handleRequest == null)
|
||||
return false;
|
||||
|
||||
if (handleRequest(createReq.Room) is string errorText)
|
||||
createReq.TriggerFailure(new APIException(errorText, null));
|
||||
else
|
||||
{
|
||||
var createdRoom = new APICreatedRoom();
|
||||
createdRoom.CopyFrom(createReq.Room);
|
||||
createReq.TriggerSuccess(createdRoom);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
||||
AddStep("create overlay", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room();
|
||||
|
||||
Child = settings = new TestRoomSettings(SelectedRoom.Value!)
|
||||
Child = settings = new TestRoomSettings(room = new Room())
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
State = { Value = Visibility.Visible }
|
||||
@@ -45,19 +61,19 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
AddStep("clear name and beatmap", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Name = "";
|
||||
SelectedRoom.Value!.Playlist = [];
|
||||
room.Name = "";
|
||||
room.Playlist = [];
|
||||
});
|
||||
|
||||
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
|
||||
|
||||
AddStep("set name", () => SelectedRoom.Value!.Name = "Room name");
|
||||
AddStep("set name", () => room.Name = "Room name");
|
||||
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
|
||||
|
||||
AddStep("set beatmap", () => SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]);
|
||||
AddStep("set beatmap", () => room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)]);
|
||||
AddAssert("button enabled", () => settings.ApplyButton.Enabled.Value);
|
||||
|
||||
AddStep("clear name", () => SelectedRoom.Value!.Name = "");
|
||||
AddStep("clear name", () => room.Name = "");
|
||||
AddAssert("button disabled", () => !settings.ApplyButton.Enabled.Value);
|
||||
}
|
||||
|
||||
@@ -73,12 +89,12 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
settings.NameField.Current.Value = expected_name;
|
||||
settings.DurationField.Current.Value = expectedDuration;
|
||||
SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
|
||||
room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
|
||||
|
||||
RoomManager.CreateRequested = r =>
|
||||
handleRequest = r =>
|
||||
{
|
||||
createdRoom = r;
|
||||
return string.Empty;
|
||||
return null;
|
||||
};
|
||||
});
|
||||
|
||||
@@ -98,22 +114,22 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
var beatmap = CreateBeatmap(Ruleset.Value).BeatmapInfo;
|
||||
|
||||
SelectedRoom.Value!.Name = "Test Room";
|
||||
SelectedRoom.Value!.Playlist = [new PlaylistItem(beatmap)];
|
||||
room.Name = "Test Room";
|
||||
room.Playlist = [new PlaylistItem(beatmap)];
|
||||
|
||||
errorMessage = $"{not_found_prefix} {beatmap.OnlineID}";
|
||||
|
||||
RoomManager.CreateRequested = _ => errorMessage;
|
||||
handleRequest = _ => errorMessage;
|
||||
});
|
||||
|
||||
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
|
||||
AddAssert("playlist item valid", () => SelectedRoom.Value!.Playlist[0].Valid.Value);
|
||||
AddAssert("playlist item valid", () => room.Playlist[0].Valid.Value);
|
||||
|
||||
AddStep("create room", () => settings.ApplyButton.Action.Invoke());
|
||||
|
||||
AddAssert("error displayed", () => settings.ErrorText.IsPresent);
|
||||
AddAssert("error has custom text", () => settings.ErrorText.Text != errorMessage);
|
||||
AddAssert("playlist item marked invalid", () => !SelectedRoom.Value!.Playlist[0].Valid.Value);
|
||||
AddAssert("playlist item marked invalid", () => !room.Playlist[0].Valid.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -125,10 +141,10 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
AddStep("setup", () =>
|
||||
{
|
||||
SelectedRoom.Value!.Name = "Test Room";
|
||||
SelectedRoom.Value!.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
|
||||
room.Name = "Test Room";
|
||||
room.Playlist = [new PlaylistItem(CreateBeatmap(Ruleset.Value).BeatmapInfo)];
|
||||
|
||||
RoomManager.CreateRequested = _ => failText;
|
||||
handleRequest = _ => failText;
|
||||
});
|
||||
AddAssert("error not displayed", () => !settings.ErrorText.IsPresent);
|
||||
|
||||
@@ -159,48 +175,5 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestDependencies : OnlinePlayTestSceneDependencies
|
||||
{
|
||||
protected override IRoomManager CreateRoomManager() => new TestRoomManager();
|
||||
}
|
||||
|
||||
protected class TestRoomManager : IRoomManager
|
||||
{
|
||||
public Func<Room, string>? CreateRequested;
|
||||
|
||||
public event Action RoomsUpdated
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public IBindable<bool> InitialRoomsReceived { get; } = new Bindable<bool>(true);
|
||||
|
||||
public IBindableList<Room> Rooms => null!;
|
||||
|
||||
public void AddOrUpdateRoom(Room room) => throw new NotImplementedException();
|
||||
|
||||
public void RemoveRoom(Room room) => throw new NotImplementedException();
|
||||
|
||||
public void ClearRooms() => throw new NotImplementedException();
|
||||
|
||||
public void CreateRoom(Room room, Action<Room>? onSuccess = null, Action<string>? onError = null)
|
||||
{
|
||||
if (CreateRequested == null)
|
||||
return;
|
||||
|
||||
string error = CreateRequested.Invoke(room);
|
||||
|
||||
if (!string.IsNullOrEmpty(error))
|
||||
onError?.Invoke(error);
|
||||
else
|
||||
onSuccess?.Invoke(room);
|
||||
}
|
||||
|
||||
public void JoinRoom(Room room, string? password, Action<Room>? onSuccess = null, Action<string>? onError = null) => throw new NotImplementedException();
|
||||
|
||||
public void PartRoom() => throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,13 +14,15 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
public partial class TestScenePlaylistsParticipantsList : OnlinePlayTestScene
|
||||
{
|
||||
private Room room = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
base.SetUpSteps();
|
||||
|
||||
AddStep("create list", () =>
|
||||
AddStep("create room", () =>
|
||||
{
|
||||
SelectedRoom.Value = new Room
|
||||
room = new Room
|
||||
{
|
||||
RoomID = 7,
|
||||
RecentParticipants = Enumerable.Range(0, 50).Select(_ => new APIUser
|
||||
@@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Horizontal)
|
||||
Child = new ParticipantsDisplay(room, Direction.Horizontal)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
@@ -52,7 +54,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
{
|
||||
AddStep("create component", () =>
|
||||
{
|
||||
Child = new ParticipantsDisplay(SelectedRoom.Value!, Direction.Vertical)
|
||||
Child = new ParticipantsDisplay(room, Direction.Vertical)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
|
||||
@@ -7,9 +7,12 @@ using System.Linq;
|
||||
using System.Net;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@@ -32,6 +35,9 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
private const int scores_per_result = 10;
|
||||
private const int real_user_position = 200;
|
||||
|
||||
[Cached]
|
||||
private readonly BeatmapLookupCache beatmapLookupCache = new BeatmapLookupCache();
|
||||
|
||||
private ResultsScreen resultsScreen = null!;
|
||||
|
||||
private int lowestScoreId; // Score ID of the lowest score in the list.
|
||||
@@ -41,6 +47,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
private int totalCount;
|
||||
private ScoreInfo userScore = null!;
|
||||
|
||||
public TestScenePlaylistsResultsScreen()
|
||||
{
|
||||
Add(beatmapLookupCache);
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
@@ -58,9 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
totalCount = 0;
|
||||
|
||||
userScore = TestResources.CreateTestScoreInfo();
|
||||
userScore.OnlineID = 1;
|
||||
userScore.TotalScore = 0;
|
||||
userScore.Statistics = new Dictionary<HitResult, int>();
|
||||
userScore.MaximumStatistics = new Dictionary<HitResult, int>();
|
||||
userScore.Position = real_user_position;
|
||||
|
||||
// Beatmap is required to be an actual beatmap so the scores can get their scores correctly
|
||||
// calculated for standardised scoring, else the tests that rely on ordering will fall over.
|
||||
@@ -143,13 +156,13 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
|
||||
AddAssert("right loading spinner shown", () =>
|
||||
AddUntilStep("right loading spinner shown", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
|
||||
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
||||
AddAssert("right loading spinner hidden", () =>
|
||||
AddUntilStep("right loading spinner hidden", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
@@ -167,26 +180,26 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
|
||||
AddAssert("right loading spinner shown", () =>
|
||||
AddUntilStep("right loading spinner shown", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
|
||||
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
||||
AddAssert("right loading spinner hidden", () =>
|
||||
AddUntilStep("right loading spinner hidden", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
|
||||
AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
|
||||
AddAssert("right loading spinner shown", () =>
|
||||
AddUntilStep("right loading spinner shown", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
|
||||
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
|
||||
AddAssert("right loading spinner hidden", () =>
|
||||
AddUntilStep("right loading spinner hidden", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
|
||||
|
||||
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
|
||||
@@ -209,13 +222,13 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
|
||||
|
||||
AddAssert("left loading spinner shown", () =>
|
||||
AddUntilStep("left loading spinner shown", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
|
||||
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
||||
AddAssert("left loading spinner hidden", () =>
|
||||
AddUntilStep("left loading spinner hidden", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
|
||||
}
|
||||
}
|
||||
@@ -229,7 +242,36 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("bind user score info handler", () => bindHandler(noScores: true));
|
||||
createUserBestResults();
|
||||
AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
|
||||
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
|
||||
AddUntilStep("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFetchingAllTheWayToFirstNeverDisplaysNegativePosition()
|
||||
{
|
||||
AddStep("set user position", () => userScore.Position = 20);
|
||||
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||
|
||||
createResultsWithScore(() => userScore);
|
||||
waitForDisplay();
|
||||
|
||||
AddStep("bind delayed handler", () => bindHandler(true));
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
AddStep("simulate user falling down ranking", () => userScore.Position += 2);
|
||||
AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
|
||||
|
||||
AddUntilStep("left loading spinner shown", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
|
||||
|
||||
waitForDisplay();
|
||||
|
||||
AddUntilStep("left loading spinner hidden", () =>
|
||||
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
AddAssert("total count is 34", () => this.ChildrenOfType<ScorePanel>().Count(), () => Is.EqualTo(34));
|
||||
AddUntilStep("all panels have non-negative position", () => this.ChildrenOfType<ScorePanel>().All(p => p.ScorePosition.Value > 0));
|
||||
}
|
||||
|
||||
private void createResultsWithScore(Func<ScoreInfo> getScore)
|
||||
@@ -279,6 +321,25 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
case IndexPlaylistScoresRequest:
|
||||
break;
|
||||
|
||||
case GetBeatmapsRequest getBeatmaps:
|
||||
getBeatmaps.TriggerSuccess(new GetBeatmapsResponse
|
||||
{
|
||||
Beatmaps = getBeatmaps.BeatmapIds.Select(id => new APIBeatmap
|
||||
{
|
||||
OnlineID = id,
|
||||
StarRating = id,
|
||||
DifficultyName = $"Beatmap {id}",
|
||||
BeatmapSet = new APIBeatmapSet
|
||||
{
|
||||
Title = $"Title {id}",
|
||||
Artist = $"Artist {id}",
|
||||
AuthorString = $"Author {id}"
|
||||
}
|
||||
}).ToList()
|
||||
});
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -301,7 +362,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
if (userScore == null)
|
||||
triggerFail(s);
|
||||
else
|
||||
triggerSuccess(s, createUserResponse(userScore));
|
||||
triggerSuccess(s, () => createUserResponse(userScore));
|
||||
|
||||
break;
|
||||
|
||||
@@ -309,12 +370,12 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
if (userScore == null)
|
||||
triggerFail(u);
|
||||
else
|
||||
triggerSuccess(u, createUserResponse(userScore));
|
||||
triggerSuccess(u, () => createUserResponse(userScore));
|
||||
|
||||
break;
|
||||
|
||||
case IndexPlaylistScoresRequest i:
|
||||
triggerSuccess(i, createIndexResponse(i, noScores));
|
||||
triggerSuccess(i, () => createIndexResponse(i, noScores));
|
||||
break;
|
||||
}
|
||||
}, delay);
|
||||
@@ -322,11 +383,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
return true;
|
||||
};
|
||||
|
||||
private void triggerSuccess<T>(APIRequest<T> req, T result)
|
||||
private void triggerSuccess<T>(APIRequest<T> req, Func<T> result)
|
||||
where T : class
|
||||
{
|
||||
requestComplete = true;
|
||||
req.TriggerSuccess(result);
|
||||
req.TriggerSuccess(result.Invoke());
|
||||
}
|
||||
|
||||
private void triggerFail(APIRequest req)
|
||||
@@ -337,33 +398,20 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
private MultiplayerScore createUserResponse(ScoreInfo userScore)
|
||||
{
|
||||
var multiplayerUserScore = new MultiplayerScore
|
||||
{
|
||||
ID = highestScoreId,
|
||||
Accuracy = userScore.Accuracy,
|
||||
Passed = userScore.Passed,
|
||||
Rank = userScore.Rank,
|
||||
Position = real_user_position,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
User = userScore.User,
|
||||
ScoresAround = new MultiplayerScoresAround
|
||||
{
|
||||
Higher = new MultiplayerScores(),
|
||||
Lower = new MultiplayerScores()
|
||||
}
|
||||
};
|
||||
var multiplayerUserScore = createMultiplayerUserScore(userScore);
|
||||
|
||||
totalCount++;
|
||||
|
||||
for (int i = 1; i <= scores_per_result; i++)
|
||||
{
|
||||
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
||||
multiplayerUserScore.ScoresAround!.Lower!.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = getNextLowestScoreId(),
|
||||
Accuracy = userScore.Accuracy,
|
||||
Passed = true,
|
||||
Rank = userScore.Rank,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
BeatmapId = RNG.Next(0, 7),
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2 + i,
|
||||
@@ -372,13 +420,14 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
},
|
||||
});
|
||||
|
||||
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
||||
multiplayerUserScore.ScoresAround!.Higher!.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = getNextHighestScoreId(),
|
||||
Accuracy = userScore.Accuracy,
|
||||
Passed = true,
|
||||
Rank = userScore.Rank,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
BeatmapId = RNG.Next(0, 7),
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2 + i,
|
||||
@@ -390,13 +439,33 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
totalCount += 2;
|
||||
}
|
||||
|
||||
addCursor(multiplayerUserScore.ScoresAround.Lower);
|
||||
addCursor(multiplayerUserScore.ScoresAround.Higher);
|
||||
addCursor(multiplayerUserScore.ScoresAround!.Lower!);
|
||||
addCursor(multiplayerUserScore.ScoresAround!.Higher!);
|
||||
|
||||
return multiplayerUserScore;
|
||||
}
|
||||
|
||||
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false)
|
||||
private MultiplayerScore createMultiplayerUserScore(ScoreInfo userScore)
|
||||
{
|
||||
return new MultiplayerScore
|
||||
{
|
||||
ID = highestScoreId,
|
||||
Accuracy = userScore.Accuracy,
|
||||
Passed = userScore.Passed,
|
||||
Rank = userScore.Rank,
|
||||
Position = userScore.Position,
|
||||
MaxCombo = userScore.MaxCombo,
|
||||
User = userScore.User,
|
||||
BeatmapId = RNG.Next(0, 7),
|
||||
ScoresAround = new MultiplayerScoresAround
|
||||
{
|
||||
Higher = new MultiplayerScores(),
|
||||
Lower = new MultiplayerScores()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores)
|
||||
{
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
@@ -404,15 +473,26 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
||||
|
||||
bool reachedEnd = false;
|
||||
|
||||
for (int i = 1; i <= scores_per_result; i++)
|
||||
{
|
||||
int nextId = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId();
|
||||
|
||||
if (userScore.OnlineID - nextId >= userScore.Position)
|
||||
{
|
||||
reachedEnd = true;
|
||||
break;
|
||||
}
|
||||
|
||||
result.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = sort == "score_asc" ? getNextHighestScoreId() : getNextLowestScoreId(),
|
||||
ID = nextId,
|
||||
Accuracy = 1,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.X,
|
||||
MaxCombo = 1000,
|
||||
BeatmapId = RNG.Next(0, 7),
|
||||
User = new APIUser
|
||||
{
|
||||
Id = 2 + i,
|
||||
@@ -424,7 +504,10 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
totalCount++;
|
||||
}
|
||||
|
||||
addCursor(result);
|
||||
if (!reachedEnd)
|
||||
addCursor(result);
|
||||
|
||||
result.UserScore = createMultiplayerUserScore(userScore);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
private BeatmapManager manager = null!;
|
||||
private TestPlaylistsRoomSubScreen match = null!;
|
||||
private BeatmapSetInfo importedBeatmap = null!;
|
||||
private Room room = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(GameHost host, AudioManager audio)
|
||||
@@ -47,11 +48,9 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[SetUpSteps]
|
||||
public void SetupSteps()
|
||||
{
|
||||
AddStep("set room", () => SelectedRoom.Value = new Room());
|
||||
|
||||
importBeatmap();
|
||||
|
||||
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(SelectedRoom.Value!)));
|
||||
AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(room = new Room())));
|
||||
AddUntilStep("wait for load", () => match.IsCurrentScreen());
|
||||
}
|
||||
|
||||
@@ -119,7 +118,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
];
|
||||
});
|
||||
|
||||
AddAssert("first playlist item selected", () => match.SelectedItem.Value == SelectedRoom.Value!.Playlist[0]);
|
||||
AddAssert("first playlist item selected", () => match.SelectedItem.Value == room.Playlist[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -197,10 +196,9 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddUntilStep("match has correct beatmap", () => realHash == match.Beatmap.Value.BeatmapInfo.MD5Hash);
|
||||
}
|
||||
|
||||
private void setupAndCreateRoom(Action<Room> room)
|
||||
private void setupAndCreateRoom(Action<Room> setupFunc)
|
||||
{
|
||||
AddStep("setup room", () => room(SelectedRoom.Value!));
|
||||
|
||||
AddStep("setup room", () => setupFunc(room));
|
||||
AddStep("click create button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<PlaylistsRoomSettingsOverlay.CreateRoomButton>().Single());
|
||||
|
||||
@@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
showPanel(TestResources.CreateTestScoreInfo(beatmap));
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
AddUntilStep("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps;
|
||||
@@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
showPanel(score);
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () =>
|
||||
AddUntilStep("pp display faded out", () =>
|
||||
{
|
||||
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods;
|
||||
@@ -116,7 +116,7 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
showPanel(score);
|
||||
});
|
||||
|
||||
AddAssert("pp display faded out", () => this.ChildrenOfType<PerformanceStatistic>().Single().Alpha == 1);
|
||||
AddUntilStep("pp display faded out", () => this.ChildrenOfType<PerformanceStatistic>().Single().Alpha == 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
@@ -17,7 +16,6 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -416,21 +414,19 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
protected override Task<ScoreInfo[]> FetchScores()
|
||||
{
|
||||
var scores = new List<ScoreInfo>();
|
||||
var scores = new ScoreInfo[20];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
for (int i = 0; i < scores.Length; i++)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo();
|
||||
score.TotalScore += 10 - i;
|
||||
score.HasOnlineReplay = true;
|
||||
scores.Add(score);
|
||||
scores[i] = score;
|
||||
}
|
||||
|
||||
scoresCallback.Invoke(scores);
|
||||
|
||||
return null;
|
||||
return Task.FromResult(scores);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -446,27 +442,25 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
this.fetchWaitTask = fetchWaitTask ?? Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
protected override Task<ScoreInfo[]> FetchScores()
|
||||
{
|
||||
Task.Run(async () =>
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
await fetchWaitTask;
|
||||
|
||||
var scores = new List<ScoreInfo>();
|
||||
var scores = new ScoreInfo[20];
|
||||
|
||||
for (int i = 0; i < 20; i++)
|
||||
for (int i = 0; i < scores.Length; i++)
|
||||
{
|
||||
var score = TestResources.CreateTestScoreInfo();
|
||||
score.TotalScore += 10 - i;
|
||||
scores.Add(score);
|
||||
scores[i] = score;
|
||||
}
|
||||
|
||||
scoresCallback?.Invoke(scores);
|
||||
|
||||
Schedule(() => FetchCompleted = true);
|
||||
});
|
||||
|
||||
return null;
|
||||
return scores;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Ranking
|
||||
{
|
||||
public partial class TestSceneUserTagControl : OsuTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("set up working beatmap", () =>
|
||||
{
|
||||
Beatmap.Value.BeatmapInfo.OnlineID = 42;
|
||||
});
|
||||
AddStep("set up network requests", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
{
|
||||
switch (request)
|
||||
{
|
||||
case ListTagsRequest listTagsRequest:
|
||||
{
|
||||
Scheduler.AddDelayed(() => listTagsRequest.TriggerSuccess(new APITagCollection
|
||||
{
|
||||
Tags =
|
||||
[
|
||||
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
|
||||
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
|
||||
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
|
||||
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
|
||||
]
|
||||
}), 500);
|
||||
return true;
|
||||
}
|
||||
|
||||
case GetBeatmapSetRequest getBeatmapSetRequest:
|
||||
{
|
||||
var beatmapSet = CreateAPIBeatmapSet(Beatmap.Value.BeatmapInfo);
|
||||
beatmapSet.Beatmaps.Single().TopTags =
|
||||
[
|
||||
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
|
||||
];
|
||||
Scheduler.AddDelayed(() => getBeatmapSetRequest.TriggerSuccess(beatmapSet), 500);
|
||||
return true;
|
||||
}
|
||||
|
||||
case AddBeatmapTagRequest:
|
||||
case RemoveBeatmapTagRequest:
|
||||
{
|
||||
Scheduler.AddDelayed(request.TriggerSuccess, 500);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("create control", () =>
|
||||
{
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new UserTagControl
|
||||
{
|
||||
Width = 500,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user