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

Merge branch 'master' into mania-key-count-mod-query

This commit is contained in:
Bartłomiej Dach 2024-04-03 09:29:32 +02:00 committed by GitHub
commit cb82fb0487
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
40 changed files with 593 additions and 174 deletions

View File

@ -126,7 +126,7 @@ jobs:
if: ${{ github.event_name == 'issue_comment' && github.event.issue.pull_request }}
steps:
- name: Create comment
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
message: |
@ -253,7 +253,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -284,7 +284,7 @@ jobs:
- name: Restore cache
id: restore-cache
uses: maxnowack/local-cache@038cc090b52e4f205fbc468bf5b0756df6f68775 # v1
uses: maxnowack/local-cache@720e69c948191660a90aa1cf6a42fc4d2dacdf30 # v2
with:
path: ${{ steps.query.outputs.DATA_NAME }}.tar.bz2
key: ${{ steps.query.outputs.DATA_NAME }}
@ -358,7 +358,7 @@ jobs:
steps:
- name: Update comment on success
if: ${{ needs.generator.result == 'success' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@ -369,7 +369,7 @@ jobs:
- name: Update comment on failure
if: ${{ needs.generator.result == 'failure' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: upsert
@ -379,7 +379,7 @@ jobs:
- name: Update comment on cancellation
if: ${{ needs.generator.result == 'cancelled' }}
uses: thollander/actions-comment-pull-request@363c6f6eae92cc5c3a66e95ba016fc771bb38943 # v2.4.2
uses: thollander/actions-comment-pull-request@fabd468d3a1a0b97feee5f6b9e499eab0dd903f6 # v2.5.0
with:
comment_tag: ${{ env.EXECUTION_ID }}
mode: delete

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
if (circle1 is null || circle2 is null || slider is null)
if (circle1 == null || circle2 == null || slider == null)
return false;
var controlPoints = slider.Path.ControlPoints;
@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider created", () =>
{
if (slider1 is null || slider2 is null || slider1Path is null)
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path is null || slider2 is null)
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
@ -231,6 +231,137 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
(pos: circle2.Position, pathType: null)));
}
[Test]
public void TestMergeSliderSliderSameStartTime()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
});
AddStep("move sliders to the same start time", () =>
{
slider2!.StartTime = slider1!.StartTime;
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
[Test]
public void TestMergeSliderSliderSameStartAndEndTime()
{
Slider? slider1 = null;
SliderPath? slider1Path = null;
Slider? slider2 = null;
AddStep("select two sliders", () =>
{
slider1 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider);
slider1Path = new SliderPath(slider1.Path.ControlPoints.Select(p => new PathControlPoint(p.Position, p.Type)).ToArray(), slider1.Path.ExpectedDistance.Value);
slider2 = (Slider)EditorBeatmap.HitObjects.First(h => h is Slider && h.StartTime > slider1.StartTime);
EditorClock.Seek(slider1.StartTime);
EditorBeatmap.SelectedHitObjects.AddRange([slider1, slider2]);
});
AddStep("move sliders to the same start & end time", () =>
{
slider2!.StartTime = slider1!.StartTime;
slider2.Path = slider1.Path;
});
mergeSelection();
AddAssert("slider created", () =>
{
if (slider1 == null || slider2 == null || slider1Path == null)
return false;
var controlPoints1 = slider1Path.ControlPoints;
var controlPoints2 = slider2.Path.ControlPoints;
(Vector2, PathType?)[] args = new (Vector2, PathType?)[controlPoints1.Count + controlPoints2.Count - 1];
for (int i = 0; i < controlPoints1.Count - 1; i++)
{
args[i] = (controlPoints1[i].Position + slider1.Position, controlPoints1[i].Type);
}
for (int i = 0; i < controlPoints2.Count; i++)
{
args[i + controlPoints1.Count - 1] = (controlPoints2[i].Position + controlPoints1[^1].Position + slider1.Position, controlPoints2[i].Type);
}
return sliderCreatedFor(args);
});
AddAssert("samples exist", sliderSampleExist);
AddAssert("merged slider matches first slider", () =>
{
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return slider1 is not null && mergedSlider.HeadCircle.Samples.SequenceEqual(slider1.HeadCircle.Samples)
&& mergedSlider.TailCircle.Samples.SequenceEqual(slider1.TailCircle.Samples)
&& mergedSlider.Samples.SequenceEqual(slider1.Samples);
});
AddAssert("slider end is at same completion for last slider", () =>
{
if (slider1Path == null || slider2 == null)
return false;
var mergedSlider = (Slider)EditorBeatmap.SelectedHitObjects.First();
return Precision.AlmostEquals(mergedSlider.Path.Distance, slider1Path.CalculatedDistance + slider2.Path.Distance);
});
}
private void mergeSelection()
{
AddStep("merge selection", () =>

View File

@ -172,6 +172,54 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
assertControlPointPathType(4, null);
}
[Test]
public void TestStackingUpdatesPointsPosition()
{
createVisualiser(true);
Vector2[] points =
[
new Vector2(200),
new Vector2(300),
new Vector2(500, 300),
new Vector2(700, 200),
new Vector2(500, 100)
];
foreach (var point in points) addControlPointStep(point);
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 1);
for (int i = 0; i < points.Length; i++)
addAssertPointPositionChanged(points, i);
}
[Test]
public void TestStackingUpdatesConnectionPosition()
{
createVisualiser(true);
Vector2 connectionPosition;
addControlPointStep(connectionPosition = new Vector2(300));
addControlPointStep(new Vector2(600));
// Apply a big number in stacking so the person running the test can clearly see if it fails
AddStep("apply stacking", () => slider.StackHeightBindable.Value += 10);
AddAssert($"Connection at {connectionPosition} changed",
() => visualiser.Connections[0].Position,
() => !Is.EqualTo(connectionPosition)
);
}
private void addAssertPointPositionChanged(Vector2[] points, int index)
{
AddAssert($"Point at {points.ElementAt(index)} changed",
() => visualiser.Pieces[index].Position,
() => !Is.EqualTo(points.ElementAt(index))
);
}
private void createVisualiser(bool allowSelection) => AddStep("create visualiser", () => Child = visualiser = new PathControlPointVisualiser<Slider>(slider, allowSelection)
{
Anchor = Anchor.Centre,

View File

@ -24,14 +24,38 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
[Test]
public void TestHotkeyHandling()
{
AddStep("select single circle", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("deselect everything", () => EditorBeatmap.SelectedHitObjects.Clear());
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddUntilStep("no popover present", getPopover, () => Is.Null);
AddStep("select single circle",
() => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects.OfType<HitCircle>().First()));
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddAssert("only playfield centre origin rotation available", () =>
{
var popover = getPopover();
var buttons = popover.ChildrenOfType<EditorRadioButton>();
return buttons.Any(btn => btn.Text == "Selection centre" && !btn.Enabled.Value)
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", getPopover, () => Is.Null);
AddStep("select first three objects", () =>
{
@ -44,14 +68,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.EqualTo(1));
AddUntilStep("popover present", getPopover, () => Is.Not.Null);
AddAssert("both origin rotation available", () =>
{
var popover = getPopover();
var buttons = popover.ChildrenOfType<EditorRadioButton>();
return buttons.Any(btn => btn.Text == "Selection centre" && btn.Enabled.Value)
&& buttons.Any(btn => btn.Text == "Playfield centre" && btn.Enabled.Value);
});
AddStep("press rotate hotkey", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Key(Key.R);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("no popover present", () => this.ChildrenOfType<PreciseRotationPopover>().Count(), () => Is.Zero);
AddUntilStep("no popover present", getPopover, () => Is.Null);
PreciseRotationPopover? getPopover() => this.ChildrenOfType<PreciseRotationPopover>().SingleOrDefault();
}
[Test]

View File

@ -178,7 +178,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddStep("add hitsounds", () =>
{
if (slider is null) return;
if (slider == null) return;
sample = new HitSampleInfo("hitwhistle", HitSampleInfo.BANK_SOFT, volume: 70);
slider.Samples.Add(sample.With());
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"move mouse to control point {index}", () =>
{
if (slider is null || visualiser is null) return;
if (slider == null || visualiser == null) return;
Vector2 position = slider.Path.ControlPoints[index].Position + slider.Position;
InputManager.MoveMouseTo(visualiser.Pieces[0].Parent!.ToScreenSpace(position));
@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
{
AddStep($"click context menu item \"{contextMenuText}\"", () =>
{
if (visualiser is null) return;
if (visualiser == null) return;
MenuItem? item = visualiser.ContextMenuItems?.FirstOrDefault(menuItem => menuItem.Text.Value == contextMenuText);

View File

@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition;
private IBindable<int> pathVersion;
private IBindable<int> stackHeight;
public PathControlPointConnectionPiece(T hitObject, int controlPointIndex)
{
@ -56,6 +57,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
pathVersion = hitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateConnectingPath());
updateConnectingPath();
}

View File

@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
private IBindable<Vector2> hitObjectPosition;
private IBindable<float> hitObjectScale;
private IBindable<int> stackHeight;
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
{
@ -105,6 +106,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
hitObjectScale = hitObject.ScaleBindable.GetBoundCopy();
hitObjectScale.BindValueChanged(_ => updateMarkerDisplay());
stackHeight = hitObject.StackHeightBindable.GetBoundCopy();
stackHeight.BindValueChanged(_ => updateMarkerDisplay());
IsSelected.BindValueChanged(_ => updateMarkerDisplay());
updateMarkerDisplay();

View File

@ -311,7 +311,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
foreach (var splitPoint in controlPointsToSplitAt)
{
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type is null)
if (splitPoint == controlPoints[0] || splitPoint == controlPoints[^1] || splitPoint.Type == null)
continue;
// Split off the section of slider before this control point so the remaining control points to split are in the latter part of the slider.

View File

@ -41,7 +41,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private void updateState()
{
var quad = GeometryUtils.GetSurroundingQuad(selectedMovableObjects);
CanRotate.Value = quad.Width > 0 || quad.Height > 0;
CanRotateSelectionOrigin.Value = quad.Width > 0 || quad.Height > 0;
CanRotatePlayfieldOrigin.Value = selectedMovableObjects.Any();
}
private OsuHitObject[]? objectsInRotation;

View File

@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Edit
private SliderWithTextBoxInput<float> angleInput = null!;
private EditorRadioButtonCollection rotationOrigin = null!;
private RadioButton selectionCentreButton = null!;
public PreciseRotationPopover(SelectionRotationHandler rotationHandler)
{
this.rotationHandler = rotationHandler;
@ -59,13 +61,17 @@ namespace osu.Game.Rulesets.Osu.Edit
new RadioButton("Playfield centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.PlayfieldCentre },
() => new SpriteIcon { Icon = FontAwesome.Regular.Square }),
new RadioButton("Selection centre",
selectionCentreButton = new RadioButton("Selection centre",
() => rotationInfo.Value = rotationInfo.Value with { Origin = RotationOrigin.SelectionCentre },
() => new SpriteIcon { Icon = FontAwesome.Solid.VectorSquare })
}
}
}
};
selectionCentreButton.Selected.DisabledChanged += isDisabled =>
{
selectionCentreButton.TooltipText = isDisabled ? "Select more than one object to perform selection-based rotation." : string.Empty;
};
}
protected override void LoadComplete()
@ -76,6 +82,11 @@ namespace osu.Game.Rulesets.Osu.Edit
angleInput.Current.BindValueChanged(angle => rotationInfo.Value = rotationInfo.Value with { Degrees = angle.NewValue });
rotationOrigin.Items.First().Select();
rotationHandler.CanRotateSelectionOrigin.BindValueChanged(e =>
{
selectionCentreButton.Selected.Disabled = !e.NewValue;
}, true);
rotationInfo.BindValueChanged(rotation =>
{
rotationHandler.Update(rotation.NewValue.Degrees, rotation.NewValue.Origin == RotationOrigin.PlayfieldCentre ? OsuPlayfield.BASE_SIZE / 2 : null);

View File

@ -22,6 +22,9 @@ namespace osu.Game.Rulesets.Osu.Edit
private EditorToolButton rotateButton = null!;
private Bindable<bool> canRotatePlayfieldOrigin = null!;
private Bindable<bool> canRotateSelectionOrigin = null!;
public SelectionRotationHandler RotationHandler { get; init; } = null!;
public TransformToolboxGroup()
@ -51,9 +54,20 @@ namespace osu.Game.Rulesets.Osu.Edit
{
base.LoadComplete();
// aggregate two values into canRotate
canRotatePlayfieldOrigin = RotationHandler.CanRotatePlayfieldOrigin.GetBoundCopy();
canRotatePlayfieldOrigin.BindValueChanged(_ => updateCanRotateAggregate());
canRotateSelectionOrigin = RotationHandler.CanRotateSelectionOrigin.GetBoundCopy();
canRotateSelectionOrigin.BindValueChanged(_ => updateCanRotateAggregate());
void updateCanRotateAggregate()
{
canRotate.Value = RotationHandler.CanRotatePlayfieldOrigin.Value || RotationHandler.CanRotateSelectionOrigin.Value;
}
// bindings to `Enabled` on the buttons are decoupled on purpose
// due to the weird `OsuButton` behaviour of resetting `Enabled` to `false` when `Action` is set.
canRotate.BindTo(RotationHandler.CanRotate);
canRotate.BindValueChanged(_ => rotateButton.Enabled.Value = canRotate.Value, true);
}

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Taiko.UI
InternalChild = textureAnimation = createTextureAnimation(state).With(animation =>
{
animation.Origin = animation.Anchor = Anchor.BottomLeft;
animation.Scale = new Vector2(0.51f); // close enough to stable
// matches stable (https://github.com/peppy/osu-stable-reference/blob/e53980dd76857ee899f66ce519ba1597e7874f28/osu!/GameModes/Play/Rulesets/Taiko/TaikoMascot.cs#L34)
animation.Scale = new Vector2(0.6f);
});
RelativeSizeAxes = Axes.Both;

View File

@ -12,6 +12,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Database
@ -77,6 +78,7 @@ namespace osu.Game.Tests.Database
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
using (var tmpStorage = new TemporaryNativeStorage("stable-songs-folder"))
using (new RealmRulesetStore(realm, storage))
{
var stableStorage = new StableStorage(tmpStorage.GetFullPath(""), host);
var songsStorage = stableStorage.GetStorageForDirectory(StableStorage.STABLE_DEFAULT_SONGS_PATH);

View File

@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Editing
{
this.getTargetContainer = getTargetContainer;
CanRotate.Value = true;
CanRotateSelectionOrigin.Value = true;
}
[CanBeNull]

View File

@ -15,6 +15,7 @@ using osu.Game.Online.API.Requests;
using osu.Game.Overlays;
using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings;
using osu.Game.Users;
using osu.Game.Users.Drawables;
using osuTK.Input;
@ -72,9 +73,24 @@ namespace osu.Game.Tests.Visual.Menus
return false;
});
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online);
assertDropdownState(UserAction.Online);
AddStep("set failing", () => { dummyAPI.SetState(APIState.Failing); });
AddStep("return to online", () => { dummyAPI.SetState(APIState.Online); });
AddStep("clear handler", () => dummyAPI.HandleRequest = null);
assertDropdownState(UserAction.Online);
AddStep("change user state", () => dummyAPI.LocalUser.Value.Status.Value = UserStatus.DoNotDisturb);
assertDropdownState(UserAction.DoNotDisturb);
}
private void assertDropdownState(UserAction state)
{
AddAssert($"dropdown state is {state}", () => loginOverlay.ChildrenOfType<UserDropdown>().First().Current.Value, () => Is.EqualTo(state));
}
private void assertAPIState(APIState expected) =>

View File

@ -14,13 +14,16 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Models;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Screens.Ranking.Expanded.Statistics;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
@ -67,6 +70,40 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("play time displayed", () => this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());
}
[Test]
public void TestPPShownAsProvisionalWhenBeatmapHasNoLeaderboard()
{
AddStep("show example score", () =>
{
var beatmap = createTestBeatmap(new RealmUser());
beatmap.Status = BeatmapOnlineStatus.Graveyard;
showPanel(TestResources.CreateTestScoreInfo(beatmap));
});
AddAssert("pp display faded out", () =>
{
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedBeatmaps;
});
}
[Test]
public void TestPPShownAsProvisionalWhenUnrankedModsArePresent()
{
AddStep("show example score", () =>
{
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(new RealmUser()));
score.Mods = score.Mods.Append(new OsuModDifficultyAdjust()).ToArray();
showPanel(score);
});
AddAssert("pp display faded out", () =>
{
var ppDisplay = this.ChildrenOfType<PerformanceStatistic>().Single();
return ppDisplay.Alpha == 0.5 && ppDisplay.TooltipText == ResultsScreenStrings.NoPPForUnrankedMods;
});
}
[Test]
public void TestWithDefaultDate()
{

View File

@ -434,6 +434,9 @@ namespace osu.Game.Beatmaps
}
}
if (!beatmaps.Any())
throw new ArgumentException("No valid beatmap files found in the beatmap archive.");
return beatmaps;
}
}

View File

@ -0,0 +1,41 @@
// 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.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Graphics.Sprites
{
public abstract partial class GlowingDrawable : BufferedContainer
{
// Inflate draw quad to prevent glow from trimming at the edges.
// Padding won't suffice since it will affect drawable position in cases when it's not centered.
protected override Quad ComputeScreenSpaceDrawQuad()
=> base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(new Vector2(Blur.KernelSize(BlurSigma.X), Blur.KernelSize(BlurSigma.Y)));
public ColourInfo GlowColour
{
get => EffectColour;
set
{
EffectColour = value;
BackgroundColour = value.MultiplyAlpha(0f);
}
}
protected GlowingDrawable()
: base(cachedFrameBuffer: true)
{
AutoSizeAxes = Axes.Both;
RedrawOnScale = false;
DrawOriginal = true;
Child = CreateDrawable();
}
protected abstract Drawable CreateDrawable();
}
}

View File

@ -4,24 +4,17 @@
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osuTK;
namespace osu.Game.Graphics.Sprites
{
public partial class GlowingSpriteText : BufferedContainer, IHasText
public partial class GlowingSpriteText : GlowingDrawable, IHasText
{
private const float blur_sigma = 3f;
// Inflate draw quad to prevent glow from trimming at the edges.
// Padding won't suffice since it will affect text position in cases when it's not centered.
protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma));
private readonly OsuSpriteText text;
private OsuSpriteText text = null!;
public LocalisableString Text
{
@ -47,16 +40,6 @@ namespace osu.Game.Graphics.Sprites
set => text.Colour = value;
}
public ColourInfo GlowColour
{
get => EffectColour;
set
{
EffectColour = value;
BackgroundColour = value.MultiplyAlpha(0f);
}
}
public Vector2 Spacing
{
get => text.Spacing;
@ -76,20 +59,16 @@ namespace osu.Game.Graphics.Sprites
}
public GlowingSpriteText()
: base(cachedFrameBuffer: true)
{
AutoSizeAxes = Axes.Both;
BlurSigma = new Vector2(blur_sigma);
RedrawOnScale = false;
DrawOriginal = true;
EffectBlending = BlendingParameters.Additive;
EffectPlacement = EffectPlacement.InFront;
Child = text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
};
}
protected override Drawable CreateDrawable() => text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
};
}
}

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;
namespace osu.Game.Localisation
{
public static class ResultsScreenStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.ResultsScreen";
/// <summary>
/// "Performance points are not granted for this score because the beatmap is not ranked."
/// </summary>
public static LocalisableString NoPPForUnrankedBeatmaps => new TranslatableString(getKey(@"no_pp_for_unranked_beatmaps"), @"Performance points are not granted for this score because the beatmap is not ranked.");
/// <summary>
/// "Performance points are not granted for this score because of unranked mods."
/// </summary>
public static LocalisableString NoPPForUnrankedMods => new TranslatableString(getKey(@"no_pp_for_unranked_mods"), @"Performance points are not granted for this score because of unranked mods.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -678,16 +678,21 @@ namespace osu.Game
/// <summary>
/// Allows a maximum of one unhandled exception, per second of execution.
/// </summary>
private bool onExceptionThrown(Exception _)
/// <returns>Whether to ignore the exception and continue running.</returns>
private bool onExceptionThrown(Exception ex)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
if (Interlocked.Decrement(ref allowableExceptions) < 0)
{
Logger.Log("Too many unhandled exceptions, crashing out.");
RulesetStore.TryDisableCustomRulesetsCausing(ex);
return false;
}
Logger.Log($"Unhandled exception has been allowed with {allowableExceptions} more allowable exceptions.");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
return true;
}
protected override void Dispose(bool isDisposing)

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Settings;
using osu.Game.Users;
using osuTK;
@ -30,15 +31,17 @@ namespace osu.Game.Overlays.Login
[Resolved]
private OsuColour colours { get; set; } = null!;
private UserDropdown dropdown = null!;
private UserDropdown? dropdown;
/// <summary>
/// Called to request a hide of a parent displaying this container.
/// </summary>
public Action? RequestHide;
private IBindable<APIUser> user = null!;
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly Bindable<UserStatus?> userStatus = new Bindable<UserStatus?>();
[Resolved]
private IAPIProvider api { get; set; } = null!;
@ -61,11 +64,21 @@ namespace osu.Game.Overlays.Login
AutoSizeAxes = Axes.Y;
}
[BackgroundDependencyLoader]
private void load()
protected override void LoadComplete()
{
base.LoadComplete();
apiState.BindTo(api.State);
apiState.BindValueChanged(onlineStateChanged, true);
user = api.LocalUser.GetBoundCopy();
user.BindValueChanged(u =>
{
status.UnbindBindings();
status.BindTo(u.NewValue.Status);
}, true);
status.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
}
private void onlineStateChanged(ValueChangedEvent<APIState> state) => Schedule(() =>
@ -144,9 +157,6 @@ namespace osu.Game.Overlays.Login
},
};
userStatus.BindTo(api.LocalUser.Value.Status);
userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
dropdown.Current.BindValueChanged(action =>
{
switch (action.NewValue)
@ -171,6 +181,7 @@ namespace osu.Game.Overlays.Login
break;
}
}, true);
break;
}
@ -180,6 +191,9 @@ namespace osu.Game.Overlays.Login
private void updateDropdownCurrent(UserStatus? status)
{
if (dropdown == null)
return;
switch (status)
{
case UserStatus.Online:

View File

@ -781,7 +781,7 @@ namespace osu.Game.Overlays.Mods
/// </remarks>>
public bool OnPressed(KeyBindingPressEvent<PlatformAction> e)
{
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton is null)
if (e.Repeat || e.Action != PlatformAction.SelectAll || SelectAllModsButton == null)
return false;
SelectAllModsButton.TriggerClick();

View File

@ -41,7 +41,7 @@ namespace osu.Game.Overlays.SkinEditor
private void updateState()
{
CanRotate.Value = selectedItems.Count > 0;
CanRotateSelectionOrigin.Value = selectedItems.Count > 0;
}
private Drawable[]? objectsInRotation;

View File

@ -212,6 +212,14 @@ namespace osu.Game.Rulesets.Edit
.Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon))
.ToList();
foreach (var item in toolboxCollection.Items)
{
item.Selected.DisabledChanged += isDisabled =>
{
item.TooltipText = isDisabled ? "Add at least one timing point first!" : string.Empty;
};
}
TernaryStates = CreateTernaryButtons().ToArray();
togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b)));
@ -244,6 +252,14 @@ namespace osu.Game.Rulesets.Edit
if (!timing.NewValue)
setSelectTool();
});
EditorBeatmap.HasTiming.BindValueChanged(hasTiming =>
{
foreach (var item in toolboxCollection.Items)
{
item.Selected.Disabled = !hasTiming.NewValue;
}
}, true);
}
protected override void Update()

View File

@ -347,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
// Edge-case rules (to match stable).
if (type == PathType.PERFECT_CURVE)
{
int endPointLength = endPoint is null ? 0 : 1;
int endPointLength = endPoint == null ? 0 : 1;
if (vertices.Length + endPointLength != 3)
type = PathType.BEZIER;

View File

@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Objects
public bool Equals(PathControlPoint other) => Position == other?.Position && Type == other.Type;
public override string ToString() => type is null
public override string ToString() => type == null
? $"Position={Position}"
: $"Position={Position}, Type={type}";
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Objects
{
var controlPoints = sliderPath.ControlPoints;
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type is null).ToList();
var inheritedLinearPoints = controlPoints.Where(p => sliderPath.PointsInSegment(p)[0].Type == PathType.LINEAR && p.Type == null).ToList();
// Inherited points after a linear point, as well as the first control point if it inherited,
// should be treated as linear points, so their types are temporarily changed to linear.
@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Objects
inheritedLinearPoints.ForEach(p => p.Type = null);
// Recalculate middle perfect curve control points at the end of the slider path.
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type is null && segmentEnds.Any())
if (controlPoints.Count >= 3 && controlPoints[^3].Type == PathType.PERFECT_CURVE && controlPoints[^2].Type == null && segmentEnds.Any())
{
double lastSegmentStart = segmentEnds.Length > 1 ? segmentEnds[^2] : 0;
double lastSegmentEnd = segmentEnds[^1];

View File

@ -3,8 +3,11 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
@ -13,17 +16,20 @@ namespace osu.Game.Rulesets
{
public class RealmRulesetStore : RulesetStore
{
private readonly RealmAccess realmAccess;
public override IEnumerable<RulesetInfo> AvailableRulesets => availableRulesets;
private readonly List<RulesetInfo> availableRulesets = new List<RulesetInfo>();
public RealmRulesetStore(RealmAccess realm, Storage? storage = null)
public RealmRulesetStore(RealmAccess realmAccess, Storage? storage = null)
: base(storage)
{
prepareDetachedRulesets(realm);
this.realmAccess = realmAccess;
prepareDetachedRulesets();
informUserAboutBrokenRulesets();
}
private void prepareDetachedRulesets(RealmAccess realmAccess)
private void prepareDetachedRulesets()
{
realmAccess.Write(realm =>
{
@ -143,5 +149,48 @@ namespace osu.Game.Rulesets
instance.CreateBeatmapProcessor(converter.Convert());
}
private void informUserAboutBrokenRulesets()
{
if (RulesetStorage == null)
return;
foreach (string brokenRulesetDll in RulesetStorage.GetFiles(@".", @"*.dll.broken"))
{
Logger.Log($"Ruleset '{Path.GetFileNameWithoutExtension(brokenRulesetDll)}' has been disabled due to causing a crash.\n\n"
+ "Please update the ruleset or report the issue to the developers of the ruleset if no updates are available.", level: LogLevel.Important);
}
}
internal void TryDisableCustomRulesetsCausing(Exception exception)
{
try
{
var stackTrace = new StackTrace(exception);
foreach (var frame in stackTrace.GetFrames())
{
var declaringAssembly = frame.GetMethod()?.DeclaringType?.Assembly;
if (declaringAssembly == null)
continue;
if (UserRulesetAssemblies.Contains(declaringAssembly))
{
string sourceLocation = declaringAssembly.Location;
string destinationLocation = Path.ChangeExtension(sourceLocation, @".dll.broken");
if (File.Exists(sourceLocation))
{
Logger.Log($"Unhandled exception traced back to custom ruleset {Path.GetFileNameWithoutExtension(sourceLocation)}. Marking as broken.");
File.Move(sourceLocation, destinationLocation);
}
}
}
}
catch (Exception ex)
{
Logger.Log($"Attempt to trace back crash to custom ruleset failed: {ex}");
}
}
}
}

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets
private const string ruleset_library_prefix = @"osu.Game.Rulesets";
protected readonly Dictionary<Assembly, Type> LoadedAssemblies = new Dictionary<Assembly, Type>();
protected readonly HashSet<Assembly> UserRulesetAssemblies = new HashSet<Assembly>();
protected readonly Storage? RulesetStorage;
/// <summary>
/// All available rulesets.
@ -41,9 +43,9 @@ namespace osu.Game.Rulesets
// to load as unable to locate the game core assembly.
AppDomain.CurrentDomain.AssemblyResolve += resolveRulesetDependencyAssembly;
var rulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
if (rulesetStorage != null)
loadUserRulesets(rulesetStorage);
RulesetStorage = storage?.GetStorageForDirectory(@"rulesets");
if (RulesetStorage != null)
loadUserRulesets(RulesetStorage);
}
/// <summary>
@ -105,7 +107,11 @@ namespace osu.Game.Rulesets
var rulesets = rulesetStorage.GetFiles(@".", @$"{ruleset_library_prefix}.*.dll");
foreach (string? ruleset in rulesets.Where(f => !f.Contains(@"Tests")))
loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
{
var assembly = loadRulesetFromFile(rulesetStorage.GetFullPath(ruleset));
if (assembly != null)
UserRulesetAssemblies.Add(assembly);
}
}
private void loadFromDisk()
@ -126,21 +132,25 @@ namespace osu.Game.Rulesets
}
}
private void loadRulesetFromFile(string file)
private Assembly? loadRulesetFromFile(string file)
{
string filename = Path.GetFileNameWithoutExtension(file);
if (LoadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
return;
return null;
try
{
addRuleset(Assembly.LoadFrom(file));
var assembly = Assembly.LoadFrom(file);
addRuleset(assembly);
return assembly;
}
catch (Exception e)
{
LogFailedLoad(filename, e);
}
return null;
}
private void addRuleset(Assembly assembly)

View File

@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.UI
modAcronym.Text = value.Acronym;
modIcon.Icon = value.Icon ?? FontAwesome.Solid.Question;
if (value.Icon is null)
if (value.Icon == null)
{
modIcon.FadeOut();
modAcronym.FadeIn();

View File

@ -33,9 +33,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
private Drawable icon = null!;
[Resolved]
private EditorBeatmap? editorBeatmap { get; set; }
public EditorRadioButton(RadioButton button)
{
Button = button;
@ -76,8 +73,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
Selected?.Invoke(Button);
};
editorBeatmap?.HasTiming.BindValueChanged(hasTiming => Button.Selected.Disabled = !hasTiming.NewValue, true);
Button.Selected.BindDisabledChanged(disabled => Enabled.Value = !disabled, true);
updateSelectionState();
}
@ -99,6 +94,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
X = 40f
};
public LocalisableString TooltipText => Enabled.Value ? string.Empty : "Add at least one timing point first!";
public LocalisableString TooltipText => Button.TooltipText;
}
}

View File

@ -4,6 +4,7 @@
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
namespace osu.Game.Screens.Edit.Components.RadioButtons
{
@ -11,6 +12,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
{
/// <summary>
/// Whether this <see cref="RadioButton"/> is selected.
/// Disable this bindable to disable the button.
/// </summary>
public readonly BindableBool Selected;
@ -50,5 +52,8 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
/// Deselects this <see cref="RadioButton"/>.
/// </summary>
public void Deselect() => Selected.Value = false;
// Tooltip text that will be shown when hovered over
public LocalisableString TooltipText { get; set; } = string.Empty;
}
}

View File

@ -512,7 +512,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected virtual void OnBlueprintDeselected(SelectionBlueprint<T> blueprint)
{
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
if (SelectionBlueprints.Contains(blueprint))
SelectionBlueprints.ChangeChildDepth(blueprint, 0);
SelectionHandler.HandleDeselected(blueprint);
}

View File

@ -174,7 +174,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void load()
{
if (rotationHandler != null)
canRotate.BindTo(rotationHandler.CanRotate);
canRotate.BindTo(rotationHandler.CanRotateSelectionOrigin);
canRotate.BindValueChanged(_ => recreate(), true);
}

View File

@ -13,9 +13,14 @@ namespace osu.Game.Screens.Edit.Compose.Components
public partial class SelectionRotationHandler : Component
{
/// <summary>
/// Whether the rotation can currently be performed.
/// Whether rotation anchored by the selection origin can currently be performed.
/// </summary>
public Bindable<bool> CanRotate { get; private set; } = new BindableBool();
public Bindable<bool> CanRotateSelectionOrigin { get; private set; } = new BindableBool();
/// <summary>
/// Whether rotation anchored by the center of the playfield can currently be performed.
/// </summary>
public Bindable<bool> CanRotatePlayfieldOrigin { get; private set; } = new BindableBool();
/// <summary>
/// Performs a single, instant, atomic rotation operation.

View File

@ -1,52 +1,16 @@
// 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.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osuTK;
namespace osu.Game.Screens.Play.Break
{
public partial class BlurredIcon : BufferedContainer
public partial class BlurredIcon : GlowIcon
{
private readonly SpriteIcon icon;
public IconUsage Icon
{
set => icon.Icon = value;
get => icon.Icon;
}
public override Vector2 Size
{
set
{
icon.Size = value;
base.Size = value + BlurSigma * 5;
ForceRedraw();
}
get => base.Size;
}
public BlurredIcon()
: base(cachedFrameBuffer: true)
{
RelativePositionAxes = Axes.X;
Child = icon = new SpriteIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Shadow = false,
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.BlueLighter;
EffectBlending = BlendingParameters.Additive;
DrawOriginal = false;
}
}
}

View File

@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.Break
private const int blurred_icon_blur_sigma = 20;
private const int blurred_icon_size = 130;
private const float blurred_icon_final_offset = 0.35f;
private const float blurred_icon_final_offset = 0.38f;
private const float blurred_icon_offscreen_offset = 0.7f;
private readonly GlowIcon leftGlowIcon;

View File

@ -3,64 +3,45 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK;
namespace osu.Game.Screens.Play.Break
{
public partial class GlowIcon : Container
public partial class GlowIcon : GlowingDrawable
{
private readonly SpriteIcon spriteIcon;
private readonly BlurredIcon blurredIcon;
public override Vector2 Size
{
get => base.Size;
set
{
blurredIcon.Size = spriteIcon.Size = value;
blurredIcon.ForceRedraw();
}
}
public Vector2 BlurSigma
{
get => blurredIcon.BlurSigma;
set => blurredIcon.BlurSigma = value;
}
private SpriteIcon icon = null!;
public IconUsage Icon
{
get => spriteIcon.Icon;
set => spriteIcon.Icon = blurredIcon.Icon = value;
set => icon.Icon = value;
get => icon.Icon;
}
public new Vector2 Size
{
set => icon.Size = value;
get => icon.Size;
}
public GlowIcon()
{
RelativePositionAxes = Axes.X;
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
blurredIcon = new BlurredIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
spriteIcon = new SpriteIcon
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Shadow = false,
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
blurredIcon.Colour = colours.Blue;
GlowColour = colours.BlueLighter;
}
protected override Drawable CreateDrawable() => icon = new SpriteIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Shadow = false,
};
}
}

View File

@ -4,20 +4,26 @@
#nullable disable
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osu.Game.Localisation;
namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
public partial class PerformanceStatistic : StatisticDisplay
public partial class PerformanceStatistic : StatisticDisplay, IHasTooltip
{
public LocalisableString TooltipText { get; private set; }
private readonly ScoreInfo score;
private readonly Bindable<int> performance = new Bindable<int>();
@ -37,7 +43,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
if (score.PP.HasValue)
{
setPerformanceValue(score.PP.Value);
setPerformanceValue(score, score.PP.Value);
}
else
{
@ -52,15 +58,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics
var result = await performanceCalculator.CalculateAsync(score, attributes.Value.Attributes, cancellationToken ?? default).ConfigureAwait(false);
Schedule(() => setPerformanceValue(result.Total));
Schedule(() => setPerformanceValue(score, result.Total));
}, cancellationToken ?? default);
}
}
private void setPerformanceValue(double? pp)
private void setPerformanceValue(ScoreInfo scoreInfo, double? pp)
{
if (pp.HasValue)
{
performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero);
if (!scoreInfo.BeatmapInfo!.Status.GrantsPerformancePoints())
{
Alpha = 0.5f;
TooltipText = ResultsScreenStrings.NoPPForUnrankedBeatmaps;
}
else if (scoreInfo.Mods.Any(m => !m.Ranked))
{
Alpha = 0.5f;
TooltipText = ResultsScreenStrings.NoPPForUnrankedMods;
}
else
{
Alpha = 1f;
TooltipText = default;
}
}
}
public override void Appear()