1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-13 16:47:46 +08:00

Merge branch 'master' into fix-slider-tick-misssing

This commit is contained in:
Bartłomiej Dach 2023-09-15 12:09:54 +02:00
commit 4275af1343
No known key found for this signature in database
56 changed files with 1447 additions and 616 deletions

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.904.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2023.914.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -13,6 +13,7 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Utils; using osu.Game.Utils;
@ -52,9 +53,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
double beatLength; double beatLength;
if (hitObject is IHasSliderVelocity hasSliderVelocity) if (hitObject is IHasSliderVelocity hasSliderVelocity)
{ beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, ManiaRuleset.SHORT_NAME);
beatLength = timingPoint.BeatLength / hasSliderVelocity.GetPrecisionAdjustedSliderVelocityMultiplier(ManiaRuleset.SHORT_NAME);
}
else else
beatLength = timingPoint.BeatLength; beatLength = timingPoint.BeatLength;

View File

@ -549,12 +549,151 @@ namespace osu.Game.Rulesets.Osu.Tests
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0); addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Miss);
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late. // the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToUnderlyingObjects()`), // this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place. // but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh)); addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
addClickActionAssert(0, ClickAction.Hit); addClickActionAssert(0, ClickAction.Hit);
} }
[Test]
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
{
const double time_first_slider = 1000;
const double time_second_slider = 1600;
Vector2 positionFirstSlider = new Vector2(100, 50);
Vector2 positionSecondSlider = new Vector2(100, 80);
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
var hitObjects = new List<OsuHitObject>
{
new Slider
{
StartTime = time_first_slider,
Position = positionFirstSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
},
new Slider
{
StartTime = time_second_slider,
Position = positionSecondSlider,
Path = new SliderPath(PathType.Linear, new[]
{
Vector2.Zero,
new Vector2(25, 0),
})
}
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
// because stable during replay playback only updates game state _when it encounters a replay frame_
new OsuReplayFrame { Time = 1250, Position = midpoint },
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
});
addJudgementAssert(hitObjects[0], HitResult.Ok);
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
addJudgementAssert(hitObjects[1], HitResult.Ok);
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
addClickActionAssert(0, ClickAction.Hit);
addClickActionAssert(1, ClickAction.Hit);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
{
const double time_first_circle = 1000;
const double time_second_circle = 1200;
Vector2 positionFirstCircle = new Vector2(100);
Vector2 positionSecondCircle = new Vector2(120);
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle,
},
new HitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle,
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
addJudgementAssert(hitObjects[1], HitResult.Meh);
addJudgementOffsetAssert(hitObjects[1], -150);
}
[Test]
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
{
const double time_first_circle = 1000;
const double time_second_circle = 1200;
const double time_third_circle = 1400;
Vector2 positionFirstCircle = new Vector2(100);
Vector2 positionSecondCircle = new Vector2(200);
var hitObjects = new List<OsuHitObject>
{
new HitCircle
{
StartTime = time_first_circle,
Position = positionFirstCircle,
},
new HitCircle
{
StartTime = time_second_circle,
Position = positionSecondCircle,
},
new HitCircle
{
StartTime = time_third_circle,
Position = positionFirstCircle,
},
};
performTest(hitObjects, new List<ReplayFrame>
{
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
});
addJudgementAssert(hitObjects[0], HitResult.Great);
addJudgementOffsetAssert(hitObjects[0], 0);
addJudgementAssert(hitObjects[1], HitResult.Great);
addJudgementOffsetAssert(hitObjects[1], 0);
addJudgementAssert(hitObjects[2], HitResult.Great);
addJudgementOffsetAssert(hitObjects[2], 0);
}
private void addJudgementAssert(OsuHitObject hitObject, HitResult result) private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
{ {
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",

View File

@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Mods
applyEarlyFading(head); applyEarlyFading(head);
if (ClassicNoteLock.Value) if (ClassicNoteLock.Value)
blockInputToUnderlyingObjects(head); blockInputToObjectsUnderSliderHead(head);
break; break;
@ -88,25 +88,23 @@ namespace osu.Game.Rulesets.Osu.Mods
if (FadeHitCircleEarly.Value && !usingHiddenFading) if (FadeHitCircleEarly.Value && !usingHiddenFading)
applyEarlyFading(circle); applyEarlyFading(circle);
if (ClassicNoteLock.Value)
blockInputToUnderlyingObjects(circle);
break; break;
} }
} }
/// <summary> /// <summary>
/// On stable, hitcircles that have already been hit block input from reaching objects that may be underneath them. /// On stable, slider heads that have already been hit block input from reaching objects that may be underneath them
/// until the sliders they're part of have been fully judged.
/// The purpose of this method is to restore that behaviour. /// The purpose of this method is to restore that behaviour.
/// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock". /// In order to avoid introducing yet another confusing config option, this behaviour is roped into the general notion of "note lock".
/// </summary> /// </summary>
private static void blockInputToUnderlyingObjects(DrawableHitCircle circle) private static void blockInputToObjectsUnderSliderHead(DrawableSliderHead slider)
{ {
var oldHitAction = circle.HitArea.Hit; var oldHitAction = slider.HitArea.Hit;
circle.HitArea.Hit = () => slider.HitArea.Hit = () =>
{ {
oldHitAction?.Invoke(); oldHitAction?.Invoke();
return true; return !slider.DrawableSlider.AllJudged;
}; };
} }

View File

@ -2,13 +2,19 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq; using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModDifficultyAdjust : ModDifficultyAdjust public partial class OsuModDifficultyAdjust : ModDifficultyAdjust
{ {
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))] [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))]
public DifficultyBindable CircleSize { get; } = new DifficultyBindable public DifficultyBindable CircleSize { get; } = new DifficultyBindable
@ -20,12 +26,13 @@ namespace osu.Game.Rulesets.Osu.Mods
ReadCurrentFromDifficulty = diff => diff.CircleSize, ReadCurrentFromDifficulty = diff => diff.CircleSize,
}; };
[SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(DifficultyAdjustSettingsControl))] [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1, SettingControlType = typeof(ApproachRateSettingsControl))]
public DifficultyBindable ApproachRate { get; } = new DifficultyBindable public DifficultyBindable ApproachRate { get; } = new DifficultyBindable
{ {
Precision = 0.1f, Precision = 0.1f,
MinValue = 0, MinValue = 0,
MaxValue = 10, MaxValue = 10,
ExtendedMinValue = -10,
ExtendedMaxValue = 11, ExtendedMaxValue = 11,
ReadCurrentFromDifficulty = diff => diff.ApproachRate, ReadCurrentFromDifficulty = diff => diff.ApproachRate,
}; };
@ -53,5 +60,34 @@ namespace osu.Game.Rulesets.Osu.Mods
if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value; if (CircleSize.Value != null) difficulty.CircleSize = CircleSize.Value.Value;
if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value; if (ApproachRate.Value != null) difficulty.ApproachRate = ApproachRate.Value.Value;
} }
private partial class ApproachRateSettingsControl : DifficultyAdjustSettingsControl
{
protected override RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) =>
new ApproachRateSlider
{
RelativeSizeAxes = Axes.X,
Current = current,
KeyboardStep = 0.1f,
};
/// <summary>
/// A slider bar with more detailed approach rate info for its given value
/// </summary>
public partial class ApproachRateSlider : RoundedSliderBar<float>
{
public override LocalisableString TooltipText =>
(Current as BindableNumber<float>)?.MinValue < 0
? $"{base.TooltipText} ({getPreemptTime(Current.Value):0} ms)"
: base.TooltipText;
private double getPreemptTime(float approachRate)
{
var hitCircle = new HitCircle();
hitCircle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { ApproachRate = approachRate });
return hitCircle.TimePreempt;
}
}
}
} }
} }

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult; private static readonly int score_per_tick = new SpinnerBonusTick.OsuSpinnerBonusTickJudgement().MaxNumericResult;
private int wholeSpins; private int completedFullSpins;
private void updateBonusScore() private void updateBonusScore()
{ {
@ -295,14 +295,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
int spins = (int)(Result.RateAdjustedRotation / 360); int spins = (int)(Result.RateAdjustedRotation / 360);
if (spins < wholeSpins) if (spins < completedFullSpins)
{ {
// rewinding, silently handle // rewinding, silently handle
wholeSpins = spins; completedFullSpins = spins;
return; return;
} }
while (wholeSpins != spins) while (completedFullSpins != spins)
{ {
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
@ -312,10 +312,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
tick.TriggerResult(true); tick.TriggerResult(true);
if (tick is DrawableSpinnerBonusTick) if (tick is DrawableSpinnerBonusTick)
gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequired); gainedBonus.Value = score_per_tick * (spins - HitObject.SpinsRequiredForBonus);
} }
wholeSpins++; completedFullSpins++;
} }
} }
} }

View File

@ -31,6 +31,16 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int SpinsRequired { get; protected set; } = 1; public int SpinsRequired { get; protected set; } = 1;
/// <summary>
/// The number of spins required to start receiving bonus score. The first bonus is awarded on this spin count.
/// </summary>
public int SpinsRequiredForBonus => SpinsRequired + bonus_spins_gap;
/// <summary>
/// The gap between spinner completion and the first bonus-awarding spin.
/// </summary>
private const int bonus_spins_gap = 2;
/// <summary> /// <summary>
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>. /// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
/// </summary> /// </summary>
@ -42,25 +52,20 @@ namespace osu.Game.Rulesets.Osu.Objects
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. const double maximum_rotations_per_second = 477f / 60f;
const double stable_matching_fudge = 0.6;
// close to 477rpm
const double maximum_rotations_per_second = 8;
double secondsDuration = Duration / 1000; double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 1.5, 2.5, 3.75);
double minimumRotationsPerSecond = stable_matching_fudge * IBeatmapDifficultyInfo.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond);
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration) - bonus_spins_gap;
} }
protected override void CreateNestedHitObjects(CancellationToken cancellationToken) protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
{ {
base.CreateNestedHitObjects(cancellationToken); base.CreateNestedHitObjects(cancellationToken);
int totalSpins = MaximumBonusSpins + SpinsRequired; int totalSpins = MaximumBonusSpins + SpinsRequired + bonus_spins_gap;
for (int i = 0; i < totalSpins; i++) for (int i = 0; i < totalSpins; i++)
{ {
@ -68,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired AddNested(i < SpinsRequiredForBonus
? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration } ? new SpinnerTick { StartTime = startTime, SpinnerDuration = Duration }
: new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } }); : new SpinnerBonusTick { StartTime = startTime, SpinnerDuration = Duration, Samples = new[] { CreateHitSampleInfo("spinnerbonus") } });
} }

View File

@ -286,7 +286,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1) if (time - part.Time >= 1)
continue; continue;
vertexBatch.Add(new TexturedTrailVertex(renderer) vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft, TexturePosition = textureRect.BottomLeft,
@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex(renderer) vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight, TexturePosition = textureRect.BottomRight,
@ -304,7 +304,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex(renderer) vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopRight, TexturePosition = textureRect.TopRight,
@ -313,7 +313,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Time = part.Time Time = part.Time
}); });
vertexBatch.Add(new TexturedTrailVertex(renderer) vertexBatch.Add(new TexturedTrailVertex
{ {
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y), Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
TexturePosition = textureRect.TopLeft, TexturePosition = textureRect.TopLeft,
@ -362,22 +362,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
[VertexMember(1, VertexAttribPointerType.Float)] [VertexMember(1, VertexAttribPointerType.Float)]
public float Time; public float Time;
[VertexMember(1, VertexAttribPointerType.Int)]
private readonly int maskingIndex;
public TexturedTrailVertex(IRenderer renderer)
{
this = default;
maskingIndex = renderer.CurrentMaskingIndex;
}
public bool Equals(TexturedTrailVertex other) public bool Equals(TexturedTrailVertex other)
{ {
return Position.Equals(other.Position) return Position.Equals(other.Position)
&& TexturePosition.Equals(other.TexturePosition) && TexturePosition.Equals(other.TexturePosition)
&& Colour.Equals(other.Colour) && Colour.Equals(other.Colour)
&& Time.Equals(other.Time) && Time.Equals(other.Time);
&& maskingIndex == other.maskingIndex;
} }
} }
} }

View File

@ -14,6 +14,7 @@ using JetBrains.Annotations;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Taiko.Beatmaps namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
@ -188,7 +189,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double beatLength; double beatLength;
if (obj is IHasSliderVelocity hasSliderVelocity) if (obj is IHasSliderVelocity hasSliderVelocity)
beatLength = timingPoint.BeatLength / hasSliderVelocity.GetPrecisionAdjustedSliderVelocityMultiplier(TaikoRuleset.SHORT_NAME); beatLength = LegacyRulesetExtensions.GetPrecisionAdjustedBeatLength(hasSliderVelocity, timingPoint, TaikoRuleset.SHORT_NAME);
else else
beatLength = timingPoint.BeatLength; beatLength = timingPoint.BeatLength;

View File

@ -1024,10 +1024,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(2000).SliderVelocity, Is.EqualTo(1));
Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1)); Assert.That(controlPoints.DifficultyPointAt(3000).SliderVelocity, Is.EqualTo(1));
#pragma warning disable 618
Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False); Assert.That(controlPoints.DifficultyPointAt(2000).GenerateTicks, Is.False);
Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True); Assert.That(controlPoints.DifficultyPointAt(3000).GenerateTicks, Is.True);
#pragma warning restore 618
} }
} }
} }

View File

@ -13,7 +13,7 @@ layout(location = 4) out mediump vec2 v_BlendRange;
void main(void) void main(void)
{ {
// Transform from screen space to masking space. // Transform from screen space to masking space.
highp vec3 maskingPos = g_MaskingInfo.ToMaskingSpace * vec3(m_Position, 1.0); highp vec3 maskingPos = g_ToMaskingSpace * vec3(m_Position, 1.0);
v_MaskingPosition = maskingPos.xy / maskingPos.z; v_MaskingPosition = maskingPos.xy / maskingPos.z;
v_Colour = m_Colour; v_Colour = m_Colour;

View File

@ -5,7 +5,9 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
@ -33,8 +35,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private GraphContainer graphs = null!; private GraphContainer graphs = null!;
private SettingsSlider<int> sliderMaxCombo = null!; private SettingsSlider<int> sliderMaxCombo = null!;
private SettingsCheckbox scaleToMax = null!;
private FillFlowContainer legend = null!; private FillFlowContainer<LegendEntry> legend = null!;
private readonly BindableBool standardisedVisible = new BindableBool(true);
private readonly BindableBool classicVisible = new BindableBool(true);
private readonly BindableBool scoreV1Visible = new BindableBool(true);
private readonly BindableBool scoreV2Visible = new BindableBool(true);
[Resolved]
private OsuColour colours { get; set; } = null!;
[Test] [Test]
public void TestBasic() public void TestBasic()
@ -43,6 +54,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.Black
},
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -63,10 +79,10 @@ namespace osu.Game.Tests.Visual.Gameplay
}, },
new Drawable[] new Drawable[]
{ {
legend = new FillFlowContainer legend = new FillFlowContainer<LegendEntry>
{ {
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
Direction = FillDirection.Full, Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
}, },
@ -78,26 +94,31 @@ namespace osu.Game.Tests.Visual.Gameplay
Padding = new MarginPadding(20), Padding = new MarginPadding(20),
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full, Direction = FillDirection.Vertical,
Spacing = new Vector2(10),
Children = new Drawable[] Children = new Drawable[]
{ {
sliderMaxCombo = new SettingsSlider<int> sliderMaxCombo = new SettingsSlider<int>
{ {
Width = 0.5f,
TransferValueOnCommit = true, TransferValueOnCommit = true,
Current = new BindableInt(1024) Current = new BindableInt(1024)
{ {
MinValue = 96, MinValue = 96,
MaxValue = 8192, MaxValue = 8192,
}, },
LabelText = "max combo", LabelText = "Max combo",
},
scaleToMax = new SettingsCheckbox
{
LabelText = "Rescale plots to 100%",
Current = { Value = true, Default = true }
}, },
new OsuTextFlowContainer new OsuTextFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Width = 0.5f,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Text = $"Left click to add miss\nRight click to add OK/{base_ok}" Text = $"Left click to add miss\nRight click to add OK/{base_ok}",
Margin = new MarginPadding { Top = 20 }
} }
} }
}, },
@ -107,6 +128,12 @@ namespace osu.Game.Tests.Visual.Gameplay
}; };
sliderMaxCombo.Current.BindValueChanged(_ => rerun()); sliderMaxCombo.Current.BindValueChanged(_ => rerun());
scaleToMax.Current.BindValueChanged(_ => rerun());
standardisedVisible.BindValueChanged(_ => rescalePlots());
classicVisible.BindValueChanged(_ => rescalePlots());
scoreV1Visible.BindValueChanged(_ => rescalePlots());
scoreV2Visible.BindValueChanged(_ => rescalePlots());
graphs.MissLocations.BindCollectionChanged((_, __) => rerun()); graphs.MissLocations.BindCollectionChanged((_, __) => rerun());
graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun()); graphs.NonPerfectLocations.BindCollectionChanged((_, __) => rerun());
@ -125,11 +152,29 @@ namespace osu.Game.Tests.Visual.Gameplay
graphs.Clear(); graphs.Clear();
legend.Clear(); legend.Clear();
runForProcessor("lazer-standardised", Color4.YellowGreen, new OsuScoreProcessor(), ScoringMode.Standardised); runForProcessor("lazer-standardised", colours.Green1, new OsuScoreProcessor(), ScoringMode.Standardised, standardisedVisible);
runForProcessor("lazer-classic", Color4.MediumPurple, new OsuScoreProcessor(), ScoringMode.Classic); runForProcessor("lazer-classic", colours.Blue1, new OsuScoreProcessor(), ScoringMode.Classic, classicVisible);
runScoreV1(); runScoreV1();
runScoreV2(); runScoreV2();
rescalePlots();
}
private void rescalePlots()
{
if (!scaleToMax.Current.Value && legend.Any(entry => entry.Visible.Value))
{
long maxScore = legend.Where(entry => entry.Visible.Value).Max(entry => entry.FinalScore);
foreach (var graph in graphs)
graph.Height = graph.Values.Max() / maxScore;
}
else
{
foreach (var graph in graphs)
graph.Height = 1;
}
} }
private void runScoreV1() private void runScoreV1()
@ -145,7 +190,9 @@ namespace osu.Game.Tests.Visual.Gameplay
return; return;
} }
const float score_multiplier = 1; // this corresponds to stable's `ScoreMultiplier`.
// value is chosen arbitrarily, towards the upper range.
const float score_multiplier = 4;
totalScore += baseScore; totalScore += baseScore;
@ -156,17 +203,16 @@ namespace osu.Game.Tests.Visual.Gameplay
currentCombo++; currentCombo++;
} }
runForAlgorithm("ScoreV1 (classic)", Color4.Purple, runForAlgorithm(new ScoringAlgorithm
() => applyHitV1(base_great), {
() => applyHitV1(base_ok), Name = "ScoreV1 (classic)",
() => applyHitV1(0), Colour = colours.Purple1,
() => ApplyHit = () => applyHitV1(base_great),
{ ApplyNonPerfect = () => applyHitV1(base_ok),
// Arbitrary value chosen towards the upper range. ApplyMiss = () => applyHitV1(0),
const double score_multiplier = 4; GetTotalScore = () => totalScore,
Visible = scoreV1Visible
return (int)(totalScore * score_multiplier); });
});
} }
private void runScoreV2() private void runScoreV2()
@ -199,15 +245,19 @@ namespace osu.Game.Tests.Visual.Gameplay
currentHits++; currentHits++;
} }
runForAlgorithm("ScoreV2", Color4.OrangeRed, runForAlgorithm(new ScoringAlgorithm
() => applyHitV2(base_great), {
() => applyHitV2(base_ok), Name = "ScoreV2",
() => Colour = colours.Red1,
ApplyHit = () => applyHitV2(base_great),
ApplyNonPerfect = () => applyHitV2(base_ok),
ApplyMiss = () =>
{ {
currentHits++; currentHits++;
maxBaseScore += base_great; maxBaseScore += base_great;
currentCombo = 0; currentCombo = 0;
}, () => },
GetTotalScore = () =>
{ {
double accuracy = currentBaseScore / maxBaseScore; double accuracy = currentBaseScore / maxBaseScore;
@ -216,10 +266,12 @@ namespace osu.Game.Tests.Visual.Gameplay
700000 * comboPortion / comboPortionMax + 700000 * comboPortion / comboPortionMax +
300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo) 300000 * Math.Pow(accuracy, 10) * ((double)currentHits / maxCombo)
); );
}); },
Visible = scoreV2Visible
});
} }
private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode) private void runForProcessor(string name, Color4 colour, ScoreProcessor processor, ScoringMode mode, BindableBool visibility)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;
@ -229,14 +281,19 @@ namespace osu.Game.Tests.Visual.Gameplay
processor.ApplyBeatmap(beatmap); processor.ApplyBeatmap(beatmap);
runForAlgorithm(name, colour, runForAlgorithm(new ScoringAlgorithm
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }), {
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }), Name = name,
() => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }), Colour = colour,
() => processor.GetDisplayScore(mode)); ApplyHit = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Great }),
ApplyNonPerfect = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Ok }),
ApplyMiss = () => processor.ApplyResult(new OsuJudgementResult(new HitCircle(), new OsuJudgement()) { Type = HitResult.Miss }),
GetTotalScore = () => processor.GetDisplayScore(mode),
Visible = visibility
});
} }
private void runForAlgorithm(string name, Color4 colour, Action applyHit, Action applyNonPerfect, Action applyMiss, Func<long> getTotalScore) private void runForAlgorithm(ScoringAlgorithm scoringAlgorithm)
{ {
int maxCombo = sliderMaxCombo.Current.Value; int maxCombo = sliderMaxCombo.Current.Value;
@ -245,49 +302,52 @@ namespace osu.Game.Tests.Visual.Gameplay
for (int i = 0; i < maxCombo; i++) for (int i = 0; i < maxCombo; i++)
{ {
if (graphs.MissLocations.Contains(i)) if (graphs.MissLocations.Contains(i))
applyMiss(); scoringAlgorithm.ApplyMiss();
else if (graphs.NonPerfectLocations.Contains(i)) else if (graphs.NonPerfectLocations.Contains(i))
applyNonPerfect(); scoringAlgorithm.ApplyNonPerfect();
else else
applyHit(); scoringAlgorithm.ApplyHit();
results.Add(getTotalScore()); results.Add(scoringAlgorithm.GetTotalScore());
} }
graphs.Add(new LineGraph LineGraph graph;
graphs.Add(graph = new LineGraph
{ {
Name = name, Name = scoringAlgorithm.Name,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
LineColour = colour, LineColour = scoringAlgorithm.Colour,
Values = results Values = results
}); });
legend.Add(new OsuSpriteText legend.Add(new LegendEntry(scoringAlgorithm, graph)
{ {
Colour = colour, AccentColour = scoringAlgorithm.Colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"{FontAwesome.Solid.Circle.Icon} {name}"
});
legend.Add(new OsuSpriteText
{
Colour = colour,
RelativeSizeAxes = Axes.X,
Width = 0.5f,
Text = $"final score {getTotalScore():#,0}"
}); });
} }
} }
public partial class GraphContainer : Container, IHasCustomTooltip<IEnumerable<LineGraph>> public class ScoringAlgorithm
{
public string Name { get; init; } = null!;
public Color4 Colour { get; init; }
public Action ApplyHit { get; init; } = () => { };
public Action ApplyNonPerfect { get; init; } = () => { };
public Action ApplyMiss { get; init; } = () => { };
public Func<long> GetTotalScore { get; init; } = null!;
public BindableBool Visible { get; init; } = null!;
}
public partial class GraphContainer : Container<LineGraph>, IHasCustomTooltip<IEnumerable<LineGraph>>
{ {
public readonly BindableList<double> MissLocations = new BindableList<double>(); public readonly BindableList<double> MissLocations = new BindableList<double>();
public readonly BindableList<double> NonPerfectLocations = new BindableList<double>(); public readonly BindableList<double> NonPerfectLocations = new BindableList<double>();
public Bindable<int> MaxCombo = new Bindable<int>(); public Bindable<int> MaxCombo = new Bindable<int>();
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; protected override Container<LineGraph> Content { get; } = new Container<LineGraph> { RelativeSizeAxes = Axes.Both };
private readonly Box hoverLine; private readonly Box hoverLine;
@ -438,7 +498,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this); public ITooltip<IEnumerable<LineGraph>> GetCustomTooltip() => tooltip ??= new GraphTooltip(this);
public IEnumerable<LineGraph> TooltipContent => Content.OfType<LineGraph>(); public IEnumerable<LineGraph> TooltipContent => Content;
public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>> public partial class GraphTooltip : CompositeDrawable, ITooltip<IEnumerable<LineGraph>>
{ {
@ -486,6 +546,8 @@ namespace osu.Game.Tests.Visual.Gameplay
foreach (var graph in content) foreach (var graph in content)
{ {
if (graph.Alpha == 0) continue;
float valueAtHover = graph.Values.ElementAt(relevantCombo); float valueAtHover = graph.Values.ElementAt(relevantCombo);
float ofTotal = valueAtHover / graph.Values.Last(); float ofTotal = valueAtHover / graph.Values.Last();
@ -496,4 +558,79 @@ namespace osu.Game.Tests.Visual.Gameplay
public void Move(Vector2 pos) => this.MoveTo(pos); public void Move(Vector2 pos) => this.MoveTo(pos);
} }
} }
public partial class LegendEntry : OsuClickableContainer, IHasAccentColour
{
public Color4 AccentColour { get; set; }
public BindableBool Visible { get; } = new BindableBool(true);
public readonly long FinalScore;
private readonly string description;
private readonly LineGraph lineGraph;
private OsuSpriteText descriptionText = null!;
private OsuSpriteText finalScoreText = null!;
public LegendEntry(ScoringAlgorithm scoringAlgorithm, LineGraph lineGraph)
{
description = scoringAlgorithm.Name;
FinalScore = scoringAlgorithm.GetTotalScore();
AccentColour = scoringAlgorithm.Colour;
Visible.BindTo(scoringAlgorithm.Visible);
this.lineGraph = lineGraph;
}
[BackgroundDependencyLoader]
private void load()
{
RelativeSizeAxes = Content.RelativeSizeAxes = Axes.X;
AutoSizeAxes = Content.AutoSizeAxes = Axes.Y;
Children = new Drawable[]
{
descriptionText = new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
finalScoreText = new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.Default.With(fixedWidth: true)
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Visible.BindValueChanged(_ => updateState(), true);
Action = Visible.Toggle;
}
protected override bool OnHover(HoverEvent e)
{
updateState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateState();
base.OnHoverLost(e);
}
private void updateState()
{
Colour = IsHovered ? AccentColour.Lighten(0.2f) : AccentColour;
descriptionText.Text = $"{(Visible.Value ? FontAwesome.Solid.CheckCircle.Icon : FontAwesome.Solid.Circle.Icon)} {description}";
finalScoreText.Text = FinalScore.ToString("#,0");
lineGraph.Alpha = Visible.Value ? 1 : 0;
}
}
} }

View File

@ -110,5 +110,31 @@ namespace osu.Game.Tests.Visual.Online
} }
}, new OsuRuleset().RulesetInfo)); }, new OsuRuleset().RulesetInfo));
} }
[Test]
public void TestPreviousUsernames()
{
AddStep("Show user w/ previous usernames", () => header.User.Value = new UserProfileData(new APIUser
{
Id = 727,
Username = "SomeoneIndecisive",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
Groups = new[]
{
new APIUserGroup { Colour = "#EB47D0", ShortName = "DEV", Name = "Developers" },
},
Statistics = new UserStatistics
{
IsRanked = false,
// web will sometimes return non-empty rank history even for unranked users.
RankHistory = new APIRankHistory
{
Mode = @"osu",
Data = Enumerable.Range(2345, 85).ToArray()
},
},
PreviousUsernames = new[] { "tsrk.", "quoicoubeh", "apagnan", "epita" }
}, new OsuRuleset().RulesetInfo));
}
} }
} }

View File

@ -5,20 +5,29 @@ using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
[TestFixture] [TestFixture]
public partial class TestSceneUserProfilePreviousUsernames : OsuTestScene public partial class TestSceneUserProfilePreviousUsernamesDisplay : OsuTestScene
{ {
private PreviousUsernames container = null!; private PreviousUsernamesDisplay container = null!;
private OverlayColourProvider colourProvider = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
{ {
Child = container = new PreviousUsernames colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
Child = new DependencyProvidingContainer
{ {
Child = container = new PreviousUsernamesDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
CachedDependencies = new (Type, object)[] { (typeof(OverlayColourProvider), colourProvider) },
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}; };

View File

@ -0,0 +1,131 @@
// 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.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModEffectPreviewPanel : OsuTestScene
{
[Cached(typeof(BeatmapDifficultyCache))]
private TestBeatmapDifficultyCache difficultyCache = new TestBeatmapDifficultyCache();
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
private Container content = null!;
protected override Container<Drawable> Content => content;
private BeatmapAttributesDisplay panel = null!;
[BackgroundDependencyLoader]
private void load()
{
base.Content.AddRange(new Drawable[]
{
difficultyCache,
content = new Container
{
RelativeSizeAxes = Axes.Both
}
});
}
[Test]
public void TestDisplay()
{
OsuModDifficultyAdjust difficultyAdjust = new OsuModDifficultyAdjust();
OsuModDoubleTime doubleTime = new OsuModDoubleTime();
AddStep("create display", () => Child = panel = new BeatmapAttributesDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
AddStep("set beatmap", () =>
{
var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo)
{
BeatmapInfo =
{
BPM = 120
}
};
Ruleset.Value = beatmap.BeatmapInfo.Ruleset;
panel.BeatmapInfo.Value = beatmap.BeatmapInfo;
});
AddSliderStep("change star rating", 0, 10d, 5, stars =>
{
if (panel.IsNotNull())
previewStarRating(stars);
});
AddStep("preview ridiculously high SR", () => previewStarRating(1234));
AddStep("add DA to mods", () => SelectedMods.Value = new[] { difficultyAdjust });
AddSliderStep("change AR", 0, 10f, 5, ar =>
{
if (panel.IsNotNull())
difficultyAdjust.ApproachRate.Value = ar;
});
AddSliderStep("change CS", 0, 10f, 5, cs =>
{
if (panel.IsNotNull())
difficultyAdjust.CircleSize.Value = cs;
});
AddSliderStep("change HP", 0, 10f, 5, hp =>
{
if (panel.IsNotNull())
difficultyAdjust.DrainRate.Value = hp;
});
AddSliderStep("change OD", 0, 10f, 5, od =>
{
if (panel.IsNotNull())
difficultyAdjust.OverallDifficulty.Value = od;
});
AddStep("add DT to mods", () => SelectedMods.Value = new Mod[] { difficultyAdjust, doubleTime });
AddSliderStep("change rate", 1.01d, 2d, 1.5d, rate =>
{
if (panel.IsNotNull())
doubleTime.SpeedChange.Value = rate;
});
AddToggleStep("toggle collapsed", collapsed => panel.Collapsed.Value = collapsed);
}
private void previewStarRating(double stars)
{
difficultyCache.Difficulty = new StarDifficulty(stars, 0);
panel.BeatmapInfo.TriggerChange();
}
private partial class TestBeatmapDifficultyCache : BeatmapDifficultyCache
{
public StarDifficulty? Difficulty { get; set; }
public override Task<StarDifficulty?> GetDifficultyAsync(IBeatmapInfo beatmapInfo, IRulesetInfo? rulesetInfo = null, IEnumerable<Mod>? mods = null,
CancellationToken cancellationToken = default)
=> Task.FromResult(Difficulty);
}
}
}

View File

@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("clear contents", Clear); AddStep("clear contents", Clear);
AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0)); AddStep("reset ruleset", () => Ruleset.Value = rulesetStore.GetRuleset(0));
AddStep("reset mods", () => SelectedMods.SetDefault()); AddStep("reset mods", () => SelectedMods.SetDefault());
AddStep("set beatmap", () => Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo));
AddStep("set up presets", () => AddStep("set up presets", () =>
{ {
Realm.Write(r => Realm.Write(r =>
@ -92,6 +93,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible }, State = { Value = Visibility.Visible },
Beatmap = Beatmap.Value,
SelectedMods = { BindTarget = SelectedMods } SelectedMods = { BindTarget = SelectedMods }
}); });
waitForColumnLoad(); waitForColumnLoad();
@ -113,7 +115,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () => AddAssert("mod multiplier correct", () =>
{ {
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value); return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
}); });
assertCustomisationToggleState(disabled: false, active: false); assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any()); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -128,7 +130,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("mod multiplier correct", () => AddAssert("mod multiplier correct", () =>
{ {
double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier); double multiplier = SelectedMods.Value.Aggregate(1d, (m, mod) => m * mod.ScoreMultiplier);
return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value); return Precision.AlmostEquals(multiplier, modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value);
}); });
assertCustomisationToggleState(disabled: false, active: false); assertCustomisationToggleState(disabled: false, active: false);
AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any()); AddAssert("setting items created", () => modSelectOverlay.ChildrenOfType<ISettingsItem>().Any());
@ -785,7 +787,7 @@ namespace osu.Game.Tests.Visual.UserInterface
InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x")); InputManager.MoveMouseTo(this.ChildrenOfType<ModPresetPanel>().Single(preset => preset.Preset.Value.Name == "Half Time 0.5x"));
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5)); AddAssert("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.5));
// this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation, // this is highly unorthodox in a test, but because the `ModSettingChangeTracker` machinery heavily leans on events and object disposal and re-creation,
// it is instrumental in the reproduction of the failure scenario that this test is supposed to cover. // it is instrumental in the reproduction of the failure scenario that this test is supposed to cover.
@ -794,7 +796,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick()); AddStep("open customisation area", () => modSelectOverlay.CustomisationButton!.TriggerClick());
AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single() AddStep("reset half time speed to default", () => modSelectOverlay.ChildrenOfType<ModSettingsArea>().Single()
.ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick()); .ChildrenOfType<RevertToDefaultButton<double>>().Single().TriggerClick());
AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<DifficultyMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7)); AddUntilStep("difficulty multiplier display shows correct value", () => modSelectOverlay.ChildrenOfType<ScoreMultiplierDisplay>().Single().Current.Value, () => Is.EqualTo(0.7));
} }
private void waitForColumnLoad() => AddUntilStep("all column content loaded", private void waitForColumnLoad() => AddUntilStep("all column content loaded",

View File

@ -1,68 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets.Mods;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public partial class TestSceneModsEffectDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Resolved]
private OsuColour colours { get; set; } = null!;
[Test]
public void TestModsEffectDisplay()
{
TestDisplay testDisplay = null!;
Box background = null!;
AddStep("add display", () =>
{
Add(testDisplay = new TestDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
var boxes = testDisplay.ChildrenOfType<Box>();
background = boxes.First();
});
AddStep("set value to default", () => testDisplay.Current.Value = 50);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == Color4.White && background.Colour == colourProvider.Background3);
AddStep("set value to less", () => testDisplay.Current.Value = 40);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyReduction));
AddStep("set value to bigger", () => testDisplay.Current.Value = 60);
AddUntilStep("colours are correct", () => testDisplay.Container.Colour == colourProvider.Background5 && background.Colour == colours.ForModType(ModType.DifficultyIncrease));
}
private partial class TestDisplay : ModsEffectDisplay
{
public Container<Drawable> Container => Content;
protected override LocalisableString Label => "Test display";
public TestDisplay()
{
Current.Default = 50;
}
}
}
}

View File

@ -1,10 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
@ -12,17 +11,17 @@ using osu.Game.Overlays.Mods;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
[TestFixture] [TestFixture]
public partial class TestSceneDifficultyMultiplierDisplay : OsuTestScene public partial class TestSceneScoreMultiplierDisplay : OsuTestScene
{ {
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test] [Test]
public void TestDifficultyMultiplierDisplay() public void TestBasic()
{ {
DifficultyMultiplierDisplay multiplierDisplay = null; ScoreMultiplierDisplay multiplierDisplay = null!;
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay AddStep("create content", () => Child = multiplierDisplay = new ScoreMultiplierDisplay
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre Origin = Anchor.Centre
@ -34,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddSliderStep("set multiplier", 0, 2, 1d, multiplier => AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
{ {
if (multiplierDisplay != null) if (multiplierDisplay.IsNotNull())
multiplierDisplay.Current.Value = multiplier; multiplierDisplay.Current.Value = multiplier;
}); });
} }

View File

@ -319,7 +319,7 @@ namespace osu.Game.Beatmaps
{ {
DateTimeOffset dateAdded = DateTimeOffset.UtcNow; DateTimeOffset dateAdded = DateTimeOffset.UtcNow;
if (reader is LegacyDirectoryArchiveReader legacyReader) if (reader is DirectoryArchiveReader legacyReader)
{ {
var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); var beatmaps = reader.Filenames.Where(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase));

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#pragma warning disable 618
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;

View File

@ -46,9 +46,29 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public ArchiveReader GetReader() public ArchiveReader GetReader()
{ {
return Stream != null if (Stream == null)
? getReaderFrom(Stream) {
: getReaderFrom(Path); if (ZipUtils.IsZipArchive(Path))
return new ZipArchiveReader(File.Open(Path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(Path));
if (Directory.Exists(Path))
return new DirectoryArchiveReader(Path);
if (File.Exists(Path))
return new SingleFileArchiveReader(Path);
throw new InvalidFormatException($"{Path} is not a valid archive");
}
if (Stream is not MemoryStream memoryStream)
{
// Path used primarily in tests (converting `ManifestResourceStream`s to `MemoryStream`s).
memoryStream = new MemoryStream(Stream.ReadAllBytesToArray());
Stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))
return new ZipArchiveReader(memoryStream, Path);
return new MemoryStreamArchiveReader(memoryStream, Path);
} }
/// <summary> /// <summary>
@ -60,43 +80,6 @@ namespace osu.Game.Database
File.Delete(Path); File.Delete(Path);
} }
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a stream.
/// </summary>
/// <param name="stream">A seekable stream containing the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(Stream stream)
{
if (!(stream is MemoryStream memoryStream))
{
// This isn't used in any current path. May need to reconsider for performance reasons (ie. if we don't expect the incoming stream to be copied out).
memoryStream = new MemoryStream(stream.ReadAllBytesToArray());
stream.Dispose();
}
if (ZipUtils.IsZipArchive(memoryStream))
return new ZipArchiveReader(memoryStream, Path);
return new LegacyByteArrayReader(memoryStream.ToArray(), Path);
}
/// <summary>
/// Creates an <see cref="ArchiveReader"/> from a valid storage path.
/// </summary>
/// <param name="path">A file or folder path resolving the archive content.</param>
/// <returns>A reader giving access to the archive's content.</returns>
private ArchiveReader getReaderFrom(string path)
{
if (ZipUtils.IsZipArchive(path))
return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path));
if (Directory.Exists(path))
return new LegacyDirectoryArchiveReader(path);
if (File.Exists(path))
return new LegacyFileArchiveReader(path);
throw new InvalidFormatException($"{path} is not a valid archive");
}
public override string ToString() => System.IO.Path.GetFileName(Path); public override string ToString() => System.IO.Path.GetFileName(Path);
} }
} }

View File

@ -17,6 +17,10 @@ namespace osu.Game.Graphics.UserInterface
{ {
public partial class ShearedButton : OsuClickableContainer public partial class ShearedButton : OsuClickableContainer
{ {
public const float HEIGHT = 50;
public const float CORNER_RADIUS = 7;
public const float BORDER_THICKNESS = 2;
public LocalisableString Text public LocalisableString Text
{ {
get => text.Text; get => text.Text;
@ -83,12 +87,10 @@ namespace osu.Game.Graphics.UserInterface
/// </param> /// </param>
public ShearedButton(float? width = null) public ShearedButton(float? width = null)
{ {
Height = 50; Height = HEIGHT;
Padding = new MarginPadding { Horizontal = shear * 50 }; Padding = new MarginPadding { Horizontal = shear * 50 };
const float corner_radius = 7; Content.CornerRadius = CORNER_RADIUS;
Content.CornerRadius = corner_radius;
Content.Shear = new Vector2(shear, 0); Content.Shear = new Vector2(shear, 0);
Content.Masking = true; Content.Masking = true;
Content.Anchor = Content.Origin = Anchor.Centre; Content.Anchor = Content.Origin = Anchor.Centre;
@ -98,9 +100,9 @@ namespace osu.Game.Graphics.UserInterface
backgroundLayer = new Container backgroundLayer = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
CornerRadius = corner_radius, CornerRadius = CORNER_RADIUS,
Masking = true, Masking = true,
BorderThickness = 2, BorderThickness = BORDER_THICKNESS,
Children = new Drawable[] Children = new Drawable[]
{ {
background = new Box background = new Box

View File

@ -9,11 +9,11 @@ namespace osu.Game.IO.Archives
/// <summary> /// <summary>
/// Allows reading a single file from the provided byte array. /// Allows reading a single file from the provided byte array.
/// </summary> /// </summary>
public class LegacyByteArrayReader : ArchiveReader public class ByteArrayArchiveReader : ArchiveReader
{ {
private readonly byte[] content; private readonly byte[] content;
public LegacyByteArrayReader(byte[] content, string filename) public ByteArrayArchiveReader(byte[] content, string filename)
: base(filename) : base(filename)
{ {
this.content = content; this.content = content;

View File

@ -8,13 +8,13 @@ using System.Linq;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
{ {
/// <summary> /// <summary>
/// Reads an archive from a directory on disk. /// Reads an archive directly from a directory on disk.
/// </summary> /// </summary>
public class LegacyDirectoryArchiveReader : ArchiveReader public class DirectoryArchiveReader : ArchiveReader
{ {
private readonly string path; private readonly string path;
public LegacyDirectoryArchiveReader(string path) public DirectoryArchiveReader(string path)
: base(Path.GetFileName(path)) : base(Path.GetFileName(path))
{ {
// re-get full path to standardise with Directory.GetFiles return values below. // re-get full path to standardise with Directory.GetFiles return values below.

View File

@ -0,0 +1,30 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.IO;
namespace osu.Game.IO.Archives
{
/// <summary>
/// Allows reading a single file from the provided memory stream.
/// </summary>
public class MemoryStreamArchiveReader : ArchiveReader
{
private readonly MemoryStream stream;
public MemoryStreamArchiveReader(MemoryStream stream, string filename)
: base(filename)
{
this.stream = stream;
}
public override Stream GetStream(string name) => new MemoryStream(stream.GetBuffer(), 0, (int)stream.Length);
public override void Dispose()
{
}
public override IEnumerable<string> Filenames => new[] { Name };
}
}

View File

@ -7,14 +7,14 @@ using System.IO;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
{ {
/// <summary> /// <summary>
/// Reads a file on disk as an archive. /// Reads a single file on disk as an archive.
/// Note: In this case, the file is not an extractable archive, use <see cref="ZipArchiveReader"/> instead. /// Note: In this case, the file is not an extractable archive, use <see cref="ZipArchiveReader"/> instead.
/// </summary> /// </summary>
public class LegacyFileArchiveReader : ArchiveReader public class SingleFileArchiveReader : ArchiveReader
{ {
private readonly string path; private readonly string path;
public LegacyFileArchiveReader(string path) public SingleFileArchiveReader(string path)
: base(Path.GetFileName(path)) : base(Path.GetFileName(path))
{ {
// re-get full path to standardise // re-get full path to standardise

View File

@ -1,19 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class DifficultyMultiplierDisplayStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DifficultyMultiplierDisplay";
/// <summary>
/// "Difficulty Multiplier"
/// </summary>
public static LocalisableString DifficultyMultiplier => new TranslatableString(getKey(@"difficulty_multiplier"), @"Difficulty Multiplier");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -44,6 +44,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search..."); public static LocalisableString TabToSearch => new TranslatableString(getKey(@"tab_to_search"), @"tab to search...");
/// <summary>
/// "Score Multiplier"
/// </summary>
public static LocalisableString ScoreMultiplier => new TranslatableString(getKey(@"score_multiplier"), @"Score Multiplier");
private static string getKey(string key) => $@"{prefix}:{key}"; private static string getKey(string key) => $@"{prefix}:{key}";
} }
} }

View File

@ -0,0 +1,188 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
using System.Threading;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// On the mod select overlay, this provides a local updating view of BPM, star rating and other
/// difficulty attributes so the user can have a better insight into what mods are changing.
/// </summary>
public partial class BeatmapAttributesDisplay : ModFooterInformationDisplay
{
private StarRatingDisplay starRatingDisplay = null!;
private BPMDisplay bpmDisplay = null!;
private VerticalAttributeDisplay circleSizeDisplay = null!;
private VerticalAttributeDisplay drainRateDisplay = null!;
private VerticalAttributeDisplay approachRateDisplay = null!;
private VerticalAttributeDisplay overallDifficultyDisplay = null!;
public Bindable<IBeatmapInfo?> BeatmapInfo { get; } = new Bindable<IBeatmapInfo?>();
[Resolved]
private Bindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
public BindableBool Collapsed { get; } = new BindableBool(true);
private ModSettingChangeTracker? modSettingChangeTracker;
[Resolved]
private BeatmapDifficultyCache difficultyCache { get; set; } = null!;
private CancellationTokenSource? cancellationSource;
private IBindable<StarDifficulty?> starDifficulty = null!;
private const float transition_duration = 250;
[BackgroundDependencyLoader]
private void load()
{
const float shear = ShearedOverlayContainer.SHEAR;
LeftContent.AddRange(new Drawable[]
{
starRatingDisplay = new StarRatingDisplay(default, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
},
bpmDisplay = new BPMDisplay
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Shear = new Vector2(-shear, 0),
AutoSizeAxes = Axes.Y,
Width = 75,
}
});
RightContent.Alpha = 0;
RightContent.AddRange(new Drawable[]
{
circleSizeDisplay = new VerticalAttributeDisplay("CS") { Shear = new Vector2(-shear, 0), },
drainRateDisplay = new VerticalAttributeDisplay("HP") { Shear = new Vector2(-shear, 0), },
approachRateDisplay = new VerticalAttributeDisplay("AR") { Shear = new Vector2(-shear, 0), },
overallDifficultyDisplay = new VerticalAttributeDisplay("OD") { Shear = new Vector2(-shear, 0), },
});
}
protected override void LoadComplete()
{
base.LoadComplete();
mods.BindValueChanged(_ =>
{
modSettingChangeTracker?.Dispose();
modSettingChangeTracker = new ModSettingChangeTracker(mods.Value);
modSettingChangeTracker.SettingChanged += _ => updateValues();
updateValues();
}, true);
BeatmapInfo.BindValueChanged(_ => updateValues(), true);
Collapsed.BindValueChanged(_ =>
{
// Only start autosize animations on first collapse toggle. This avoids an ugly initial presentation.
startAnimating();
updateCollapsedState();
});
updateCollapsedState();
}
protected override bool OnHover(HoverEvent e)
{
startAnimating();
updateCollapsedState();
return true;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateCollapsedState();
base.OnHoverLost(e);
}
protected override bool OnMouseDown(MouseDownEvent e) => true;
protected override bool OnClick(ClickEvent e) => true;
private void startAnimating()
{
Content.AutoSizeEasing = Easing.OutQuint;
Content.AutoSizeDuration = transition_duration;
}
private void updateCollapsedState()
{
RightContent.FadeTo(Collapsed.Value && !IsHovered ? 0 : 1, transition_duration, Easing.OutQuint);
}
private void updateValues() => Scheduler.AddOnce(() =>
{
if (BeatmapInfo.Value == null)
return;
cancellationSource?.Cancel();
starDifficulty = difficultyCache.GetBindableDifficulty(BeatmapInfo.Value, (cancellationSource = new CancellationTokenSource()).Token);
starDifficulty.BindValueChanged(s =>
{
starRatingDisplay.Current.Value = s.NewValue ?? default;
if (!starRatingDisplay.IsPresent)
starRatingDisplay.FinishTransforms(true);
});
double rate = 1;
foreach (var mod in mods.Value.OfType<IApplicableToRate>())
rate = mod.ApplyToRate(0, rate);
bpmDisplay.Current.Value = BeatmapInfo.Value.BPM * rate;
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
foreach (var mod in mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(adjustedDifficulty);
circleSizeDisplay.Current.Value = adjustedDifficulty.CircleSize;
drainRateDisplay.Current.Value = adjustedDifficulty.DrainRate;
approachRateDisplay.Current.Value = adjustedDifficulty.ApproachRate;
overallDifficultyDisplay.Current.Value = adjustedDifficulty.OverallDifficulty;
});
private partial class BPMDisplay : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Font = OsuFont.Default.With(size: 20, weight: FontWeight.SemiBold),
UseFullGlyphHeight = false,
};
}
}
}

View File

@ -1,41 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osuTK;
using osu.Game.Localisation;
namespace osu.Game.Overlays.Mods
{
public sealed partial class DifficultyMultiplierDisplay : ModsEffectDisplay
{
protected override LocalisableString Label => DifficultyMultiplierDisplayStrings.DifficultyMultiplier;
protected override string CounterFormat => @"N2";
public DifficultyMultiplierDisplay()
{
Current.Default = 1d;
Current.Value = 1d;
Add(new SpriteIcon
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Icon = FontAwesome.Solid.Times,
Size = new Vector2(7),
Margin = new MarginPadding { Top = 1 }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
// required to prevent the counter initially rolling up from 0 to 1
// due to `Current.Value` having a nonstandard default value of 1.
Counter.SetCountWithoutRolling(Current.Value);
}
}
}

View File

@ -0,0 +1,109 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterface;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods
{
public abstract partial class ModFooterInformationDisplay : CompositeDrawable
{
protected FillFlowContainer LeftContent { get; private set; } = null!;
protected FillFlowContainer RightContent { get; private set; } = null!;
protected Container Content { get; private set; } = null!;
private Container innerContent = null!;
protected Box MainBackground { get; private set; } = null!;
protected Box FrontBackground { get; private set; } = null!;
[Resolved]
protected OverlayColourProvider ColourProvider { get; private set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
InternalChild = Content = new Container
{
Origin = Anchor.BottomRight,
Anchor = Anchor.BottomRight,
AutoSizeAxes = Axes.X,
Height = ShearedButton.HEIGHT,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
CornerRadius = ShearedButton.CORNER_RADIUS,
BorderThickness = ShearedButton.BORDER_THICKNESS,
Masking = true,
Children = new Drawable[]
{
MainBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer // divide inner and outer content
{
Origin = Anchor.BottomLeft,
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
innerContent = new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
BorderThickness = ShearedButton.BORDER_THICKNESS,
CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
FrontBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
LeftContent = new FillFlowContainer // actual inner content
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Margin = new MarginPadding { Horizontal = 15 },
Spacing = new Vector2(10),
}
}
},
RightContent = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
}
}
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
MainBackground.Colour = ColourProvider.Background4;
FrontBackground.Colour = ColourProvider.Background3;
Color4 glowColour = ColourProvider.Background1;
Content.BorderColour = ColourInfo.GradientVertical(MainBackground.Colour, glowColour);
innerContent.BorderColour = ColourInfo.GradientVertical(FrontBackground.Colour, glowColour);
}
}
}

View File

@ -26,6 +26,8 @@ namespace osu.Game.Overlays.Mods
[Resolved] [Resolved]
private IBindable<RulesetInfo> ruleset { get; set; } = null!; private IBindable<RulesetInfo> ruleset { get; set; } = null!;
private const float contracted_width = WIDTH - 120;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
@ -42,6 +44,8 @@ namespace osu.Game.Overlays.Mods
base.LoadComplete(); base.LoadComplete();
ruleset.BindValueChanged(_ => rulesetChanged(), true); ruleset.BindValueChanged(_ => rulesetChanged(), true);
Width = contracted_width;
} }
private IDisposable? presetSubscription; private IDisposable? presetSubscription;
@ -65,7 +69,11 @@ namespace osu.Game.Overlays.Mods
{ {
cancellationTokenSource?.Cancel(); cancellationTokenSource?.Cancel();
if (!presets.Any()) bool hasPresets = presets.Any();
this.ResizeWidthTo(hasPresets ? WIDTH : contracted_width, 200, Easing.OutQuint);
if (!hasPresets)
{ {
removeAndDisposePresetPanels(); removeAndDisposePresetPanels();
return; return;

View File

@ -61,9 +61,11 @@ namespace osu.Game.Overlays.Mods
private const float header_height = 42; private const float header_height = 42;
protected const float WIDTH = 320;
protected ModSelectColumn() protected ModSelectColumn()
{ {
Width = 320; Width = WIDTH;
RelativeSizeAxes = Axes.Y; RelativeSizeAxes = Axes.Y;
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0); Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0);

View File

@ -17,6 +17,7 @@ using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -77,9 +78,9 @@ namespace osu.Game.Overlays.Mods
public ShearedSearchTextBox SearchTextBox { get; private set; } = null!; public ShearedSearchTextBox SearchTextBox { get; private set; } = null!;
/// <summary> /// <summary>
/// Whether the total score multiplier calculated from the current selected set of mods should be shown. /// Whether the effects (on score multiplier, on or beatmap difficulty) of the current selected set of mods should be shown.
/// </summary> /// </summary>
protected virtual bool ShowTotalMultiplier => true; protected virtual bool ShowModEffects => true;
/// <summary> /// <summary>
/// Whether per-mod customisation controls are visible. /// Whether per-mod customisation controls are visible.
@ -119,10 +120,12 @@ namespace osu.Game.Overlays.Mods
private ColumnScrollContainer columnScroll = null!; private ColumnScrollContainer columnScroll = null!;
private ColumnFlowContainer columnFlow = null!; private ColumnFlowContainer columnFlow = null!;
private FillFlowContainer<ShearedButton> footerButtonFlow = null!; private FillFlowContainer<ShearedButton> footerButtonFlow = null!;
private FillFlowContainer footerContentFlow = null!;
private DeselectAllModsButton deselectAllModsButton = null!; private DeselectAllModsButton deselectAllModsButton = null!;
private Container aboveColumnsContent = null!; private Container aboveColumnsContent = null!;
private DifficultyMultiplierDisplay? multiplierDisplay; private ScoreMultiplierDisplay? multiplierDisplay;
private BeatmapAttributesDisplay? beatmapAttributesDisplay;
protected ShearedButton BackButton { get; private set; } = null!; protected ShearedButton BackButton { get; private set; } = null!;
protected ShearedToggleButton? CustomisationButton { get; private set; } protected ShearedToggleButton? CustomisationButton { get; private set; }
@ -130,6 +133,21 @@ namespace osu.Game.Overlays.Mods
private Sample? columnAppearSample; private Sample? columnAppearSample;
private WorkingBeatmap? beatmap;
public WorkingBeatmap? Beatmap
{
get => beatmap;
set
{
if (beatmap == value) return;
beatmap = value;
if (IsLoaded && beatmapAttributesDisplay != null)
beatmapAttributesDisplay.BeatmapInfo.Value = beatmap?.BeatmapInfo;
}
}
protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green) protected ModSelectOverlay(OverlayColourScheme colourScheme = OverlayColourScheme.Green)
: base(colourScheme) : base(colourScheme)
{ {
@ -164,7 +182,7 @@ namespace osu.Game.Overlays.Mods
aboveColumnsContent = new Container aboveColumnsContent = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = ModsEffectDisplay.HEIGHT, Height = ScoreMultiplierDisplay.HEIGHT,
Padding = new MarginPadding { Horizontal = 100 }, Padding = new MarginPadding { Horizontal = 100 },
Child = SearchTextBox = new ShearedSearchTextBox Child = SearchTextBox = new ShearedSearchTextBox
{ {
@ -179,7 +197,7 @@ namespace osu.Game.Overlays.Mods
{ {
Padding = new MarginPadding Padding = new MarginPadding
{ {
Top = ModsEffectDisplay.HEIGHT + PADDING, Top = ScoreMultiplierDisplay.HEIGHT + PADDING,
Bottom = PADDING Bottom = PADDING
}, },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -210,16 +228,7 @@ namespace osu.Game.Overlays.Mods
} }
}); });
if (ShowTotalMultiplier) FooterContent.Add(footerButtonFlow = new FillFlowContainer<ShearedButton>
{
aboveColumnsContent.Add(multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight
});
}
FooterContent.Child = footerButtonFlow = new FillFlowContainer<ShearedButton>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -239,7 +248,38 @@ namespace osu.Game.Overlays.Mods
DarkerColour = colours.Pink2, DarkerColour = colours.Pink2,
LighterColour = colours.Pink1 LighterColour = colours.Pink1
}) })
}; });
if (ShowModEffects)
{
FooterContent.Add(footerContentFlow = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(30, 10),
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
Margin = new MarginPadding
{
Vertical = PADDING,
Horizontal = 20
},
Children = new Drawable[]
{
multiplierDisplay = new ScoreMultiplierDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight
},
beatmapAttributesDisplay = new BeatmapAttributesDisplay
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
BeatmapInfo = { Value = beatmap?.BeatmapInfo }
},
}
});
}
globalAvailableMods.BindTo(game.AvailableMods); globalAvailableMods.BindTo(game.AvailableMods);
} }
@ -309,6 +349,25 @@ namespace osu.Game.Overlays.Mods
base.Update(); base.Update();
SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch; SearchTextBox.PlaceholderText = SearchTextBox.HasFocus ? Resources.Localisation.Web.CommonStrings.InputSearch : ModSelectOverlayStrings.TabToSearch;
if (beatmapAttributesDisplay != null)
{
float rightEdgeOfLastButton = footerButtonFlow.Last().ScreenSpaceDrawQuad.TopRight.X;
// this is cheating a bit; the 640 value is hardcoded based on how wide the expanded panel _generally_ is.
// due to the transition applied, the raw screenspace quad of the panel cannot be used, as it will trigger an ugly feedback cycle of expanding and collapsing.
float projectedLeftEdgeOfExpandedBeatmapAttributesDisplay = footerButtonFlow.ToScreenSpace(footerButtonFlow.DrawSize - new Vector2(640, 0)).X;
bool screenIsntWideEnough = rightEdgeOfLastButton > projectedLeftEdgeOfExpandedBeatmapAttributesDisplay;
// only update preview panel's collapsed state after we are fully visible, to ensure all the buttons are where we expect them to be.
if (Alpha == 1)
beatmapAttributesDisplay.Collapsed.Value = screenIsntWideEnough;
footerContentFlow.LayoutDuration = 200;
footerContentFlow.LayoutEasing = Easing.OutQuint;
footerContentFlow.Direction = screenIsntWideEnough ? FillDirection.Vertical : FillDirection.Horizontal;
}
} }
/// <summary> /// <summary>
@ -886,6 +945,9 @@ namespace osu.Game.Overlays.Mods
OnClicked?.Invoke(); OnClicked?.Invoke();
return true; return true;
case HoverEvent:
return false;
case MouseEvent: case MouseEvent:
return true; return true;
} }

View File

@ -1,223 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// Base class for displays of mods effects.
/// </summary>
public abstract partial class ModsEffectDisplay : Container, IHasCurrentValue<double>
{
public const float HEIGHT = 42;
private const float transition_duration = 200;
private readonly Box contentBackground;
private readonly Box labelBackground;
private readonly FillFlowContainer content;
public Bindable<double> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
/// <summary>
/// Text to display in the left area of the display.
/// </summary>
protected abstract LocalisableString Label { get; }
protected virtual float ValueAreaWidth => 56;
protected virtual string CounterFormat => @"N0";
protected override Container<Drawable> Content => content;
protected readonly RollingCounter<double> Counter;
protected ModsEffectDisplay()
{
Height = HEIGHT;
AutoSizeAxes = Axes.X;
InternalChild = new InputBlockingContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
Children = new Drawable[]
{
contentBackground = new Box
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Y,
Width = ValueAreaWidth + ModSelectPanel.CORNER_RADIUS
},
new GridContainer
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, ValueAreaWidth)
},
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Y,
AutoSizeAxes = Axes.X,
Masking = true,
CornerRadius = ModSelectPanel.CORNER_RADIUS,
Children = new Drawable[]
{
labelBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Margin = new MarginPadding { Horizontal = 18 },
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Text = Label,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
}
}
},
content = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Direction = FillDirection.Horizontal,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Spacing = new Vector2(2, 0),
Child = Counter = new EffectCounter(CounterFormat)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Current = { BindTarget = Current }
}
}
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load()
{
labelBackground.Colour = colourProvider.Background4;
}
protected override void LoadComplete()
{
Current.BindValueChanged(e =>
{
var effect = CalculateEffectForComparison(e.NewValue.CompareTo(Current.Default));
setColours(effect);
}, true);
}
/// <summary>
/// Fades colours of text and its background according to displayed value.
/// </summary>
/// <param name="effect">Effect of the value.</param>
private void setColours(ModEffect effect)
{
switch (effect)
{
case ModEffect.NotChanged:
contentBackground.FadeColour(colourProvider.Background3, transition_duration, Easing.OutQuint);
content.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
break;
case ModEffect.DifficultyReduction:
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
break;
case ModEffect.DifficultyIncrease:
contentBackground.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
content.FadeColour(colourProvider.Background5, transition_duration, Easing.OutQuint);
break;
default:
throw new ArgumentOutOfRangeException(nameof(effect));
}
}
/// <summary>
/// Converts signed integer into <see cref="ModEffect"/>. Negative values are counted as difficulty reduction, positive as increase.
/// </summary>
/// <param name="comparison">Value to convert. Will arrive from comparison between <see cref="Current"/> bindable once it changes and it's <see cref="Bindable{T}.Default"/>.</param>
/// <returns>Effect of the value.</returns>
protected virtual ModEffect CalculateEffectForComparison(int comparison)
{
if (comparison == 0)
return ModEffect.NotChanged;
if (comparison < 0)
return ModEffect.DifficultyReduction;
return ModEffect.DifficultyIncrease;
}
protected enum ModEffect
{
NotChanged,
DifficultyReduction,
DifficultyIncrease
}
private partial class EffectCounter : RollingCounter<double>
{
private readonly string? format;
public EffectCounter(string? format)
{
this.format = format;
}
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(format);
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
};
}
}
}

View File

@ -0,0 +1,160 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Rulesets.Mods;
using osuTK;
namespace osu.Game.Overlays.Mods
{
/// <summary>
/// On the mod select overlay, this provides a local updating view of the aggregate score multiplier coming from mods.
/// </summary>
public partial class ScoreMultiplierDisplay : ModFooterInformationDisplay, IHasCurrentValue<double>
{
public const float HEIGHT = 42;
public Bindable<double> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
private const float transition_duration = 200;
private RollingCounter<double> counter = null!;
private Box flashLayer = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
public ScoreMultiplierDisplay()
{
Current.Default = 1d;
Current.Value = 1d;
}
[BackgroundDependencyLoader]
private void load()
{
// You would think that we could add this to `Content`, but borders don't mix well
// with additive blending children elements.
AddInternal(new Container
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
RelativeSizeAxes = Axes.Both,
Shear = new Vector2(ShearedOverlayContainer.SHEAR, 0),
CornerRadius = ShearedButton.CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
flashLayer = new Box
{
Alpha = 0,
Blending = BlendingParameters.Additive,
RelativeSizeAxes = Axes.Both,
}
}
});
LeftContent.AddRange(new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Text = ModSelectOverlayStrings.ScoreMultiplier,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
}
});
RightContent.Add(new Container
{
Width = 40,
RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding(10),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = counter = new EffectCounter
{
Shear = new Vector2(-ShearedOverlayContainer.SHEAR, 0),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Current = { BindTarget = Current }
}
});
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.BindValueChanged(e =>
{
if (e.NewValue > Current.Default)
{
MainBackground
.FadeColour(colours.ForModType(ModType.DifficultyIncrease), transition_duration, Easing.OutQuint);
counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
}
else if (e.NewValue < Current.Default)
{
MainBackground
.FadeColour(colours.ForModType(ModType.DifficultyReduction), transition_duration, Easing.OutQuint);
counter.FadeColour(ColourProvider.Background5, transition_duration, Easing.OutQuint);
}
else
{
MainBackground.FadeColour(ColourProvider.Background4, transition_duration, Easing.OutQuint);
counter.FadeColour(Colour4.White, transition_duration, Easing.OutQuint);
}
flashLayer
.FadeOutFromOne()
.FadeTo(0.15f, 60, Easing.OutQuint)
.Then().FadeOut(500, Easing.OutQuint);
const float move_amount = 4;
if (e.NewValue > e.OldValue)
counter.MoveToY(Math.Max(-move_amount * 2, counter.Y - move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint);
else
counter.MoveToY(Math.Min(move_amount * 2, counter.Y + move_amount)).Then().MoveToY(0, transition_duration * 2, Easing.OutQuint);
}, true);
// required to prevent the counter initially rolling up from 0 to 1
// due to `Current.Value` having a nonstandard default value of 1.
counter.SetCountWithoutRolling(Current.Value);
}
private partial class EffectCounter : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString(@"0.00x");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(size: 17, weight: FontWeight.SemiBold)
};
}
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Mods
{
public partial class VerticalAttributeDisplay : Container, IHasCurrentValue<double>
{
public Bindable<double> Current
{
get => current.Current;
set => current.Current = value;
}
private readonly BindableWithCurrent<double> current = new BindableWithCurrent<double>();
/// <summary>
/// Text to display in the top area of the display.
/// </summary>
public LocalisableString Label { get; protected set; }
public VerticalAttributeDisplay(LocalisableString label)
{
Label = label;
AutoSizeAxes = Axes.X;
Origin = Anchor.CentreLeft;
Anchor = Anchor.CentreLeft;
InternalChild = new FillFlowContainer
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = Label,
Margin = new MarginPadding { Horizontal = 15 }, // to reserve space for 0.XX value
Font = OsuFont.Default.With(size: 20, weight: FontWeight.Bold)
},
new EffectCounter
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Current = { BindTarget = Current },
}
}
};
}
private partial class EffectCounter : RollingCounter<double>
{
protected override double RollingDuration => 500;
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0.0");
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.Default.With(size: 18, weight: FontWeight.SemiBold)
};
}
}
}

View File

@ -18,12 +18,13 @@ using osuTK;
namespace osu.Game.Overlays.Profile.Header.Components namespace osu.Game.Overlays.Profile.Header.Components
{ {
public partial class PreviousUsernames : CompositeDrawable public partial class PreviousUsernamesDisplay : CompositeDrawable
{ {
private const int duration = 200; private const int duration = 200;
private const int margin = 10; private const int margin = 10;
private const int width = 310; private const int width = 300;
private const int move_offset = 15; private const int move_offset = 15;
private const int base_y_offset = -3; // eye balled to make it look good
public readonly Bindable<APIUser?> User = new Bindable<APIUser?>(); public readonly Bindable<APIUser?> User = new Bindable<APIUser?>();
@ -31,14 +32,15 @@ namespace osu.Game.Overlays.Profile.Header.Components
private readonly Box background; private readonly Box background;
private readonly SpriteText header; private readonly SpriteText header;
public PreviousUsernames() public PreviousUsernamesDisplay()
{ {
HoverIconContainer hoverIcon; HoverIconContainer hoverIcon;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Width = width; Width = width;
Masking = true; Masking = true;
CornerRadius = 5; CornerRadius = 6;
Y = base_y_offset;
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
@ -84,6 +86,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full, Direction = FillDirection.Full,
// Prevents the tooltip of having a sudden size reduction and flickering when the text is being faded out.
// Also prevents a potential OnHover/HoverLost feedback loop.
AlwaysPresent = true,
Margin = new MarginPadding { Bottom = margin, Top = margin / 2f } Margin = new MarginPadding { Bottom = margin, Top = margin / 2f }
} }
} }
@ -96,9 +101,9 @@ namespace osu.Game.Overlays.Profile.Header.Components
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OverlayColourProvider colours)
{ {
background.Colour = colours.GreySeaFoamDarker; background.Colour = colours.Background6;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -134,7 +139,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
text.FadeIn(duration, Easing.OutQuint); text.FadeIn(duration, Easing.OutQuint);
header.FadeIn(duration, Easing.OutQuint); header.FadeIn(duration, Easing.OutQuint);
background.FadeIn(duration, Easing.OutQuint); background.FadeIn(duration, Easing.OutQuint);
this.MoveToY(-move_offset, duration, Easing.OutQuint); this.MoveToY(base_y_offset - move_offset, duration, Easing.OutQuint);
} }
private void hideContent() private void hideContent()
@ -142,7 +147,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
text.FadeOut(duration, Easing.OutQuint); text.FadeOut(duration, Easing.OutQuint);
header.FadeOut(duration, Easing.OutQuint); header.FadeOut(duration, Easing.OutQuint);
background.FadeOut(duration, Easing.OutQuint); background.FadeOut(duration, Easing.OutQuint);
this.MoveToY(0, duration, Easing.OutQuint); this.MoveToY(base_y_offset, duration, Easing.OutQuint);
} }
private partial class HoverIconContainer : Container private partial class HoverIconContainer : Container
@ -156,7 +161,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
{ {
Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 }, Margin = new MarginPadding { Top = 6, Left = margin, Right = margin * 2 },
Size = new Vector2(15), Size = new Vector2(15),
Icon = FontAwesome.Solid.IdCard, Icon = FontAwesome.Solid.AddressCard,
}; };
} }

View File

@ -46,6 +46,7 @@ namespace osu.Game.Overlays.Profile.Header
private OsuSpriteText userCountryText = null!; private OsuSpriteText userCountryText = null!;
private GroupBadgeFlow groupBadgeFlow = null!; private GroupBadgeFlow groupBadgeFlow = null!;
private ToggleCoverButton coverToggle = null!; private ToggleCoverButton coverToggle = null!;
private PreviousUsernamesDisplay previousUsernamesDisplay = null!;
private Bindable<bool> coverExpanded = null!; private Bindable<bool> coverExpanded = null!;
@ -143,6 +144,11 @@ namespace osu.Game.Overlays.Profile.Header
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft, Origin = Anchor.CentreLeft,
}, },
new Container
{
// Intentionally use a zero-size container, else the fill flow will adjust to (and cancel) the upwards animation.
Child = previousUsernamesDisplay = new PreviousUsernamesDisplay(),
}
} }
}, },
titleText = new OsuSpriteText titleText = new OsuSpriteText
@ -216,6 +222,7 @@ namespace osu.Game.Overlays.Profile.Header
titleText.Text = user?.Title ?? string.Empty; titleText.Text = user?.Title ?? string.Empty;
titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff"); titleText.Colour = Color4Extensions.FromHex(user?.Colour ?? "fff");
groupBadgeFlow.User.Value = user; groupBadgeFlow.User.Value = user;
previousUsernamesDisplay.User.Value = user;
} }
private void updateCoverState() private void updateCoverState()

View File

@ -0,0 +1,24 @@
// 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.Localisation;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings
{
public partial class MultiplierSettingsSlider : SettingsSlider<double, MultiplierSettingsSlider.MultiplierRoundedSliderBar>
{
public MultiplierSettingsSlider()
{
KeyboardStep = 0.01f;
}
/// <summary>
/// A slider bar which adds a "x" to the end of the tooltip string.
/// </summary>
public partial class MultiplierRoundedSliderBar : RoundedSliderBar<double>
{
public override LocalisableString TooltipText => $"{base.TooltipText}x";
}
}
}

View File

@ -29,7 +29,14 @@ namespace osu.Game.Rulesets.Mods
/// </remarks> /// </remarks>
private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>(); private readonly BindableNumber<float> sliderDisplayCurrent = new BindableNumber<float>();
protected override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent); protected sealed override Drawable CreateControl() => new SliderControl(sliderDisplayCurrent, CreateSlider);
protected virtual RoundedSliderBar<float> CreateSlider(BindableNumber<float> current) => new RoundedSliderBar<float>
{
RelativeSizeAxes = Axes.X,
Current = current,
KeyboardStep = 0.1f,
};
/// <summary> /// <summary>
/// Guards against beatmap values displayed on slider bars being transferred to user override. /// Guards against beatmap values displayed on slider bars being transferred to user override.
@ -100,16 +107,11 @@ namespace osu.Game.Rulesets.Mods
set => current.Current = value; set => current.Current = value;
} }
public SliderControl(BindableNumber<float> currentNumber) public SliderControl(BindableNumber<float> currentNumber, Func<BindableNumber<float>, RoundedSliderBar<float>> createSlider)
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new RoundedSliderBar<float> createSlider(currentNumber)
{
RelativeSizeAxes = Axes.X,
Current = currentNumber,
KeyboardStep = 0.1f,
}
}; };
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;

View File

@ -34,9 +34,18 @@ namespace osu.Game.Rulesets.Mods
set => CurrentNumber.Precision = value; set => CurrentNumber.Precision = value;
} }
private float minValue;
public float MinValue public float MinValue
{ {
set => CurrentNumber.MinValue = value; set
{
if (value == minValue)
return;
minValue = value;
updateExtents();
}
} }
private float maxValue; private float maxValue;
@ -49,7 +58,24 @@ namespace osu.Game.Rulesets.Mods
return; return;
maxValue = value; maxValue = value;
updateMaxValue(); updateExtents();
}
}
private float? extendedMinValue;
/// <summary>
/// The minimum value to be used when extended limits are applied.
/// </summary>
public float? ExtendedMinValue
{
set
{
if (value == extendedMinValue)
return;
extendedMinValue = value;
updateExtents();
} }
} }
@ -66,7 +92,7 @@ namespace osu.Game.Rulesets.Mods
return; return;
extendedMaxValue = value; extendedMaxValue = value;
updateMaxValue(); updateExtents();
} }
} }
@ -78,7 +104,7 @@ namespace osu.Game.Rulesets.Mods
public DifficultyBindable(float? defaultValue = null) public DifficultyBindable(float? defaultValue = null)
: base(defaultValue) : base(defaultValue)
{ {
ExtendedLimits.BindValueChanged(_ => updateMaxValue()); ExtendedLimits.BindValueChanged(_ => updateExtents());
} }
public override float? Value public override float? Value
@ -94,8 +120,9 @@ namespace osu.Game.Rulesets.Mods
} }
} }
private void updateMaxValue() private void updateExtents()
{ {
CurrentNumber.MinValue = ExtendedLimits.Value && extendedMinValue != null ? extendedMinValue.Value : minValue;
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue; CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Localisation;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) }; public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust), typeof(ModTimeRamp), typeof(ModAutoplay) };
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track", SettingControlType = typeof(MultiplierSettingsSlider))]
public BindableNumber<double> InitialRate { get; } = new BindableDouble(1) public BindableNumber<double> InitialRate { get; } = new BindableDouble(1)
{ {
MinValue = 0.5, MinValue = 0.5,

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyIncrease; public override ModType Type => ModType.DifficultyIncrease;
public override LocalisableString Description => "Zoooooooooom..."; public override LocalisableString Description => "Zoooooooooom...";
[SettingSource("Speed increase", "The actual increase to apply")] [SettingSource("Speed increase", "The actual increase to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(1.5)
{ {
MinValue = 1.01, MinValue = 1.01,

View File

@ -6,6 +6,7 @@ using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -17,7 +18,7 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.DifficultyReduction; public override ModType Type => ModType.DifficultyReduction;
public override LocalisableString Description => "Less zoom..."; public override LocalisableString Description => "Less zoom...";
[SettingSource("Speed decrease", "The actual decrease to apply")] [SettingSource("Speed decrease", "The actual decrease to apply", SettingControlType = typeof(MultiplierSettingsSlider))]
public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75) public override BindableNumber<double> SpeedChange { get; } = new BindableDouble(0.75)
{ {
MinValue = 0.5, MinValue = 0.5,

View File

@ -7,6 +7,7 @@ using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
@ -20,10 +21,10 @@ namespace osu.Game.Rulesets.Mods
public override double ScoreMultiplier => 0.5; public override double ScoreMultiplier => 0.5;
[SettingSource("Initial rate", "The starting speed of the track")] [SettingSource("Initial rate", "The starting speed of the track", SettingControlType = typeof(MultiplierSettingsSlider))]
public abstract BindableNumber<double> InitialRate { get; } public abstract BindableNumber<double> InitialRate { get; }
[SettingSource("Final rate", "The final speed to ramp to")] [SettingSource("Final rate", "The final speed to ramp to", SettingControlType = typeof(MultiplierSettingsSlider))]
public abstract BindableNumber<double> FinalRate { get; } public abstract BindableNumber<double> FinalRate { get; }
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]

View File

@ -0,0 +1,42 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy
{
public static class LegacyRulesetExtensions
{
/// <summary>
/// Introduces floating-point errors to post-multiplied beat length for legacy rulesets that depend on it.
/// You should definitely not use this unless you know exactly what you're doing.
/// </summary>
public static double GetPrecisionAdjustedBeatLength(IHasSliderVelocity hasSliderVelocity, TimingControlPoint timingControlPoint, string rulesetShortName)
{
double sliderVelocityAsBeatLength = -100 / hasSliderVelocity.SliderVelocityMultiplier;
// Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?).
double bpmMultiplier;
switch (rulesetShortName)
{
case "taiko":
case "mania":
bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 10000) / 100.0 : 1;
break;
case "osu":
case "fruits":
bpmMultiplier = sliderVelocityAsBeatLength < 0 ? Math.Clamp((float)-sliderVelocityAsBeatLength, 10, 1000) / 100.0 : 1;
break;
default:
throw new ArgumentException("Must be a legacy ruleset", nameof(rulesetShortName));
}
return timingControlPoint.BeatLength * bpmMultiplier;
}
}
}

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables; using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Objects.Types namespace osu.Game.Rulesets.Objects.Types
@ -17,23 +16,5 @@ namespace osu.Game.Rulesets.Objects.Types
double SliderVelocityMultiplier { get; set; } double SliderVelocityMultiplier { get; set; }
BindableNumber<double> SliderVelocityMultiplierBindable { get; } BindableNumber<double> SliderVelocityMultiplierBindable { get; }
/// <summary>
/// Introduces floating-point errors for rulesets that depend on it.
/// </summary>
public double GetPrecisionAdjustedSliderVelocityMultiplier(string rulesetShortName)
{
double beatLength = -100 / SliderVelocityMultiplier;
switch (rulesetShortName)
{
case "taiko":
case "mania":
return 1 / (beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1);
default:
return 1 / (beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 1000) / 100.0 : 1);
}
}
} }
} }

View File

@ -126,9 +126,12 @@ namespace osu.Game.Screens
private void load(ShaderManager manager) private void load(ShaderManager manager)
{ {
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2_NO_MASKING, FragmentShaderDescriptor.BLUR)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, FragmentShaderDescriptor.BLUR));
loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder")); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_2, "TriangleBorder"));
loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE)); loadTargets.Add(manager.Load(VertexShaderDescriptor.TEXTURE_3, FragmentShaderDescriptor.TEXTURE));
} }

View File

@ -14,7 +14,7 @@ namespace osu.Game.Screens.OnlinePlay
{ {
public partial class FreeModSelectOverlay : ModSelectOverlay public partial class FreeModSelectOverlay : ModSelectOverlay
{ {
protected override bool ShowTotalMultiplier => false; protected override bool ShowModEffects => false;
protected override bool AllowCustomisation => false; protected override bool AllowCustomisation => false;

View File

@ -1144,14 +1144,14 @@ namespace osu.Game.Screens.Play
if (DrawableRuleset.ReplayScore != null) if (DrawableRuleset.ReplayScore != null)
return Task.CompletedTask; return Task.CompletedTask;
LegacyByteArrayReader replayReader = null; ByteArrayArchiveReader replayReader = null;
if (score.ScoreInfo.Ruleset.IsLegacyRuleset()) if (score.ScoreInfo.Ruleset.IsLegacyRuleset())
{ {
using (var stream = new MemoryStream()) using (var stream = new MemoryStream())
{ {
new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream); new LegacyScoreEncoder(score, GameplayState.Beatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); replayReader = new ByteArrayArchiveReader(stream.ToArray(), "replay.osr");
} }
} }

View File

@ -792,6 +792,8 @@ namespace osu.Game.Screens.Select
BeatmapDetails.Beatmap = beatmap; BeatmapDetails.Beatmap = beatmap;
ModSelect.Beatmap = beatmap;
bool beatmapSelected = beatmap is not DummyWorkingBeatmap; bool beatmapSelected = beatmap is not DummyWorkingBeatmap;
if (beatmapSelected) if (beatmapSelected)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Skinning
{ {
public class LegacyBeatmapSkin : LegacySkin public class LegacyBeatmapSkin : LegacySkin
{ {
protected override bool AllowManiaSkin => false; protected override bool AllowManiaConfigLookups => false;
protected override bool UseCustomSampleBanks => true; protected override bool UseCustomSampleBanks => true;
/// <summary> /// <summary>

View File

@ -30,13 +30,7 @@ namespace osu.Game.Skinning
{ {
public class LegacySkin : Skin public class LegacySkin : Skin
{ {
/// <summary> protected virtual bool AllowManiaConfigLookups => true;
/// Whether texture for the keys exists.
/// Used to determine if the mania ruleset is skinned.
/// </summary>
private readonly Lazy<bool> hasKeyTexture;
protected virtual bool AllowManiaSkin => hasKeyTexture.Value;
/// <summary> /// <summary>
/// Whether this skin can use samples with a custom bank (custom sample set in stable terminology). /// Whether this skin can use samples with a custom bank (custom sample set in stable terminology).
@ -62,10 +56,6 @@ namespace osu.Game.Skinning
protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini") protected LegacySkin(SkinInfo skin, IStorageResourceProvider? resources, IResourceStore<byte[]>? storage, string configurationFilename = @"skin.ini")
: base(skin, resources, storage, configurationFilename) : base(skin, resources, storage, configurationFilename)
{ {
// todo: this shouldn't really be duplicated here (from ManiaLegacySkinTransformer). we need to come up with a better solution.
hasKeyTexture = new Lazy<bool>(() => this.GetAnimation(
lookupForMania<string>(new LegacyManiaSkinConfigurationLookup(4, LegacyManiaSkinConfigurationLookups.KeyImage, 0))?.Value ?? "mania-key1", true,
true) != null);
} }
protected override void ParseConfigurationStream(Stream stream) protected override void ParseConfigurationStream(Stream stream)
@ -115,7 +105,7 @@ namespace osu.Game.Skinning
return SkinUtils.As<TValue>(getCustomColour(Configuration, customColour.Lookup.ToString() ?? string.Empty)); return SkinUtils.As<TValue>(getCustomColour(Configuration, customColour.Lookup.ToString() ?? string.Empty));
case LegacyManiaSkinConfigurationLookup maniaLookup: case LegacyManiaSkinConfigurationLookup maniaLookup:
if (!AllowManiaSkin) if (!AllowManiaConfigLookups)
break; break;
var result = lookupForMania<TValue>(maniaLookup); var result = lookupForMania<TValue>(maniaLookup);

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.1.2" /> <PackageReference Include="Realm" Version="11.1.2" />
<PackageReference Include="ppy.osu.Framework" Version="2023.904.0" /> <PackageReference Include="ppy.osu.Framework" Version="2023.914.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2023.822.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2023.914.0" />
<PackageReference Include="Sentry" Version="3.28.1" /> <PackageReference Include="Sentry" Version="3.28.1" />
<PackageReference Include="SharpCompress" Version="0.32.2" /> <PackageReference Include="SharpCompress" Version="0.32.2" />
<PackageReference Include="NUnit" Version="3.13.3" /> <PackageReference Include="NUnit" Version="3.13.3" />

View File

@ -23,6 +23,6 @@
<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier> <RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.iOS" Version="2023.904.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2023.914.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>