mirror of
https://github.com/ppy/osu.git
synced 2026-05-27 09:19:56 +08:00
9727d95ad9
- Part of https://github.com/ppy/osu/issues/37818
During review, I would like to direct particular attention to the
following changes:
## [Migrate song select to new score multiplier
API](https://github.com/ppy/osu/commit/945fd78539da3ae57d1550a5bbfb0f859d153cc4)
This was a confusing change to write because of the way song selects
hook their mod overlays up to global bindables. In particular different
things happen in different circumstances.
- When going through `SongSelect.CreateModOverlay()`, which is called by
the base `SongSelect`, the mod overlay is automatically bound to global
bindables via `SongSelect.on{ArrivingAt,Leaving}Screen()`.
- For multiplayer user mod select overlays, which are bolted on by
subclasses of `SongSelect`, manual hook-up is required.
- As for free mod select overlays, they don't show mod multipliers at
all, and don't have easy access to the ruleset, and thus the hookup is
skipped entirely as redundant.
## [Fix score multiplier registrations being shared between
implementations via superclass static
fields](https://github.com/ppy/osu/commit/ba0a7ad421e0c84c2d8162b6bbdd3a0683f5a6a6)
Revealed by `ScoreMultiplierCalculatorTest` starting to fail due to
interference from `OsuScoreMultiplierCalculator`.
It's not ideal from a performance standpoint but it's the simplest
choice for now. Tricks could be pulled to salvage the static. One is
```csharp
public class ScoreMultiplierCalculator<T>
where T : ScoreMultiplierCalculator<T>
{
}
```
This works because of generics internals; static instance members are
not shared between different specialisations of a generic class. It is
also very unintuitive, so I would rather not. (It trips a ReSharper
inspection too, which would have to be silenced.)
From a performance standpoint this is not ideal, but a significant chunk
of migrated usages already precede the construction of the calculator
via the known-expensive `RulesetInfo.CreateInstance()`, and the paths
that actually construct the calculator do not appear to be that hot. If
need be, this can be handled by actually caching ruleset instances and
their derivative subcomponents.
## [Introduce passing of context to score multiplier
calculator](https://github.com/ppy/osu/pull/37845/changes/9e9242b3221dddacd226f4b3b9c5632d7350e998)
This is required for two reasons:
- The upcoming mod rebalance will require out-of-band supplementary
information that is not available for reading from the mod instances
themselves for calculating the multiplier.
- This context, namely passing of `ScoreInfo`, will be used for
implementing backwards compatibility with old scores and their score
multipliers. This is required because it has turned out under inspection
that all server-side lazer replays recorded until now are missing
`TotalScoreWithoutMods` due to an omission of not sending it across the
wire to spectator server.
Because the score import flow uses replays, filtered through
`LegacyScoreDecoder`, to populate total score in the realm database, it
is basically impossible to ignore scores that are missing
`TotalScoreWithoutMods`, because that will result in bug reports that
the scores do not have the new score multipliers applied.
Thus, passing of `ScoreInfo` will facilitate implementation of
versioning score multipliers, which should result in less breakage than
not doing so.
An example of this is added in 341b2d6e55,
which should handle the case of mania mod multipliers having been
changed without any attempt to facilitate for it in
https://github.com/ppy/osu/pull/30506.
---------
Co-authored-by: Dean Herbert <pe@ppy.sh>
432 lines
16 KiB
C#
432 lines
16 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using osu.Framework.Allocation;
|
|
using osu.Framework.Bindables;
|
|
using osu.Framework.Extensions.LocalisationExtensions;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Containers;
|
|
using osu.Framework.Graphics.Cursor;
|
|
using osu.Framework.Graphics.Effects;
|
|
using osu.Framework.Graphics.Shapes;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Input.Events;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Configuration;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Graphics.Sprites;
|
|
using osu.Game.Localisation;
|
|
using osu.Game.Overlays;
|
|
using osu.Game.Overlays.Mods;
|
|
using osu.Game.Rulesets;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Screens.Footer;
|
|
using osu.Game.Screens.Play.HUD;
|
|
using osu.Game.Utils;
|
|
using osuTK;
|
|
using osuTK.Graphics;
|
|
using osuTK.Input;
|
|
|
|
namespace osu.Game.Screens.Select
|
|
{
|
|
public partial class FooterButtonMods : ScreenFooterButton
|
|
{
|
|
public Action? RequestDeselectAllMods { get; init; }
|
|
|
|
public const float BAR_HEIGHT = 30f;
|
|
|
|
private const float mod_display_portion = 0.65f;
|
|
|
|
private readonly BindableWithCurrent<IReadOnlyList<Mod>> mods = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
|
|
|
public Bindable<IReadOnlyList<Mod>> Mods
|
|
{
|
|
get => mods.Current;
|
|
set => mods.Current = value;
|
|
}
|
|
|
|
private readonly BindableWithCurrent<RulesetInfo?> ruleset = new BindableWithCurrent<RulesetInfo?>();
|
|
|
|
public Bindable<RulesetInfo?> Ruleset
|
|
{
|
|
get => ruleset.Current;
|
|
set => ruleset.Current = value;
|
|
}
|
|
|
|
private Container modDisplayBar = null!;
|
|
|
|
private Drawable unrankedBadge = null!;
|
|
|
|
private ModDisplay modDisplay = null!;
|
|
|
|
private OsuSpriteText multiplierText { get; set; } = null!;
|
|
|
|
private Container modContainer = null!;
|
|
|
|
private ModCountText overflowModCountDisplay = null!;
|
|
|
|
[Resolved]
|
|
private OsuColour colours { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
|
|
|
[Resolved]
|
|
private OsuGameBase game { get; set; } = null!;
|
|
|
|
private IBindable<Language> currentLanguage = null!;
|
|
|
|
public FooterButtonMods(ModSelectOverlay overlay)
|
|
: base(overlay)
|
|
{
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
Text = SongSelectStrings.Mods;
|
|
Icon = FontAwesome.Solid.ExchangeAlt;
|
|
AccentColour = colours.Lime1;
|
|
|
|
AddRange(new[]
|
|
{
|
|
unrankedBadge = new UnrankedBadge(),
|
|
modDisplayBar = new InputBlockingContainer
|
|
{
|
|
Y = -5f,
|
|
Depth = float.MaxValue,
|
|
Origin = Anchor.BottomLeft,
|
|
Shear = OsuGame.SHEAR,
|
|
CornerRadius = CORNER_RADIUS,
|
|
Size = new Vector2(BUTTON_WIDTH, BAR_HEIGHT),
|
|
Masking = true,
|
|
EdgeEffect = new EdgeEffectParameters
|
|
{
|
|
Type = EdgeEffectType.Shadow,
|
|
Radius = 4,
|
|
// Figma says 50% opacity, but it does not match up visually if taken at face value, and looks bad.
|
|
Colour = Colour4.Black.Opacity(0.25f),
|
|
Offset = new Vector2(0, 2),
|
|
},
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Colour = colourProvider.Background4,
|
|
RelativeSizeAxes = Axes.Both,
|
|
},
|
|
new Container
|
|
{
|
|
Anchor = Anchor.CentreRight,
|
|
Origin = Anchor.CentreRight,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Width = 1f - mod_display_portion,
|
|
Masking = true,
|
|
Child = multiplierText = new OsuSpriteText
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Shear = -OsuGame.SHEAR,
|
|
UseFullGlyphHeight = false,
|
|
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold)
|
|
}
|
|
},
|
|
modContainer = new Container
|
|
{
|
|
CornerRadius = CORNER_RADIUS,
|
|
RelativeSizeAxes = Axes.Both,
|
|
Width = mod_display_portion,
|
|
Masking = true,
|
|
Children = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Colour = colourProvider.Background3,
|
|
RelativeSizeAxes = Axes.Both,
|
|
},
|
|
modDisplay = new ModDisplay(showExtendedInformation: true)
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Shear = -OsuGame.SHEAR,
|
|
Scale = new Vector2(0.5f),
|
|
Current = { BindTarget = Mods },
|
|
ExpansionMode = ExpansionMode.AlwaysContracted,
|
|
},
|
|
overflowModCountDisplay = new ModCountText { Mods = { BindTarget = Mods }, },
|
|
}
|
|
},
|
|
}
|
|
},
|
|
});
|
|
}
|
|
|
|
private ModSettingChangeTracker? modSettingChangeTracker;
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
currentLanguage = game.CurrentLanguage.GetBoundCopy();
|
|
currentLanguage.BindValueChanged(_ => ScheduleAfterChildren(updateDisplay));
|
|
|
|
Ruleset.BindValueChanged(_ => updateDisplay());
|
|
Mods.BindValueChanged(m =>
|
|
{
|
|
modSettingChangeTracker?.Dispose();
|
|
|
|
updateDisplay();
|
|
|
|
if (m.NewValue != null)
|
|
{
|
|
modSettingChangeTracker = new ModSettingChangeTracker(m.NewValue);
|
|
modSettingChangeTracker.SettingChanged += _ => updateDisplay();
|
|
}
|
|
}, true);
|
|
|
|
FinishTransforms(true);
|
|
}
|
|
|
|
protected override bool OnMouseDown(MouseDownEvent e)
|
|
{
|
|
// should probably be OnClick but right mouse button clicks isn't setup well.
|
|
if (e.Button == MouseButton.Right)
|
|
{
|
|
RequestDeselectAllMods?.Invoke();
|
|
return true;
|
|
}
|
|
|
|
return base.OnMouseDown(e);
|
|
}
|
|
|
|
private const double duration = 240;
|
|
private const Easing easing = Easing.OutQuint;
|
|
|
|
private void updateDisplay()
|
|
{
|
|
if (Mods.Value.Count == 0)
|
|
{
|
|
modDisplayBar.MoveToY(20, duration, easing);
|
|
modDisplayBar.FadeOut(duration, easing);
|
|
modDisplay.FadeOut(duration, easing);
|
|
overflowModCountDisplay.FadeOut(duration, easing);
|
|
|
|
unrankedBadge.MoveToY(20, duration, easing);
|
|
unrankedBadge.FadeOut(duration, easing);
|
|
|
|
// add delay to let unranked indicator hide first before resizing the button back to its original width.
|
|
this.Delay(duration).ResizeWidthTo(BUTTON_WIDTH, duration, easing);
|
|
}
|
|
else
|
|
{
|
|
if (Mods.Value.Any(m => !m.Ranked))
|
|
{
|
|
unrankedBadge.MoveToX(0, duration, easing);
|
|
unrankedBadge.FadeIn(duration, easing);
|
|
|
|
this.ResizeWidthTo(BUTTON_WIDTH + 5 + unrankedBadge.DrawWidth, duration, easing);
|
|
}
|
|
else
|
|
{
|
|
unrankedBadge.MoveToX(-unrankedBadge.DrawWidth, duration, easing);
|
|
unrankedBadge.FadeOut(duration, easing);
|
|
|
|
this.ResizeWidthTo(BUTTON_WIDTH, duration, easing);
|
|
}
|
|
|
|
modDisplayBar.MoveToY(-5, duration, Easing.OutQuint);
|
|
unrankedBadge.MoveToY(-5, duration, easing);
|
|
modDisplayBar.FadeIn(duration, easing);
|
|
modDisplay.FadeIn(duration, easing);
|
|
}
|
|
|
|
var scoreMultiplierCalculator = Ruleset.Value?.CreateInstance().CreateScoreMultiplierCalculator(new ScoreMultiplierContext());
|
|
double multiplier = scoreMultiplierCalculator?.CalculateFor(Mods.Value) ?? 1;
|
|
multiplierText.Text = ModUtils.FormatScoreMultiplier(multiplier);
|
|
|
|
if (multiplier > 1)
|
|
multiplierText.FadeColour(colours.Red1, duration, easing);
|
|
else if (multiplier < 1)
|
|
multiplierText.FadeColour(colours.Lime1, duration, easing);
|
|
else
|
|
multiplierText.FadeColour(Color4.White, duration, easing);
|
|
}
|
|
|
|
protected override void Update()
|
|
{
|
|
base.Update();
|
|
|
|
if (Mods.Value.Count == 0)
|
|
return;
|
|
|
|
if (modDisplay.DrawWidth * modDisplay.Scale.X > modContainer.DrawWidth)
|
|
overflowModCountDisplay.Show();
|
|
else
|
|
overflowModCountDisplay.Hide();
|
|
}
|
|
|
|
public partial class ModCountText : VisibilityContainer, IHasCustomTooltip<IReadOnlyList<Mod>>
|
|
{
|
|
public readonly Bindable<IReadOnlyList<Mod>> Mods = new Bindable<IReadOnlyList<Mod>>();
|
|
|
|
private LocalisableString? customText;
|
|
|
|
/// <summary>
|
|
/// When set, this will be shown instead of a mod count.
|
|
/// </summary>
|
|
public LocalisableString? CustomText
|
|
{
|
|
get => customText;
|
|
set
|
|
{
|
|
customText = value;
|
|
if (IsLoaded)
|
|
updateText();
|
|
}
|
|
}
|
|
|
|
private OsuSpriteText text = null!;
|
|
|
|
[Resolved]
|
|
private OverlayColourProvider colourProvider { get; set; } = null!;
|
|
|
|
protected override void LoadComplete()
|
|
{
|
|
base.LoadComplete();
|
|
|
|
RelativeSizeAxes = Axes.Both;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Colour = colourProvider.Background3,
|
|
Alpha = 0.8f,
|
|
RelativeSizeAxes = Axes.Both,
|
|
},
|
|
text = new OsuSpriteText
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
|
|
Shear = -OsuGame.SHEAR,
|
|
}
|
|
};
|
|
|
|
Mods.BindValueChanged(_ => updateText(), true);
|
|
}
|
|
|
|
public ITooltip<IReadOnlyList<Mod>> GetCustomTooltip() => new ModOverflowTooltip(colourProvider);
|
|
|
|
public IReadOnlyList<Mod>? TooltipContent => Mods.Value;
|
|
|
|
protected override void PopIn() => this.FadeIn(300, Easing.OutExpo);
|
|
protected override void PopOut() => this.FadeOut(300, Easing.OutExpo);
|
|
|
|
private void updateText()
|
|
{
|
|
if (CustomText != null)
|
|
text.Text = CustomText.Value;
|
|
else
|
|
text.Text = ModSelectOverlayStrings.Mods(Mods.Value.Count).ToUpper();
|
|
}
|
|
|
|
public partial class ModOverflowTooltip : VisibilityContainer, ITooltip<IReadOnlyList<Mod>>
|
|
{
|
|
private ModFlowDisplay extendedModDisplay = null!;
|
|
|
|
[Cached]
|
|
private OverlayColourProvider colourProvider;
|
|
|
|
public ModOverflowTooltip(OverlayColourProvider colourProvider)
|
|
{
|
|
this.colourProvider = colourProvider;
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load()
|
|
{
|
|
AutoSizeAxes = Axes.Both;
|
|
CornerRadius = CORNER_RADIUS;
|
|
Masking = true;
|
|
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
RelativeSizeAxes = Axes.Both,
|
|
Colour = colourProvider.Background5,
|
|
},
|
|
extendedModDisplay = new ModFlowDisplay
|
|
{
|
|
AutoSizeAxes = Axes.Both,
|
|
MaximumSize = new Vector2(400, 0),
|
|
Margin = new MarginPadding { Vertical = 2f, Horizontal = 10f },
|
|
Scale = new Vector2(0.6f),
|
|
},
|
|
};
|
|
}
|
|
|
|
public void SetContent(IReadOnlyList<Mod> content)
|
|
{
|
|
extendedModDisplay.Current.Value = content;
|
|
}
|
|
|
|
public void Move(Vector2 pos) => Position = pos;
|
|
|
|
protected override void PopIn() => this.FadeIn(240, Easing.OutQuint);
|
|
protected override void PopOut() => this.FadeOut(240, Easing.OutQuint);
|
|
}
|
|
}
|
|
|
|
internal partial class UnrankedBadge : InputBlockingContainer, IHasTooltip
|
|
{
|
|
public LocalisableString TooltipText { get; }
|
|
|
|
public UnrankedBadge()
|
|
{
|
|
Margin = new MarginPadding { Left = BUTTON_WIDTH + 5f };
|
|
Y = -5f;
|
|
Depth = float.MaxValue;
|
|
Origin = Anchor.BottomLeft;
|
|
Shear = OsuGame.SHEAR;
|
|
CornerRadius = CORNER_RADIUS;
|
|
AutoSizeAxes = Axes.X;
|
|
Height = BAR_HEIGHT;
|
|
Masking = true;
|
|
BorderColour = Color4.White;
|
|
BorderThickness = 2f;
|
|
TooltipText = ModSelectOverlayStrings.UnrankedExplanation;
|
|
}
|
|
|
|
[BackgroundDependencyLoader]
|
|
private void load(OsuColour colours)
|
|
{
|
|
InternalChildren = new Drawable[]
|
|
{
|
|
new Box
|
|
{
|
|
Colour = colours.Orange2,
|
|
RelativeSizeAxes = Axes.Both,
|
|
},
|
|
new OsuSpriteText
|
|
{
|
|
Anchor = Anchor.Centre,
|
|
Origin = Anchor.Centre,
|
|
Shear = -OsuGame.SHEAR,
|
|
Text = ModSelectOverlayStrings.Unranked.ToUpper(),
|
|
Margin = new MarginPadding { Horizontal = 15 },
|
|
UseFullGlyphHeight = false,
|
|
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
|
|
Colour = Color4.Black,
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|