mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 05:39:53 +08:00
Compare commits
134 Commits
2024.725.0
...
2024.731.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;
|
||||
|
||||
@@ -220,7 +220,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 +235,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 +275,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)
|
||||
@@ -287,7 +287,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 * (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,
|
||||
@@ -296,7 +296,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(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,
|
||||
@@ -305,7 +305,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(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,
|
||||
@@ -314,7 +314,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(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,
|
||||
@@ -322,6 +322,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
});
|
||||
}
|
||||
|
||||
renderer.PopLocalMatrix();
|
||||
|
||||
vertexBatch.Draw();
|
||||
shader.Unbind();
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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))]
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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's daily challenge has concluded – thanks for playing!
|
||||
///
|
||||
/// Tomorrow'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'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}";
|
||||
}
|
||||
}
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
+18
-4
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -157,6 +157,7 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
updateDropdownCurrent(status.Value);
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
switch (action.NewValue)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,12 +13,15 @@ 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;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
@@ -27,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;
|
||||
@@ -39,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;
|
||||
@@ -50,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!;
|
||||
@@ -58,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]
|
||||
@@ -90,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)
|
||||
@@ -126,169 +142,175 @@ 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 OsuContextMenuContainer
|
||||
{
|
||||
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,
|
||||
SelectedMods = { BindTarget = userMods },
|
||||
},
|
||||
// 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 +318,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
LoadComponent(userModsSelectOverlay = new RoomModSelectOverlay
|
||||
{
|
||||
Beatmap = { BindTarget = Beatmap },
|
||||
SelectedMods = { BindTarget = userMods },
|
||||
IsValidMod = _ => false
|
||||
});
|
||||
@@ -314,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)
|
||||
@@ -346,10 +372,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());
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -367,6 +394,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
apiState.BindTo(API.State);
|
||||
apiState.BindValueChanged(onlineStateChanged, true);
|
||||
|
||||
dailyChallengeInfo.BindValueChanged(dailyChallengeChanged);
|
||||
}
|
||||
|
||||
private void trySetDailyChallengeBeatmap()
|
||||
@@ -374,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();
|
||||
}
|
||||
|
||||
@@ -383,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()`
|
||||
@@ -416,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;
|
||||
@@ -428,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)
|
||||
@@ -436,6 +481,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
userModsSelectOverlay.Hide();
|
||||
cancelTrackLooping();
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
@@ -443,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();
|
||||
@@ -486,7 +533,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())
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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;
|
||||
@@ -118,14 +131,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;
|
||||
@@ -143,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();
|
||||
@@ -161,6 +183,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
Rank = userBest.Position,
|
||||
IsPersonalBest = true,
|
||||
Action = () => PresentScore?.Invoke(userBest.OnlineID),
|
||||
SelectedMods = { BindTarget = SelectedMods },
|
||||
IsValidMod = IsValidMod,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
</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" />
|
||||
|
||||
Reference in New Issue
Block a user