1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-12 18:23:04 +08:00

Merge branch 'master' into bugfix/visual/long-commet-tooltip-overflow

This commit is contained in:
Dean Herbert 2024-08-05 17:01:20 +09:00 committed by GitHub
commit 17eb134797
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 916 additions and 144 deletions

View File

@ -21,7 +21,7 @@
] ]
}, },
"ppy.localisationanalyser.tools": { "ppy.localisationanalyser.tools": {
"version": "2024.517.0", "version": "2024.802.0",
"commands": [ "commands": [
"localisation" "localisation"
] ]

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.720.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.802.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

@ -42,7 +42,12 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
base.PostProcess(); base.PostProcess();
var hitObjects = Beatmap.HitObjects as List<OsuHitObject> ?? Beatmap.HitObjects.OfType<OsuHitObject>().ToList(); ApplyStacking(Beatmap);
}
internal static void ApplyStacking(IBeatmap beatmap)
{
var hitObjects = beatmap.HitObjects as List<OsuHitObject> ?? beatmap.HitObjects.OfType<OsuHitObject>().ToList();
if (hitObjects.Count > 0) if (hitObjects.Count > 0)
{ {
@ -50,14 +55,14 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.StackHeight = 0; h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(Beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(Beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap.BeatmapInfo, hitObjects);
} }
} }
private void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -209,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {

View File

@ -295,6 +295,12 @@ namespace osu.Game.Rulesets.Osu.Edit
if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius) if (Vector2.Distance(closestSnapPosition, screenSpacePosition) < snapRadius)
{ {
// if the snap target is a stacked object, snap to its unstacked position rather than its stacked position.
// this is intended to make working with stacks easier (because thanks to this, you can drag an object to any
// of the items on the stack to add an object to it, rather than having to drag to the position of the *first* object on it at all times).
if (b.Item is OsuHitObject osuObject && osuObject.StackOffset != Vector2.Zero)
closestSnapPosition = b.ToScreenSpace(b.ToLocalSpace(closestSnapPosition) - osuObject.StackOffset);
// only return distance portion, since time is not really valid // only return distance portion, since time is not really valid
snapResult = new SnapResult(closestSnapPosition, null, playfield); snapResult = new SnapResult(closestSnapPosition, null, playfield);
return true; return true;

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components;
@ -50,12 +51,33 @@ namespace osu.Game.Rulesets.Osu.Edit
{ {
var hitObjects = selectedMovableObjects; var hitObjects = selectedMovableObjects;
var localDelta = this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta);
// this conditional is a rather ugly special case for stacks.
// as it turns out, adding the `EditorBeatmap.Update()` call at the end of this would cause stacked objects to jitter when moved around
// (they would stack and then unstack every frame).
// the reason for that is that the selection handling abstractions are not aware of the distinction between "displayed" and "actual" position
// which is unique to osu! due to stacking being applied as a post-processing step.
// therefore, the following loop would occur:
// - on frame 1 the blueprint is snapped to the stack's baseline position. `EditorBeatmap.Update()` applies stacking successfully,
// the blueprint moves up the stack from its original drag position.
// - on frame 2 the blueprint's position is now the *stacked* position, which is interpreted higher up as *manually performing an unstack*
// to the blueprint's unstacked position (as the machinery higher up only cares about differences in screen space position).
if (hitObjects.Any(h => Precision.AlmostEquals(localDelta, -h.StackOffset)))
return true;
// this will potentially move the selection out of bounds... // this will potentially move the selection out of bounds...
foreach (var h in hitObjects) foreach (var h in hitObjects)
h.Position += this.ScreenSpaceDeltaToParentSpace(moveEvent.ScreenSpaceDelta); h.Position += localDelta;
// but this will be corrected. // but this will be corrected.
moveSelectionInBounds(); moveSelectionInBounds();
// manually update stacking.
// this intentionally bypasses the editor `UpdateState()` / beatmap processor flow for performance reasons,
// as the entire flow is too expensive to run on every movement.
Scheduler.AddOnce(OsuBeatmapProcessor.ApplyStacking, EditorBeatmap);
return true; return true;
} }

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input; using osu.Framework.Input;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing; using osu.Framework.Timing;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -63,8 +62,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
// -1 signals that the part is unusable, and should not be drawn // -1 signals that the part is unusable, and should not be drawn
parts[i].InvalidationID = -1; parts[i].InvalidationID = -1;
} }
AddLayout(partSizeCache);
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -95,12 +92,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private readonly LayoutValue<Vector2> partSizeCache = new LayoutValue<Vector2>(Invalidation.DrawInfo | Invalidation.RequiredParentSizeToFit | Invalidation.Presence);
private Vector2 partSize => partSizeCache.IsValid
? partSizeCache.Value
: (partSizeCache.Value = new Vector2(Texture.DisplayWidth, Texture.DisplayHeight) * DrawInfo.Matrix.ExtractScale().Xy);
/// <summary> /// <summary>
/// The amount of time to fade the cursor trail pieces. /// The amount of time to fade the cursor trail pieces.
/// </summary> /// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position) protected void AddTrail(Vector2 position)
{ {
position = ToLocalSpace(position);
if (InterpolateMovements) if (InterpolateMovements)
{ {
if (!lastPosition.HasValue) if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length; float distance = diff.Length;
Vector2 direction = diff / distance; Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier; float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0); float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
for (float d = interval; d < stopAt; d += interval) for (float d = interval; d < stopAt; d += interval)
@ -191,9 +184,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
} }
} }
private void addPart(Vector2 screenSpacePosition) private void addPart(Vector2 localSpacePosition)
{ {
parts[currentIndex].Position = ToLocalSpace(screenSpacePosition); parts[currentIndex].Position = localSpacePosition;
parts[currentIndex].Time = time + 1; parts[currentIndex].Time = time + 1;
++parts[currentIndex].InvalidationID; ++parts[currentIndex].InvalidationID;

View File

@ -206,6 +206,15 @@ namespace osu.Game.Rulesets.Osu.UI
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos);
private OsuResumeOverlay.OsuResumeOverlayInputBlocker? resumeInputBlocker;
public void AttachResumeOverlayInputBlocker(OsuResumeOverlay.OsuResumeOverlayInputBlocker resumeInputBlocker)
{
Debug.Assert(this.resumeInputBlocker == null);
this.resumeInputBlocker = resumeInputBlocker;
AddInternal(resumeInputBlocker);
}
private partial class ProxyContainer : LifetimeManagementContainer private partial class ProxyContainer : LifetimeManagementContainer
{ {
public void Add(Drawable proxy) => AddInternal(proxy); public void Add(Drawable proxy) => AddInternal(proxy);

View File

@ -33,9 +33,30 @@ namespace osu.Game.Rulesets.Osu.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
OsuResumeOverlayInputBlocker? inputBlocker = null;
if (drawableRuleset != null)
{
var osuPlayfield = (OsuPlayfield)drawableRuleset.Playfield;
osuPlayfield.AttachResumeOverlayInputBlocker(inputBlocker = new OsuResumeOverlayInputBlocker());
}
Add(cursorScaleContainer = new Container Add(cursorScaleContainer = new Container
{ {
Child = clickToResumeCursor = new OsuClickToResumeCursor { ResumeRequested = Resume } Child = clickToResumeCursor = new OsuClickToResumeCursor
{
ResumeRequested = () =>
{
// since the user had to press a button to tap the resume cursor,
// block that press event from potentially reaching a hit circle that's behind the cursor.
// we cannot do this from OsuClickToResumeCursor directly since we're in a different input manager tree than the gameplay one,
// so we rely on a dedicated input blocking component that's implanted in there to do that for us.
if (inputBlocker != null)
inputBlocker.BlockNextPress = true;
Resume();
}
}
}); });
} }
@ -115,10 +136,7 @@ namespace osu.Game.Rulesets.Osu.UI
return false; return false;
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint); scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
ResumeRequested?.Invoke();
// When resuming with a button, we do not want the osu! input manager to see this button press and include it in the score.
// To ensure that this works correctly, schedule the resume operation one frame forward, since the resume operation enables the input manager to see input events.
Schedule(() => ResumeRequested?.Invoke());
return true; return true;
} }
@ -143,5 +161,27 @@ namespace osu.Game.Rulesets.Osu.UI
this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint); this.FadeColour(IsHovered ? Color4.White : Color4.Orange, 400, Easing.OutQuint);
} }
} }
public partial class OsuResumeOverlayInputBlocker : Drawable, IKeyBindingHandler<OsuAction>
{
public bool BlockNextPress;
public OsuResumeOverlayInputBlocker()
{
RelativeSizeAxes = Axes.Both;
Depth = float.MinValue;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
{
bool block = BlockNextPress;
BlockNextPress = false;
return block;
}
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
} }
} }

View File

@ -259,6 +259,44 @@ namespace osu.Game.Tests.Database
}); });
} }
[Test]
public void TestNoChangesAfterDelete()
{
RunTestWithRealmAsync(async (realm, storage) =>
{
var importer = new BeatmapImporter(storage, realm);
using var rulesets = new RealmRulesetStore(realm, storage);
using var __ = getBeatmapArchive(out string pathOriginal);
using var _ = getBeatmapArchive(out string pathOriginalSecond);
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
importBeforeUpdate!.PerformWrite(s => s.DeletePending = true);
var dateBefore = importBeforeUpdate.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null);
checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
});
}
[Test] [Test]
public void TestNoChanges() public void TestNoChanges()
{ {
@ -272,21 +310,25 @@ namespace osu.Game.Tests.Database
var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal)); var importBeforeUpdate = await importer.Import(new ImportTask(pathOriginal));
var dateBefore = importBeforeUpdate!.Value.DateAdded;
Assert.That(importBeforeUpdate, Is.Not.Null); Assert.That(importBeforeUpdate, Is.Not.Null);
Debug.Assert(importBeforeUpdate != null); Debug.Assert(importBeforeUpdate != null);
var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value); var importAfterUpdate = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(pathOriginalSecond), importBeforeUpdate.Value);
realm.Run(r => r.Refresh());
Assert.That(importAfterUpdate, Is.Not.Null); Assert.That(importAfterUpdate, Is.Not.Null);
Debug.Assert(importAfterUpdate != null); Debug.Assert(importAfterUpdate != null);
realm.Run(r => r.Refresh());
checkCount<BeatmapSetInfo>(realm, 1); checkCount<BeatmapSetInfo>(realm, 1);
checkCount<BeatmapInfo>(realm, count_beatmaps); checkCount<BeatmapInfo>(realm, count_beatmaps);
checkCount<BeatmapMetadata>(realm, count_beatmaps); checkCount<BeatmapMetadata>(realm, count_beatmaps);
Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1)); Assert.That(importBeforeUpdate.Value.Beatmaps.First().OnlineID, Is.GreaterThan(-1));
Assert.That(importBeforeUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importAfterUpdate.Value.DateAdded, Is.EqualTo(dateBefore));
Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID)); Assert.That(importBeforeUpdate.ID, Is.EqualTo(importAfterUpdate.ID));
}); });
} }

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -13,6 +14,7 @@ using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -32,6 +34,23 @@ namespace osu.Game.Tests.Visual.Gameplay
[Resolved] [Resolved]
private AudioManager audioManager { get; set; } = null!; private AudioManager audioManager { get; set; } = null!;
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap
{
HitObjects =
{
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 0,
},
new HitCircle
{
Position = OsuPlayfield.BASE_SIZE / 2,
StartTime = 5000,
}
}
};
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) => protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@ -70,18 +89,16 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
// Z key was released before pause, resuming should not trigger it
checkKey(() => counter, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 2, true); checkKey(() => counter, 2, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z)); AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 2, false); checkKey(() => counter, 2, false);
AddStep("press Z", () => InputManager.PressKey(Key.Z));
checkKey(() => counter, 3, true);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 3, false);
} }
[Test] [Test]
@ -90,30 +107,29 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
checkKey(() => counter, 0, false); checkKey(() => counter, 0, false);
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, false);
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 2, true); checkKey(() => counter, 2, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 2, false); checkKey(() => counter, 2, false);
} }
@ -145,8 +161,11 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false); checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, false); checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
} }
[Test] [Test]
@ -155,12 +174,12 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
@ -202,12 +221,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single())); AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z)); AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
checkKey(() => counterZ, 1, false); checkKey(() => counterZ, 2, true);
checkKey(() => counterX, 1, true); checkKey(() => counterX, 1, true);
AddStep("release X", () => InputManager.ReleaseKey(Key.X)); AddStep("release X", () => InputManager.ReleaseKey(Key.X));
checkKey(() => counterZ, 1, false);
checkKey(() => counterX, 1, false); checkKey(() => counterX, 1, false);
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counterZ, 2, false);
} }
[Test] [Test]
@ -216,24 +237,50 @@ namespace osu.Game.Tests.Visual.Gameplay
KeyCounter counter = null!; KeyCounter counter = null!;
loadPlayer(() => new ManiaRuleset()); loadPlayer(() => new ManiaRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Key1)); AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<ManiaAction> actionTrigger && actionTrigger.Action == ManiaAction.Special1));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("pause", () => Player.Pause()); AddStep("pause", () => Player.Pause());
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
AddStep("press D", () => InputManager.PressKey(Key.D)); AddStep("press space", () => InputManager.PressKey(Key.Space));
AddStep("resume", () => Player.Resume()); AddStep("resume", () => Player.Resume());
AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning); AddUntilStep("wait for resume", () => Player.GameplayClockContainer.IsRunning);
checkKey(() => counter, 1, true); checkKey(() => counter, 1, true);
AddStep("release D", () => InputManager.ReleaseKey(Key.D)); AddStep("release space", () => InputManager.ReleaseKey(Key.Space));
checkKey(() => counter, 1, false); checkKey(() => counter, 1, false);
} }
[Test]
public void TestOsuRegisterInputFromPressingOrangeCursorButPressIsBlocked()
{
KeyCounter counter = null!;
loadPlayer(() => new OsuRuleset());
AddStep("get key counter", () => counter = this.ChildrenOfType<KeyCounter>().Single(k => k.Trigger is KeyCounterActionTrigger<OsuAction> actionTrigger && actionTrigger.Action == OsuAction.LeftButton));
AddStep("pause", () => Player.Pause());
AddStep("resume", () => Player.Resume());
AddStep("go to resume cursor", () => InputManager.MoveMouseTo(this.ChildrenOfType<OsuResumeOverlay.OsuClickToResumeCursor>().Single()));
AddStep("press Z to resume", () => InputManager.PressKey(Key.Z));
// ensure the input manager receives the Z button press...
checkKey(() => counter, 1, true);
AddAssert("button is pressed in kbc", () => Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Single() == OsuAction.LeftButton);
// ...but also ensure the hit circle in front of the cursor isn't hit by checking max combo.
AddAssert("circle not hit", () => Player.ScoreProcessor.HighestCombo.Value, () => Is.EqualTo(0));
AddStep("release Z", () => InputManager.ReleaseKey(Key.Z));
checkKey(() => counter, 1, false);
AddAssert("button is released in kbc", () => !Player.DrawableRuleset.Playfield.FindClosestParent<OsuInputManager>()!.PressedActions.Any());
}
private void loadPlayer(Func<Ruleset> createRuleset) private void loadPlayer(Func<Ruleset> createRuleset)
{ {
AddStep("set ruleset", () => currentRuleset = createRuleset()); AddStep("set ruleset", () => currentRuleset = createRuleset());
@ -241,9 +288,10 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1); AddUntilStep("player loaded", () => Player.IsLoaded && Player.Alpha == 1);
AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded)); AddUntilStep("wait for hud", () => Player.HUDOverlay.ChildrenOfType<SkinComponentsContainer>().All(s => s.ComponentsLoaded));
AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(20000)); AddStep("seek to gameplay", () => Player.GameplayClockContainer.Seek(0));
AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(20000).Within(500)); AddUntilStep("wait for seek to finish", () => Player.DrawableRuleset.FrameStableClock.CurrentTime, () => Is.EqualTo(0).Within(500));
AddAssert("not in break", () => !Player.IsBreakTime.Value); AddAssert("not in break", () => !Player.IsBreakTime.Value);
AddStep("move cursor to center", () => InputManager.MoveMouseTo(Player.DrawableRuleset.Playfield));
} }
private void checkKey(Func<KeyCounter> counter, int count, bool active) private void checkKey(Func<KeyCounter> counter, int count, bool active)

View File

@ -0,0 +1,64 @@
// 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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.Profile;
using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Rulesets.Osu;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneUserProfileDailyChallenge : OsuManualInputManagerTestScene
{
[Cached]
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>(new UserProfileData(new APIUser(), new OsuRuleset().RulesetInfo));
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
protected override void LoadComplete()
{
base.LoadComplete();
DailyChallengeStatsDisplay display = null!;
AddSliderStep("daily", 0, 999, 2, v => update(s => s.DailyStreakCurrent = v));
AddSliderStep("daily best", 0, 999, 2, v => update(s => s.DailyStreakBest = v));
AddSliderStep("weekly", 0, 250, 1, v => update(s => s.WeeklyStreakCurrent = v));
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v));
AddStep("create", () =>
{
Clear();
Add(new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background2,
});
Add(display = new DailyChallengeStatsDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(1f),
User = { BindTarget = User },
});
});
AddStep("hover", () => InputManager.MoveMouseTo(display));
}
private void update(Action<APIUserDailyChallengeStatistics> change)
{
change.Invoke(User.Value!.User.DailyChallengeStatistics);
User.Value = new UserProfileData(User.Value.User, User.Value.Ruleset);
}
}
}

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -24,7 +25,17 @@ namespace osu.Game.Tests.Visual.Online
[SetUpSteps] [SetUpSteps]
public void SetUp() public void SetUp()
{ {
AddStep("create profile overlay", () => Child = profile = new UserProfileOverlay()); AddStep("create profile overlay", () =>
{
profile = new UserProfileOverlay();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[] { (typeof(UserProfileOverlay), profile) },
Child = profile,
};
});
} }
[Test] [Test]
@ -131,6 +142,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue, ProfileHue = hue,
PlayMode = "osu",
}); });
return true; return true;
} }
@ -174,6 +186,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue, ProfileHue = hue,
PlayMode = "osu",
})); }));
int hue2 = 0; int hue2 = 0;
@ -189,6 +202,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = CountryCode.JP, CountryCode = CountryCode.JP,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
ProfileHue = hue2, ProfileHue = hue2,
PlayMode = "osu",
})); }));
} }
@ -282,6 +296,15 @@ namespace osu.Game.Tests.Visual.Online
ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png", ImageUrlLowRes = "https://assets.ppy.sh/profile-badges/contributor.png",
}, },
}, },
DailyChallengeStatistics = new APIUserDailyChallengeStatistics
{
DailyStreakCurrent = 231,
WeeklyStreakCurrent = 18,
DailyStreakBest = 370,
WeeklyStreakBest = 51,
Top10PercentPlacements = 345,
Top50PercentPlacements = 427,
},
Title = "osu!volunteer", Title = "osu!volunteer",
Colour = "ff0000", Colour = "ff0000",
Achievements = Array.Empty<APIUserAchievement>(), Achievements = Array.Empty<APIUserAchievement>(),

View File

@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
} }
} }
@ -156,7 +156,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
@ -191,7 +191,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
} }
} }

View File

@ -43,6 +43,8 @@ namespace osu.Game.Beatmaps
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
{ {
var originalDateAdded = original.DateAdded;
Guid originalId = original.ID; Guid originalId = original.ID;
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false); var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false);
@ -57,8 +59,11 @@ namespace osu.Game.Beatmaps
// If there were no changes, ensure we don't accidentally nuke ourselves. // If there were no changes, ensure we don't accidentally nuke ourselves.
if (first.ID == originalId) if (first.ID == originalId)
{ {
first.PerformRead(s => first.PerformWrite(s =>
{ {
// Transfer local values which should be persisted across a beatmap update.
s.DateAdded = originalDateAdded;
// Re-run processing even in this case. We might have outdated metadata. // Re-run processing even in this case. We might have outdated metadata.
ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst); ProcessBeatmap?.Invoke(s, MetadataLookupScope.OnlineFirst);
}); });
@ -79,7 +84,7 @@ namespace osu.Game.Beatmaps
original.DeletePending = true; original.DeletePending = true;
// Transfer local values which should be persisted across a beatmap update. // Transfer local values which should be persisted across a beatmap update.
updated.DateAdded = original.DateAdded; updated.DateAdded = originalDateAdded;
transferCollectionReferences(realm, original, updated); transferCollectionReferences(realm, original, updated);
@ -278,6 +283,9 @@ namespace osu.Game.Beatmaps
protected override void UndeleteForReuse(BeatmapSetInfo existing) protected override void UndeleteForReuse(BeatmapSetInfo existing)
{ {
if (!existing.DeletePending)
return;
base.UndeleteForReuse(existing); base.UndeleteForReuse(existing);
existing.DateAdded = DateTimeOffset.UtcNow; existing.DateAdded = DateTimeOffset.UtcNow;
} }

View File

@ -43,6 +43,9 @@ namespace osu.Game.Collections
// //
// if we want to support user sorting (but changes will need to be made to realm to persist). // if we want to support user sorting (but changes will need to be made to realm to persist).
ShowDragHandle.Value = false; ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
} }
protected override Drawable CreateContent() => new ItemContent(Model); protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary> /// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>. /// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary> /// </summary>
private partial class ItemContent : CircularContainer private partial class ItemContent : CompositeDrawable
{ {
private readonly Live<BeatmapCollection> collection; private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
Height = item_height; Height = item_height;
Masking = true;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Children = new[] InternalChildren = new[]
{ {
collection.IsManaged collection.IsManaged
? new DeleteButton(collection) ? new DeleteButton(collection)
@ -132,7 +134,7 @@ namespace osu.Game.Collections
} }
} }
public partial class DeleteButton : CompositeDrawable public partial class DeleteButton : OsuClickableContainer
{ {
public Func<Vector2, bool> IsTextBoxHovered = null!; public Func<Vector2, bool> IsTextBoxHovered = null!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
InternalChild = fadeContainer = new Container Child = fadeContainer = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Alpha = 0.1f, Alpha = 0.1f,
@ -176,6 +178,14 @@ namespace osu.Game.Collections
} }
} }
}; };
Action = () =>
{
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
};
} }
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{ {
background.FlashColour(Color4.White, 150); background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0) return base.OnClick(e);
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
} }
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c)); private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));

View File

@ -64,6 +64,7 @@ namespace osu.Game.Graphics.Containers
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new HoverClickSounds(),
new GridContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -92,7 +93,6 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
}, },
new HoverClickSounds()
}; };
} }

View File

@ -272,6 +272,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty("groups")] [JsonProperty("groups")]
public APIUserGroup[] Groups; public APIUserGroup[] Groups;
[JsonProperty("daily_challenge_user_stats")]
public APIUserDailyChallengeStatistics DailyChallengeStatistics = new APIUserDailyChallengeStatistics();
public override string ToString() => Username; public override string ToString() => Username;
/// <summary> /// <summary>

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 System;
using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIUserDailyChallengeStatistics
{
[JsonProperty("user_id")]
public int UserID;
[JsonProperty("daily_streak_best")]
public int DailyStreakBest;
[JsonProperty("daily_streak_current")]
public int DailyStreakCurrent;
[JsonProperty("weekly_streak_best")]
public int WeeklyStreakBest;
[JsonProperty("weekly_streak_current")]
public int WeeklyStreakCurrent;
[JsonProperty("top_10p_placements")]
public int Top10PercentPlacements;
[JsonProperty("top_50p_placements")]
public int Top50PercentPlacements;
[JsonProperty("playcount")]
public int PlayCount;
[JsonProperty("last_update")]
public DateTimeOffset? LastUpdate;
[JsonProperty("last_weekly_streak")]
public DateTimeOffset? LastWeeklyStreak;
}
}

View File

@ -4,6 +4,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Allocation; 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.Shapes; using osu.Framework.Graphics.Shapes;
@ -11,14 +12,16 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation; using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Overlays.Mods namespace osu.Game.Overlays.Mods
{ {
public partial class ModCustomisationHeader : OsuHoverContainer public partial class ModCustomisationHeader : OsuHoverContainer
{ {
private Box background = null!; private Box background = null!;
private Box backgroundFlash = null!;
private SpriteIcon icon = null!; private SpriteIcon icon = null!;
[Resolved] [Resolved]
@ -46,6 +49,13 @@ namespace osu.Game.Overlays.Mods
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
backgroundFlash = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive,
Alpha = 0,
},
new OsuSpriteText new OsuSpriteText
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.CentreLeft,
@ -84,6 +94,12 @@ namespace osu.Game.Overlays.Mods
TooltipText = e.NewValue TooltipText = e.NewValue
? string.Empty ? string.Empty
: ModSelectOverlayStrings.CustomisationPanelDisabledReason; : ModSelectOverlayStrings.CustomisationPanelDisabledReason;
if (e.NewValue)
{
backgroundFlash.FadeInFromZero(150, Easing.OutQuad).Then()
.FadeOutFromOne(350, Easing.OutQuad);
}
}, true); }, true);
Expanded.BindValueChanged(v => Expanded.BindValueChanged(v =>

View File

@ -138,6 +138,7 @@ namespace osu.Game.Overlays.Mods
}, },
new GridContainer new GridContainer
{ {
Padding = new MarginPadding { Top = 1, Bottom = 3 },
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
RowDimensions = new[] RowDimensions = new[]
{ {

View File

@ -668,6 +668,8 @@ namespace osu.Game.Overlays.Mods
[Cached] [Cached]
internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer> internal partial class ColumnScrollContainer : OsuScrollContainer<ColumnFlowContainer>
{ {
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public ColumnScrollContainer() public ColumnScrollContainer()
: base(Direction.Horizontal) : base(Direction.Horizontal)
{ {

View File

@ -0,0 +1,121 @@
// 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.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class DailyChallengeStatsDisplay : CompositeDrawable, IHasCustomTooltip<DailyChallengeTooltipData>
{
public readonly Bindable<UserProfileData?> User = new Bindable<UserProfileData?>();
public DailyChallengeTooltipData? TooltipContent { get; private set; }
private OsuSpriteText dailyPlayCount = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 5;
Masking = true;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding(5f),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 12))
{
AutoSizeAxes = Axes.Both,
// can't use this because osu-web does weird stuff with \\n.
// Text = UsersStrings.ShowDailyChallengeTitle.,
Text = "Daily\nChallenge",
Margin = new MarginPadding { Horizontal = 5f, Bottom = 2f },
},
new Container
{
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
CornerRadius = 5f,
Masking = true,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background6,
},
dailyPlayCount = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
UseFullGlyphHeight = false,
Colour = colourProvider.Content2,
Margin = new MarginPadding { Horizontal = 10f, Vertical = 5f },
},
}
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
User.BindValueChanged(_ => updateDisplay(), true);
}
private void updateDisplay()
{
if (User.Value == null || User.Value.Ruleset.OnlineID != 0)
{
Hide();
return;
}
APIUserDailyChallengeStatistics stats = User.Value.User.DailyChallengeStatistics;
dailyPlayCount.Text = UsersStrings.ShowDailyChallengeUnitDay(stats.PlayCount.ToLocalisableString("N0"));
dailyPlayCount.Colour = colours.ForRankingTier(tierForPlayCount(stats.PlayCount));
TooltipContent = new DailyChallengeTooltipData(colourProvider, stats);
Show();
static RankingTier tierForPlayCount(int playCount) => DailyChallengeStatsTooltip.TierForDaily(playCount / 3);
}
public ITooltip<DailyChallengeTooltipData> GetCustomTooltip() => new DailyChallengeStatsTooltip();
}
}

View File

@ -0,0 +1,241 @@
// 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.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osuTK;
using Box = osu.Framework.Graphics.Shapes.Box;
using Color4 = osuTK.Graphics.Color4;
namespace osu.Game.Overlays.Profile.Header.Components
{
public partial class DailyChallengeStatsTooltip : VisibilityContainer, ITooltip<DailyChallengeTooltipData>
{
private StreakPiece currentDaily = null!;
private StreakPiece currentWeekly = null!;
private StatisticsPiece bestDaily = null!;
private StatisticsPiece bestWeekly = null!;
private StatisticsPiece topTen = null!;
private StatisticsPiece topFifty = null!;
private Box topBackground = null!;
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
CornerRadius = 20f;
Masking = true;
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.25f),
Radius = 30f,
};
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
topBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding(15f),
Spacing = new Vector2(30f),
Children = new[]
{
currentDaily = new StreakPiece(UsersStrings.ShowDailyChallengeDailyStreakCurrent),
currentWeekly = new StreakPiece(UsersStrings.ShowDailyChallengeWeeklyStreakCurrent),
}
},
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding(15f),
Spacing = new Vector2(10f),
Children = new[]
{
bestDaily = new StatisticsPiece(UsersStrings.ShowDailyChallengeDailyStreakBest),
bestWeekly = new StatisticsPiece(UsersStrings.ShowDailyChallengeWeeklyStreakBest),
topTen = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop10pPlacements),
topFifty = new StatisticsPiece(UsersStrings.ShowDailyChallengeTop50pPlacements),
}
},
}
}
};
}
public void SetContent(DailyChallengeTooltipData content)
{
var statistics = content.Statistics;
var colourProvider = content.ColourProvider;
background.Colour = colourProvider.Background4;
topBackground.Colour = colourProvider.Background5;
currentDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0"));
currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0"));
currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
bestDaily.Value = UsersStrings.ShowDailyChallengeUnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0"));
bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestWeekly.Value = UsersStrings.ShowDailyChallengeUnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0"));
bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0");
topTen.ValueColour = colourProvider.Content2;
topFifty.Value = statistics.Top50PercentPlacements.ToLocalisableString(@"N0");
topFifty.ValueColour = colourProvider.Content2;
}
// reference: https://github.com/ppy/osu-web/blob/8206e0e91eeea80ccf92f0586561346dd40e085e/resources/js/profile-page/daily-challenge.tsx#L13-L43
public static RankingTier TierForDaily(int daily)
{
if (daily > 360)
return RankingTier.Lustrous;
if (daily > 240)
return RankingTier.Radiant;
if (daily > 120)
return RankingTier.Rhodium;
if (daily > 60)
return RankingTier.Platinum;
if (daily > 30)
return RankingTier.Gold;
if (daily > 10)
return RankingTier.Silver;
if (daily > 5)
return RankingTier.Bronze;
return RankingTier.Iron;
}
public static RankingTier TierForWeekly(int weekly) => TierForDaily((weekly - 1) * 7);
protected override void PopIn() => this.FadeIn(200, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(200, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private partial class StreakPiece : FillFlowContainer
{
private readonly OsuSpriteText valueText;
public LocalisableString Value
{
set => valueText.Text = value;
}
public ColourInfo ValueColour
{
set => valueText.Colour = value;
}
public StreakPiece(LocalisableString title)
{
AutoSizeAxes = Axes.Both;
Direction = FillDirection.Vertical;
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Text = title,
},
valueText = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 40, weight: FontWeight.Light),
}
};
}
}
private partial class StatisticsPiece : CompositeDrawable
{
private readonly OsuSpriteText valueText;
public LocalisableString Value
{
set => valueText.Text = value;
}
public ColourInfo ValueColour
{
set => valueText.Colour = value;
}
public StatisticsPiece(LocalisableString title)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChildren = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12),
Text = title,
},
valueText = new OsuSpriteText
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Font = OsuFont.GetFont(size: 12),
}
};
}
}
}
public record DailyChallengeTooltipData(OverlayColourProvider ColourProvider, APIUserDailyChallengeStatistics Statistics);
}

View File

@ -44,22 +44,41 @@ namespace osu.Game.Overlays.Profile.Header.Components
Spacing = new Vector2(0, 15), Spacing = new Vector2(0, 15),
Children = new Drawable[] Children = new Drawable[]
{ {
new FillFlowContainer new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, ColumnDimensions = new[]
Spacing = new Vector2(20),
Children = new Drawable[]
{ {
detailGlobalRank = new ProfileValueDisplay(true) new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new[]
{ {
Title = UsersStrings.ShowRankGlobalSimple, detailGlobalRank = new ProfileValueDisplay(true)
}, {
detailCountryRank = new ProfileValueDisplay(true) Title = UsersStrings.ShowRankGlobalSimple,
{ },
Title = UsersStrings.ShowRankCountrySimple, Empty(),
}, detailCountryRank = new ProfileValueDisplay(true)
{
Title = UsersStrings.ShowRankCountrySimple,
},
new DailyChallengeStatsDisplay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
User = { BindTarget = User },
}
}
} }
}, },
new Container new Container

View File

@ -2,7 +2,11 @@
// 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 System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
@ -10,6 +14,8 @@ namespace osu.Game.Overlays.Settings
public partial class SettingsEnumDropdown<T> : SettingsDropdown<T> public partial class SettingsEnumDropdown<T> : SettingsDropdown<T>
where T : struct, Enum where T : struct, Enum
{ {
public override IEnumerable<LocalisableString> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.GetLocalisableDescription()));
protected override OsuDropdown<T> CreateDropdown() => new DropdownControl(); protected override OsuDropdown<T> CreateDropdown() => new DropdownControl();
protected new partial class DropdownControl : OsuEnumDropdown<T> protected new partial class DropdownControl : OsuEnumDropdown<T>

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.
#nullable disable
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
@ -30,11 +28,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Drawable userContent; private readonly Drawable userContent;
[Resolved] private bool alwaysShowControlPoints;
private EditorClock editorClock { get; set; }
public bool AlwaysShowControlPoints
{
get => alwaysShowControlPoints;
set
{
if (value == alwaysShowControlPoints)
return;
alwaysShowControlPoints = value;
controlPointsVisible.TriggerChange();
}
}
[Resolved] [Resolved]
private EditorBeatmap editorBeatmap { get; set; } private EditorClock editorClock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
/// <summary> /// <summary>
/// The timeline's scroll position in the last frame. /// The timeline's scroll position in the last frame.
@ -61,6 +74,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary> /// </summary>
private float defaultTimelineZoom; private float defaultTimelineZoom;
private WaveformGraph waveform = null!;
private TimelineTickDisplay ticks = null!;
private TimelineControlPointDisplay controlPoints = null!;
private Container mainContent = null!;
private Bindable<float> waveformOpacity = null!;
private Bindable<bool> controlPointsVisible = null!;
private Bindable<bool> ticksVisible = null!;
private double trackLengthForZoom;
private readonly IBindable<Track> track = new Bindable<Track>();
public Timeline(Drawable userContent) public Timeline(Drawable userContent)
{ {
this.userContent = userContent; this.userContent = userContent;
@ -73,22 +102,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
ScrollbarVisible = false; ScrollbarVisible = false;
} }
private WaveformGraph waveform;
private TimelineTickDisplay ticks;
private TimelineControlPointDisplay controlPoints;
private Container mainContent;
private Bindable<float> waveformOpacity;
private Bindable<bool> controlPointsVisible;
private Bindable<bool> ticksVisible;
private double trackLengthForZoom;
private readonly IBindable<Track> track = new Bindable<Track>();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config) private void load(IBindable<WorkingBeatmap> beatmap, OsuColour colours, OsuConfigManager config)
{ {
@ -178,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
controlPointsVisible.BindValueChanged(visible => controlPointsVisible.BindValueChanged(visible =>
{ {
if (visible.NewValue) if (visible.NewValue || alwaysShowControlPoints)
{ {
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint); this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
mainContent.MoveToY(15, 200, Easing.OutQuint); mainContent.MoveToY(15, 200, Easing.OutQuint);
@ -318,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
} }
[Resolved] [Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
/// <summary> /// <summary>
/// The total amount of time visible on the timeline. /// The total amount of time visible on the timeline.

View File

@ -69,19 +69,24 @@ namespace osu.Game.Screens.Edit.Compose
if (ruleset == null || composer == null) if (ruleset == null || composer == null)
return base.CreateTimelineContent(); return base.CreateTimelineContent();
TimelineBreakDisplay breakDisplay = new TimelineBreakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
};
return wrapSkinnableContent(new Container return wrapSkinnableContent(new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new[]
{ {
// We want to display this below hitobjects to better expose placement objects visually.
// It needs to be above the blueprint container to handle drags on breaks though.
breakDisplay.CreateProxy(),
new TimelineBlueprintContainer(composer), new TimelineBlueprintContainer(composer),
new TimelineBreakDisplay breakDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Height = 0.75f,
},
} }
}); });
} }

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit namespace osu.Game.Screens.Edit
@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider colourProvider) private void load()
{ {
// Grid with only two rows. // Grid with only two rows.
// First is the timeline area, which should be allowed to expand as required. // First is the timeline area, which should be allowed to expand as required.
@ -107,10 +106,18 @@ namespace osu.Game.Screens.Edit
MainContent.Add(content); MainContent.Add(content);
content.FadeInFromZero(300, Easing.OutQuint); content.FadeInFromZero(300, Easing.OutQuint);
LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add); LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timeline =>
{
ConfigureTimeline(timeline);
timelineContent.Add(timeline);
});
}); });
} }
protected virtual void ConfigureTimeline(TimelineArea timelineArea)
{
}
protected abstract Drawable CreateMainContent(); protected abstract Drawable CreateMainContent();
protected virtual Drawable CreateTimelineContent() => new Container(); protected virtual Drawable CreateTimelineContent() => new Container();

View File

@ -6,6 +6,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit.Timing namespace osu.Game.Screens.Edit.Timing
{ {
@ -53,5 +54,12 @@ namespace osu.Game.Screens.Edit.Timing
SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time); SelectedGroup.Value = EditorBeatmap.ControlPointInfo.GroupAt(nearestTimingPoint.Time);
} }
} }
protected override void ConfigureTimeline(TimelineArea timelineArea)
{
base.ConfigureTimeline(timelineArea);
timelineArea.Timeline.AlwaysShowControlPoints = true;
}
} }
} }

View File

@ -267,7 +267,8 @@ namespace osu.Game.Screens.Ranking
foreach (var s in scores) foreach (var s in scores)
addScore(s); addScore(s);
lastFetchCompleted = true; // allow a frame for scroll container to adjust its dimensions with the added scores before fetching again.
Schedule(() => lastFetchCompleted = true);
if (ScorePanelList.IsEmpty) if (ScorePanelList.IsEmpty)
{ {

View File

@ -40,6 +40,7 @@ namespace osu.Game.Screens.Select
{ {
case "star": case "star":
case "stars": case "stars":
case "sr":
return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2);
case "ar": case "ar":

View File

@ -15,7 +15,6 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
@ -38,6 +37,7 @@ using osu.Game.Users.Drawables;
using osu.Game.Utils; using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using CommonStrings = osu.Game.Localisation.CommonStrings;
namespace osu.Game.Screens.SelectV2.Leaderboards namespace osu.Game.Screens.SelectV2.Leaderboards
{ {
@ -61,7 +61,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
private const float statistics_regular_min_width = 175; private const float statistics_regular_min_width = 175;
private const float statistics_compact_min_width = 100; private const float statistics_compact_min_width = 100;
private const float rank_label_width = 65; private const float rank_label_width = 65;
private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width;
private readonly ScoreInfo score; private readonly ScoreInfo score;
private readonly bool sheared; private readonly bool sheared;
@ -560,33 +559,34 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint); background.FadeColour(IsHovered ? backgroundColour.Lighten(0.2f) : backgroundColour, transition_duration, Easing.OutQuint);
totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint); totalScoreBackground.FadeColour(IsHovered ? lightenedGradient : totalScoreBackgroundGradient, transition_duration, Easing.OutQuint);
if (DrawWidth < rank_label_visibility_width_cutoff && IsHovered) if (IsHovered && currentMode != DisplayMode.Full)
rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint); rankLabelOverlay.FadeIn(transition_duration, Easing.OutQuint);
else else
rankLabelOverlay.FadeOut(transition_duration, Easing.OutQuint); rankLabelOverlay.FadeOut(transition_duration, Easing.OutQuint);
} }
protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) private DisplayMode? currentMode;
{
Scheduler.AddOnce(() =>
{
// when width decreases
// - hide rank and show rank overlay on avatar when hovered, then
// - compact statistics, then
// - hide statistics
if (DrawWidth >= rank_label_visibility_width_cutoff) protected override void Update()
{
base.Update();
DisplayMode mode = getCurrentDisplayMode();
if (currentMode != mode)
{
if (mode >= DisplayMode.Full)
rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); rankLabel.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint);
else else
rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint); rankLabel.FadeOut(transition_duration, Easing.OutQuint).MoveToX(-rankLabel.DrawWidth, transition_duration, Easing.OutQuint);
if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width) if (mode >= DisplayMode.Regular)
{ {
statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint);
statisticsContainer.Direction = FillDirection.Horizontal; statisticsContainer.Direction = FillDirection.Horizontal;
statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint); statisticsContainer.ScaleTo(1, transition_duration, Easing.OutQuint);
} }
else if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width) else if (mode >= DisplayMode.Compact)
{ {
statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint); statisticsContainer.FadeIn(transition_duration, Easing.OutQuint).MoveToX(0, transition_duration, Easing.OutQuint);
statisticsContainer.Direction = FillDirection.Vertical; statisticsContainer.Direction = FillDirection.Vertical;
@ -594,13 +594,35 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
} }
else else
statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint); statisticsContainer.FadeOut(transition_duration, Easing.OutQuint).MoveToX(statisticsContainer.DrawWidth, transition_duration, Easing.OutQuint);
});
return base.OnInvalidate(invalidation, source); currentMode = mode;
}
}
private DisplayMode getCurrentDisplayMode()
{
if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width + rank_label_width)
return DisplayMode.Full;
if (DrawWidth >= height + username_min_width + statistics_regular_min_width + expanded_right_content_width)
return DisplayMode.Regular;
if (DrawWidth >= height + username_min_width + statistics_compact_min_width + expanded_right_content_width)
return DisplayMode.Compact;
return DisplayMode.Minimal;
} }
#region Subclasses #region Subclasses
private enum DisplayMode
{
Minimal,
Compact,
Regular,
Full
}
private partial class DateLabel : DrawableDate private partial class DateLabel : DrawableDate
{ {
public DateLabel(DateTimeOffset date) public DateLabel(DateTimeOffset date)
@ -749,8 +771,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
if (score.Files.Count <= 0) return items.ToArray(); if (score.Files.Count <= 0) return items.ToArray();
items.Add(new OsuMenuItem(Localisation.CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score))); items.Add(new OsuMenuItem(CommonStrings.Export, MenuItemType.Standard, () => scoreManager.Export(score)));
items.Add(new OsuMenuItem(CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); items.Add(new OsuMenuItem(Resources.Localisation.Web.CommonStrings.ButtonsDelete, MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score))));
return items.ToArray(); return items.ToArray();
} }

View File

@ -30,13 +30,13 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" /> <PackageReference Include="Microsoft.Toolkit.HighPerformance" Version="7.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ppy.LocalisationAnalyser" Version="2024.517.0"> <PackageReference Include="ppy.LocalisationAnalyser" Version="2024.802.0">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="11.5.0" /> <PackageReference Include="Realm" Version="11.5.0" />
<PackageReference Include="ppy.osu.Framework" Version="2024.720.0" /> <PackageReference Include="ppy.osu.Framework" Version="2024.802.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2024.731.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2024.802.0" />
<PackageReference Include="Sentry" Version="4.3.0" /> <PackageReference Include="Sentry" Version="4.3.0" />
<!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. --> <!-- Held back due to 0.34.0 failing AOT compilation on ZstdSharp.dll dependency. -->
<PackageReference Include="SharpCompress" Version="0.36.0" /> <PackageReference Include="SharpCompress" Version="0.36.0" />

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="2024.720.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2024.802.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>