1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-26 16:53:20 +08:00

Merge branch 'master' into hover-open-mod-customise

This commit is contained in:
Caiyi Shyu 2024-08-02 19:07:27 +08:00 committed by GitHub
commit c85dc40f91
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 1182 additions and 253 deletions

View File

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

View File

@ -16,7 +16,6 @@ using osu.Framework.Graphics.Shaders.Types;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Layout;
using osu.Framework.Timing;
using osuTK;
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
parts[i].InvalidationID = -1;
}
AddLayout(partSizeCache);
}
[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>
/// The amount of time to fade the cursor trail pieces.
/// </summary>
@ -156,6 +147,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
protected void AddTrail(Vector2 position)
{
position = ToLocalSpace(position);
if (InterpolateMovements)
{
if (!lastPosition.HasValue)
@ -174,7 +167,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f * IntervalMultiplier;
float interval = Texture.DisplayWidth / 2.5f * IntervalMultiplier;
float stopAt = distance - (AvoidDrawingNearCursor ? interval : 0);
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].InvalidationID;
@ -220,7 +213,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
private float fadeExponent;
private readonly TrailPart[] parts = new TrailPart[max_sprites];
private Vector2 size;
private Vector2 originPosition;
private IVertexBatch<TexturedTrailVertex> vertexBatch;
@ -236,7 +228,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
shader = Source.shader;
texture = Source.texture;
size = Source.partSize;
time = Source.time;
fadeExponent = Source.FadeExponent;
@ -277,6 +268,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
RectangleF textureRect = texture.GetTextureRect();
renderer.PushLocalMatrix(DrawInfo.Matrix);
foreach (var part in parts)
{
if (part.InvalidationID == -1)
@ -285,11 +278,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (time - part.Time >= 1)
continue;
Vector2 screenSpacePos = Source.ToScreenSpace(part.Position);
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
@ -298,7 +289,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y + texture.DisplayHeight * (1 - originPosition.Y)),
TexturePosition = textureRect.BottomRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.BottomRight.Linear,
@ -307,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y - size.Y * originPosition.Y),
Position = new Vector2(part.Position.X + texture.DisplayWidth * (1 - originPosition.X), part.Position.Y - texture.DisplayHeight * originPosition.Y),
TexturePosition = textureRect.TopRight,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopRight.Linear,
@ -316,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
vertexBatch.Add(new TexturedTrailVertex
{
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y - size.Y * originPosition.Y),
Position = new Vector2(part.Position.X - texture.DisplayWidth * originPosition.X, part.Position.Y - texture.DisplayHeight * originPosition.Y),
TexturePosition = textureRect.TopLeft,
TextureRect = new Vector4(0, 0, 1, 1),
Colour = DrawColourInfo.Colour.TopLeft.Linear,
@ -324,6 +315,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
});
}
renderer.PopLocalMatrix();
vertexBatch.Draw();
shader.Unbind();
}

View File

@ -25,6 +25,9 @@ namespace osu.Game.Tests.Editing
new object?[] { "1:02:3000", false, null, null },
new object?[] { "1:02:300 ()", false, null, null },
new object?[] { "1:02:300 (1,2,3)", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - ", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
new object?[] { "1:02:300 (1,2,3) - following mod\nwith newlines", true, new TimeSpan(0, 0, 1, 2, 300), "1,2,3" },
};
[TestCaseSource(nameof(test_cases))]

View File

@ -8,6 +8,8 @@ using osu.Game.Online.API;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
namespace osu.Game.Tests.Mods
@ -105,9 +107,6 @@ namespace osu.Game.Tests.Mods
testMod.ResetSettingsToDefaults();
Assert.That(testMod.DrainRate.Value, Is.Null);
// ReSharper disable once HeuristicUnreachableCode
// see https://youtrack.jetbrains.com/issue/RIDER-70159.
Assert.That(testMod.OverallDifficulty.Value, Is.Null);
var applied = applyDifficulty(new BeatmapDifficulty
@ -119,6 +118,48 @@ namespace osu.Game.Tests.Mods
Assert.That(applied.OverallDifficulty, Is.EqualTo(10));
}
[Test]
public void TestDeserializeIncorrectRange()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"circle_size"] = -727,
[@"approach_rate"] = -727,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.Multiple(() =>
{
Assert.That(mod.CircleSize.Value, Is.GreaterThanOrEqualTo(0).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
});
}
[Test]
public void TestDeserializeNegativeApproachRate()
{
var apiMod = new APIMod
{
Acronym = @"DA",
Settings = new Dictionary<string, object>
{
[@"approach_rate"] = -9,
}
};
var ruleset = new OsuRuleset();
var mod = (OsuModDifficultyAdjust)apiMod.ToMod(ruleset);
Assert.That(mod.ApproachRate.Value, Is.GreaterThanOrEqualTo(-10).And.LessThanOrEqualTo(11));
Assert.That(mod.ApproachRate.Value, Is.EqualTo(-9));
}
/// <summary>
/// Applies a <see cref="BeatmapDifficulty"/> to the mod and returns a new <see cref="BeatmapDifficulty"/>
/// representing the result if the mod were applied to a fresh <see cref="BeatmapDifficulty"/> instance.

View File

@ -4,16 +4,34 @@
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Metadata;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallenge : OnlinePlayTestScene
{
[Cached(typeof(MetadataClient))]
private TestMetadataClient metadataClient = new TestMetadataClient();
[Cached(typeof(INotificationOverlay))]
private NotificationOverlay notificationOverlay = new NotificationOverlay();
[BackgroundDependencyLoader]
private void load()
{
base.Content.Add(notificationOverlay);
base.Content.Add(metadataClient);
}
[Test]
public void TestDailyChallenge()
{
@ -36,5 +54,33 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("push screen", () => LoadScreen(new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
}
[Test]
public void TestNotifications()
{
var room = new Room
{
RoomID = { Value = 1234 },
Name = { Value = "Daily Challenge: June 4, 2024" },
Playlist =
{
new PlaylistItem(TestResources.CreateTestBeatmapSetInfo().Beatmaps.First())
{
RequiredMods = [new APIMod(new OsuModTraceable())],
AllowedMods = [new APIMod(new OsuModDoubleTime())]
}
},
EndDate = { Value = DateTimeOffset.Now.AddHours(12) },
Category = { Value = RoomCategory.DailyChallenge }
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
AddUntilStep("wait for screen", () => screen.IsCurrentScreen());
AddStep("daily challenge ended", () => metadataClient.DailyChallengeInfo.Value = null);
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
@ -17,14 +18,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeEventFeed : OsuTestScene
{
private DailyChallengeEventFeed feed = null!;
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
[SetUpSteps]
public void SetUpSteps()
{
DailyChallengeEventFeed feed = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
@ -35,22 +36,28 @@ namespace osu.Game.Tests.Visual.DailyChallenge
feed = new DailyChallengeEventFeed
{
RelativeSizeAxes = Axes.Both,
Height = 0.3f,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (feed.IsNotNull())
feed.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
AddSliderStep("adjust height", 0.1f, 1, 0.3f, height =>
{
if (feed.IsNotNull())
feed.Height = height;
});
}
AddStep("add normal score", () =>
[Test]
public void TestBasicAppearance()
{
AddRepeatStep("add normal score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
@ -60,9 +67,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}, RNG.Next(1_000_000), null);
feed.AddNewScore(ev);
});
}, 50);
AddStep("add new user best", () =>
AddRepeatStep("add new user best", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
@ -75,9 +82,9 @@ namespace osu.Game.Tests.Visual.DailyChallenge
testScore.TotalScore = RNG.Next(1_000_000);
feed.AddNewScore(ev);
});
}, 50);
AddStep("add top 10 score", () =>
AddRepeatStep("add top 10 score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
@ -87,6 +94,25 @@ namespace osu.Game.Tests.Visual.DailyChallenge
}, RNG.Next(1_000_000), RNG.Next(1, 10));
feed.AddNewScore(ev);
}, 50);
}
[Test]
public void TestMassAdd()
{
AddStep("add 1000 scores at once", () =>
{
for (int i = 0; i < 1000; i++)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
feed.AddNewScore(ev);
}
});
}
}

View File

@ -6,8 +6,10 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
@ -19,11 +21,11 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
{
DailyChallengeScoreBreakdown breakdown = null!;
private DailyChallengeScoreBreakdown breakdown = null!;
[SetUpSteps]
public void SetUpSteps()
{
AddStep("create content", () => Children = new Drawable[]
{
new Box
@ -49,7 +51,14 @@ namespace osu.Game.Tests.Visual.DailyChallenge
breakdown.Height = height;
});
AddToggleStep("toggle visible", v => breakdown.Alpha = v ? 1 : 0);
AddStep("set initial data", () => breakdown.SetInitialCounts([1, 4, 9, 16, 25, 36, 49, 36, 25, 16, 9, 4, 1]));
}
[Test]
public void TestBasicAppearance()
{
AddStep("add new score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
@ -61,6 +70,27 @@ namespace osu.Game.Tests.Visual.DailyChallenge
breakdown.AddNewScore(ev);
});
AddStep("set user score", () => breakdown.UserBestScore.Value = new MultiplayerScore { TotalScore = RNG.Next(1_000_000) });
AddStep("unset user score", () => breakdown.UserBestScore.Value = null);
}
[Test]
public void TestMassAdd()
{
AddStep("add 1000 scores at once", () =>
{
for (int i = 0; i < 1000; i++)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
breakdown.AddNewScore(ev);
}
});
}
}
}

View File

@ -55,6 +55,8 @@ namespace osu.Game.Tests.Visual.DailyChallenge
if (ring.IsNotNull())
ring.Height = height;
});
AddToggleStep("toggle visible", v => ring.Alpha = v ? 1 : 0);
AddStep("just started", () =>
{
room.Value.StartDate.Value = DateTimeOffset.Now.AddMinutes(-1);

View File

@ -0,0 +1,87 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.DailyChallenge
{
public partial class TestSceneDailyChallengeTotalsDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
[Test]
public void TestBasicAppearance()
{
DailyChallengeTotalsDisplay totals = null!;
AddStep("create content", () => Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4,
},
totals = new DailyChallengeTotalsDisplay
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
});
AddSliderStep("adjust width", 0.1f, 1, 1, width =>
{
if (totals.IsNotNull())
totals.Width = width;
});
AddSliderStep("adjust height", 0.1f, 1, 1, height =>
{
if (totals.IsNotNull())
totals.Height = height;
});
AddToggleStep("toggle visible", v => totals.Alpha = v ? 1 : 0);
AddStep("set counts", () => totals.SetInitialCounts(totalPassCount: 9650, cumulativeTotalScore: 10_000_000_000));
AddStep("add normal score", () =>
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), null);
totals.AddNewScore(ev);
});
AddStep("spam scores", () =>
{
for (int i = 0; i < 1000; ++i)
{
var ev = new NewScoreEvent(1, new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, RNG.Next(1_000_000), RNG.Next(11, 1000));
var testScore = TestResources.CreateTestScoreInfo();
testScore.TotalScore = RNG.Next(1_000_000);
totals.AddNewScore(ev);
}
});
}
}
}

View File

@ -117,6 +117,9 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("state entered downloading", () => downloadStarted);
AddUntilStep("state left downloading", () => downloadFinished);
AddStep("change score to null", () => downloadButton.Score.Value = null);
AddUntilStep("state changed to unknown", () => downloadButton.State.Value, () => Is.EqualTo(DownloadState.Unknown));
}
[Test]

View File

@ -6,6 +6,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Chat;
@ -40,6 +41,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3,
Username = "LocalUser"
};
string uuid = Guid.NewGuid().ToString();
AddStep("add local echo message", () => channel.AddLocalEcho(new LocalEchoMessage
{
@ -83,5 +85,38 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("three day separators present", () => drawableChannel.ChildrenOfType<DaySeparator>().Count() == 3);
AddAssert("last day separator is from correct day", () => drawableChannel.ChildrenOfType<DaySeparator>().Last().Date.Date == new DateTime(2022, 11, 22));
}
[Test]
public void TestBackgroundAlternating()
{
int messageCount = 1;
AddRepeatStep("add messages", () =>
{
channel.AddNewMessages(new Message(messageCount)
{
Sender = new APIUser
{
Id = 3,
Username = "LocalUser " + RNG.Next(0, int.MaxValue - 100).ToString("N")
},
Content = "Hi there all!",
Timestamp = new DateTimeOffset(2022, 11, 21, 20, messageCount, 13, TimeSpan.Zero),
Uuid = Guid.NewGuid().ToString(),
});
messageCount++;
}, 10);
AddUntilStep("10 message present", () => drawableChannel.ChildrenOfType<ChatLine>().Count() == 10);
int checkCount = 0;
AddRepeatStep("check background", () =>
{
// +1 because the day separator take one index
Assert.AreEqual((checkCount + 1) % 2 == 0, drawableChannel.ChildrenOfType<ChatLine>().ToList()[checkCount].AlternatingBackground);
checkCount++;
}, 10);
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -10,6 +11,7 @@ using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.Menu;
using osuTK.Input;
using Color4 = osuTK.Graphics.Color4;
@ -39,8 +41,6 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test]
public void TestDailyChallengeButton()
{
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddStep("set up API", () => dummyAPI.HandleRequest = req =>
{
switch (req)
@ -67,17 +67,45 @@ namespace osu.Game.Tests.Visual.UserInterface
}
});
AddStep("add button", () => Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
});
NotificationOverlay notificationOverlay = null!;
DependencyProvidingContainer buttonContainer = null!;
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
AddStep("add content", () =>
{
notificationOverlay = new NotificationOverlay();
Children = new Drawable[]
{
notificationOverlay,
buttonContainer = new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Both,
CachedDependencies = [(typeof(INotificationOverlay), notificationOverlay)],
Child = new DailyChallengeButton(@"button-default-select", new Color4(102, 68, 204, 255), _ => { }, 0, Key.D)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
ButtonSystemState = ButtonSystemState.TopLevel,
},
},
};
});
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
AddStep("beatmap of the day not active", () => metadataClient.DailyChallengeUpdated(null));
AddAssert("no notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.Zero);
AddStep("hide button's parent", () => buttonContainer.Hide());
AddStep("beatmap of the day active", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo
{
RoomID = 1234,
}));
AddAssert("notification posted", () => notificationOverlay.AllNotifications.Count(), () => Is.EqualTo(1));
}
}
}

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}
[Test]
public void TestOutOfRangeValueStillApplied()
public void TestValueAboveRangeStillApplied()
{
AddStep("set override cs to 11", () => modDifficultyAdjust.CircleSize.Value = 11);
@ -91,6 +91,28 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", 11);
}
[Test]
public void TestValueBelowRangeStillApplied()
{
AddStep("set override cs to -5", () => modDifficultyAdjust.ApproachRate.Value = -5);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
// this is a no-op, just showing that it won't reset the value during deserialisation.
setExtendedLimits(false);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
// setting extended limits will reset the serialisation exception.
// this should be fine as the goal is to allow, at most, the value of extended limits.
setExtendedLimits(true);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
}
[Test]
public void TestExtendedLimits()
{
@ -109,6 +131,11 @@ namespace osu.Game.Tests.Visual.UserInterface
checkSliderAtValue("Circle Size", 11);
checkBindableAtValue("Circle Size", 11);
setSliderValue("Approach Rate", -5);
checkSliderAtValue("Approach Rate", -5);
checkBindableAtValue("Approach Rate", -5);
setExtendedLimits(false);
checkSliderAtValue("Circle Size", 10);

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).
ShowDragHandle.Value = false;
Masking = true;
CornerRadius = item_height / 2;
}
protected override Drawable CreateContent() => new ItemContent(Model);
@ -50,7 +53,7 @@ namespace osu.Game.Collections
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary>
private partial class ItemContent : CircularContainer
private partial class ItemContent : CompositeDrawable
{
private readonly Live<BeatmapCollection> collection;
@ -65,13 +68,12 @@ namespace osu.Game.Collections
RelativeSizeAxes = Axes.X;
Height = item_height;
Masking = true;
}
[BackgroundDependencyLoader]
private void load()
{
Children = new[]
InternalChildren = new[]
{
collection.IsManaged
? 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!;
@ -155,7 +157,7 @@ namespace osu.Game.Collections
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = fadeContainer = new Container
Child = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
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);
@ -195,12 +205,7 @@ namespace osu.Game.Collections
{
background.FlashColour(Color4.White, 150);
if (collection.PerformRead(c => c.BeatmapMD5Hashes.Count) == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
return base.OnClick(e);
}
private void deleteCollection() => collection.PerformWrite(c => c.Realm!.Remove(c));

View File

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

View File

@ -0,0 +1,29 @@
// 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 DailyChallengeStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.DailyChallenge";
/// <summary>
/// "Today&#39;s daily challenge has concluded thanks for playing!
///
/// Tomorrow&#39;s challenge is now being prepared and will appear soon."
/// </summary>
public static LocalisableString ChallengeEndedNotification => new TranslatableString(getKey(@"todays_daily_challenge_has_concluded"),
@"Today's daily challenge has concluded thanks for playing!
Tomorrow's challenge is now being prepared and will appear soon.");
/// <summary>
/// "Today&#39;s daily challenge is now live! Click here to play."
/// </summary>
public static LocalisableString ChallengeLiveNotification => new TranslatableString(getKey(@"todays_daily_challenge_is_now"), @"Today's daily challenge is now live! Click here to play.");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -118,12 +118,11 @@ namespace osu.Game.Online.API
u.OldValue?.Activity.UnbindFrom(activity);
u.NewValue.Activity.BindTo(activity);
if (u.OldValue != null)
localUserStatus.UnbindFrom(u.OldValue.Status);
localUserStatus.BindTo(u.NewValue.Status);
u.OldValue?.Status.UnbindFrom(localUserStatus);
u.NewValue.Status.BindTo(localUserStatus);
}, true);
localUserStatus.BindValueChanged(val => configStatus.Value = val.NewValue);
localUserStatus.BindTo(configStatus);
var thread = new Thread(run)
{
@ -600,6 +599,7 @@ namespace osu.Game.Online.API
password = null;
SecondFactorCode = null;
authentication.Clear();
configStatus.Value = UserStatus.Online;
// Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present
Schedule(() =>

View File

@ -178,7 +178,7 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneDaySeparator : DaySeparator
{
protected override float TextSize => 14;
protected override float TextSize => 13;
protected override float LineHeight => 1;
protected override float Spacing => 5;
protected override float DateAlign => 125;
@ -198,9 +198,9 @@ namespace osu.Game.Online.Chat
protected partial class StandAloneMessage : ChatLine
{
protected override float FontSize => 15;
protected override float FontSize => 13;
protected override float Spacing => 5;
protected override float UsernameWidth => 75;
protected override float UsernameWidth => 90;
public StandAloneMessage(Message message)
: base(message)

View File

@ -25,5 +25,17 @@ namespace osu.Game.Online.Metadata
/// </summary>
[Key(1)]
public long[] TotalScoreDistribution { get; set; } = new long[TOTAL_SCORE_DISTRIBUTION_BINS];
/// <summary>
/// The cumulative total of all passing scores (across all users) for the playlist item so far.
/// </summary>
[Key(2)]
public long CumulativeScore { get; set; }
/// <summary>
/// The last score to have been processed into provided statistics. Generally only for server-side accounting purposes.
/// </summary>
[Key(3)]
public ulong LastProcessedScoreID { get; set; }
}
}

View File

@ -215,6 +215,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.BeginWatchingUserPresence)).ConfigureAwait(false);
Schedule(() => isWatchingUserPresence.Value = true);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching user presence", LoggingTarget.Network);
}
public override async Task EndWatchingUserPresence()
@ -228,6 +229,7 @@ namespace osu.Game.Online.Metadata
Schedule(() => userStates.Clear());
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingUserPresence)).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching user presence", LoggingTarget.Network);
}
finally
{
@ -247,7 +249,9 @@ namespace osu.Game.Online.Metadata
throw new OperationCanceledException();
Debug.Assert(connection != null);
return await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
var result = await connection.InvokeAsync<MultiplayerPlaylistItemStats[]>(nameof(IMetadataServer.BeginWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} began watching multiplayer room with ID {id}", LoggingTarget.Network);
return result;
}
public override async Task EndWatchingMultiplayerRoom(long id)
@ -257,6 +261,7 @@ namespace osu.Game.Online.Metadata
Debug.Assert(connection != null);
await connection.InvokeAsync(nameof(IMetadataServer.EndWatchingMultiplayerRoom), id).ConfigureAwait(false);
Logger.Log($@"{nameof(OnlineMetadataClient)} stopped watching multiplayer room with ID {id}", LoggingTarget.Network);
}
public override async Task DisconnectRequested()

View File

@ -63,6 +63,7 @@ using osu.Game.Screens;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Footer;
using osu.Game.Screens.Menu;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -749,23 +750,36 @@ namespace osu.Game
return;
}
// This should be able to be performed from song select, but that is disabled for now
// This should be able to be performed from song select always, but that is disabled for now
// due to the weird decoupled ruleset logic (which can cause a crash in certain filter scenarios).
//
// As a special case, if the beatmap and ruleset already match, allow immediately displaying the score from song select.
// This is guaranteed to not crash, and feels better from a user's perspective (ie. if they are clicking a score in the
// song select leaderboard).
// Similar exemptions are made here for daily challenge where it is guaranteed that beatmap and ruleset match.
// `OnlinePlayScreen` is excluded because when resuming back to it,
// `RoomSubScreen` changes the global beatmap to the next playlist item on resume,
// which may not match the score, and thus crash.
IEnumerable<Type> validScreens =
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
? new[] { typeof(SongSelect) }
? new[] { typeof(SongSelect), typeof(DailyChallenge) }
: Array.Empty<Type>();
PerformFromScreen(screen =>
{
Logger.Log($"{nameof(PresentScore)} updating beatmap ({databasedBeatmap}) and ruleset ({databasedScore.ScoreInfo.Ruleset}) to match score");
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
// some screens (mostly online) disable the ruleset/beatmap bindable.
// attempting to set the ruleset/beatmap in that state will crash.
// however, the `validScreens` pre-check above should ensure that we actually never come from one of those screens
// while simultaneously having mismatched ruleset/beatmap.
// therefore this is just a safety against touching the possibly-disabled bindables if we don't actually have to touch them.
// if it ever fails, then this probably *should* crash anyhow (so that we can fix it).
if (!Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset))
Ruleset.Value = databasedScore.ScoreInfo.Ruleset;
if (!Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap))
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(databasedBeatmap);
switch (presentType)
{

View File

@ -20,8 +20,8 @@ using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osuTK;
using osuTK.Graphics;
using Message = osu.Game.Online.Chat.Message;
namespace osu.Game.Overlays.Chat
{
@ -47,11 +47,11 @@ namespace osu.Game.Overlays.Chat
public IReadOnlyCollection<Drawable> DrawableContentFlow => drawableContentFlow;
protected virtual float FontSize => 14;
protected virtual float FontSize => 12;
protected virtual float Spacing => 15;
protected virtual float UsernameWidth => 130;
protected virtual float UsernameWidth => 150;
[Resolved]
private ChannelManager? chatManager { get; set; }
@ -69,6 +69,41 @@ namespace osu.Game.Overlays.Chat
private Container? highlight;
private Drawable? background;
private bool alternatingBackground;
private bool requiresTimestamp = true;
public bool RequiresTimestamp
{
get => requiresTimestamp;
set
{
if (requiresTimestamp == value)
return;
requiresTimestamp = value;
if (!IsLoaded)
return;
updateMessageContent();
}
}
public bool AlternatingBackground
{
get => alternatingBackground;
set
{
if (alternatingBackground == value)
return;
alternatingBackground = value;
updateBackground();
}
}
/// <summary>
/// The colour used to paint the author's username.
/// </summary>
@ -102,48 +137,74 @@ namespace osu.Game.Overlays.Chat
configManager.BindWith(OsuSetting.Prefer24HourTime, prefer24HourTime);
prefer24HourTime.BindValueChanged(_ => updateTimestamp());
InternalChild = new GridContainer
Padding = new MarginPadding { Right = 5 };
InternalChildren = new[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
background = new Container
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing),
new Dimension(),
},
Content = new[]
{
new Drawable[]
Masking = true,
CornerRadius = 4,
Alpha = 0,
RelativeSizeAxes = Axes.Both,
Blending = BlendingParameters.Additive,
Child = new Box
{
drawableTimestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Font = OsuFont.GetFont(size: FontSize * 0.75f, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true,
},
drawableUsername = new DrawableChatUsername(message.Sender)
{
Width = UsernameWidth,
FontSize = FontSize,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding { Horizontal = Spacing },
AccentColour = UsernameColour,
Inverted = !string.IsNullOrEmpty(message.Sender.Colour),
},
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
}
Colour = Colour4.FromHex("#3b3234"),
RelativeSizeAxes = Axes.Both,
},
},
new GridContainer
{
Padding = new MarginPadding
{
Horizontal = 2,
Vertical = 2,
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 45),
new Dimension(GridSizeMode.Absolute, Spacing + UsernameWidth + Spacing),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
drawableTimestamp = new OsuSpriteText
{
Shadow = false,
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
Spacing = new Vector2(-1, 0),
Font = OsuFont.GetFont(size: FontSize, weight: FontWeight.SemiBold, fixedWidth: true),
AlwaysPresent = true,
},
drawableUsername = new DrawableChatUsername(message.Sender)
{
Width = UsernameWidth,
FontSize = FontSize,
AutoSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Margin = new MarginPadding { Horizontal = Spacing },
AccentColour = UsernameColour,
Inverted = !string.IsNullOrEmpty(message.Sender.Colour),
},
drawableContentFlow = new LinkFlowContainer(styleMessageContent)
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
}
},
}
}
};
updateBackground();
}
protected override void LoadComplete()
@ -203,9 +264,17 @@ namespace osu.Game.Overlays.Chat
private void updateMessageContent()
{
this.FadeTo(message is LocalEchoMessage ? 0.4f : 1.0f, 500, Easing.OutQuint);
drawableTimestamp.FadeTo(message is LocalEchoMessage ? 0 : 1, 500, Easing.OutQuint);
updateTimestamp();
if (requiresTimestamp && !(message is LocalEchoMessage))
{
drawableTimestamp.Show();
updateTimestamp();
}
else
{
drawableTimestamp.Hide();
}
drawableUsername.Text = $@"{message.Sender.Username}";
// remove non-existent channels from the link list
@ -217,7 +286,7 @@ namespace osu.Game.Overlays.Chat
private void updateTimestamp()
{
drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm:ss" : @"hh:mm:ss tt");
drawableTimestamp.Text = message.Timestamp.LocalDateTime.ToLocalisableString(prefer24HourTime.Value ? @"HH:mm" : @"hh:mm tt");
}
private static readonly Color4[] default_username_colours =
@ -258,5 +327,11 @@ namespace osu.Game.Overlays.Chat
Color4Extensions.FromHex("812a96"),
Color4Extensions.FromHex("992861"),
};
private void updateBackground()
{
if (background != null)
background.Alpha = alternatingBackground ? 0.2f : 0;
}
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Chat
{
public partial class DaySeparator : Container
{
protected virtual float TextSize => 15;
protected virtual float TextSize => 13;
protected virtual float LineHeight => 2;

View File

@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Chat
Padding = new MarginPadding { Bottom = 5 },
Child = ChatLineFlow = new FillFlowContainer
{
Padding = new MarginPadding { Horizontal = 10 },
Padding = new MarginPadding { Left = 3, Right = 10 },
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
@ -84,6 +84,25 @@ namespace osu.Game.Overlays.Chat
highlightedMessage.BindValueChanged(_ => processMessageHighlighting(), true);
}
protected override void Update()
{
base.Update();
long? lastMinutes = null;
for (int i = 0; i < ChatLineFlow.Count; i++)
{
if (ChatLineFlow[i] is ChatLine chatline)
{
long minutes = chatline.Message.Timestamp.ToUnixTimeSeconds() / 60;
chatline.AlternatingBackground = i % 2 == 0;
chatline.RequiresTimestamp = minutes != lastMinutes;
lastMinutes = minutes;
}
}
}
/// <summary>
/// Processes any pending message in <see cref="highlightedMessage"/>.
/// </summary>

View File

@ -157,6 +157,7 @@ namespace osu.Game.Overlays.Login
},
};
updateDropdownCurrent(status.Value);
dropdown.Current.BindValueChanged(action =>
{
switch (action.NewValue)

View File

@ -4,6 +4,7 @@
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -12,8 +13,9 @@ using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osuTK;
using osu.Game.Localisation;
using osuTK;
using osuTK.Graphics;
using static osu.Game.Overlays.Mods.ModCustomisationPanel;
namespace osu.Game.Overlays.Mods
@ -21,6 +23,7 @@ namespace osu.Game.Overlays.Mods
public partial class ModCustomisationHeader : OsuHoverContainer
{
private Box background = null!;
private Box backgroundFlash = null!;
private SpriteIcon icon = null!;
[Resolved]
@ -50,6 +53,12 @@ namespace osu.Game.Overlays.Mods
{
RelativeSizeAxes = Axes.Both,
},
backgroundFlash = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White.Opacity(0.4f),
Blending = BlendingParameters.Additive,
},
new OsuSpriteText
{
Anchor = Anchor.CentreLeft,
@ -88,6 +97,12 @@ namespace osu.Game.Overlays.Mods
TooltipText = e.NewValue
? string.Empty
: ModSelectOverlayStrings.CustomisationPanelDisabledReason;
if (e.NewValue)
{
backgroundFlash.FadeInFromZero(150, Easing.OutQuad).Then()
.FadeOutFromOne(350, Easing.OutQuad);
}
}, true);
ExpandedState.BindValueChanged(v =>

View File

@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osuTK;
@ -19,7 +18,7 @@ namespace osu.Game.Overlays.Mods
private const double transition_duration = 200;
private readonly OsuSpriteText descriptionText;
private readonly TextFlowContainer descriptionText;
public ModPresetTooltip(OverlayColourProvider colourProvider)
{
@ -44,11 +43,15 @@ namespace osu.Game.Overlays.Mods
Spacing = new Vector2(7),
Children = new[]
{
descriptionText = new OsuSpriteText
descriptionText = new TextFlowContainer(f =>
{
Font = OsuFont.GetFont(weight: FontWeight.Regular),
Colour = colourProvider.Content1,
},
f.Font = OsuFont.GetFont(weight: FontWeight.Regular);
f.Colour = colourProvider.Content1;
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
}
}
}
};

View File

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

View File

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

View File

@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// Used for parsing in contexts where we don't want e.g. normal times of day to be parsed as timestamps (e.g. chat)
/// Original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
/// Original osu-web regex:
/// https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
/// </summary>
/// <example>
/// 00:00:000 (...) - test
@ -32,7 +33,10 @@ namespace osu.Game.Rulesets.Edit
/// <item>1:02:300 (1,2,3) - parses to 01:02:300 with selection</item>
/// </list>
/// </example>
private static readonly Regex time_regex_lenient = new Regex(@"^(((?<minutes>\d{1,3}):(?<seconds>([0-5]?\d))([:.](?<milliseconds>\d{0,3}))?)(?<selection>\s\([^)]+\))?)$", RegexOptions.Compiled);
private static readonly Regex time_regex_lenient = new Regex(
@"^(((?<minutes>\d{1,3}):(?<seconds>([0-5]?\d))([:.](?<milliseconds>\d{0,3}))?)(?<selection>\s\([^)]+\))?)(?<suffix>\s-.*)?$",
RegexOptions.Compiled | RegexOptions.Singleline
);
public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection)
{

View File

@ -38,6 +38,7 @@ namespace osu.Game.Rulesets.Mods
public float MinValue
{
get => minValue;
set
{
if (value == minValue)
@ -52,6 +53,7 @@ namespace osu.Game.Rulesets.Mods
public float MaxValue
{
get => maxValue;
set
{
if (value == maxValue)
@ -69,6 +71,7 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
public float? ExtendedMinValue
{
get => extendedMinValue;
set
{
if (value == extendedMinValue)
@ -86,6 +89,7 @@ namespace osu.Game.Rulesets.Mods
/// </summary>
public float? ExtendedMaxValue
{
get => extendedMaxValue;
set
{
if (value == extendedMaxValue)
@ -114,9 +118,14 @@ namespace osu.Game.Rulesets.Mods
{
// Ensure that in the case serialisation runs in the wrong order (and limit extensions aren't applied yet) the deserialised value is still propagated.
if (value != null)
CurrentNumber.MaxValue = MathF.Max(CurrentNumber.MaxValue, value.Value);
{
CurrentNumber.MinValue = Math.Clamp(MathF.Min(CurrentNumber.MinValue, value.Value), ExtendedMinValue ?? MinValue, MinValue);
CurrentNumber.MaxValue = Math.Clamp(MathF.Max(CurrentNumber.MaxValue, value.Value), MaxValue, ExtendedMaxValue ?? MaxValue);
base.Value = value;
base.Value = Math.Clamp(value.Value, CurrentNumber.MinValue, CurrentNumber.MaxValue);
}
else
base.Value = value;
}
}
@ -138,6 +147,8 @@ namespace osu.Game.Rulesets.Mods
// the following max value copies are only safe as long as these values are effectively constants.
otherDifficultyBindable.MaxValue = maxValue;
otherDifficultyBindable.ExtendedMaxValue = extendedMaxValue;
otherDifficultyBindable.MinValue = minValue;
otherDifficultyBindable.ExtendedMinValue = extendedMinValue;
}
public override void BindTo(Bindable<float?> them)

View File

@ -58,7 +58,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
case IHasPosition pos:
AddHeader("Position");
AddValue($"x:{pos.X:#,0.##} y:{pos.Y:#,0.##}");
AddValue($"x:{pos.X:#,0.##}");
AddValue($"y:{pos.Y:#,0.##}");
break;
case IHasXPosition x:

View File

@ -1,8 +1,6 @@
// 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.
#nullable disable
using System;
using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
@ -30,11 +28,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
private readonly Drawable userContent;
[Resolved]
private EditorClock editorClock { get; set; }
private bool alwaysShowControlPoints;
public bool AlwaysShowControlPoints
{
get => alwaysShowControlPoints;
set
{
if (value == alwaysShowControlPoints)
return;
alwaysShowControlPoints = value;
controlPointsVisible.TriggerChange();
}
}
[Resolved]
private EditorBeatmap editorBeatmap { get; set; }
private EditorClock editorClock { get; set; } = null!;
[Resolved]
private EditorBeatmap editorBeatmap { get; set; } = null!;
/// <summary>
/// The timeline's scroll position in the last frame.
@ -61,6 +74,22 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
/// </summary>
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)
{
this.userContent = userContent;
@ -73,22 +102,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
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]
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 =>
{
if (visible.NewValue)
if (visible.NewValue || alwaysShowControlPoints)
{
this.ResizeHeightTo(timeline_expanded_height, 200, Easing.OutQuint);
mainContent.MoveToY(15, 200, Easing.OutQuint);
@ -318,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
}
[Resolved]
private IBeatSnapProvider beatSnapProvider { get; set; }
private IBeatSnapProvider beatSnapProvider { get; set; } = null!;
/// <summary>
/// The total amount of time visible on the timeline.

View File

@ -5,7 +5,6 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
namespace osu.Game.Screens.Edit
@ -26,7 +25,7 @@ namespace osu.Game.Screens.Edit
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider colourProvider)
private void load()
{
// Grid with only two rows.
// 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);
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 virtual Drawable CreateTimelineContent() => new Container();

View File

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

View File

@ -23,6 +23,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge;
using osuTK;
using osuTK.Graphics;
using osuTK.Input;
@ -44,6 +45,9 @@ namespace osu.Game.Screens.Menu
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
public DailyChallengeButton(string sampleName, Color4 colour, Action<MainMenuButton>? clickAction = null, params Key[] triggerKeys)
: base(ButtonSystemStrings.DailyChallenge, sampleName, OsuIcon.DailyChallenge, colour, clickAction, triggerKeys)
{
@ -100,7 +104,8 @@ namespace osu.Game.Screens.Menu
{
base.LoadComplete();
info.BindValueChanged(updateDisplay, true);
info.BindValueChanged(_ => dailyChallengeChanged(postNotification: true));
dailyChallengeChanged(postNotification: false);
}
protected override void Update()
@ -126,27 +131,30 @@ namespace osu.Game.Screens.Menu
}
}
private void updateDisplay(ValueChangedEvent<DailyChallengeInfo?> info)
private void dailyChallengeChanged(bool postNotification)
{
UpdateState();
scheduledCountdownUpdate?.Cancel();
scheduledCountdownUpdate = null;
if (info.NewValue == null)
if (info.Value == null)
{
Room = null;
cover.OnlineInfo = TooltipContent = null;
}
else
{
var roomRequest = new GetRoomRequest(info.NewValue.Value.RoomID);
var roomRequest = new GetRoomRequest(info.Value.Value.RoomID);
roomRequest.Success += room =>
{
Room = room;
cover.OnlineInfo = TooltipContent = room.Playlist.FirstOrDefault()?.Beatmap.BeatmapSet as APIBeatmapSet;
if (postNotification)
notificationOverlay?.Post(new NewDailyChallengeNotification(room));
updateCountdown();
Scheduler.AddDelayed(updateCountdown, 1000, true);
};

View File

@ -21,8 +21,8 @@ namespace osu.Game.Screens.OnlinePlay.Components
private void load(OnlinePlayBeatmapAvailabilityTracker beatmapTracker)
{
availability.BindTo(beatmapTracker.Availability);
availability.BindValueChanged(_ => updateState());
Enabled.BindValueChanged(_ => updateState(), true);
}
@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{
get
{
if (Enabled.Value)
if (base.Enabled.Value)
return string.Empty;
if (availability.Value.State != DownloadState.LocallyAvailable)

View File

@ -17,9 +17,11 @@ using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Logging;
using osu.Framework.Screens;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
@ -28,6 +30,7 @@ using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.OnlinePlay.Components;
@ -40,7 +43,8 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallenge : OsuScreen
[Cached(typeof(IPreviewTrackOwner))]
public partial class DailyChallenge : OsuScreen, IPreviewTrackOwner
{
private readonly Room room;
private readonly PlaylistItem playlistItem;
@ -51,6 +55,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private readonly Bindable<IReadOnlyList<Mod>> userMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
private readonly IBindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
private OnlinePlayScreenWaveContainer waves = null!;
private DailyChallengeLeaderboard leaderboard = null!;
@ -59,6 +64,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
private IDisposable? userModsSelectOverlayRegistration;
private DailyChallengeScoreBreakdown breakdown = null!;
private DailyChallengeTotalsDisplay totals = null!;
private DailyChallengeEventFeed feed = null!;
[Cached]
@ -91,13 +97,22 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
[Resolved]
protected IAPIProvider API { get; private set; } = null!;
[Resolved]
private PreviewTrackManager previewTrackManager { get; set; } = null!;
[Resolved]
private INotificationOverlay? notificationOverlay { get; set; }
public override bool DisallowExternalBeatmapRulesetChanges => true;
public override bool? ApplyModTrackAdjustments => true;
public DailyChallenge(Room room)
{
this.room = room;
playlistItem = room.Playlist.Single();
roomManager = new RoomManager();
Padding = new MarginPadding { Horizontal = -HORIZONTAL_OVERFLOW_PADDING };
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
@ -160,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
},
null,
[
new Container
new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
@ -211,6 +226,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
new DailyChallengeTimeRemainingRing(),
breakdown = new DailyChallengeScoreBreakdown(),
totals = new DailyChallengeTotalsDisplay(),
}
}
},
@ -229,6 +245,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
RelativeSizeAxes = Axes.Both,
PresentScore = presentScore,
SelectedMods = { BindTarget = userMods },
},
// Spacer
null,
@ -301,6 +318,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay
{
Beatmap = { BindTarget = Beatmap },
SelectedMods = { BindTarget = userMods },
IsValidMod = _ => false
});
@ -319,10 +337,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
var rulesetInstance = rulesets.GetRuleset(playlistItem.RulesetID)!.CreateInstance();
var allowedMods = playlistItem.AllowedMods.Select(m => m.ToMod(rulesetInstance));
userModsSelectOverlay.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
userModsSelectOverlay.IsValidMod = leaderboard.IsValidMod = m => allowedMods.Any(a => a.GetType() == m.GetType());
}
metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet;
dailyChallengeInfo.BindTo(metadataClient.DailyChallengeInfo);
((IBindable<MultiplayerScore?>)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore);
}
private void presentScore(long id)
@ -351,6 +372,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Schedule(() =>
{
breakdown.AddNewScore(ev);
totals.AddNewScore(ev);
feed.AddNewScore(ev);
if (e.NewRank <= 50)
@ -372,6 +394,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
apiState.BindTo(API.State);
apiState.BindValueChanged(onlineStateChanged, true);
dailyChallengeInfo.BindValueChanged(dailyChallengeChanged);
}
private void trySetDailyChallengeBeatmap()
@ -379,6 +403,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
var beatmap = beatmapManager.QueryBeatmap(b => b.OnlineID == playlistItem.Beatmap.OnlineID);
Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmap); // this will gracefully fall back to dummy beatmap if missing locally.
Ruleset.Value = rulesets.GetRuleset(playlistItem.RulesetID);
applyLoopingToTrack();
}
@ -388,9 +413,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Schedule(forcefullyExit);
});
private void dailyChallengeChanged(ValueChangedEvent<DailyChallengeInfo?> change)
{
if (change.OldValue?.RoomID == room.RoomID.Value && change.NewValue == null)
{
notificationOverlay?.Post(new SimpleNotification { Text = DailyChallengeStrings.ChallengeEndedNotification });
}
}
private void forcefullyExit()
{
Logger.Log($"{this} forcefully exiting due to loss of API connection");
Logger.Log(@$"{this} forcefully exiting due to loss of API connection");
// This is temporary since we don't currently have a way to force screens to be exited
// See also: `OnlinePlayScreen.forcefullyExit()`
@ -421,7 +454,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
var itemStats = stats.SingleOrDefault(item => item.PlaylistItemID == playlistItem.ID);
if (itemStats == null) return;
Schedule(() => breakdown.SetInitialCounts(itemStats.TotalScoreDistribution));
Schedule(() =>
{
breakdown.SetInitialCounts(itemStats.TotalScoreDistribution);
totals.SetInitialCounts(itemStats.TotalScoreDistribution.Sum(c => c), itemStats.CumulativeScore);
});
});
beatmapAvailabilityTracker.SelectedItem.Value = playlistItem;
@ -433,6 +470,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
base.OnResuming(e);
applyLoopingToTrack();
// re-apply mods as they may have been changed by a child screen
// (one known instance of this is showing a replay).
updateMods();
}
public override void OnSuspending(ScreenTransitionEvent e)
@ -441,6 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
userModsSelectOverlay.Hide();
cancelTrackLooping();
previewTrackManager.StopAnyPlaying(this);
}
public override bool OnExiting(ScreenExitEvent e)
@ -448,6 +489,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
waves.Hide();
userModsSelectOverlay.Hide();
cancelTrackLooping();
previewTrackManager.StopAnyPlaying(this);
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
roomManager.PartRoom();

View File

@ -50,7 +50,6 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
drawable.RelativeSizeAxes = Axes.Both;
drawable.Size = Vector2.One;
drawable.AlwaysPresent = true;
drawable.Alpha = 0;
base.Add(drawable);

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@ -22,6 +23,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
public Action<long>? PresentScore { get; init; }
private readonly Queue<NewScoreEvent> newScores = new Queue<NewScoreEvent>();
[BackgroundDependencyLoader]
private void load()
{
@ -47,24 +50,33 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
public void AddNewScore(NewScoreEvent newScoreEvent)
{
var row = new NewScoreEventRow(newScoreEvent)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
PresentScore = PresentScore,
};
flow.Add(row);
row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire();
newScores.Enqueue(newScoreEvent);
// ensure things don't get too out-of-hand.
if (newScores.Count > 25)
newScores.Dequeue();
}
protected override void Update()
{
base.Update();
while (newScores.TryDequeue(out var newScore))
{
flow.Add(new NewScoreEventRow(newScore)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
PresentScore = PresentScore,
});
}
for (int i = 0; i < flow.Count; ++i)
{
var row = flow[i];
row.Alpha = Interpolation.ValueAt(Math.Clamp(row.Y + flow.DrawHeight, 0, flow.DrawHeight), 0f, 1f, 0, flow.DrawHeight, Easing.Out);
if (row.Y < -flow.DrawHeight)
{
row.RemoveAndDisposeImmediately();
@ -109,7 +121,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
},
text = new LinkFlowContainer(t =>
{
t.Font = OsuFont.Default.With(weight: newScore.NewRank == null ? FontWeight.Medium : FontWeight.Bold);
FontWeight fontWeight = FontWeight.Medium;
if (newScore.NewRank < 100)
fontWeight = FontWeight.Bold;
else if (newScore.NewRank < 1000)
fontWeight = FontWeight.SemiBold;
t.Font = OsuFont.Default.With(weight: fontWeight);
t.Colour = newScore.NewRank < 10 ? colours.Orange1 : Colour4.White;
})
{
@ -120,8 +139,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
};
text.AddUserLink(newScore.User);
text.AddText(" got ");
text.AddLink($"{newScore.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.ScoreID));
text.AddText(" scored ");
text.AddLink($"{newScore.TotalScore:N0}", () => PresentScore?.Invoke(newScore.ScoreID));
if (newScore.NewRank != null)
text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}");

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
@ -14,6 +15,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.SelectV2.Leaderboards;
using osuTK;
@ -22,6 +24,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeLeaderboard : CompositeDrawable
{
public IBindable<MultiplayerScore?> UserBestScore => userBestScore;
private readonly Bindable<MultiplayerScore?> userBestScore = new Bindable<MultiplayerScore?>();
public Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>();
/// <summary>
/// A function determining whether each mod in the score can be selected.
/// A return value of <see langword="true"/> means that the mod can be selected in the current context.
/// A return value of <see langword="false"/> means that the mod cannot be selected in the current context.
/// </summary>
public Func<Mod, bool> IsValidMod { get; set; } = _ => true;
public Action<long>? PresentScore { get; init; }
private readonly Room room;
@ -130,7 +143,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
request.Success += req => Schedule(() =>
{
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray();
var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
userBestScore.Value = req.UserScore;
var userBest = userBestScore.Value?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
cancellationTokenSource?.Cancel();
cancellationTokenSource = null;
@ -148,6 +163,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Rank = index + 1,
IsPersonalBest = s.UserID == api.LocalUser.Value.Id,
Action = () => PresentScore?.Invoke(s.OnlineID),
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = IsValidMod,
}), loaded =>
{
scoreFlow.Clear();
@ -166,6 +183,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
Rank = userBest.Position,
IsPersonalBest = true,
Action = () => PresentScore?.Invoke(userBest.OnlineID),
SelectedMods = { BindTarget = SelectedMods },
IsValidMod = IsValidMod,
});
}

View File

@ -2,16 +2,20 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Metadata;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osuTK;
@ -20,6 +24,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeScoreBreakdown : CompositeDrawable
{
public Bindable<MultiplayerScore?> UserBestScore { get; } = new Bindable<MultiplayerScore?>();
private FillFlowContainer<Bar> barsContainer = null!;
private const int bin_count = MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS;
@ -44,57 +50,79 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
for (int i = 0; i < bin_count; ++i)
{
LocalisableString? label = null;
switch (i)
{
case 2:
case 4:
case 6:
case 8:
label = @$"{100 * i}k";
break;
case 10:
label = @"1M";
break;
}
barsContainer.Add(new Bar(label)
barsContainer.Add(new Bar(100_000 * i, 100_000 * (i + 1) - 1)
{
Width = 1f / bin_count,
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
UserBestScore.BindValueChanged(_ =>
{
foreach (var bar in barsContainer)
bar.ContainsLocalUser.Value = UserBestScore.Value is not null && bar.BinStart <= UserBestScore.Value.TotalScore && UserBestScore.Value.TotalScore <= bar.BinEnd;
});
}
private readonly Queue<NewScoreEvent> newScores = new Queue<NewScoreEvent>();
public void AddNewScore(NewScoreEvent newScoreEvent)
{
int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1);
bins[targetBin] += 1;
updateCounts();
newScores.Enqueue(newScoreEvent);
var text = new OsuSpriteText
// ensure things don't get too out-of-hand.
if (newScores.Count > 25)
{
Text = newScoreEvent.TotalScore.ToString(@"N0"),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Default.With(size: 30),
RelativePositionAxes = Axes.X,
X = (targetBin + 0.5f) / bin_count - 0.5f,
Alpha = 0,
};
AddInternal(text);
bins[getTargetBin(newScores.Dequeue())] += 1;
Scheduler.AddOnce(updateCounts);
}
}
Scheduler.AddDelayed(() =>
private double lastScoreDisplay;
protected override void Update()
{
base.Update();
if (Time.Current - lastScoreDisplay > 150 && newScores.TryDequeue(out var newScore))
{
float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y;
text.FadeInFromZero()
.ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf)
.MoveToY(startY)
.MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint)
.FadeOut(2500, Easing.OutQuint)
.Expire();
}, 150);
if (lastScoreDisplay < Time.Current)
lastScoreDisplay = Time.Current;
int targetBin = getTargetBin(newScore);
bins[targetBin] += 1;
updateCounts();
var text = new OsuSpriteText
{
Text = newScore.TotalScore.ToString(@"N0"),
Anchor = Anchor.TopCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.Default.With(size: 30),
RelativePositionAxes = Axes.X,
X = (targetBin + 0.5f) / bin_count - 0.5f,
Alpha = 0,
};
AddInternal(text);
Scheduler.AddDelayed(() =>
{
float startY = ToLocalSpace(barsContainer[targetBin].CircularBar.ScreenSpaceDrawQuad.TopLeft).Y;
text.FadeInFromZero()
.ScaleTo(new Vector2(0.8f), 500, Easing.OutElasticHalf)
.MoveToY(startY)
.MoveToOffset(new Vector2(0, -50), 2500, Easing.OutQuint)
.FadeOut(2500, Easing.OutQuint)
.Expire();
}, 150);
lastScoreDisplay = Time.Current;
}
}
public void SetInitialCounts(long[] counts)
@ -106,6 +134,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
updateCounts();
}
private static int getTargetBin(NewScoreEvent score) =>
(int)Math.Clamp(Math.Floor((float)score.TotalScore / 100000), 0, bin_count - 1);
private void updateCounts()
{
long max = Math.Max(bins.Max(), 1);
@ -113,20 +144,34 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
barsContainer[i].UpdateCounts(bins[i], max);
}
private partial class Bar : CompositeDrawable
private partial class Bar : CompositeDrawable, IHasTooltip
{
private readonly LocalisableString? label;
public BindableBool ContainsLocalUser { get; } = new BindableBool();
public readonly int BinStart;
public readonly int BinEnd;
private long count;
private long max;
public Container CircularBar { get; private set; } = null!;
public Bar(LocalisableString? label = null)
private Box fill = null!;
private Box flashLayer = null!;
private OsuSpriteText userIndicator = null!;
public Bar(int binStart, int binEnd)
{
this.label = label;
BinStart = binStart;
BinEnd = binEnd;
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
@ -142,35 +187,83 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
},
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Masking = true,
Child = CircularBar = new Container
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0.01f,
Masking = true,
CornerRadius = 10,
Colour = colourProvider.Highlight1,
Child = new Box
CircularBar = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Height = 0.01f,
Masking = true,
CornerRadius = 10,
Children = new Drawable[]
{
fill = new Box
{
RelativeSizeAxes = Axes.Both,
},
flashLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
}
},
userIndicator = new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Colour = colours.Orange1,
Text = "You",
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Alpha = 0,
RelativePositionAxes = Axes.Y,
Margin = new MarginPadding { Bottom = 5, },
}
}
},
});
string? label = null;
switch (BinStart)
{
case 200_000:
case 400_000:
case 600_000:
case 800_000:
label = @$"{BinStart / 1000}k";
break;
case 1_000_000:
label = @"1M";
break;
}
if (label != null)
{
AddInternal(new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
Text = label.Value,
Text = label,
Colour = colourProvider.Content2,
});
}
}
protected override void LoadComplete()
{
base.LoadComplete();
ContainsLocalUser.BindValueChanged(_ =>
{
fill.FadeColour(ContainsLocalUser.Value ? colours.Orange1 : colourProvider.Highlight1, 300, Easing.OutQuint);
userIndicator.FadeTo(ContainsLocalUser.Value ? 1 : 0, 300, Easing.OutQuint);
}, true);
FinishTransforms(true);
}
protected override void Update()
{
base.Update();
@ -185,10 +278,14 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
count = newCount;
max = newMax;
CircularBar.ResizeHeightTo(0.01f + 0.99f * count / max, 300, Easing.OutQuint);
float height = 0.01f + 0.99f * count / max;
CircularBar.ResizeHeightTo(height, 300, Easing.OutQuint);
userIndicator.MoveToY(-height, 300, Easing.OutQuint);
if (isIncrement)
CircularBar.FlashColour(Colour4.White, 600, Easing.OutQuint);
flashLayer.FadeOutFromOne(600, Easing.OutQuint);
}
public LocalisableString TooltipText => LocalisableString.Format("{0:N0} passes in {1:N0} - {2:N0} range", count, BinStart, BinEnd);
}
}
}

View File

@ -0,0 +1,141 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Screens.OnlinePlay.DailyChallenge.Events;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class DailyChallengeTotalsDisplay : CompositeDrawable
{
private Container passCountContainer = null!;
private TotalRollingCounter passCounter = null!;
private Container totalScoreContainer = null!;
private TotalRollingCounter totalScoreCounter = null!;
private long totalPassCountInstantaneous;
private long cumulativeTotalScoreInstantaneous;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
],
Content = new[]
{
new Drawable[]
{
new SectionHeader("Total pass count")
},
new Drawable[]
{
passCountContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = passCounter = new TotalRollingCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
new Drawable[]
{
new SectionHeader("Cumulative total score")
},
new Drawable[]
{
totalScoreContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Child = totalScoreCounter = new TotalRollingCounter
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
}
};
}
public void SetInitialCounts(long totalPassCount, long cumulativeTotalScore)
{
totalPassCountInstantaneous = totalPassCount;
cumulativeTotalScoreInstantaneous = cumulativeTotalScore;
}
public void AddNewScore(NewScoreEvent ev)
{
totalPassCountInstantaneous += 1;
cumulativeTotalScoreInstantaneous += ev.TotalScore;
}
protected override void Update()
{
base.Update();
passCounter.Current.Value = totalPassCountInstantaneous;
totalScoreCounter.Current.Value = cumulativeTotalScoreInstantaneous;
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
var totalPassCountProportionOfParent = Vector2.Divide(passCountContainer.DrawSize, passCounter.DrawSize);
passCounter.Scale = new Vector2(Math.Min(Math.Min(totalPassCountProportionOfParent.X, totalPassCountProportionOfParent.Y) * 0.8f, 1));
var totalScoreTextProportionOfParent = Vector2.Divide(totalScoreContainer.DrawSize, totalScoreCounter.DrawSize);
totalScoreCounter.Scale = new Vector2(Math.Min(Math.Min(totalScoreTextProportionOfParent.X, totalScoreTextProportionOfParent.Y) * 0.8f, 1));
}
private partial class TotalRollingCounter : RollingCounter<long>
{
protected override double RollingDuration => 1000;
protected override Easing RollingEasing => Easing.OutPow10;
protected override bool IsRollingProportional => true;
protected override double GetProportionalDuration(long currentValue, long newValue)
{
long change = Math.Abs(newValue - currentValue);
if (change < 10)
return 0;
return Math.Min(6000, RollingDuration * Math.Sqrt(change) / 100);
}
protected override OsuSpriteText CreateSpriteText() => new OsuSpriteText
{
Font = OsuFont.Default.With(size: 80f, fixedWidth: true),
Spacing = new Vector2(-4, 0)
};
protected override LocalisableString FormatCount(long count) => count.ToLocalisableString(@"N0");
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Screens;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays.Notifications;
using osu.Game.Screens.Menu;
using osu.Game.Localisation;
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
{
public partial class NewDailyChallengeNotification : SimpleNotification
{
private readonly Room room;
private BeatmapCardNano card = null!;
public NewDailyChallengeNotification(Room room)
{
this.room = room;
}
[BackgroundDependencyLoader]
private void load(OsuGame? game)
{
Text = DailyChallengeStrings.ChallengeLiveNotification;
Content.Add(card = new BeatmapCardNano((APIBeatmapSet)room.Playlist.Single().Beatmap.BeatmapSet!));
Activated = () =>
{
game?.PerformFromScreen(s => s.Push(new DailyChallenge(room)), [typeof(MainMenu)]);
return true;
};
}
protected override void Update()
{
base.Update();
card.Width = Content.DrawWidth;
}
}
}

View File

@ -68,9 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{
get
{
if (Enabled.Value)
return string.Empty;
if (!enoughTimeLeft)
return "No time left!";

View File

@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking
{
public partial class ReplayDownloadButton : CompositeDrawable, IKeyBindingHandler<GlobalAction>
{
public readonly Bindable<ScoreInfo> Score = new Bindable<ScoreInfo>();
public readonly Bindable<ScoreInfo?> Score = new Bindable<ScoreInfo?>();
protected readonly Bindable<DownloadState> State = new Bindable<DownloadState>();
@ -44,7 +44,7 @@ namespace osu.Game.Screens.Ranking
}
}
public ReplayDownloadButton(ScoreInfo score)
public ReplayDownloadButton(ScoreInfo? score)
{
Score.Value = score;
Size = new Vector2(50, 30);
@ -67,11 +67,11 @@ namespace osu.Game.Screens.Ranking
switch (State.Value)
{
case DownloadState.LocallyAvailable:
game?.PresentScore(Score.Value, ScorePresentType.Gameplay);
game?.PresentScore(Score.Value!, ScorePresentType.Gameplay);
break;
case DownloadState.NotDownloaded:
scoreDownloader.Download(Score.Value);
scoreDownloader.Download(Score.Value!);
break;
case DownloadState.Importing:
@ -88,6 +88,8 @@ namespace osu.Game.Screens.Ranking
State.ValueChanged -= exportWhenReady;
downloadTracker?.RemoveAndDisposeImmediately();
downloadTracker = null;
State.SetDefault();
if (score.NewValue != null)
{
@ -147,7 +149,7 @@ namespace osu.Game.Screens.Ranking
{
if (state.NewValue != DownloadState.LocallyAvailable) return;
scoreManager.Export(Score.Value);
scoreManager.Export(Score.Value!);
State.ValueChanged -= exportWhenReady;
}

View File

@ -179,11 +179,11 @@ namespace osu.Game.Screens.Ranking
Scheduler.AddDelayed(() => OverlayActivationMode.Value = OverlayActivation.All, shouldFlair ? AccuracyCircle.TOTAL_DURATION + 1000 : 0);
}
if (SelectedScore.Value != null && AllowWatchingReplay)
if (AllowWatchingReplay)
{
buttons.Add(new ReplayDownloadButton(SelectedScore.Value)
{
Score = { BindTarget = SelectedScore! },
Score = { BindTarget = SelectedScore },
Width = 300
});
}

View File

@ -43,6 +43,15 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
{
public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
{
public Bindable<IReadOnlyList<Mod>> SelectedMods = new Bindable<IReadOnlyList<Mod>>();
/// <summary>
/// A function determining whether each mod in the score can be selected.
/// A return value of <see langword="true"/> means that the mod can be selected in the current context.
/// A return value of <see langword="false"/> means that the mod cannot be selected in the current context.
/// </summary>
public Func<Mod, bool> IsValidMod { get; set; } = _ => true;
public int? Rank { get; init; }
public bool IsPersonalBest { get; init; }
@ -68,9 +77,6 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[Resolved]
private SongSelect? songSelect { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
@ -738,8 +744,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
{
List<MenuItem> items = new List<MenuItem>();
if (score.Mods.Length > 0 && songSelect != null)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods));
if (score.Mods.Length > 0)
items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => SelectedMods.Value = score.Mods.Where(m => IsValidMod.Invoke(m)).ToArray()));
if (score.Files.Count <= 0) return items.ToArray();

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Metadata
public override IBindableDictionary<int, UserPresence> UserStates => userStates;
private readonly BindableDictionary<int, UserPresence> userStates = new BindableDictionary<int, UserPresence>();
public override IBindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
public override Bindable<DailyChallengeInfo?> DailyChallengeInfo => dailyChallengeInfo;
private readonly Bindable<DailyChallengeInfo?> dailyChallengeInfo = new Bindable<DailyChallengeInfo?>();
[Resolved]
@ -88,7 +88,14 @@ namespace osu.Game.Tests.Visual.Metadata
}
public override Task<MultiplayerPlaylistItemStats[]> BeginWatchingMultiplayerRoom(long id)
=> Task.FromResult(new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS]);
{
var stats = new MultiplayerPlaylistItemStats[MultiplayerPlaylistItemStats.TOTAL_SCORE_DISTRIBUTION_BINS];
for (int i = 0; i < stats.Length; i++)
stats[i] = new MultiplayerPlaylistItemStats { PlaylistItemID = i };
return Task.FromResult(stats);
}
public override Task EndWatchingMultiplayerRoom(long id) => Task.CompletedTask;
}

View File

@ -115,6 +115,7 @@ namespace osu.Game.Tests.Visual.OnlinePlay
MaxCombo = 1000,
TotalScore = 1000000,
User = new APIUser { Username = "best user" },
Mods = [new APIMod { Acronym = @"DT" }],
Statistics = new Dictionary<HitResult, int>()
},
new MultiplayerScore

View File

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