1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:07:52 +08:00

Merge branch 'master' into autoplay-rate-independence

This commit is contained in:
Dean Herbert 2021-02-11 17:39:45 +09:00
commit aaa0362b12
78 changed files with 1884 additions and 641 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.128.0" />
</ItemGroup>
</Project>

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public class CatchModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 1,
@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods
Value = 5,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get

View File

@ -0,0 +1,491 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Screens;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Replays;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneObjectOrderedHitPolicy : RateAdjustedBeatmapTestScene
{
private const double early_miss_window = 1000; // time after -1000 to -500 is considered a miss
private const double late_miss_window = 500; // time after +500 is considered a miss
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAtFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAfterFirstCircleTime()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Miss);
addJudgementOffsetAssert(hitObjects[0], late_miss_window);
}
/// <summary>
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
/// </summary>
[Test]
public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100
}
/// <summary>
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged.
/// </summary>
[Test]
public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged()
{
const double time_first_circle = 1500;
const double time_second_circle = 1600;
Vector2 positionFirstCircle = Vector2.Zero;
Vector2 positionSecondCircle = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle
},
new TestHitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
}
/// <summary>
/// Tests clicking a future circle after a slider's start time, but hitting all slider ticks.
/// </summary>
[Test]
public void TestMissSliderHeadAndHitAllSliderTicks()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
});
addJudgementAssert(hitObjects[0], HitResult.Miss);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
/// <summary>
/// Tests clicking hitting future slider ticks before a circle.
/// </summary>
[Test]
public void TestHitSliderTicksBeforeCircle()
{
const double time_slider = 1500;
const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(30);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_circle + late_miss_window - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
}
/// <summary>
/// Tests clicking a future circle before a spinner.
/// </summary>
[Test]
public void TestHitCircleBeforeSpinner()
{
const double time_spinner = 1500;
const double time_circle = 1800;
Vector2 positionCircle = Vector2.Zero;
var hitObjects = new List<OsuHitObject>
{
new TestSpinner
{
StartTime = time_spinner,
Position = new Vector2(256, 192),
EndTime = time_spinner + 1000,
},
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_spinner - 100, Position = positionCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_spinner + 10, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 20, Position = new Vector2(256, 172), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 30, Position = new Vector2(276, 192), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 40, Position = new Vector2(256, 212), Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_spinner + 50, Position = new Vector2(236, 192), Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
[Test]
public void TestHitSliderHeadBeforeHitCircle()
{
const double time_circle = 1000;
const double time_slider = 1200;
Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80);
var hitObjects = new List<OsuHitObject>
{
new TestHitCircle
{
StartTime = time_circle,
Position = positionCircle
},
new TestSlider
{
StartTime = time_slider,
Position = positionSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementAssert(hitObjects[1], HitResult.Great);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject).Type == result);
}
private void addJudgementAssert(string name, Func<OsuHitObject> hitObject, HitResult result)
{
AddAssert($"{name} judgement is {result}",
() => judgementResults.Single(r => r.HitObject == hitObject()).Type == result);
}
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
{
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
() => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100));
}
private ScoreAccessibleReplayPlayer currentPlayer;
private List<JudgementResult> judgementResults;
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames)
{
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
{
HitObjects = hitObjects,
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 },
Ruleset = new OsuRuleset().RulesetInfo
},
});
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
SelectedMods.Value = new[] { new OsuModClassic() };
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });
p.OnLoadComplete += _ =>
{
p.ScoreProcessor.NewJudgement += result =>
{
if (currentPlayer == p) judgementResults.Add(result);
};
};
LoadScreen(currentPlayer = p);
judgementResults = new List<JudgementResult>();
});
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
}
private class TestHitCircle : HitCircle
{
protected override HitWindows CreateHitWindows() => new TestHitWindows();
}
private class TestSlider : Slider
{
public TestSlider()
{
DefaultsApplied += _ =>
{
HeadCircle.HitWindows = new TestHitWindows();
TailCircle.HitWindows = new TestHitWindows();
HeadCircle.HitWindows.SetDifficulty(0);
TailCircle.HitWindows.SetDifficulty(0);
};
}
}
private class TestSpinner : Spinner
{
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = 1;
}
}
private class TestHitWindows : HitWindows
{
private static readonly DifficultyRange[] ranges =
{
new DifficultyRange(HitResult.Great, 500, 500, 500),
new DifficultyRange(HitResult.Miss, early_miss_window, early_miss_window, early_miss_window),
};
public override bool IsHitResultAllowed(HitResult result) => result == HitResult.Great || result == HitResult.Miss;
protected override DifficultyRange[] GetRanges() => ranges;
}
private class ScoreAccessibleReplayPlayer : ReplayPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
protected override bool PauseOnFocusLost => false;
public ScoreAccessibleReplayPlayer(Score score)
: base(score, new PlayerConfiguration
{
AllowPause = false,
ShowResults = false,
})
{
}
}
}
}

View File

@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
base.Update();
CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle);
CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle);
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.

View File

@ -0,0 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class SliderTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}

View File

@ -0,0 +1,86 @@
// 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.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Classic";
public override string Acronym => "CL";
public override double ScoreMultiplier => 1;
public override IconUsage? Icon => FontAwesome.Solid.History;
public override string Description => "Feeling nostalgic?";
public override bool Ranked => false;
public override ModType Type => ModType.Conversion;
[SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")]
public Bindable<bool> NoSliderHeadAccuracy { get; } = new BindableBool(true);
[SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")]
public Bindable<bool> NoSliderHeadMovement { get; } = new BindableBool(true);
[SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")]
public Bindable<bool> ClassicNoteLock { get; } = new BindableBool(true);
[SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")]
public Bindable<bool> FixedFollowCircleHitArea { get; } = new BindableBool(true);
public void ApplyToHitObject(HitObject hitObject)
{
switch (hitObject)
{
case Slider slider:
slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value;
foreach (var head in slider.NestedHitObjects.OfType<SliderHeadCircle>())
head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value;
break;
}
}
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
var osuRuleset = (DrawableOsuRuleset)drawableRuleset;
if (ClassicNoteLock.Value)
osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy();
}
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var obj in drawables)
{
switch (obj)
{
case DrawableSlider slider:
slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value;
break;
case DrawableSliderHead head:
head.TrackFollowCircle = !NoSliderHeadMovement.Value;
break;
}
}
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModDifficultyAdjust : ModDifficultyAdjust
{
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
public BindableNumber<float> CircleSize { get; } = new BindableFloat
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)]
public BindableNumber<float> ApproachRate { get; } = new BindableFloat
public BindableNumber<float> ApproachRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods
Value = 5,
};
protected override void ApplyLimits(bool extended)
{
base.ApplyLimits(extended);
CircleSize.MaxValue = extended ? 11 : 10;
ApproachRate.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get

View File

@ -110,8 +110,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections
double startTime = start.GetEndTime();
double duration = end.StartTime - startTime;
// Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject).
// Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN);
fadeOutTime = startTime + fraction * duration;
fadeInTime = fadeOutTime - PREEMPT;
fadeInTime = fadeOutTime - preempt;
}
}
}

View File

@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
}
var result = HitObject.HitWindows.ResultFor(timeOffset);
var result = ResultFor(timeOffset);
if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false)
{
@ -146,6 +146,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
});
}
/// <summary>
/// Retrieves the <see cref="HitResult"/> for a time offset.
/// </summary>
/// <param name="timeOffset">The time offset.</param>
/// <returns>The hit result, or <see cref="HitResult.None"/> if <paramref name="timeOffset"/> doesn't result in a judgement.</returns>
protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();

View File

@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (JudgedObject?.HitObject is OsuHitObject osuObject)
{
Position = osuObject.StackedPosition;
Position = osuObject.StackedEndPosition;
Scale = new Vector2(osuObject.Scale);
}
}

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public SliderBall Ball { get; private set; }
public SkinnableDrawable Body { get; private set; }
public override bool DisplayResult => false;
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody;
@ -249,7 +250,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (userTriggered || Time.Current < HitObject.EndTime)
return;
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
if (HitObject.OnlyJudgeNestedObjects)
{
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
return;
}
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r =>
{
int totalTicks = NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks)
r.Type = HitResult.Great;
else if (hitTicks == 0)
r.Type = HitResult.Miss;
else
{
double hitFraction = (double)hitTicks / totalTicks;
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
}
});
}
public override void PlaySamples()

View File

@ -7,16 +7,27 @@ using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderHead : DrawableHitCircle
{
public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject;
[CanBeNull]
public Slider Slider => DrawableSlider?.HitObject;
protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject;
public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult;
/// <summary>
/// Makes this <see cref="DrawableSliderHead"/> track the follow circle when the start time is reached.
/// If <c>false</c>, this <see cref="DrawableSliderHead"/> will be pinned to its initial position in the slider.
/// </summary>
public bool TrackFollowCircle = true;
private readonly IBindable<int> pathVersion = new Bindable<int>();
protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle;
@ -59,13 +70,29 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
base.Update();
Debug.Assert(Slider != null);
Debug.Assert(HitObject != null);
if (TrackFollowCircle)
{
double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1);
//todo: we probably want to reconsider this before adding scoring, but it looks and feels nice.
if (!IsHit)
Position = Slider.CurvePositionAt(completionProgress);
}
}
protected override HitResult ResultFor(double timeOffset)
{
Debug.Assert(HitObject != null);
if (HitObject.JudgeAsNormalHitCircle)
return base.ResultFor(timeOffset);
// If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring.
var result = base.ResultFor(timeOffset);
return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss;
}
public Action<double> OnShake;

View File

@ -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.Linq;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
@ -25,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
internal const float BASE_SCORING_DISTANCE = 100;
/// <summary>
/// Minimum preempt time at AR=10.
/// </summary>
public const double PREEMPT_MIN = 450;
public double TimePreempt = 600;
public double TimeFadeIn = 400;
@ -112,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
TimeFadeIn = 400; // as per osu-stable
TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN);
// Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR.
// This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above.
// Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good.
// This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in.
TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN);
Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2;
}

View File

@ -114,8 +114,14 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary>
public double TickDistanceMultiplier = 1;
/// <summary>
/// Whether this <see cref="Slider"/>'s judgement is fully handled by its nested <see cref="HitObject"/>s.
/// If <c>false</c>, this <see cref="Slider"/> will be judged proportionally to the number of nested <see cref="HitObject"/>s hit.
/// </summary>
public bool OnlyJudgeNestedObjects = true;
[JsonIgnore]
public HitCircle HeadCircle { get; protected set; }
public SliderHeadCircle HeadCircle { get; protected set; }
[JsonIgnore]
public SliderTailCircle TailCircle { get; protected set; }
@ -233,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects
HeadCircle.Samples = this.GetNodeSamples(0);
}
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}

View File

@ -1,9 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderHeadCircle : HitCircle
{
/// <summary>
/// Whether to treat this <see cref="SliderHeadCircle"/> as a normal <see cref="HitCircle"/> for judgement purposes.
/// If <c>false</c>, this <see cref="SliderHeadCircle"/> will be judged as a <see cref="SliderTick"/> instead.
/// </summary>
public bool JudgeAsNormalHitCircle = true;
public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement();
}
}

View File

@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTickJudgement();
public class SliderTickJudgement : OsuJudgement
{
public override HitResult MaxResult => HitResult.LargeTickHit;
}
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu
protected override bool Handle(UIEvent e)
{
if (e is MouseMoveEvent && !AllowUserCursorMovement) return false;
if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false;
return base.Handle(e);
}

View File

@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
new OsuModClassic()
};
case ModType.Automation:

View File

@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
[Resolved(CanBeNull = true)]
private OsuRulesetConfigManager config { get; set; }
private readonly Bindable<bool> configSnakingOut = new Bindable<bool>();
[BackgroundDependencyLoader]
private void load(ISkinSource skin, DrawableHitObject drawableObject)
{
@ -36,10 +38,28 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true);
config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut);
config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut);
SnakingOut.BindTo(configSnakingOut);
BorderSize = skin.GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1;
BorderColour = skin.GetConfig<OsuSkinColour, Color4>(OsuSkinColour.SliderBorder)?.Value ?? Color4.White;
drawableObject.HitObjectApplied += onHitObjectApplied;
}
private void onHitObjectApplied(DrawableHitObject obj)
{
var drawableSlider = (DrawableSlider)obj;
if (drawableSlider.HitObject == null)
return;
// When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way.
if (!drawableSlider.HeadCircle.TrackFollowCircle)
{
SnakingOut.UnbindFrom(configSnakingOut);
SnakingOut.Value = false;
}
}
private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour)

View File

@ -31,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
set => ball.Colour = value;
}
/// <summary>
/// Whether to track accurately to the visual size of this <see cref="SliderBall"/>.
/// If <c>false</c>, tracking will be performed at the final scale at all times.
/// </summary>
public bool InputTracksVisualSize = true;
private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider;
private readonly Drawable ball;
@ -94,7 +100,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
tracking = value;
if (InputTracksVisualSize)
followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint);
else
{
// We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration.
followCircle.ScaleTo(tracking ? 2.4f : 1f);
}
followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint);
}
}

View File

@ -0,0 +1,54 @@
// 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.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
/// <summary>
/// Ensures that <see cref="HitObject"/>s are hit in order of appearance. The classic note lock.
/// <remarks>
/// Hits will be blocked until the previous <see cref="HitObject"/>s have been judged.
/// </remarks>
/// </summary>
public class ObjectOrderedHitPolicy : IHitPolicy
{
public IHitObjectContainer HitObjectContainer { get; set; }
public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged);
public void HandleHit(DrawableHitObject hitObject)
{
}
private IEnumerable<DrawableHitObject> enumerateHitObjectsUpTo(double targetTime)
{
foreach (var obj in HitObjectContainer.AliveObjects)
{
if (obj.HitObject.StartTime >= targetTime)
yield break;
switch (obj)
{
case DrawableSpinner _:
continue;
case DrawableSlider slider:
yield return slider.HeadCircle;
break;
default:
yield return obj;
break;
}
}
}
}
}

View File

@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
private readonly StartTimeOrderedHitPolicy hitPolicy;
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -54,10 +53,9 @@ namespace osu.Game.Rulesets.Osu.UI
approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both },
};
hitPolicy = new StartTimeOrderedHitPolicy { HitObjectContainer = HitObjectContainer };
HitPolicy = new StartTimeOrderedHitPolicy();
var hitWindows = new OsuHitWindows();
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded));
@ -66,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.UI
NewResult += onNewResult;
}
private IHitPolicy hitPolicy;
public IHitPolicy HitPolicy
{
get => hitPolicy;
set
{
hitPolicy = value ?? throw new ArgumentNullException(nameof(value));
hitPolicy.HitObjectContainer = HitObjectContainer;
}
}
protected override void OnNewDrawableHitObject(DrawableHitObject drawable)
{
((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable;

View File

@ -68,12 +68,29 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
[Test]
public void TestDeserialiseDifficultyAdjustModWithExtendedLimits()
{
var apiMod = new APIMod(new TestModDifficultyAdjust
{
OverallDifficulty = { Value = 11 },
ExtendedLimits = { Value = true }
});
var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset());
Assert.That(converted.ExtendedLimits.Value, Is.True);
Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
{
new TestMod(),
new TestModTimeRamp(),
new TestModDifficultyAdjust()
};
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
@ -135,5 +152,9 @@ namespace osu.Game.Tests.Online
Value = true
};
}
private class TestModDifficultyAdjust : ModDifficultyAdjust
{
}
}
}

View File

@ -68,6 +68,16 @@ namespace osu.Game.Tests.Online
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
[Test]
public void TestDeserialiseEnumMod()
{
var apiMod = new APIMod(new TestModEnum { TestSetting = { Value = TestEnum.Value2 } });
var deserialized = MessagePackSerializer.Deserialize<APIMod>(MessagePackSerializer.Serialize(apiMod));
Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(1));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
@ -135,5 +145,22 @@ namespace osu.Game.Tests.Online
Value = true
};
}
private class TestModEnum : Mod
{
public override string Name => "Test Mod";
public override string Acronym => "TM";
public override double ScoreMultiplier => 1;
[SettingSource("Test")]
public Bindable<TestEnum> TestSetting { get; } = new Bindable<TestEnum>();
}
private enum TestEnum
{
Value1 = 0,
Value2 = 1,
Value3 = 2
}
}
}

View File

@ -7,7 +7,10 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
@ -19,16 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene
{
[SetUp]
public new void Setup() => Schedule(() =>
{
Child = new ParticipantsList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Size = new Vector2(380, 0.7f)
};
});
public new void Setup() => Schedule(createNewParticipantsList);
[Test]
public void TestAddUser()
@ -75,6 +69,50 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("single panel is for second user", () => this.ChildrenOfType<ParticipantPanel>().Single().User.User == secondUser);
}
[Test]
public void TestGameStateHasPriorityOverDownloadState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results));
checkProgressBarVisibility(false);
AddUntilStep("ready mark visible", () => this.ChildrenOfType<StateDisplay>().Single().IsPresent);
AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle));
checkProgressBarVisibility(true);
}
[Test]
public void TestCorrectInitialState()
{
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
AddStep("recreate list", createNewParticipantsList);
checkProgressBarVisibility(true);
}
[Test]
public void TestBeatmapDownloadingStates()
{
AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded()));
AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
checkProgressBarVisibility(true);
AddRepeatStep("increment progress", () =>
{
var progress = this.ChildrenOfType<ParticipantPanel>().Single().User.BeatmapAvailability.DownloadProgress ?? 0;
Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f)));
}, 25);
AddAssert("progress bar increased", () => this.ChildrenOfType<ProgressBar>().Single().Current.Value > 0);
AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing()));
checkProgressBarVisibility(false);
AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable()));
}
[Test]
public void TestToggleReadyState()
{
@ -122,6 +160,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1));
if (RNG.NextBool())
{
var beatmapState = (DownloadState)RNG.Next(0, (int)DownloadState.LocallyAvailable + 1);
switch (beatmapState)
{
case DownloadState.NotDownloaded:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded());
break;
case DownloadState.Downloading:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle()));
break;
case DownloadState.Importing:
Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing());
break;
}
}
}
});
}
@ -152,5 +210,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
}
}
private void createNewParticipantsList()
{
Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) };
}
private void checkProgressBarVisibility(bool visible) =>
AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () =>
this.ChildrenOfType<ProgressBar>().Single().IsPresent == visible);
}
}

View File

@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Ruleset.Value = new OsuRuleset().RulesetInfo;
Beatmap.SetDefault();
SelectedMods.Value = Array.Empty<Mod>();
});
AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect()));
@ -143,7 +144,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// Tests that the same <see cref="Mod"/> instances are not shared between two playlist items.
/// </summary>
[Test]
[Ignore("Temporarily disabled due to a non-trivial test failure")]
public void TestNewItemHasNewModInstances()
{
AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() });

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
private class TestFullscreenOverlay : FullscreenOverlay<OverlayHeader>
{
public TestFullscreenOverlay()
: base(OverlayColourScheme.Pink, null)
: base(OverlayColourScheme.Pink)
{
Children = new Drawable[]
{
@ -52,6 +52,17 @@ namespace osu.Game.Tests.Visual.Online
},
};
}
protected override OverlayHeader CreateHeader() => new TestHeader();
internal class TestHeader : OverlayHeader
{
protected override OverlayTitle CreateTitle() => new TestTitle();
internal class TestTitle : OverlayTitle
{
}
}
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online
Add(rankingsOverlay = new TestRankingsOverlay
{
Country = { BindTarget = countryBindable },
Scope = { BindTarget = scope },
Header = { Current = { BindTarget = scope } },
});
}
@ -65,8 +65,6 @@ namespace osu.Game.Tests.Visual.Online
private class TestRankingsOverlay : RankingsOverlay
{
public new Bindable<Country> Country => base.Country;
public new Bindable<RankingsScope> Scope => base.Scope;
}
}
}

View File

@ -39,7 +39,11 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[SetUp]
public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay()));
public void SetUp() => Schedule(() =>
{
SelectedMods.Value = Array.Empty<Mod>();
createDisplay(() => new TestModSelectOverlay());
});
[SetUpSteps]
public void SetUpSteps()
@ -47,6 +51,24 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("show", () => modSelect.Show());
}
[Test]
public void TestSettingsResetOnDeselection()
{
var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } };
changeRuleset(0);
AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; });
AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2);
AddStep("deselect", () => modSelect.DeselectAllButton.Click());
AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0);
AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click());
AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true);
}
[Test]
public void TestAnimationFlushOnClose()
{
@ -152,6 +174,45 @@ namespace osu.Game.Tests.Visual.UserInterface
});
}
[Test]
public void TestSettingsAreRetainedOnReload()
{
changeRuleset(0);
AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } });
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay()));
AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01);
}
[Test]
public void TestExternallySetModIsReplacedByOverlayInstance()
{
Mod external = new OsuModDoubleTime();
Mod overlayButtonMod = null;
changeRuleset(0);
AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; });
AddAssert("ensure button is selected", () =>
{
var button = modSelect.GetModButton(SelectedMods.Value.Single());
overlayButtonMod = button.SelectedMod;
return overlayButtonMod.GetType() == external.GetType();
});
// Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own
AddAssert("mod instance doesn't match", () => external != overlayButtonMod);
AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1);
AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod));
AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external));
}
[Test]
public void TestNonStacked()
{
@ -313,7 +374,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc)
{
SelectedMods.Value = Array.Empty<Mod>();
Children = new Drawable[]
{
modSelect = createOverlayFunc().With(d =>

View File

@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface
checkDisplayedCount(3);
}
[Test]
public void TestError()
{
setState(Visibility.Visible);
AddStep(@"error #1", sendErrorNotification);
AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible);
checkDisplayedCount(1);
}
[Test]
public void TestSpam()
{
@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void sendBarrage()
{
switch (RNG.Next(0, 4))
switch (RNG.Next(0, 5))
{
case 0:
sendHelloNotification();
@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface
case 3:
sendDownloadProgress();
break;
case 4:
sendErrorNotification();
break;
}
}
@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface
notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" });
}
private void sendErrorNotification()
{
notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" });
}
private void sendManyNotifications()
{
for (int i = 0; i < 10; i++)

View File

@ -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;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Configuration
{
/// <summary>
/// A helper class for tracking changes to the settings of a set of <see cref="Mod"/>s.
/// </summary>
/// <remarks>
/// Ensure to dispose when usage is finished.
/// </remarks>
public class ModSettingChangeTracker : IDisposable
{
/// <summary>
/// Notifies that the setting of a <see cref="Mod"/> has changed.
/// </summary>
public Action<Mod> SettingChanged;
private readonly List<ISettingsItem> settings = new List<ISettingsItem>();
/// <summary>
/// Creates a new <see cref="ModSettingChangeTracker"/> for a set of <see cref="Mod"/>s.
/// </summary>
/// <param name="mods">The set of <see cref="Mod"/>s whose settings need to be tracked.</param>
public ModSettingChangeTracker(IEnumerable<Mod> mods)
{
foreach (var mod in mods)
{
foreach (var setting in mod.CreateSettingsControls().OfType<ISettingsItem>())
{
setting.SettingChanged += () => SettingChanged?.Invoke(mod);
settings.Add(setting);
}
}
}
public void Dispose()
{
SettingChanged = null;
foreach (var r in settings)
r.Dispose();
settings.Clear();
}
}
}

View File

@ -20,6 +20,8 @@ namespace osu.Game.Graphics.Containers
{
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
protected virtual string PopInSampleName => "UI/overlay-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out";
protected override bool BlockNonPositionalInput => true;
@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader(true)]
private void load(AudioManager audio)
{
samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in");
samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out");
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override void LoadComplete()

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Framework.Utils;
namespace osu.Game.Graphics.UserInterface
{
@ -47,15 +48,20 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e)
{
if (sampleHover == null)
return false;
bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime;
if (enoughTimePassedSinceLastPlayback)
{
sampleHover?.Play();
sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08);
sampleHover.Play();
lastPlaybackTime.Value = Time.Current;
}
return base.OnHover(e);
return false;
}
}
@ -68,6 +74,9 @@ namespace osu.Game.Graphics.UserInterface
Normal,
[Description("-softer")]
Soft
Soft,
[Description("-toolbar")]
Toolbar
}
}

View File

@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface
RelativeSizeAxes = Axes.X,
},
Nub = new Nub(),
new HoverClickSounds()
new HoverSounds()
};
if (nubOnRight)

View File

@ -40,8 +40,19 @@ namespace osu.Game.Graphics.UserInterface
set => CurrentNumber.Value = value;
}
public ProgressBar()
private readonly bool allowSeek;
public override bool HandlePositionalInput => allowSeek;
public override bool HandleNonPositionalInput => allowSeek;
/// <summary>
/// Construct a new progress bar.
/// </summary>
/// <param name="allowSeek">Whether the user should be allowed to click/drag to adjust the value.</param>
public ProgressBar(bool allowSeek)
{
this.allowSeek = allowSeek;
CurrentNumber.MinValue = 0;
CurrentNumber.MaxValue = 1;
RelativeSizeAxes = Axes.X;

View File

@ -3,6 +3,7 @@
using System.Buffers;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using MessagePack;
using MessagePack.Formatters;
@ -41,6 +42,13 @@ namespace osu.Game.Online.API
primitiveFormatter.Serialize(ref writer, b.Value, options);
break;
case IBindable u:
// A mod with unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options);
break;
default:
// fall back for non-bindable cases.
primitiveFormatter.Serialize(ref writer, kvp.Value, options);

View File

@ -239,6 +239,7 @@ namespace osu.Game.Online.Multiplayer
newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted);
newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady);
newConnection.On<int, IEnumerable<APIMod>>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged);
newConnection.On<int, BeatmapAvailability>(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged);
newConnection.Closed += ex =>
{

View File

@ -5,6 +5,7 @@ using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Logging;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
namespace osu.Game.Online.Rooms
@ -24,19 +25,33 @@ namespace osu.Game.Online.Rooms
/// </summary>
public IBindable<BeatmapAvailability> Availability => availability;
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>();
private readonly Bindable<BeatmapAvailability> availability = new Bindable<BeatmapAvailability>(BeatmapAvailability.LocallyAvailable());
public OnlinePlayBeatmapAvailablilityTracker()
{
State.BindValueChanged(_ => updateAvailability());
Progress.BindValueChanged(_ => updateAvailability(), true);
}
private ScheduledDelegate progressUpdate;
protected override void LoadComplete()
{
base.LoadComplete();
SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true);
SelectedItem.BindValueChanged(item =>
{
// the underlying playlist is regularly cleared for maintenance purposes (things which probably need to be fixed eventually).
// to avoid exposing a state change when there may actually be none, ignore all nulls for now.
if (item.NewValue == null)
return;
Model.Value = item.NewValue.Beatmap.Value.BeatmapSet;
}, true);
Progress.BindValueChanged(_ =>
{
// incoming progress changes are going to be at a very high rate.
// we don't want to flood the network with this, so rate limit how often we send progress updates.
if (progressUpdate?.Completed != false)
progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500);
});
State.BindValueChanged(_ => updateAvailability(), true);
}
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)

View File

@ -778,7 +778,7 @@ namespace osu.Game
if (recentLogCount < short_term_display_limit)
{
Schedule(() => notifications.Post(new SimpleNotification
Schedule(() => notifications.Post(new SimpleErrorNotification
{
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),

View File

@ -98,7 +98,14 @@ namespace osu.Game
[Cached(typeof(IBindable<RulesetInfo>))]
protected readonly Bindable<RulesetInfo> Ruleset = new Bindable<RulesetInfo>();
// todo: move this to SongSelect once Screen has the ability to unsuspend.
/// <summary>
/// The current mod selection for the local user.
/// </summary>
/// <remarks>
/// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy.
/// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection.
/// As such, all settings should be finalised before adding a mod to this collection.
/// </remarks>
[Cached]
[Cached(typeof(IBindable<IReadOnlyList<Mod>>))]
protected readonly Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());

View File

@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
public DownloadProgressBar(BeatmapSetInfo beatmapSet)
: base(beatmapSet)
{
AddInternal(progressBar = new InteractionDisabledProgressBar
AddInternal(progressBar = new ProgressBar(false)
{
Height = 0,
Alpha = 0,
@ -64,11 +64,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
}
}, true);
}
private class InteractionDisabledProgressBar : ProgressBar
{
public override bool HandlePositionalInput => false;
public override bool HandleNonPositionalInput => false;
}
}
}

View File

@ -15,57 +15,40 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Overlays.BeatmapListing.Panels;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public class BeatmapListingOverlay : FullscreenOverlay<BeatmapListingHeader>
public class BeatmapListingOverlay : OnlineOverlay<BeatmapListingHeader>
{
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; }
private Drawable currentContent;
private LoadingLayer loadingLayer;
private Container panelTarget;
private FillFlowContainer<BeatmapPanel> foundContent;
private NotFoundDrawable notFoundContent;
private OverlayScrollContainer resultScrollContainer;
private BeatmapListingFilterControl filterControl;
public BeatmapListingOverlay()
: base(OverlayColourScheme.Blue, new BeatmapListingHeader())
: base(OverlayColourScheme.Blue)
{
}
private BeatmapListingFilterControl filterControl;
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
Child = new FillFlowContainer
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
},
resultScrollContainer = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
filterControl = new BeatmapListingFilterControl
{
TypingStarted = onTypingStarted,
@ -97,16 +80,17 @@ namespace osu.Game.Overlays
},
},
}
},
},
loadingLayer = new LoadingLayer(true)
};
}
protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
private void onTypingStarted()
{
// temporary until the textbox/header is updated to always stay on screen.
resultScrollContainer.ScrollToStart();
ScrollFlow.ScrollToStart();
}
protected override void OnFocus(FocusEvent e)
@ -125,7 +109,7 @@ namespace osu.Game.Overlays
previewTrackManager.StopAnyPlaying(this);
if (panelTarget.Any())
loadingLayer.Show();
Loading.Show();
}
private Task panelLoadDelegate;
@ -173,7 +157,7 @@ namespace osu.Game.Overlays
private void addContentToPlaceholder(Drawable content)
{
loadingLayer.Hide();
Loading.Hide();
lastFetchDisplayedTime = Time.Current;
if (content == currentContent)
@ -267,7 +251,7 @@ namespace osu.Game.Overlays
bool shouldShowMore = panelLoadDelegate?.IsCompleted != false
&& Time.Current - lastFetchDisplayedTime > time_between_fetches
&& (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance));
&& (ScrollFlow.ScrollableExtent > 0 && ScrollFlow.IsScrolledToEnd(pagination_scroll_distance));
if (shouldShowMore)
filterControl.FetchNextPage();

View File

@ -6,20 +6,19 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.BeatmapSet;
using osu.Game.Overlays.BeatmapSet.Scores;
using osu.Game.Overlays.Comments;
using osu.Game.Rulesets;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public class BeatmapSetOverlay : FullscreenOverlay<BeatmapSetHeader>
public class BeatmapSetOverlay : OnlineOverlay<BeatmapSetHeader>
{
public const float X_PADDING = 40;
public const float Y_PADDING = 25;
@ -33,55 +32,27 @@ namespace osu.Game.Overlays
// receive input outside our bounds so we can trigger a close event on ourselves.
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private readonly Box background;
public BeatmapSetOverlay()
: base(OverlayColourScheme.Blue, new BeatmapSetHeader())
: base(OverlayColourScheme.Blue)
{
OverlayScrollContainer scroll;
Info info;
CommentsSection comments;
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scroll = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<BeatmapSetLayoutSection>
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 20),
Children = new[]
{
new BeatmapSetLayoutSection
{
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
info = new Info()
}
},
},
info = new Info(),
new ScoresContainer
{
Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
},
comments = new CommentsSection()
},
},
},
}
};
Header.BeatmapSet.BindTo(beatmapSet);
@ -91,16 +62,13 @@ namespace osu.Game.Overlays
Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{
info.Beatmap = b.NewValue;
scroll.ScrollToStart();
ScrollFlow.ScrollToStart();
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = ColourProvider.Background6;
}
protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
protected override void PopOutComplete()
{

View File

@ -11,22 +11,18 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Input.Bindings;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Changelog;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
public class ChangelogOverlay : FullscreenOverlay<ChangelogHeader>
public class ChangelogOverlay : OnlineOverlay<ChangelogHeader>
{
public readonly Bindable<APIChangelogBuild> Current = new Bindable<APIChangelogBuild>();
private Container<ChangelogContent> content;
private SampleChannel sampleBack;
private List<APIChangelogBuild> builds;
@ -34,45 +30,14 @@ namespace osu.Game.Overlays
protected List<APIUpdateStream> Streams;
public ChangelogOverlay()
: base(OverlayColourScheme.Purple, new ChangelogHeader())
: base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background4,
},
new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h =>
{
h.ListingSelected = ShowListing;
h.Build.BindTarget = Current;
}),
content = new Container<ChangelogContent>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
},
},
},
};
Header.Build.BindTarget = Current;
sampleBack = audio.Samples.Get(@"UI/generic-select-soft");
@ -85,6 +50,13 @@ namespace osu.Game.Overlays
});
}
protected override ChangelogHeader CreateHeader() => new ChangelogHeader
{
ListingSelected = ShowListing,
};
protected override Color4 BackgroundColour => ColourProvider.Background4;
public void ShowListing()
{
Current.Value = null;
@ -198,16 +170,16 @@ namespace osu.Game.Overlays
private void loadContent(ChangelogContent newContent)
{
content.FadeTo(0.2f, 300, Easing.OutQuint);
Content.FadeTo(0.2f, 300, Easing.OutQuint);
loadContentCancellation?.Cancel();
LoadComponentAsync(newContent, c =>
{
content.FadeIn(300, Easing.OutQuint);
Content.FadeIn(300, Easing.OutQuint);
c.BuildSelected = ShowBuild;
content.Child = c;
Child = c;
}, (loadContentCancellation = new CancellationTokenSource()).Token);
}
}

View File

@ -2,155 +2,35 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Overlays.Dashboard;
using osu.Game.Overlays.Dashboard.Friends;
namespace osu.Game.Overlays
{
public class DashboardOverlay : FullscreenOverlay<DashboardOverlayHeader>
public class DashboardOverlay : TabbableOnlineOverlay<DashboardOverlayHeader, DashboardOverlayTabs>
{
private CancellationTokenSource cancellationToken;
private Container content;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public DashboardOverlay()
: base(OverlayColourScheme.Purple, new DashboardOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
})
: base(OverlayColourScheme.Purple)
{
}
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader();
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
protected override void CreateDisplayToLoad(DashboardOverlayTabs tab)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
loading = new LoadingLayer(true),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Header.Current.BindValueChanged(onTabChanged);
}
private bool displayUpdateRequired = true;
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
Header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
loadDisplay(Empty());
displayUpdateRequired = true;
}
private void loadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
loading.Hide();
content.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
private void onTabChanged(ValueChangedEvent<DashboardOverlayTabs> tab)
{
cancellationToken?.Cancel();
loading.Show();
if (!API.IsLoggedIn)
{
loadDisplay(Empty());
return;
}
switch (tab.NewValue)
switch (tab)
{
case DashboardOverlayTabs.Friends:
loadDisplay(new FriendDisplay());
LoadDisplay(new FriendDisplay());
break;
case DashboardOverlayTabs.CurrentlyPlaying:
loadDisplay(new CurrentlyPlayingDisplay());
LoadDisplay(new CurrentlyPlayingDisplay());
break;
default:
throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented");
}
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
if (State.Value == Visibility.Hidden)
return;
Header.Current.TriggerChange();
});
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
throw new NotImplementedException($"Display for {tab} tab is not implemented");
}
}
}
}

View File

@ -14,6 +14,9 @@ namespace osu.Game.Overlays
{
private readonly Container dialogContainer;
protected override string PopInSampleName => "UI/dialog-pop-in";
protected override string PopOutSampleName => "UI/dialog-pop-out";
public PopupDialog CurrentDialog { get; private set; }
public DialogOverlay()

View File

@ -1,11 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osuTK.Graphics;
@ -15,21 +17,27 @@ namespace osu.Game.Overlays
public abstract class FullscreenOverlay<T> : WaveOverlayContainer, INamedOverlayComponent
where T : OverlayHeader
{
public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty;
public virtual string Title => Header?.Title.Title ?? string.Empty;
public virtual string Description => Header?.Title.Description ?? string.Empty;
public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty;
public virtual string Title => Header.Title.Title ?? string.Empty;
public virtual string Description => Header.Title.Description ?? string.Empty;
public T Header { get; }
protected virtual Color4 BackgroundColour => ColourProvider.Background5;
[Resolved]
protected IAPIProvider API { get; private set; }
[Cached]
protected readonly OverlayColourProvider ColourProvider;
protected FullscreenOverlay(OverlayColourScheme colourScheme, T header)
protected override Container<Drawable> Content => content;
private readonly Container content;
protected FullscreenOverlay(OverlayColourScheme colourScheme)
{
Header = header;
Header = CreateHeader();
ColourProvider = new OverlayColourProvider(colourScheme);
@ -47,6 +55,19 @@ namespace osu.Game.Overlays
Type = EdgeEffectType.Shadow,
Radius = 10
};
base.Content.AddRange(new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = BackgroundColour
},
content = new Container
{
RelativeSizeAxes = Axes.Both
}
});
}
[BackgroundDependencyLoader]
@ -58,6 +79,9 @@ namespace osu.Game.Overlays
Waves.FourthWaveColour = ColourProvider.Dark3;
}
[NotNull]
protected abstract T CreateHeader();
public override void Show()
{
if (State.Value == Visibility.Visible)

View File

@ -46,8 +46,9 @@ namespace osu.Game.Overlays.Mods
/// Change the selected mod index of this button.
/// </summary>
/// <param name="newIndex">The new index.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns>
private bool changeSelectedIndex(int newIndex)
private bool changeSelectedIndex(int newIndex, bool resetSettings = true)
{
if (newIndex == selectedIndex) return false;
@ -69,6 +70,9 @@ namespace osu.Game.Overlays.Mods
Mod newSelection = SelectedMod ?? Mods[0];
if (resetSettings)
newSelection.ResetSettingsToDefaults();
Schedule(() =>
{
if (beforeSelected != Selected)
@ -209,11 +213,17 @@ namespace osu.Game.Overlays.Mods
Deselect();
}
public bool SelectAt(int index)
/// <summary>
/// Select the mod at the provided index.
/// </summary>
/// <param name="index">The index to select.</param>
/// <param name="resetSettings">Whether any settings applied to the mod should be reset on selection.</param>
/// <returns>Whether the selection changed.</returns>
public bool SelectAt(int index, bool resetSettings = true)
{
if (!Mods[index].HasImplementation) return false;
changeSelectedIndex(index);
changeSelectedIndex(index, resetSettings);
return true;
}

View File

@ -197,8 +197,11 @@ namespace osu.Game.Overlays.Mods
continue;
var buttonMod = button.Mods[index];
// as this is likely coming from an external change, ensure the settings of the mod are in sync.
buttonMod.CopyFrom(mod);
button.SelectAt(index);
button.SelectAt(index, false);
return;
}

View File

@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods
/// </summary>
protected virtual bool Stacked => true;
/// <summary>
/// Whether configurable <see cref="Mod"/>s can be configured by the local user.
/// </summary>
protected virtual bool AllowConfiguration => true;
[NotNull]
private Func<Mod, bool> isValidMod = m => true;
@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods
Text = "Customisation",
Action = () => ModSettingsContainer.ToggleVisibility(),
Enabled = { Value = false },
Alpha = AllowConfiguration ? 1 : 0,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
},
@ -372,7 +378,10 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete();
availableMods.BindValueChanged(_ => updateAvailableMods(), true);
SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true);
// intentionally bound after the above line to avoid a potential update feedback cycle.
// i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible.
SelectedMods.BindValueChanged(_ => updateSelectedButtons());
}
protected override void PopOut()
@ -479,10 +488,10 @@ namespace osu.Game.Overlays.Mods
foreach (var section in ModSectionsContainer.Children)
section.UpdateSelectedButtons(selectedMods);
updateMods();
updateMultiplier();
}
private void updateMods()
private void updateMultiplier()
{
var multiplier = 1.0;
@ -509,7 +518,8 @@ namespace osu.Game.Overlays.Mods
OnModSelected(selectedMod);
if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show();
if (selectedMod.RequiresConfiguration && AllowConfiguration)
ModSettingsContainer.Show();
}
else
{

View File

@ -2,67 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.News;
using osu.Game.Overlays.News.Displays;
namespace osu.Game.Overlays
{
public class NewsOverlay : FullscreenOverlay<NewsHeader>
public class NewsOverlay : OnlineOverlay<NewsHeader>
{
private readonly Bindable<string> article = new Bindable<string>(null);
private Container content;
private LoadingLayer loading;
private OverlayScrollContainer scrollFlow;
public NewsOverlay()
: base(OverlayColourScheme.Purple, new NewsHeader())
: base(OverlayColourScheme.Purple)
{
}
[BackgroundDependencyLoader]
private void load()
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background5,
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h =>
{
h.ShowFrontPage = ShowFrontPage;
}),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
},
},
},
loading = new LoadingLayer(true),
};
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -71,6 +26,11 @@ namespace osu.Game.Overlays
article.BindValueChanged(onArticleChanged);
}
protected override NewsHeader CreateHeader() => new NewsHeader
{
ShowFrontPage = ShowFrontPage
};
private bool displayUpdateRequired = true;
protected override void PopIn()
@ -107,7 +67,7 @@ namespace osu.Game.Overlays
private void onArticleChanged(ValueChangedEvent<string> e)
{
cancellationToken?.Cancel();
loading.Show();
Loading.Show();
if (e.NewValue == null)
{
@ -122,11 +82,11 @@ namespace osu.Game.Overlays
protected void LoadDisplay(Drawable display)
{
scrollFlow.ScrollToStart();
ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
content.Child = loaded;
loading.Hide();
Child = loaded;
Loading.Hide();
}, (cancellationToken = new CancellationTokenSource()).Token);
}

View File

@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications
/// </summary>
public virtual bool DisplayOnTop => true;
private SampleChannel samplePopIn;
private SampleChannel samplePopOut;
protected virtual string PopInSampleName => "UI/notification-pop-in";
protected virtual string PopOutSampleName => "UI/overlay-pop-out"; // TODO: replace with a unique sample?
protected NotificationLight Light;
private readonly CloseButton closeButton;
protected Container IconContent;
@ -107,7 +114,7 @@ namespace osu.Game.Overlays.Notifications
closeButton = new CloseButton
{
Alpha = 0,
Action = Close,
Action = () => Close(),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding
@ -120,6 +127,13 @@ namespace osu.Game.Overlays.Notifications
});
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
samplePopIn = audio.Samples.Get(PopInSampleName);
samplePopOut = audio.Samples.Get(PopOutSampleName);
}
protected override bool OnHover(HoverEvent e)
{
closeButton.FadeIn(75);
@ -143,6 +157,9 @@ namespace osu.Game.Overlays.Notifications
protected override void LoadComplete()
{
base.LoadComplete();
samplePopIn?.Play();
this.FadeInFromZero(200);
NotificationContent.MoveToX(DrawSize.X);
NotificationContent.MoveToX(0, 500, Easing.OutQuint);
@ -150,12 +167,15 @@ namespace osu.Game.Overlays.Notifications
public bool WasClosed;
public virtual void Close()
public virtual void Close(bool playSound = true)
{
if (WasClosed) return;
WasClosed = true;
if (playSound)
samplePopOut?.Play();
Closed?.Invoke();
this.FadeOut(100);
Expire();

View File

@ -109,7 +109,12 @@ namespace osu.Game.Overlays.Notifications
private void clearAll()
{
notifications.Children.ForEach(c => c.Close());
bool first = true;
notifications.Children.ForEach(c =>
{
c.Close(first);
first = false;
});
}
protected override void Update()

View File

@ -150,12 +150,12 @@ namespace osu.Game.Overlays.Notifications
colourCancelled = colours.Red;
}
public override void Close()
public override void Close(bool playSound = true)
{
switch (State)
{
case ProgressNotificationState.Cancelled:
base.Close();
base.Close(playSound);
break;
case ProgressNotificationState.Active:

View File

@ -0,0 +1,17 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Notifications
{
public class SimpleErrorNotification : SimpleNotification
{
protected override string PopInSampleName => "UI/error-notification-pop-in";
public SimpleErrorNotification()
{
Icon = FontAwesome.Solid.Bomb;
}
}
}

View File

@ -51,6 +51,9 @@ namespace osu.Game.Overlays
private Container dragContainer;
private Container playerContainer;
protected override string PopInSampleName => "UI/now-playing-pop-in";
protected override string PopOutSampleName => "UI/now-playing-pop-out";
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>
@ -84,11 +87,6 @@ namespace osu.Game.Overlays
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
playlist = new PlaylistOverlay
{
RelativeSizeAxes = Axes.X,
Y = player_height + 10,
},
playerContainer = new Container
{
RelativeSizeAxes = Axes.X,
@ -171,7 +169,7 @@ namespace osu.Game.Overlays
Anchor = Anchor.CentreRight,
Position = new Vector2(-bottom_black_area_height / 2, 0),
Icon = FontAwesome.Solid.Bars,
Action = () => playlist.ToggleVisibility(),
Action = togglePlaylist
},
}
},
@ -191,13 +189,35 @@ namespace osu.Game.Overlays
};
}
protected override void LoadComplete()
private void togglePlaylist()
{
base.LoadComplete();
if (playlist == null)
{
LoadComponentAsync(playlist = new PlaylistOverlay
{
RelativeSizeAxes = Axes.X,
Y = player_height + 10,
}, _ =>
{
dragContainer.Add(playlist);
playlist.BeatmapSets.BindTo(musicController.BeatmapSets);
playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true);
togglePlaylist();
});
return;
}
if (!beatmap.Disabled)
playlist.ToggleVisibility();
}
protected override void LoadComplete()
{
base.LoadComplete();
beatmap.BindDisabledChanged(beatmapDisabledChanged, true);
musicController.TrackChanged += trackChanged;
@ -306,7 +326,7 @@ namespace osu.Game.Overlays
private void beatmapDisabledChanged(bool disabled)
{
if (disabled)
playlist.Hide();
playlist?.Hide();
prevButton.Enabled.Value = !disabled;
nextButton.Enabled.Value = !disabled;
@ -411,6 +431,11 @@ namespace osu.Game.Overlays
private class HoverableProgressBar : ProgressBar
{
public HoverableProgressBar()
: base(true)
{
}
protected override bool OnHover(HoverEvent e)
{
this.ResizeHeightTo(progress_height, 500, Easing.OutQuint);

View File

@ -3,6 +3,8 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -19,6 +21,13 @@ namespace osu.Game.Overlays.OSD
{
private const int lights_bottom_margin = 40;
private readonly int optionCount;
private readonly int selectedOption = -1;
private SampleChannel sampleOn;
private SampleChannel sampleOff;
private SampleChannel sampleChange;
public TrackedSettingToast(SettingDescription description)
: base(description.Name, description.Value, description.Shortcut)
{
@ -46,9 +55,6 @@ namespace osu.Game.Overlays.OSD
}
};
int optionCount = 0;
int selectedOption = -1;
switch (description.RawValue)
{
case bool val:
@ -69,6 +75,34 @@ namespace osu.Game.Overlays.OSD
optionLights.Add(new OptionLight { Glowing = i == selectedOption });
}
protected override void LoadComplete()
{
base.LoadComplete();
if (optionCount == 1)
{
if (selectedOption == 0)
sampleOn?.Play();
else
sampleOff?.Play();
}
else
{
if (sampleChange == null) return;
sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f;
sampleChange.Play();
}
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
sampleOn = audio.Samples.Get("UI/osd-on");
sampleOff = audio.Samples.Get("UI/osd-off");
sampleChange = audio.Samples.Get("UI/osd-change");
}
private class OptionLight : Container
{
private Color4 glowingColour, idleColour;

View File

@ -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.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays
{
public abstract class OnlineOverlay<T> : FullscreenOverlay<T>
where T : OverlayHeader
{
protected override Container<Drawable> Content => content;
protected readonly OverlayScrollContainer ScrollFlow;
protected readonly LoadingLayer Loading;
private readonly Container content;
protected OnlineOverlay(OverlayColourScheme colourScheme)
: base(colourScheme)
{
base.Content.AddRange(new Drawable[]
{
ScrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header.With(h => h.Depth = float.MinValue),
content = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
}
},
Loading = new LoadingLayer(true)
});
}
}
}

View File

@ -4,96 +4,32 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays.Rankings;
using osu.Game.Users;
using osu.Game.Rulesets;
using osu.Game.Online.API;
using System.Threading;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Rankings.Tables;
namespace osu.Game.Overlays
{
public class RankingsOverlay : FullscreenOverlay<RankingsOverlayHeader>
public class RankingsOverlay : TabbableOnlineOverlay<RankingsOverlayHeader, RankingsScope>
{
protected Bindable<Country> Country => Header.Country;
protected Bindable<RankingsScope> Scope => Header.Current;
private readonly OverlayScrollContainer scrollFlow;
private readonly Container contentContainer;
private readonly LoadingLayer loading;
private readonly Box background;
private APIRequest lastRequest;
private CancellationTokenSource cancellationToken;
[Resolved]
private IAPIProvider api { get; set; }
public RankingsOverlay()
: base(OverlayColourScheme.Green, new RankingsOverlayHeader
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Depth = -float.MaxValue
})
{
loading = new LoadingLayer(true);
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
scrollFlow = new OverlayScrollContainer
{
RelativeSizeAxes = Axes.Both,
ScrollbarVisible = false,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Header,
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
contentContainer = new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Bottom = 10 }
},
}
}
}
}
},
loading
};
}
[BackgroundDependencyLoader]
private void load()
{
background.Colour = ColourProvider.Background5;
}
[Resolved]
private Bindable<RulesetInfo> ruleset { get; set; }
public RankingsOverlay()
: base(OverlayColourScheme.Green)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
@ -104,31 +40,33 @@ namespace osu.Game.Overlays
{
// if a country is requested, force performance scope.
if (Country.Value != null)
Scope.Value = RankingsScope.Performance;
Header.Current.Value = RankingsScope.Performance;
Scheduler.AddOnce(loadNewContent);
});
Scope.BindValueChanged(_ =>
{
// country filtering is only valid for performance scope.
if (Scope.Value != RankingsScope.Performance)
Country.Value = null;
Scheduler.AddOnce(loadNewContent);
Scheduler.AddOnce(triggerTabChanged);
});
ruleset.BindValueChanged(_ =>
{
if (Scope.Value == RankingsScope.Spotlights)
if (Header.Current.Value == RankingsScope.Spotlights)
return;
Scheduler.AddOnce(loadNewContent);
Scheduler.AddOnce(triggerTabChanged);
});
Scheduler.AddOnce(loadNewContent);
}
protected override void OnTabChanged(RankingsScope tab)
{
// country filtering is only valid for performance scope.
if (Header.Current.Value != RankingsScope.Performance)
Country.Value = null;
Scheduler.AddOnce(triggerTabChanged);
}
private void triggerTabChanged() => base.OnTabChanged(Header.Current.Value);
protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader();
public void ShowCountry(Country requested)
{
if (requested == null)
@ -139,22 +77,13 @@ namespace osu.Game.Overlays
Country.Value = requested;
}
public void ShowSpotlights()
protected override void CreateDisplayToLoad(RankingsScope tab)
{
Scope.Value = RankingsScope.Spotlights;
Show();
}
private void loadNewContent()
{
loading.Show();
cancellationToken?.Cancel();
lastRequest?.Cancel();
if (Scope.Value == RankingsScope.Spotlights)
if (Header.Current.Value == RankingsScope.Spotlights)
{
loadContent(new SpotlightsLayout
LoadDisplay(new SpotlightsLayout
{
Ruleset = { BindTarget = ruleset }
});
@ -166,19 +95,19 @@ namespace osu.Game.Overlays
if (request == null)
{
loadContent(null);
LoadDisplay(Empty());
return;
}
request.Success += () => Schedule(() => loadContent(createTableFromResponse(request)));
request.Failure += _ => Schedule(() => loadContent(null));
request.Success += () => Schedule(() => LoadDisplay(createTableFromResponse(request)));
request.Failure += _ => Schedule(() => LoadDisplay(Empty()));
api.Queue(request);
}
private APIRequest createScopedRequest()
{
switch (Scope.Value)
switch (Header.Current.Value)
{
case RankingsScope.Performance:
return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName);
@ -216,29 +145,9 @@ namespace osu.Game.Overlays
return null;
}
private void loadContent(Drawable content)
{
scrollFlow.ScrollToStart();
if (content == null)
{
contentContainer.Clear();
loading.Hide();
return;
}
LoadComponentAsync(content, loaded =>
{
loading.Hide();
contentContainer.Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected override void Dispose(bool isDisposing)
{
lastRequest?.Cancel();
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}

View File

@ -40,6 +40,8 @@ namespace osu.Game.Overlays
private SeekLimitedSearchTextBox searchTextBox;
protected override string PopInSampleName => "UI/settings-pop-in";
/// <summary>
/// Provide a source for the toolbar height.
/// </summary>

View File

@ -0,0 +1,101 @@
// 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.Threading;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API;
namespace osu.Game.Overlays
{
public abstract class TabbableOnlineOverlay<THeader, TEnum> : OnlineOverlay<THeader>
where THeader : TabControlOverlayHeader<TEnum>
{
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private CancellationTokenSource cancellationToken;
private bool displayUpdateRequired = true;
protected TabbableOnlineOverlay(OverlayColourScheme colourScheme)
: base(colourScheme)
{
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api)
{
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
}
protected override void LoadComplete()
{
base.LoadComplete();
Header.Current.BindValueChanged(tab => OnTabChanged(tab.NewValue));
}
protected override void PopIn()
{
base.PopIn();
// We don't want to create a new display on every call, only when exiting from fully closed state.
if (displayUpdateRequired)
{
Header.Current.TriggerChange();
displayUpdateRequired = false;
}
}
protected override void PopOutComplete()
{
base.PopOutComplete();
LoadDisplay(Empty());
displayUpdateRequired = true;
}
protected void LoadDisplay(Drawable display)
{
ScrollFlow.ScrollToStart();
LoadComponentAsync(display, loaded =>
{
if (API.IsLoggedIn)
Loading.Hide();
Child = loaded;
}, (cancellationToken = new CancellationTokenSource()).Token);
}
protected virtual void OnTabChanged(TEnum tab)
{
cancellationToken?.Cancel();
Loading.Show();
if (!API.IsLoggedIn)
{
LoadDisplay(Empty());
return;
}
CreateDisplayToLoad(tab);
}
protected abstract void CreateDisplayToLoad(TEnum tab);
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
{
if (State.Value == Visibility.Hidden)
return;
Header.Current.TriggerChange();
});
protected override void Dispose(bool isDisposing)
{
cancellationToken?.Cancel();
base.Dispose(isDisposing);
}
}
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar
private KeyBindingStore keyBindings { get; set; }
protected ToolbarButton()
: base(HoverSampleSet.Loud)
: base(HoverSampleSet.Toolbar)
{
Width = Toolbar.HEIGHT;
RelativeSizeAxes = Axes.Y;

View File

@ -15,6 +15,7 @@ using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Sections;
using osu.Game.Users;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays
{
@ -29,10 +30,14 @@ namespace osu.Game.Overlays
public const float CONTENT_X_MARGIN = 70;
public UserProfileOverlay()
: base(OverlayColourScheme.Pink, new ProfileHeader())
: base(OverlayColourScheme.Pink)
{
}
protected override ProfileHeader CreateHeader() => new ProfileHeader();
protected override Color4 BackgroundColour => ColourProvider.Background6;
public void ShowUser(int userId) => ShowUser(new User { Id = userId });
public void ShowUser(User user, bool fetchOnline = true)
@ -72,12 +77,6 @@ namespace osu.Game.Overlays
Origin = Anchor.TopCentre,
};
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider.Background6
});
Add(sectionsContainer = new ProfileSectionsContainer
{
ExpandableHeader = Header,

View File

@ -18,6 +18,8 @@ namespace osu.Game.Overlays
protected override bool StartHidden => true;
protected override string PopInSampleName => "UI/wave-pop-in";
protected WaveOverlayContainer()
{
AddInternal(Waves = new WaveContainer

View File

@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mods
}
/// <summary>
/// Copies mod setting values from <paramref name="source"/> into this instance.
/// Copies mod setting values from <paramref name="source"/> into this instance, overwriting all existing settings.
/// </summary>
/// <param name="source">The mod to copy properties from.</param>
public void CopyFrom(Mod source)
@ -147,8 +147,6 @@ namespace osu.Game.Rulesets.Mods
var targetBindable = (IBindable)prop.GetValue(this);
var sourceBindable = (IBindable)prop.GetValue(source);
// we only care about changes that have been made away from defaults.
if (!sourceBindable.IsDefault)
CopyAdjustedSetting(targetBindable, sourceBindable);
}
}
@ -175,5 +173,10 @@ namespace osu.Game.Rulesets.Mods
}
public bool Equals(IMod other) => GetType() == other?.GetType();
/// <summary>
/// Reset all custom settings for this mod back to their defaults.
/// </summary>
public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType()));
}
}

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
protected const int LAST_SETTING_ORDER = 2;
[SettingSource("HP Drain", "Override a beatmap's set HP.", FIRST_SETTING_ORDER)]
public BindableNumber<float> DrainRate { get; } = new BindableFloat
public BindableNumber<float> DrainRate { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods
};
[SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)]
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloat
public BindableNumber<float> OverallDifficulty { get; } = new BindableFloatWithLimitExtension
{
Precision = 0.1f,
MinValue = 0,
@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods
Value = 5,
};
[SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")]
public BindableBool ExtendedLimits { get; } = new BindableBool();
protected ModDifficultyAdjust()
{
ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue));
}
/// <summary>
/// Changes the difficulty adjustment limits. Occurs when the value of <see cref="ExtendedLimits"/> is changed.
/// </summary>
/// <param name="extended">Whether limits should extend beyond sane ranges.</param>
protected virtual void ApplyLimits(bool extended)
{
DrainRate.MaxValue = extended ? 11 : 10;
OverallDifficulty.MaxValue = extended ? 11 : 10;
}
public override string SettingDescription
{
get
@ -141,5 +159,73 @@ namespace osu.Game.Rulesets.Mods
ApplySetting(DrainRate, dr => difficulty.DrainRate = dr);
ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od);
}
public override void ResetSettingsToDefaults()
{
base.ResetSettingsToDefaults();
if (difficulty != null)
{
// base implementation potentially overwrite modified defaults that came from a beatmap selection.
TransferSettings(difficulty);
}
}
/// <summary>
/// A <see cref="BindableDouble"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableDoubleWithLimitExtension : BindableDouble
{
public override double Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableFloat"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableFloatWithLimitExtension : BindableFloat
{
public override float Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
/// <summary>
/// A <see cref="BindableInt"/> that extends its min/max values to support any assigned value.
/// </summary>
protected class BindableIntWithLimitExtension : BindableInt
{
public override int Value
{
get => base.Value;
set
{
if (value < MinValue)
MinValue = value;
if (value > MaxValue)
MaxValue = value;
base.Value = value;
}
}
}
}
}

View File

@ -129,7 +129,7 @@ namespace osu.Game.Screens.Menu
buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play);
buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P));
buttonsTopLevel.Add(new Button(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E));
buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D));
if (host.CanExit)

View File

@ -45,6 +45,7 @@ namespace osu.Game.Screens.Menu
private SampleChannel sampleClick;
private SampleChannel sampleBeat;
private SampleChannel sampleDownbeat;
private readonly Container colourAndTriangles;
private readonly Triangles triangles;
@ -259,6 +260,7 @@ namespace osu.Game.Screens.Menu
{
sampleClick = audio.Samples.Get(@"Menu/osu-logo-select");
sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat");
sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat");
logo.Texture = textures.Get(@"Menu/logo");
ripple.Texture = textures.Get(@"Menu/logo");
@ -281,7 +283,15 @@ namespace osu.Game.Screens.Menu
if (beatIndex < 0) return;
if (IsHovered)
this.Delay(early_activation).Schedule(() => sampleBeat.Play());
{
this.Delay(early_activation).Schedule(() =>
{
if (beatIndex % (int)timingPoint.TimeSignature == 0)
sampleDownbeat.Play();
else
sampleBeat.Play();
});
}
logoBeatContainer
.ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then()

View File

@ -20,17 +20,18 @@ namespace osu.Game.Screens.OnlinePlay
{
protected override bool Stacked => false;
protected override bool AllowConfiguration => false;
public new Func<Mod, bool> IsValidMod
{
get => base.IsValidMod;
set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m);
set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m);
}
public FreeModSelectOverlay()
{
IsValidMod = m => true;
CustomiseButton.Alpha = 0;
MultiplierSection.Alpha = 0;
DeselectAllButton.Alpha = 0;

View File

@ -49,6 +49,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
[Cached]
protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; }
protected IBindable<BeatmapAvailability> BeatmapAvailability => BeatmapAvailablilityTracker.Availability;
protected RoomSubScreen()
{
AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker

View File

@ -59,6 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea();
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration;
protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust);
}
}

View File

@ -12,6 +12,8 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Screens;
using osu.Framework.Threading;
using osu.Game.Configuration;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Mods;
@ -267,6 +269,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
base.LoadComplete();
Playlist.BindCollectionChanged(onPlaylistChanged, true);
BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true);
UserMods.BindValueChanged(onUserModsChanged);
client.LoadRequested += onLoadRequested;
@ -313,12 +316,46 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
}
}
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedModSettingsUpdate;
private void onUserModsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
modSettingChangeTracker?.Dispose();
if (client.Room == null)
return;
client.ChangeUserMods(mods.NewValue);
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += onModSettingsChanged;
}
private void onModSettingsChanged(Mod mod)
{
// Debounce changes to mod settings so as to not thrash the network.
debouncedModSettingsUpdate?.Cancel();
debouncedModSettingsUpdate = Scheduler.AddDelayed(() =>
{
if (client.Room == null)
return;
client.ChangeUserMods(UserMods.Value);
}, 500);
}
private void updateBeatmapAvailability(ValueChangedEvent<BeatmapAvailability> availability)
{
if (client.Room == null)
return;
client.ChangeBeatmapAvailability(availability.NewValue);
// while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap.
if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable()
&& client.LocalUser?.State == MultiplayerUserState.Ready)
client.ChangeState(MultiplayerUserState.Idle);
}
private void onReadyClick()
@ -371,14 +408,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (client != null)
client.LoadRequested -= onLoadRequested;
modSettingChangeTracker?.Dispose();
}
private class UserModSelectOverlay : LocalPlayerModSelectOverlay
{
public UserModSelectOverlay()
{
CustomiseButton.Alpha = 0;
}
}
}
}

View File

@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
userStateDisplay.Status = User.State;
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if (Room.Host?.Equals(User) == true)
crown.FadeIn(fade_time);

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@ -8,83 +10,94 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
{
public class StateDisplay : CompositeDrawable
{
private const double fade_time = 50;
private SpriteIcon icon;
private OsuSpriteText text;
private ProgressBar progressBar;
public StateDisplay()
{
AutoSizeAxes = Axes.Both;
Alpha = 0;
}
private MultiplayerUserState status;
private OsuSpriteText text;
private SpriteIcon icon;
private const double fade_time = 50;
public MultiplayerUserState Status
{
set
{
if (value == status)
return;
status = value;
if (IsLoaded)
updateStatus();
}
}
[BackgroundDependencyLoader]
private void load()
private void load(OsuColour colours)
{
this.colours = colours;
InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Spacing = new Vector2(5),
Children = new Drawable[]
{
icon = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
},
new CircularContainer
{
Masking = true,
AutoSizeAxes = Axes.Both,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Children = new Drawable[]
{
progressBar = new ProgressBar(false)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
BackgroundColour = Color4.Black.Opacity(0.4f),
FillColour = colours.Blue,
Alpha = 0f,
},
text = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Horizontal = 5f, Vertical = 1f },
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12),
Colour = Color4Extensions.FromHex("#DDFFFF")
},
icon = new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.CheckCircle,
Size = new Vector2(12),
}
},
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
updateStatus();
}
private OsuColour colours;
[Resolved]
private OsuColour colours { get; set; }
public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability)
{
// the only case where the progress bar is used does its own local fade in.
// starting by fading out is a sane default.
progressBar.FadeOut(fade_time);
this.FadeIn(fade_time);
private void updateStatus()
switch (state)
{
switch (status)
{
default:
this.FadeOut(fade_time);
return;
case MultiplayerUserState.Idle:
showBeatmapAvailability(availability);
break;
case MultiplayerUserState.Ready:
text.Text = "ready";
@ -121,9 +134,43 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
icon.Icon = FontAwesome.Solid.ArrowAltCircleUp;
icon.Colour = colours.BlueLighter;
break;
default:
throw new ArgumentOutOfRangeException(nameof(state), state, null);
}
}
this.FadeIn(fade_time);
private void showBeatmapAvailability(BeatmapAvailability availability)
{
switch (availability.State)
{
default:
this.FadeOut(fade_time);
break;
case DownloadState.NotDownloaded:
text.Text = "no map";
icon.Icon = FontAwesome.Solid.MinusCircle;
icon.Colour = colours.RedLight;
break;
case DownloadState.Downloading:
Debug.Assert(availability.DownloadProgress != null);
progressBar.FadeIn(fade_time);
progressBar.CurrentTime = availability.DownloadProgress.Value;
text.Text = "downloading map";
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
icon.Colour = colours.Blue;
break;
case DownloadState.Importing:
text.Text = "importing map";
icon.Icon = FontAwesome.Solid.ArrowAltCircleDown;
icon.Colour = colours.Yellow;
break;
}
}
}
}

View File

@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel
[BackgroundDependencyLoader]
private void load(AudioManager audio, OsuColour colours)
{
sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}");
sampleHover = audio.Samples.Get("SongSelect/song-ping");
hoverLayer.Colour = colours.Blue.Opacity(0.1f);
}
@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Carousel
protected override bool OnHover(HoverEvent e)
{
sampleHover?.Play();
if (sampleHover != null)
{
sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2);
sampleHover.Play();
}
hoverLayer.FadeIn(100, Easing.OutQuint);
return base.OnHover(e);

View File

@ -18,7 +18,6 @@ using System.Threading;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets;
namespace osu.Game.Screens.Select.Details
@ -83,33 +82,23 @@ namespace osu.Game.Screens.Select.Details
mods.BindValueChanged(modsChanged, true);
}
private readonly List<ISettingsItem> references = new List<ISettingsItem>();
private ModSettingChangeTracker modSettingChangeTracker;
private ScheduledDelegate debouncedStatisticsUpdate;
private void modsChanged(ValueChangedEvent<IReadOnlyList<Mod>> mods)
{
// TODO: find a more permanent solution for this if/when it is needed in other components.
// this is generating drawables for the only purpose of storing bindable references.
foreach (var r in references)
r.Dispose();
modSettingChangeTracker?.Dispose();
references.Clear();
modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue);
modSettingChangeTracker.SettingChanged += m =>
{
if (!(m is IApplicableToDifficulty))
return;
ScheduledDelegate debounce = null;
foreach (var mod in mods.NewValue.OfType<IApplicableToDifficulty>())
{
foreach (var setting in mod.CreateSettingsControls().OfType<ISettingsItem>())
{
setting.SettingChanged += () =>
{
debounce?.Cancel();
debounce = Scheduler.AddDelayed(updateStatistics, 100);
debouncedStatisticsUpdate?.Cancel();
debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100);
};
references.Add(setting);
}
}
updateStatistics();
}
@ -173,6 +162,7 @@ namespace osu.Game.Screens.Select.Details
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
modSettingChangeTracker?.Dispose();
starDifficultyCancellationSource?.Cancel();
}

View File

@ -30,7 +30,7 @@
<PackageReference Include="Microsoft.NETCore.Targets" Version="3.1.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2021.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.0" />
<PackageReference Include="Sentry" Version="3.0.1" />
<PackageReference Include="SharpCompress" Version="0.27.1" />
<PackageReference Include="NUnit" Version="3.12.0" />

View File

@ -71,7 +71,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1202.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.211.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>