mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 19:04:00 +08:00
Compare commits
61 Commits
2024.725.0
...
2024.727.0
@@ -64,7 +64,8 @@ jobs:
|
||||
matrix:
|
||||
os:
|
||||
- { prettyname: Windows, fullname: windows-latest }
|
||||
- { prettyname: macOS, fullname: macos-latest }
|
||||
# macOS runner performance has gotten unbearably slow so let's turn them off temporarily.
|
||||
# - { prettyname: macOS, fullname: macos-latest }
|
||||
- { prettyname: Linux, fullname: ubuntu-latest }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 120
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ When in doubt, it's probably best to start with a discussion first. We will esca
|
||||
|
||||
While pull requests from unaffiliated contributors are welcome, please note that due to significant community interest and limited review throughput, the core team's primary focus is on the issues which are currently [on the roadmap](https://github.com/orgs/ppy/projects/7/views/6). Reviewing PRs that fall outside of the scope of the roadmap is done on a best-effort basis, so please be aware that it may take a while before a core maintainer gets around to review your change.
|
||||
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good-first-issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3Agood-first-issue) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
The [issue tracker](https://github.com/ppy/osu/issues) should provide plenty of issues to start with. We also have a [`good first issue`](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) label, although from experience it is not used very often, as it is relatively rare that we can spot an issue that will definitively be a good first issue for a new contributor regardless of their programming experience.
|
||||
|
||||
In the case of simple issues, a direct PR is okay. However, if you decide to work on an existing issue which doesn't seem trivial, **please ask us first**. This way we can try to estimate if it is a good fit for you and provide the correct direction on how to address it. In addition, note that while we do not rule out external contributors from working on roadmapped issues, we will generally prefer to handle them ourselves unless they're not very time sensitive.
|
||||
|
||||
|
||||
@@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
private void addPart(Vector2 screenSpacePosition)
|
||||
{
|
||||
parts[currentIndex].Position = screenSpacePosition;
|
||||
parts[currentIndex].Position = ToLocalSpace(screenSpacePosition);
|
||||
parts[currentIndex].Time = time + 1;
|
||||
++parts[currentIndex].InvalidationID;
|
||||
|
||||
@@ -285,9 +285,11 @@ 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(part.Position.X - size.X * originPosition.X, part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomLeft.Linear,
|
||||
@@ -296,7 +298,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y + size.Y * (1 - originPosition.Y)),
|
||||
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y + size.Y * (1 - originPosition.Y)),
|
||||
TexturePosition = textureRect.BottomRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.BottomRight.Linear,
|
||||
@@ -305,7 +307,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X + size.X * (1 - originPosition.X), part.Position.Y - size.Y * originPosition.Y),
|
||||
Position = new Vector2(screenSpacePos.X + size.X * (1 - originPosition.X), screenSpacePos.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopRight,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopRight.Linear,
|
||||
@@ -314,7 +316,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
|
||||
vertexBatch.Add(new TexturedTrailVertex
|
||||
{
|
||||
Position = new Vector2(part.Position.X - size.X * originPosition.X, part.Position.Y - size.Y * originPosition.Y),
|
||||
Position = new Vector2(screenSpacePos.X - size.X * originPosition.X, screenSpacePos.Y - size.Y * originPosition.Y),
|
||||
TexturePosition = textureRect.TopLeft,
|
||||
TextureRect = new Vector4(0, 0, 1, 1),
|
||||
Colour = DrawColourInfo.Colour.TopLeft.Linear,
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
|
||||
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Timelines.Summary;
|
||||
using osu.Game.Screens.Edit.GameplayTest;
|
||||
using osu.Game.Storyboards;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneTaikoEditorTestGameplay : EditorTestScene
|
||||
{
|
||||
protected override bool IsolateSavingFromDatabase => false;
|
||||
|
||||
protected override Ruleset CreateEditorRuleset() => new TaikoRuleset();
|
||||
|
||||
[Resolved]
|
||||
private OsuGameBase game { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmaps { get; set; } = null!;
|
||||
|
||||
private BeatmapSetInfo importedBeatmapSet = null!;
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
AddStep("import test beatmap", () => importedBeatmapSet = BeatmapImportHelper.LoadOszIntoOsu(game).GetResultSafely());
|
||||
base.SetUpSteps();
|
||||
}
|
||||
|
||||
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard? storyboard = null)
|
||||
=> beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1));
|
||||
|
||||
[Test]
|
||||
public void TestBasicGameplayTest()
|
||||
{
|
||||
AddStep("add objects", () =>
|
||||
{
|
||||
EditorBeatmap.Clear();
|
||||
EditorBeatmap.Add(new Swell { StartTime = 500, EndTime = 1500 });
|
||||
EditorBeatmap.Add(new Hit { StartTime = 3000 });
|
||||
});
|
||||
AddStep("seek to 250", () => EditorClock.Seek(250));
|
||||
AddUntilStep("wait for seek", () => EditorClock.CurrentTime, () => Is.EqualTo(250));
|
||||
|
||||
AddStep("click test gameplay button", () =>
|
||||
{
|
||||
var button = Editor.ChildrenOfType<TestGameplayButton>().Single();
|
||||
|
||||
InputManager.MoveMouseTo(button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("save prompt shown", () => DialogOverlay.CurrentDialog is SaveRequiredPopupDialog);
|
||||
|
||||
AddStep("save changes", () => DialogOverlay.CurrentDialog!.PerformOkAction());
|
||||
AddUntilStep("player pushed", () => Stack.CurrentScreen is EditorPlayer);
|
||||
AddUntilStep("wait for return to editor", () => Stack.CurrentScreen is Screens.Edit.Editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,10 +315,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
hitObjectContainer.Add(drawableSwell);
|
||||
});
|
||||
|
||||
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
|
||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||
// But for sample playback purposes they can be ignored as noise.
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, SampleControlPoint.DEFAULT_BANK);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, SampleControlPoint.DEFAULT_BANK);
|
||||
|
||||
@@ -352,10 +349,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
||||
hitObjectContainer.Add(drawableSwell);
|
||||
});
|
||||
|
||||
// You might think that this should be a SwellTick since we're before the swell, but SwellTicks get no StartTime (ie. they are zero).
|
||||
// This works fine in gameplay because they are judged whenever the user pressed, rather than being timed hits.
|
||||
// But for sample playback purposes they can be ignored as noise.
|
||||
AddAssert("most valid object is swell", () => triggerSource.GetMostValidObject(), Is.InstanceOf<Swell>);
|
||||
AddAssert("most valid object is swell tick", () => triggerSource.GetMostValidObject(), Is.InstanceOf<SwellTick>);
|
||||
checkSamples(HitType.Centre, false, HitSampleInfo.HIT_NORMAL, HitSampleInfo.BANK_DRUM);
|
||||
checkSamples(HitType.Rim, false, HitSampleInfo.HIT_CLAP, HitSampleInfo.BANK_DRUM);
|
||||
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup Label="Project References">
|
||||
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
|
||||
<ProjectReference Include="..\osu.Game.Tests\osu.Game.Tests.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
AddNested(new SwellTick
|
||||
{
|
||||
StartTime = StartTime,
|
||||
Samples = Samples
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
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;
|
||||
@@ -61,6 +62,8 @@ 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
// 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;
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -76,6 +76,24 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321");
|
||||
|
||||
AddStep("click away", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Vector2.Zero);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
Drawable textContainer = null!;
|
||||
|
||||
AddStep("move mouse to end of text", () =>
|
||||
{
|
||||
textContainer = numberBoxes.First().ChildrenOfType<Container>().ElementAt(1);
|
||||
InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.TopRight);
|
||||
});
|
||||
AddStep("hold mouse", () => InputManager.PressButton(MouseButton.Left));
|
||||
AddStep("drag to half", () => InputManager.MoveMouseTo(textContainer.ScreenSpaceDrawQuad.BottomRight - new Vector2(textContainer.ScreenSpaceDrawQuad.Width / 2 + 1f, 0)));
|
||||
AddStep("release mouse", () => InputManager.ReleaseButton(MouseButton.Left));
|
||||
AddAssert("half text selected", () => numberBoxes.First().SelectedText == "54321");
|
||||
}
|
||||
|
||||
private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));
|
||||
|
||||
@@ -261,7 +261,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
base.OnFocus(e);
|
||||
|
||||
if (SelectAllOnFocus)
|
||||
// we may become focused from an ongoing drag operation, we don't want to overwrite selection in that case.
|
||||
if (SelectAllOnFocus && string.IsNullOrEmpty(SelectedText))
|
||||
SelectAll();
|
||||
}
|
||||
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("statistics")]
|
||||
public Dictionary<HitResult, int> Statistics = new Dictionary<HitResult, int>();
|
||||
|
||||
[JsonProperty("maximum_statistics")]
|
||||
public Dictionary<HitResult, int> MaximumStatistics = new Dictionary<HitResult, int>();
|
||||
|
||||
[JsonProperty("passed")]
|
||||
public bool Passed { get; set; }
|
||||
|
||||
@@ -58,9 +61,15 @@ namespace osu.Game.Online.Rooms
|
||||
[JsonProperty("position")]
|
||||
public int? Position { get; set; }
|
||||
|
||||
[JsonProperty("pp")]
|
||||
public double? PP { get; set; }
|
||||
|
||||
[JsonProperty("has_replay")]
|
||||
public bool HasReplay { get; set; }
|
||||
|
||||
[JsonProperty("ranked")]
|
||||
public bool Ranked { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Any scores in the room around this score.
|
||||
/// </summary>
|
||||
@@ -83,13 +92,17 @@ namespace osu.Game.Online.Rooms
|
||||
MaxCombo = MaxCombo,
|
||||
BeatmapInfo = beatmap,
|
||||
Ruleset = rulesets.GetRuleset(playlistItem.RulesetID) ?? throw new InvalidOperationException($"Ruleset with ID of {playlistItem.RulesetID} not found locally"),
|
||||
Passed = Passed,
|
||||
Statistics = Statistics,
|
||||
MaximumStatistics = MaximumStatistics,
|
||||
User = User,
|
||||
Accuracy = Accuracy,
|
||||
Date = EndedAt,
|
||||
HasOnlineReplay = HasReplay,
|
||||
Rank = Rank,
|
||||
Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty<Mod>(),
|
||||
PP = PP,
|
||||
Ranked = Ranked,
|
||||
Position = Position,
|
||||
};
|
||||
|
||||
|
||||
+17
-4
@@ -63,6 +63,8 @@ using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Footer;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
@@ -749,23 +751,34 @@ 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 online flows where there are good chances that beatmap and ruleset match
|
||||
// (playlists / multiplayer / daily challenge).
|
||||
IEnumerable<Type> validScreens =
|
||||
Beatmap.Value.BeatmapInfo.Equals(databasedBeatmap) && Ruleset.Value.Equals(databasedScore.ScoreInfo.Ruleset)
|
||||
? new[] { typeof(SongSelect) }
|
||||
? new[] { typeof(SongSelect), typeof(OnlinePlayScreen), 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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -13,9 +13,11 @@ using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
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;
|
||||
@@ -39,7 +41,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;
|
||||
@@ -58,6 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
private IDisposable? userModsSelectOverlayRegistration;
|
||||
|
||||
private DailyChallengeScoreBreakdown breakdown = null!;
|
||||
private DailyChallengeTotalsDisplay totals = null!;
|
||||
private DailyChallengeEventFeed feed = null!;
|
||||
|
||||
[Cached]
|
||||
@@ -90,8 +94,13 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
public override bool DisallowExternalBeatmapRulesetChanges => true;
|
||||
|
||||
public override bool? ApplyModTrackAdjustments => true;
|
||||
|
||||
public DailyChallenge(Room room)
|
||||
{
|
||||
this.room = room;
|
||||
@@ -126,169 +135,174 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new Header(ButtonSystemStrings.DailyChallenge.ToSentence(), null),
|
||||
new GridContainer
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
Child = new GridContainer
|
||||
{
|
||||
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
|
||||
Top = Header.HEIGHT,
|
||||
},
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 30),
|
||||
new Dimension(GridSizeMode.Absolute, 50)
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
new DrawableRoomPlaylistItem(playlistItem)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AllowReordering = false,
|
||||
Scale = new Vector2(1.4f),
|
||||
Width = 1 / 1.4f,
|
||||
}
|
||||
Horizontal = WaveOverlayContainer.WIDTH_PADDING,
|
||||
Top = Header.HEIGHT,
|
||||
},
|
||||
null,
|
||||
RowDimensions =
|
||||
[
|
||||
new Container
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 30),
|
||||
new Dimension(GridSizeMode.Absolute, 50)
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
new DrawableRoomPlaylistItem(playlistItem)
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AllowReordering = false,
|
||||
Scale = new Vector2(1.4f),
|
||||
Width = 1 / 1.4f,
|
||||
}
|
||||
},
|
||||
null,
|
||||
[
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 10,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
ColumnDimensions =
|
||||
[
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension()
|
||||
],
|
||||
Content = new[]
|
||||
new Box
|
||||
{
|
||||
new Drawable?[]
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(10),
|
||||
ColumnDimensions =
|
||||
[
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 10),
|
||||
new Dimension()
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new GridContainer
|
||||
new Drawable?[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(),
|
||||
new Dimension()
|
||||
],
|
||||
Content = new[]
|
||||
new GridContainer
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new DailyChallengeCarousel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DailyChallengeTimeRemainingRing(),
|
||||
breakdown = new DailyChallengeScoreBreakdown(),
|
||||
}
|
||||
}
|
||||
},
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RowDimensions =
|
||||
[
|
||||
feed = new DailyChallengeEventFeed
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PresentScore = presentScore
|
||||
}
|
||||
new Dimension(),
|
||||
new Dimension()
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
// Middle column (leaderboard)
|
||||
leaderboard = new DailyChallengeLeaderboard(room, playlistItem)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PresentScore = presentScore,
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Main right column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
Content = new[]
|
||||
{
|
||||
new SectionHeader("Chat")
|
||||
new Drawable[]
|
||||
{
|
||||
new DailyChallengeCarousel
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DailyChallengeTimeRemainingRing(),
|
||||
breakdown = new DailyChallengeScoreBreakdown(),
|
||||
totals = new DailyChallengeTotalsDisplay(),
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
feed = new DailyChallengeEventFeed
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PresentScore = presentScore
|
||||
}
|
||||
],
|
||||
},
|
||||
[new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
|
||||
},
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension()
|
||||
]
|
||||
},
|
||||
null,
|
||||
// Middle column (leaderboard)
|
||||
leaderboard = new DailyChallengeLeaderboard(room, playlistItem)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PresentScore = presentScore,
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
// Main right column
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new SectionHeader("Chat")
|
||||
},
|
||||
[new MatchChatDisplay(room) { RelativeSizeAxes = Axes.Both }]
|
||||
},
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension()
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
null,
|
||||
[
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
],
|
||||
null,
|
||||
[
|
||||
new Container
|
||||
{
|
||||
Horizontal = -WaveOverlayContainer.WIDTH_PADDING,
|
||||
},
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
Horizontal = -WaveOverlayContainer.WIDTH_PADDING,
|
||||
},
|
||||
footerButtons = new FillFlowContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding(5),
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
new Box
|
||||
{
|
||||
new PlaylistsReadyButton
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
footerButtons = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Padding = new MarginPadding(5),
|
||||
Spacing = new Vector2(10),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(250, 1),
|
||||
Action = startPlay
|
||||
new PlaylistsReadyButton
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Size = new Vector2(250, 1),
|
||||
Action = startPlay
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
],
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,6 +310,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
SelectedMods = { BindTarget = userMods },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
@@ -318,6 +333,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
}
|
||||
|
||||
metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet;
|
||||
|
||||
((IBindable<MultiplayerScore?>)breakdown.UserBestScore).BindTo(leaderboard.UserBestScore);
|
||||
}
|
||||
|
||||
private void presentScore(long id)
|
||||
@@ -346,10 +363,11 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
Schedule(() =>
|
||||
{
|
||||
breakdown.AddNewScore(ev);
|
||||
totals.AddNewScore(ev);
|
||||
feed.AddNewScore(ev);
|
||||
|
||||
if (e.NewRank <= 50)
|
||||
Schedule(() => leaderboard.RefetchScores());
|
||||
Scheduler.AddOnce(() => leaderboard.RefetchScores());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -374,6 +392,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();
|
||||
}
|
||||
|
||||
@@ -416,7 +435,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;
|
||||
@@ -436,6 +459,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
userModsSelectOverlay.Hide();
|
||||
cancelTrackLooping();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
@@ -443,6 +467,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
waves.Hide();
|
||||
userModsSelectOverlay.Hide();
|
||||
cancelTrackLooping();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
|
||||
|
||||
roomManager.PartRoom();
|
||||
@@ -486,7 +511,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
sampleStart?.Play();
|
||||
this.Push(new PlayerLoader(() => new PlaylistsPlayer(room, playlistItem)
|
||||
{
|
||||
Exited = () => leaderboard.RefetchScores()
|
||||
Exited = () => Scheduler.AddOnce(() => leaderboard.RefetchScores())
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -22,6 +22,9 @@ 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 Action<long>? PresentScore { get; init; }
|
||||
|
||||
private readonly Room room;
|
||||
@@ -118,14 +121,21 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
RefetchScores();
|
||||
}
|
||||
|
||||
private IndexPlaylistScoresRequest? request;
|
||||
|
||||
public void RefetchScores()
|
||||
{
|
||||
var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID);
|
||||
if (request?.CompletionState == APIRequestCompletionState.Waiting)
|
||||
return;
|
||||
|
||||
request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID);
|
||||
|
||||
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;
|
||||
|
||||
@@ -4,14 +4,17 @@
|
||||
using System;
|
||||
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 +23,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,29 +49,24 @@ 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;
|
||||
});
|
||||
}
|
||||
|
||||
public void AddNewScore(NewScoreEvent newScoreEvent)
|
||||
{
|
||||
int targetBin = (int)Math.Clamp(Math.Floor((float)newScoreEvent.TotalScore / 100000), 0, bin_count - 1);
|
||||
@@ -113,20 +113,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 +156,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 +247,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
|
||||
|
||||
Schedule(() => SelectedScore.Value = scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
|
||||
Schedule(() => SelectedScore.Value ??= scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
|
||||
|
||||
return scoreInfos;
|
||||
}
|
||||
|
||||
@@ -68,9 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Enabled.Value)
|
||||
return string.Empty;
|
||||
|
||||
if (!enoughTimeLeft)
|
||||
return "No time left!";
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
@@ -398,7 +398,8 @@ namespace osu.Game.Screens.Ranking
|
||||
break;
|
||||
|
||||
case GlobalAction.Select:
|
||||
StatisticsPanel.ToggleVisibility();
|
||||
if (SelectedScore.Value != null)
|
||||
StatisticsPanel.ToggleVisibility();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user