1
0
mirror of https://github.com/ppy/osu.git synced 2026-06-02 23:41:00 +08:00

Compare commits

...

74 Commits

111 changed files with 2007 additions and 762 deletions
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.930.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.1008.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods;
using osu.Framework.Graphics.Sprites;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Beatmaps;
@@ -34,6 +35,8 @@ namespace osu.Game.Rulesets.Mania.Mods
{
var maniaBeatmap = (ManiaBeatmap)beatmap;
double mostCommonBeatLengthBefore = beatmap.GetMostCommonBeatLength();
var newObjects = new List<ManiaHitObject>();
foreach (var h in beatmap.HitObjects.OfType<HoldNote>())
@@ -48,6 +51,17 @@ namespace osu.Game.Rulesets.Mania.Mods
}
maniaBeatmap.HitObjects = maniaBeatmap.HitObjects.OfType<Note>().Concat(newObjects).OrderBy(h => h.StartTime).ToList();
double mostCommonBeatLengthAfter = beatmap.GetMostCommonBeatLength();
// the process of removing hold notes can result in shortening the beatmap's play time,
// and therefore, as a side effect, changing the most common BPM, which will change scroll speed.
// to compensate for this, apply a multiplier to effect points in order to maintain the beatmap's original intended scroll speed.
if (!Precision.AlmostEquals(mostCommonBeatLengthBefore, mostCommonBeatLengthAfter))
{
foreach (var effectPoint in beatmap.ControlPointInfo.EffectPoints)
effectPoint.ScrollSpeed *= mostCommonBeatLengthBefore / mostCommonBeatLengthAfter;
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 KiB

@@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (score.Mods.Any(m => m is OsuModTraceable))
{
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, attributes.SliderFactor);
aimValue *= 1.0 + OsuRatingCalculator.CalculateVisibilityBonus(score.Mods, approachRate, sliderFactor: attributes.SliderFactor);
}
aimValue *= accuracy;
@@ -187,7 +187,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
bool isAlwaysPartiallyVisible = mods.OfType<OsuModHidden>().Any(m => m.OnlyFadeApproachCircles.Value) || mods.OfType<OsuModTraceable>().Any();
// Start from normal curve, rewarding lower AR up to AR7
double readingBonus = 0.04 * (12.0 - Math.Max(approachRate, 7));
// TC forcefully requires a lower reading bonus for now as it's post-applied in PP which makes it multiplicative with the regular AR bonuses
// This means it has an advantage over HD, so we decrease the multiplier to compensate
// This should be removed once we're able to apply TC bonuses in SR (depends on real-time difficulty calculations being possible)
double readingBonus = (isAlwaysPartiallyVisible ? 0.025 : 0.04) * (12.0 - Math.Max(approachRate, 7));
readingBonus *= visibilityFactor;
@@ -196,11 +199,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// For AR up to 0 - reduce reward for very low ARs when object is visible
if (approachRate < 7)
readingBonus += (isAlwaysPartiallyVisible ? 0.03 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor;
readingBonus += (isAlwaysPartiallyVisible ? 0.02 : 0.045) * (7.0 - Math.Max(approachRate, 0)) * sliderVisibilityFactor;
// Starting from AR0 - cap values so they won't grow to infinity
if (approachRate < 0)
readingBonus += (isAlwaysPartiallyVisible ? 0.075 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor;
readingBonus += (isAlwaysPartiallyVisible ? 0.01 : 0.1) * (1 - Math.Pow(1.5, approachRate)) * sliderVisibilityFactor;
return readingBonus;
}
@@ -62,24 +62,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
spin = new Sprite
{
Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 335,
},
clear = new Sprite
{
Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-clear"),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 115,
},
bonusCounter = new LegacySpriteText(LegacyFont.Score)
{
Alpha = 0,
@@ -103,6 +85,24 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Scale = new Vector2(SPRITE_SCALE * 0.9f),
Position = new Vector2(80, 448 + spm_hide_offset),
},
spin = new Sprite
{
Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-spin"),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 335,
},
clear = new Sprite
{
Alpha = 0,
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-clear"),
Scale = new Vector2(SPRITE_SCALE),
Y = SPINNER_TOP_OFFSET + 115,
},
}
});
}
@@ -4,6 +4,7 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Configuration;
using osu.Game.Localisation;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Screens.Play.PlayerSettings;
@@ -13,19 +14,19 @@ namespace osu.Game.Rulesets.Osu.UI
{
private readonly OsuRulesetConfigManager config;
[SettingSource("Show click markers", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowClickMarkers), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowClickMarkers { get; } = new BindableBool();
[SettingSource("Show frame markers", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowFrameMarkers), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowAimMarkers { get; } = new BindableBool();
[SettingSource("Show cursor path", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.ShowCursorPath), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool ShowCursorPath { get; } = new BindableBool();
[SettingSource("Hide gameplay cursor", SettingControlType = typeof(PlayerCheckbox))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.HideGameplayCursor), SettingControlType = typeof(PlayerCheckbox))]
public BindableBool HideSkinCursor { get; } = new BindableBool();
[SettingSource("Display length", SettingControlType = typeof(PlayerSliderBar<int>))]
[SettingSource(typeof(PlayerSettingsOverlayStrings), nameof(PlayerSettingsOverlayStrings.DisplayLength), SettingControlType = typeof(PlayerSliderBar<int>))]
public BindableInt DisplayLength { get; } = new BindableInt
{
MinValue = 200,
@@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.UI
};
public ReplayAnalysisSettings(OsuRulesetConfigManager config)
: base("Analysis Settings")
: base(PlayerSettingsOverlayStrings.AnalysisSettingsTitle)
{
this.config = config;
}
+7 -7
View File
@@ -24,7 +24,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
var rulesets = new RealmRulesetStore(realm, storage);
using var rulesets = new RealmRulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, realm.Realm.All<RulesetInfo>().Count());
@@ -36,8 +36,8 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
var rulesets = new RealmRulesetStore(realm, storage);
var rulesets2 = new RealmRulesetStore(realm, storage);
using var rulesets = new RealmRulesetStore(realm, storage);
using var rulesets2 = new RealmRulesetStore(realm, storage);
Assert.AreEqual(4, rulesets.AvailableRulesets.Count());
Assert.AreEqual(4, rulesets2.AvailableRulesets.Count());
@@ -52,7 +52,7 @@ namespace osu.Game.Tests.Database
{
RunTestWithRealm((realm, storage) =>
{
var rulesets = new RealmRulesetStore(realm, storage);
using var rulesets = new RealmRulesetStore(realm, storage);
Assert.IsFalse(rulesets.AvailableRulesets.First().IsManaged);
Assert.IsFalse(rulesets.GetRuleset(0)?.IsManaged);
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Database
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
_ = new RealmRulesetStore(realm, storage);
using var _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
});
@@ -104,13 +104,13 @@ namespace osu.Game.Tests.Database
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
// Availability is updated on construction of a RealmRulesetStore
_ = new RealmRulesetStore(realm, storage);
using var _ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.False);
// Simulate the ruleset getting updated
LoadTestRuleset.Version = Ruleset.CURRENT_RULESET_API_VERSION;
_ = new RealmRulesetStore(realm, storage);
using var __ = new RealmRulesetStore(realm, storage);
Assert.That(realm.Run(r => r.Find<RulesetInfo>(rulesetShortName)!.Available), Is.True);
});
@@ -299,6 +299,23 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[Test]
[TestCase("artist")]
[TestCase("unicode")]
public void TestCriteriaNotMatchingArtist(string excludedTerm)
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
Artist = new FilterCriteria.OptionalTextFilter { SearchTerm = excludedTerm, ExcludeTerm = true }
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.True(carouselItem.Filtered.Value);
}
[TestCase("simple", false)]
[TestCase("\"style/clean\"", false)]
[TestCase("\"style/clean\"!", false)]
@@ -350,6 +367,41 @@ namespace osu.Game.Tests.NonVisual.Filtering
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaMatchingTagExcluded()
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
UserTags =
[
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!", ExcludeTerm = true },
]
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestCriteriaOneTagIncludedAndOneTagExcluded()
{
var beatmap = getExampleBeatmap();
var criteria = new FilterCriteria
{
UserTags =
[
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"song representation/simple\"!" },
new FilterCriteria.OptionalTextFilter { SearchTerm = "\"style/clean\"!", ExcludeTerm = true }
]
};
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(true, carouselItem.Filtered.Value);
}
[Test]
public void TestBeatmapMustHaveAtLeastOneTagIfUserTagFilterActive()
{
@@ -257,6 +257,14 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("score in database", () => Realm.Run(r => r.Find<ScoreInfo>(Player.Score.ScoreInfo.ID) != null));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private class CustomRuleset : OsuRuleset, ILegacyRuleset
{
public override string Description => "custom";
@@ -10,6 +10,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect;
@@ -86,9 +87,9 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("add selection 1", () => grid.ChildrenOfType<BeatmapSelectPanel>().First().AddUser(new APIUser
{
Id = 6411631,
Id = DummyAPIAccess.DUMMY_USER_ID,
Username = "Maarvin",
}, isOwnUser: true));
}));
AddStep("add selection 2", () => grid.ChildrenOfType<BeatmapSelectPanel>().Skip(5).First().AddUser(new APIUser
{
Id = 2,
@@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
@@ -30,9 +31,9 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("add maarvin", () => panel!.AddUser(new APIUser
{
Id = 6411631,
Id = DummyAPIAccess.DUMMY_USER_ID,
Username = "Maarvin",
}, isOwnUser: true));
}));
AddStep("add peppy", () => panel!.AddUser(new APIUser
{
Id = 2,
@@ -1,90 +0,0 @@
// 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 System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Screens;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.RoundWarmup;
using osu.Game.Tests.Visual.Multiplayer;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneIdleScreen : MultiplayerTestScene
{
private const int user_count = 8;
private (MultiplayerRoomUser user, int score)[] userScores = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("add list", () =>
{
userScores = Enumerable.Range(1, user_count).Select(i =>
{
var user = new MultiplayerRoomUser(i)
{
User = new APIUser
{
Username = $"Player {i}"
}
};
return (user, 0);
}).ToArray();
Child = new ScreenStack(new SubScreenRoundWarmup())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(0.8f)
};
});
AddStep("join users", () =>
{
foreach (var (user, _) in userScores)
MultiplayerClient.AddUser(user);
});
}
[Test]
public void TestRandomChanges()
{
AddStep("apply random changes", () =>
{
int[] deltas = Enumerable.Range(1, userScores.Length).ToArray();
new Random().Shuffle(deltas);
for (int i = 0; i < userScores.Length; i++)
userScores[i] = (userScores[i].user, userScores[i].score + deltas[i]);
userScores = userScores.OrderByDescending(u => u.score).ToArray();
MultiplayerClient.ChangeMatchRoomState(new MatchmakingRoomState
{
Users =
{
UserDictionary = userScores.Select((tuple, i) => new MatchmakingUser
{
UserId = tuple.user.UserID,
Points = tuple.score,
Placement = i + 1
}).ToDictionary(s => s.UserId)
}
}).WaitSafely();
});
}
}
}
@@ -120,12 +120,16 @@ namespace osu.Game.Tests.Visual.Matchmaking
changeStage(MatchmakingStage.Ended, state =>
{
int localUserId = API.LocalUser.Value.OnlineID;
int i = 1;
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Rounds[1].Placement = 1;
state.Users[localUserId].Rounds[1].TotalScore = 1;
state.Users[localUserId].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
foreach (var user in MultiplayerClient.ServerRoom!.Users.OrderBy(_ => RNG.Next()))
{
state.Users[user.UserID].Placement = i++;
state.Users[user.UserID].Points = (8 - i) * 7;
state.Users[user.UserID].Rounds[1].Placement = 1;
state.Users[user.UserID].Rounds[1].TotalScore = 1;
state.Users[user.UserID].Rounds[1].Statistics[HitResult.LargeBonus] = 1;
}
});
}
@@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
base.SetUpSteps();
AddStep("add statistic", () => Child = new PanelRoomAward("Statistic description", 1)
AddStep("add award", () => Child = new PanelRoomAward("Award name", "Description of what this award means", 1)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
@@ -1,6 +1,7 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
@@ -66,7 +67,10 @@ namespace osu.Game.Tests.Visual.Matchmaking
}
}).WaitSafely());
AddToggleStep("toggle horizontal", h => panel.Horizontal = h);
foreach (var layout in Enum.GetValues<PlayerPanelDisplayMode>())
{
AddStep($"set layout to {layout}", () => panel.DisplayMode = layout);
}
}
[Test]
@@ -14,12 +14,11 @@ using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Users;
using osuTK;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneUserPanelOverlay : MultiplayerTestScene
public partial class TestScenePlayerPanelOverlay : MultiplayerTestScene
{
private PlayerPanelOverlay list = null!;
@@ -118,10 +117,10 @@ namespace osu.Game.Tests.Visual.Matchmaking
});
});
AddUntilStep("two panels displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(2));
AddUntilStep("two panels displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(2));
AddStep("remove a user", () => MultiplayerClient.RemoveUser(new APIUser { Id = 1 }));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<UserPanel>().Count(), () => Is.EqualTo(1));
AddUntilStep("one panel displayed", () => this.ChildrenOfType<PlayerPanel>().Count(), () => Is.EqualTo(1));
}
[Test]
@@ -18,8 +18,6 @@ namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneResultsScreen : MultiplayerTestScene
{
private const int invalid_user_id = 1;
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -27,6 +25,43 @@ namespace osu.Game.Tests.Visual.Matchmaking
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("set initial results", () =>
{
var state = new MatchmakingRoomState
{
CurrentRound = 6,
Stage = MatchmakingStage.Ended
};
int localUserId = API.LocalUser.Value.OnlineID;
// Overall state.
state.Users[localUserId].Placement = 1;
state.Users[localUserId].Points = 8;
for (int round = 1; round <= state.CurrentRound; round++)
state.Users[localUserId].Rounds[round].Placement = round;
// Highest score.
state.Users[localUserId].Rounds[1].TotalScore = 1000;
// Highest accuracy.
state.Users[localUserId].Rounds[2].Accuracy = 0.9995;
// Highest combo.
state.Users[localUserId].Rounds[3].MaxCombo = 100;
// Most bonus score.
state.Users[localUserId].Rounds[4].Statistics[HitResult.LargeBonus] = 50;
// Smallest score difference.
state.Users[localUserId].Rounds[5].TotalScore = 1000;
// Largest score difference.
state.Users[localUserId].Rounds[6].TotalScore = 1000;
MultiplayerClient.ChangeMatchRoomState(state).WaitSafely();
});
AddStep("add results screen", () =>
{
Child = new ScreenStack(new SubScreenResults())
@@ -36,7 +71,18 @@ namespace osu.Game.Tests.Visual.Matchmaking
Size = new Vector2(0.8f)
};
});
}
[Test]
public void TestBasic()
{
AddStep("do nothing", () => { });
}
[Test]
public void TestInvalidUser()
{
const int invalid_user_id = 1;
AddStep("join another user", () => MultiplayerClient.AddUser(new MultiplayerRoomUser(invalid_user_id)
{
User = new APIUser
@@ -45,11 +91,7 @@ namespace osu.Game.Tests.Visual.Matchmaking
Username = "Invalid user"
}
}));
}
[Test]
public void TestResults()
{
AddStep("set results stage", () =>
{
var state = new MatchmakingRoomState
@@ -1,49 +0,0 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Online.Matchmaking;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneStageSegment : MultiplayerTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("add bubble", () => Child = new StageDisplay.StageSegment(null, MatchmakingStage.RoundWarmupTime, "Next Round")
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
}
[Test]
public void TestStartStopCountdown()
{
MultiplayerCountdown countdown = null!;
AddStep("start countdown", () => MultiplayerClient.StartCountdown(countdown = new MatchmakingStageCountdown
{
Stage = MatchmakingStage.RoundWarmupTime,
TimeRemaining = TimeSpan.FromSeconds(5)
}).WaitSafely());
AddWaitStep("wait a bit", 10);
AddStep("stop countdown", () => MultiplayerClient.StopCountdown(countdown).WaitSafely());
}
}
}
@@ -1,41 +0,0 @@
// 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 NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match;
using osu.Game.Tests.Visual.Multiplayer;
namespace osu.Game.Tests.Visual.Matchmaking
{
public partial class TestSceneStatusText : MultiplayerTestScene
{
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom(MatchType.Matchmaking)));
WaitForJoined();
AddStep("create display", () => Child = new StageDisplay.StatusText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestChangeStage()
{
foreach (var stage in Enum.GetValues<MatchmakingStage>())
{
AddStep($"{stage}", () => MultiplayerClient.MatchmakingChangeStage(stage).WaitSafely());
AddWaitStep("wait a bit", 10);
}
}
}
}
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -36,6 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private RulesetStore rulesets = null!;
private TestMultiplayerComponents multiplayerComponents = null!;
@@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm);
@@ -115,5 +117,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for player", () => multiplayerComponents.CurrentScreen is Player player && player.IsLoaded);
AddStep("exit player", () => multiplayerComponents.MultiplayerScreen.MakeCurrent());
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -8,6 +8,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -37,13 +38,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneDrawableRoomPlaylist : MultiplayerTestScene
{
private RulesetStore rulesets = null!;
private TestPlaylist playlist = null!;
private BeatmapManager manager = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -436,6 +438,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestPlaylist : DrawableRoomPlaylist
{
public new IReadOnlyDictionary<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap;
@@ -51,6 +51,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayer : ScreenTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapSetInfo importedSet2 = null!;
@@ -67,7 +68,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm);
@@ -1247,5 +1248,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => multiplayerClient.RoomJoined);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
@@ -170,6 +171,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
.All(b => b.Mod.GetType() != type));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect
{
public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods;
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
@@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneMultiplayerMatchSubScreen : MultiplayerTestScene
{
private MultiplayerMatchSubScreen screen = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private Room room = null!;
@@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
Dependencies.CacheAs<BeatmapStore>(new RealmDetachedBeatmapStore());
@@ -462,6 +464,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("settings still open", () => this.ChildrenOfType<MultiplayerMatchSettingsOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
{
[Resolved(canBeNull: true)]
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -28,6 +29,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneMultiplayerPlaylist : MultiplayerTestScene
{
private MultiplayerPlaylist list = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
@@ -35,7 +37,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -290,5 +292,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
.Single()
.Items.Any(i => i.ID == playlistItemId);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
@@ -26,6 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public partial class TestSceneMultiplayerQueueList : MultiplayerTestScene
{
private MultiplayerQueueList playlist = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
@@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -168,5 +170,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
var button = playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAtOrDefault(index);
return (button?.Alpha > 0) == visible;
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -32,12 +33,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
private Room room = null!;
private BeatmapSetInfo importedSet = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -162,5 +164,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void assertReadyButtonEnablement(bool shouldBeEnabled)
=> AddUntilStep($"ready button {(shouldBeEnabled ? "is" : "is not")} enabled", () => startControl.ChildrenOfType<MultiplayerReadyButton>().Single().Enabled.Value == shouldBeEnabled);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
@@ -31,6 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestScenePlaylistsSongSelect : OnlinePlayTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager manager = null!;
private TestPlaylistsSongSelect songSelect = null!;
private Room room = null!;
@@ -40,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm);
@@ -189,6 +191,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("mod select visible", () => this.ChildrenOfType<FreeModSelectOverlay>().Single().State.Value, () => Is.EqualTo(Visibility.Visible));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestPlaylistsSongSelect : PlaylistsSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
@@ -27,6 +28,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneTeamVersus : ScreenTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
@@ -37,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -182,5 +184,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => multiplayerClient.RoomJoined);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -10,10 +10,12 @@ using osu.Framework.Extensions.TypeExtensions;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Leaderboards;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
@@ -24,6 +26,7 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.SelectV2;
using osu.Game.Tests.Beatmaps.IO;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Navigation
@@ -271,6 +274,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("selected beatmap is still osu! ruleset", () => Game.Beatmap.Value.BeatmapInfo, () => Is.EqualTo(selectedBeatmap));
}
/// <summary>
/// Note: This test was written to demonstrate the failure described at https://github.com/ppy/osu/issues/35023,
/// but because the failure scenario there entailed a race condition, it was possible for the test to pass regardless
/// unless <see cref="osu.Game.Screens.SelectV2.SongSelect.SELECTION_DEBOUNCE"/> was increased.
/// </summary>
[Test]
public void TestPresentFromResults()
{
BeatmapSetInfo beatmapToPresent = null!;
BeatmapSetInfo beatmapToPlay = null!;
AddStep("manually insert beatmap to be presented", () =>
{
Game.Realm.Write(r =>
{
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(3, [r.Find<RulesetInfo>("osu")]);
r.Add(beatmapSet);
beatmapToPresent = beatmapSet.Detach();
});
});
AddStep("import beatmap", () => beatmapToPlay = BeatmapImportHelper.LoadQuickOszIntoOsu(Game).GetResultSafely());
AddStep("set global beatmap", () => Game.Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(beatmapToPlay.Beatmaps.First()));
playToResults();
AddStep("present beatmap from results", () => Game.PresentBeatmap(beatmapToPresent));
AddUntilStep("back at song select", () => Game.ScreenStack.CurrentScreen is SoloSongSelect);
AddUntilStep("presented beatmap is current", () => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapToPresent));
}
private Func<Player> playToResults()
{
var player = playToCompletion();
@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -25,6 +26,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestSceneAddPlaylistToCollectionButton : OsuManualInputManagerTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager manager = null!;
private BeatmapSetInfo importedBeatmap = null!;
private Room room = null!;
@@ -33,7 +35,7 @@ namespace osu.Game.Tests.Visual.Playlists
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -112,5 +114,13 @@ namespace osu.Game.Tests.Visual.Playlists
}
];
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Framework.Testing;
@@ -32,6 +33,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsRoomCreation : OnlinePlayTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager manager = null!;
private TestPlaylistsRoomSubScreen match = null!;
private BeatmapSetInfo importedBeatmap = null!;
@@ -40,7 +42,7 @@ namespace osu.Game.Tests.Visual.Playlists
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -220,6 +222,14 @@ namespace osu.Game.Tests.Visual.Playlists
});
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
{
public new Bindable<PlaylistItem?> SelectedItem => base.SelectedItem;
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
@@ -38,6 +39,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
@@ -46,7 +48,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm);
@@ -579,6 +581,14 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("mods set", () => SelectedMods.Value.Count == 1 && SelectedMods.Value.OfType<OsuModDoubleTime>().Any());
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
private partial class TestPlaylistsScreen : OsuScreen
{
public TestPlaylistsScreen(PlaylistsRoomSubScreen screen)
@@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -531,5 +532,13 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("only one score with ID 12345", () => this.ChildrenOfType<ScorePanel>().Count(s => s.Score.OnlineID == 12345), () => Is.EqualTo(1));
AddUntilStep("user best position preserved", () => this.ChildrenOfType<ScorePanel>().Any(p => p.ScorePosition.Value == 133_337));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetStore.IsNotNull())
rulesetStore.Dispose();
}
}
}
@@ -12,6 +12,7 @@ using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
@@ -219,8 +220,15 @@ namespace osu.Game.Tests.Visual.Ranking
Tags =
[
new APITag { Id = 1, Name = "song representation/simple", Description = "Accessible and straightforward map design.", },
new APITag { Id = 2, Name = "style/clean", Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.", },
new APITag { Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.", },
new APITag
{
Id = 2, Name = "style/clean",
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects.",
},
new APITag
{
Id = 3, Name = "aim/aim control", Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern.",
},
new APITag { Id = 4, Name = "tap/bursts", Description = "Patterns requiring continuous movement and alternating, typically 9 notes or less.", },
]
}), 500);
@@ -403,6 +411,14 @@ namespace osu.Game.Tests.Visual.Ranking
return hitEvents;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetStore.IsNotNull())
rulesetStore?.Dispose();
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type)
@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -28,6 +29,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
private CollectionDropdown dropdown = null!;
@@ -37,7 +39,7 @@ namespace osu.Game.Tests.Visual.SongSelect
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -269,5 +271,13 @@ namespace osu.Game.Tests.Visual.SongSelect
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -5,6 +5,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
@@ -27,13 +28,14 @@ namespace osu.Game.Tests.Visual.SongSelect
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
private DialogOverlay dialogOverlay = null!;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
private ManageCollectionsDialog dialog = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -379,5 +381,13 @@ namespace osu.Game.Tests.Visual.SongSelect
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name",
() => dialog.ChildrenOfType<DrawableCollectionList>().Single().OrderedItems.ElementAtOrDefault(index)?.ChildrenOfType<TextBox>().First().Text == name);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -6,6 +6,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Testing;
@@ -211,5 +212,13 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("No rank displayed", () => topLocalRank.DisplayedRank, () => Is.Null);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -190,5 +190,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
protected void WaitForSuspension() => AddUntilStep("wait for not current", () => !SongSelect.AsNonNull().IsCurrentScreen());
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (Rulesets.IsNotNull())
Rulesets.Dispose();
}
}
}
@@ -283,5 +283,18 @@ namespace osu.Game.Tests.Visual.SongSelectV2
CheckHasSelection();
}
[Test]
public void TestManuallyCollapsingCurrentGroupAndOpeningAnother()
{
SelectNextSet();
ToggleGroupCollapse();
SelectNextGroup();
AddUntilStep("no beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.Zero);
SelectNextSet();
SelectNextSet();
AddUntilStep("no beatmap panels visible", () => GetVisiblePanels<PanelBeatmap>().Count(), () => Is.Zero);
}
}
}
@@ -151,5 +151,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
},
});
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetStore.IsNotNull())
rulesetStore.Dispose();
}
}
}
@@ -578,5 +578,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
},
};
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetStore.IsNotNull())
rulesetStore.Dispose();
}
}
}
@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
@@ -29,6 +30,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
{
public partial class TestSceneCollectionDropdown : OsuManualInputManagerTestScene
{
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
private CollectionDropdown dropdown = null!;
@@ -38,7 +40,7 @@ namespace osu.Game.Tests.Visual.SongSelectV2
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
@@ -260,5 +262,13 @@ namespace osu.Game.Tests.Visual.SongSelectV2
CollectionFilterMenuItem item = dropdown.ChildrenOfType<CollectionDropdown>().Single().ItemSource.ElementAt(index);
return dropdown.ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value == item.CollectionName);
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -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.Graphics;
using osu.Framework.Graphics.Containers;
@@ -145,6 +146,62 @@ namespace osu.Game.Tests.Visual.SongSelectV2
}
}
[Test]
public void TestStatuses()
{
foreach (var status in Enum.GetValues<BeatmapOnlineStatus>().Where(s => s != BeatmapOnlineStatus.Approved))
{
AddStep($"display {status} status", () =>
{
ContentContainer.Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), new OverlayColourProvider(OverlayColourScheme.Aquamarine))
},
Child = new OsuContextMenuContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 0.5f,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0f, 5f),
Children = new[]
{
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(0, status))
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(1, status)),
KeyboardSelected = { Value = true },
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(2, status)),
Expanded = { Value = true },
},
new PanelGroupRankedStatus
{
Item = new CarouselItem(new RankedStatusGroupDefinition(3, status)),
Expanded = { Value = true },
KeyboardSelected = { Value = true },
},
},
}
}
};
});
}
}
protected override Drawable CreateContent()
{
return new OsuContextMenuContainer
@@ -9,6 +9,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Platform;
@@ -37,6 +38,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private readonly ContextMenuContainer contextMenuContainer;
private readonly BeatmapLeaderboard leaderboard;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager;
private ScoreManager scoreManager;
@@ -71,7 +73,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(new RealmRulesetStore(Realm));
dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, dependencies.Get<AudioManager>(), Resources, dependencies.Get<GameHost>(), Beatmap.Default));
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
@@ -151,7 +153,8 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click delete option", () =>
{
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>()
.First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.Click(MouseButton.Left);
});
@@ -178,5 +181,13 @@ namespace osu.Game.Tests.Visual.UserInterface
AddUntilStep("wait for fetch", () => leaderboard.Scores.Any());
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -469,5 +469,13 @@ namespace osu.Game.Tests.Visual.UserInterface
Ruleset = rulesets.GetRuleset(3).AsNonNull()
}
};
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
@@ -7,6 +7,7 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
@@ -1057,6 +1058,14 @@ namespace osu.Game.Tests.Visual.UserInterface
private ModPanel getPanelForMod(Type modType)
=> modSelectOverlay.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.GetType() == modType);
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesetStore.IsNotNull())
rulesetStore.Dispose();
}
private partial class TestModSelectOverlay : UserModSelectOverlay
{
public TestModSelectOverlay()
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
@@ -21,6 +22,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
protected override bool UseFreshStoragePerRun => true;
private RulesetStore rulesets = null!;
private BeatmapManager beatmapManager = null!;
private const int item_count = 20;
@@ -30,7 +32,7 @@ namespace osu.Game.Tests.Visual.UserInterface
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
}
@@ -62,5 +64,13 @@ namespace osu.Game.Tests.Visual.UserInterface
// Ensure all the initial imports are present before running any tests.
Realm.Run(r => r.Refresh());
});
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (rulesets.IsNotNull())
rulesets.Dispose();
}
}
}
+11
View File
@@ -284,6 +284,7 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Returns a list of all usable <see cref="BeatmapSetInfo"/>s.
/// IMPORTANT: This should not be used outside of tests. Consider using <see cref="RealmDetachedBeatmapStore"/> instead.
/// </summary>
/// <returns>A list of available <see cref="BeatmapSetInfo"/>.</returns>
public List<BeatmapSetInfo> GetAllUsableBeatmapSets()
@@ -567,6 +568,16 @@ namespace osu.Game.Beatmaps
transaction.Commit();
});
public void MarkNotPlayed(BeatmapInfo beatmapSetInfo) => Realm.Run(r =>
{
using var transaction = r.BeginWrite();
var beatmap = r.Find<BeatmapInfo>(beatmapSetInfo.ID)!;
beatmap.LastPlayed = null;
transaction.Commit();
});
#region Implementation of ICanAcceptFiles
public Task Import(params string[] paths) => beatmapImporter.Import(paths);
@@ -187,6 +187,11 @@ namespace osu.Game.Beatmaps.Drawables
@"1841885 cYsmix - triangles.osz",
// winner of https://osu.ppy.sh/home/news/2023-02-01-twin-trials-contest-beatmapping-phase
@"1971987 James Landino - Aresene's Bazaar.osz",
// locus 2025 https://osu.ppy.sh/home/news/2025-08-21-locus-2025-results
"2412244 Kry.exe - Rift Walker.osz",
"2412260 Koto Spirit - Locus of Hexagram.osz",
"2412232 Will Stetson - Of Our Time.osz",
"2412292 ArXe - Locus Amoenus (feat. Megurine Luka).osz",
};
private static readonly string[] bundled_osu =
@@ -280,8 +280,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
}
createStatistics();
Action = () => beatmapSetOverlay?.FetchAndShowBeatmapSet(BeatmapSet.OnlineID);
}
private LocalisableString createArtistText()
+33 -9
View File
@@ -109,6 +109,8 @@ namespace osu.Game.Database
/// </summary>
private readonly SemaphoreSlim realmRetrievalLock = new SemaphoreSlim(1);
private readonly CountdownEvent pendingAsyncOperations = new CountdownEvent(0);
/// <summary>
/// <c>true</c> when the current thread has already entered the <see cref="realmRetrievalLock"/>.
/// </summary>
@@ -467,6 +469,30 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Run work on realm on a TPL thread, in a way that ensures that the realm isn't disposed before the work is done.
/// </summary>
public Task<T> RunAsync<T>(Func<Realm, T> action, CancellationToken token = default)
{
ObjectDisposedException.ThrowIf(isDisposed, this);
// Required to ensure the read is tracked and accounted for before disposal.
// Can potentially be avoided if we have a need to do so in the future.
if (!ThreadSafety.IsUpdateThread)
throw new InvalidOperationException($@"{nameof(RunAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
return Task.Run(() =>
{
var result = Run(action);
pendingAsyncOperations.Signal();
return result;
}, token);
}
/// <summary>
/// Write changes to realm.
/// </summary>
@@ -507,8 +533,6 @@ namespace osu.Game.Database
}
}
private readonly CountdownEvent pendingAsyncWrites = new CountdownEvent(0);
/// <summary>
/// Write changes to realm asynchronously, guaranteeing order of execution.
/// </summary>
@@ -523,8 +547,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncWrites.TryAddCount())
pendingAsyncWrites.Reset(1);
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -539,7 +563,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
pendingAsyncWrites.Signal();
pendingAsyncOperations.Signal();
});
return writeTask;
@@ -559,8 +583,8 @@ namespace osu.Game.Database
throw new InvalidOperationException(@$"{nameof(WriteAsync)} must be called from the update thread.");
// CountdownEvent will fail if already at zero.
if (!pendingAsyncWrites.TryAddCount())
pendingAsyncWrites.Reset(1);
if (!pendingAsyncOperations.TryAddCount())
pendingAsyncOperations.Reset(1);
// Regardless of calling Realm.GetInstance or Realm.GetInstanceAsync, there is a blocking overhead on retrieval.
// Adding a forced Task.Run resolves this.
@@ -576,7 +600,7 @@ namespace osu.Game.Database
// ReSharper disable once AccessToDisposedClosure (WriteAsync should be marked as [InstantHandle]).
result = await realm.WriteAsync(() => action(realm)).ConfigureAwait(false);
pendingAsyncWrites.Signal();
pendingAsyncOperations.Signal();
return result;
});
@@ -1494,7 +1518,7 @@ namespace osu.Game.Database
public void Dispose()
{
if (!pendingAsyncWrites.Wait(10000))
if (!pendingAsyncOperations.Wait(10000))
Logger.Log("Realm took too long waiting on pending async writes", level: LogLevel.Error);
updateRealm?.Dispose();
+36 -1
View File
@@ -82,6 +82,34 @@ namespace osu.Game.Database
return;
}
if (changes.InsertedIndices.Length == 1 && changes.DeletedIndices.Length == 1)
{
lock (detachedBeatmapSets)
{
var deletedSet = detachedBeatmapSets[changes.DeletedIndices[0]];
var insertedSet = sender[changes.InsertedIndices[0]];
// this handles beatmap updates using a heuristic that a beatmap update will preserve the online ID.
// it relies on the fact that updates are performed by removing the old set and adding a new one, in a single transaction.
// instead of removing the old set and adding a new one to the collection too, which would trigger consumers' logic related to set removals,
// move the deleted set to the index occupied by the new one and then replace it in-place.
// due to this, the operation can be presented to consumer in a manner that permits them to actually handle this as a replace operation
// and not trigger any set removal logic that may result in selections changing or similar undesirable side effects.
if (deletedSet.OnlineID == insertedSet.OnlineID)
{
pendingOperations.Enqueue(new OperationArgs
{
Type = OperationType.MoveAndReplace,
BeatmapSet = insertedSet.Detach(),
Index = changes.DeletedIndices[0],
NewIndex = changes.InsertedIndices[0],
});
return;
}
}
}
foreach (int i in changes.DeletedIndices.OrderDescending())
{
pendingOperations.Enqueue(new OperationArgs
@@ -138,6 +166,11 @@ namespace osu.Game.Database
detachedBeatmapSets.ReplaceRange(op.Index, 1, new[] { op.BeatmapSet! });
break;
case OperationType.MoveAndReplace:
detachedBeatmapSets.Move(op.Index, op.NewIndex!.Value);
detachedBeatmapSets.ReplaceRange(op.NewIndex!.Value, 1, [op.BeatmapSet!]);
break;
case OperationType.Remove:
detachedBeatmapSets.RemoveAt(op.Index);
break;
@@ -160,13 +193,15 @@ namespace osu.Game.Database
public OperationType Type;
public BeatmapSetInfo? BeatmapSet;
public int Index;
public int? NewIndex;
}
private enum OperationType
{
Insert,
Update,
Remove
Remove,
MoveAndReplace,
}
}
}
+2
View File
@@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Logging;
using Realms;
namespace osu.Game.Database
@@ -29,6 +30,7 @@ namespace osu.Game.Database
// It may be that we access this from the update thread before a refresh has taken place.
// To ensure that behaviour matches what we'd expect (the object generally *should be* available), force
// a refresh to bring in any off-thread changes immediately.
Logger.Log($"{nameof(FindWithRefresh)} triggered a realm refresh because it couldn't find the requested guid {id}");
realm.Refresh();
found = realm.Find<T>(id);
}
+1 -1
View File
@@ -231,7 +231,7 @@ namespace osu.Game.Graphics
/// Retrieves colour for a <see cref="RankingTier"/>.
/// See https://www.figma.com/file/YHWhp9wZ089YXgB7pe6L1k/Tier-Colours
/// </summary>
public ColourInfo ForRankingTier(RankingTier tier)
public static ColourInfo ForRankingTier(RankingTier tier)
{
switch (tier)
{
+24
View File
@@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public class BreakInfoStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.BreakInfo";
/// <summary>
/// "Current Progress"
/// </summary>
public static LocalisableString CurrentProgressTitle => new TranslatableString(getKey(@"current_progress_title"), @"Current Progress");
/// <summary>
/// "Grade"
/// </summary>
public static LocalisableString ShowInfoGrade => new TranslatableString(getKey(@"show_info_grade"), @"Grade");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
+5
View File
@@ -194,6 +194,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString Details => new TranslatableString(getKey(@"details"), @"Details...");
/// <summary>
/// "Mapper"
/// </summary>
public static LocalisableString Mapper => new TranslatableString(getKey(@"mapper"), @"Mapper");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -29,6 +29,61 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0);
/// <summary>
/// "Playback speed"
/// </summary>
public static LocalisableString PlaybackSpeed => new TranslatableString(getKey(@"playback_speed"), @"Playback speed");
/// <summary>
/// "Show click markers"
/// </summary>
public static LocalisableString ShowClickMarkers => new TranslatableString(getKey(@"show_click_markers"), @"Show click markers");
/// <summary>
/// "Show frame markers"
/// </summary>
public static LocalisableString ShowFrameMarkers => new TranslatableString(getKey(@"show_frame_markers"), @"Show frame markers");
/// <summary>
/// "Show cursor path"
/// </summary>
public static LocalisableString ShowCursorPath => new TranslatableString(getKey(@"show_cursor_path"), @"Show cursor path");
/// <summary>
/// "Hide gameplay cursor"
/// </summary>
public static LocalisableString HideGameplayCursor => new TranslatableString(getKey(@"hide_gameplay_cursor"), @"Hide gameplay cursor");
/// <summary>
/// "Display length"
/// </summary>
public static LocalisableString DisplayLength => new TranslatableString(getKey(@"display_length"), @"Display length");
/// <summary>
/// "Playback"
/// </summary>
public static LocalisableString PlaybackTitle => new TranslatableString(getKey(@"playback_title"), @"Playback");
/// <summary>
/// "Visual Settings"
/// </summary>
public static LocalisableString VisualSettingsTitle => new TranslatableString(getKey(@"visual_settings_title"), @"Visual Settings");
/// <summary>
/// "Audio Settings"
/// </summary>
public static LocalisableString AudioSettingsTitle => new TranslatableString(getKey(@"audio_settings_title"), @"Audio Settings");
/// <summary>
/// "Input Settings"
/// </summary>
public static LocalisableString InputSettingsTitle => new TranslatableString(getKey(@"input_settings_title"), @"Input Settings");
/// <summary>
/// "Analysis Settings"
/// </summary>
public static LocalisableString AnalysisSettingsTitle => new TranslatableString(getKey(@"analysis_settings_title"), @"Analysis Settings");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
@@ -129,6 +129,11 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString MarkAsPlayed => new TranslatableString(getKey(@"mark_as_played"), @"Mark as played");
/// <summary>
/// "Remove from played"
/// </summary>
public static LocalisableString RemoveFromPlayed => new TranslatableString(getKey(@"remove_from_played"), @"Remove from played");
/// <summary>
/// "Clear all local scores"
/// </summary>
@@ -58,11 +58,13 @@ namespace osu.Game.Overlays
};
audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioVolume);
audio.Samples.AddAdjustment(AdjustableProperty.Volume, audioVolume);
}
protected override void Dispose(bool isDisposing)
{
audio?.Tracks.RemoveAdjustment(AdjustableProperty.Volume, audioVolume);
audio?.Samples.RemoveAdjustment(AdjustableProperty.Volume, audioVolume);
base.Dispose(isDisposing);
}
}
@@ -157,7 +157,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
}
dailyPlayCount.Text = DailyChallengeStatsDisplayStrings.UnitDay(stats.PlayCount.ToLocalisableString("N0"));
dailyPlayCount.Colour = colours.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount));
dailyPlayCount.Colour = OsuColour.ForRankingTier(DailyChallengeStatsTooltip.TierForPlayCount(stats.PlayCount));
bool playedToday = stats.LastUpdate?.Date == DateTimeOffset.UtcNow.Date;
bool userIsOnOwnProfile = stats.UserID == api.LocalUser.Value.Id;
@@ -36,9 +36,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
private Box topBackground = null!;
private Box background = null!;
[Resolved]
private OsuColour colours { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@@ -117,19 +114,19 @@ namespace osu.Game.Overlays.Profile.Header.Components
topBackground.Colour = colourProvider.Background5;
totalParticipation.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.PlayCount.ToLocalisableString(@"N0"));
totalParticipation.ValueColour = colours.ForRankingTier(TierForPlayCount(statistics.PlayCount));
totalParticipation.ValueColour = OsuColour.ForRankingTier(TierForPlayCount(statistics.PlayCount));
currentDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(content.Statistics.DailyStreakCurrent.ToLocalisableString(@"N0"));
currentDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakCurrent));
currentWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakCurrent.ToLocalisableString(@"N0"));
currentWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
currentWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakCurrent));
bestDaily.Value = DailyChallengeStatsDisplayStrings.UnitDay(statistics.DailyStreakBest.ToLocalisableString(@"N0"));
bestDaily.ValueColour = colours.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestDaily.ValueColour = OsuColour.ForRankingTier(TierForDaily(statistics.DailyStreakBest));
bestWeekly.Value = DailyChallengeStatsDisplayStrings.UnitWeek(statistics.WeeklyStreakBest.ToLocalisableString(@"N0"));
bestWeekly.ValueColour = colours.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
bestWeekly.ValueColour = OsuColour.ForRankingTier(TierForWeekly(statistics.WeeklyStreakBest));
topTen.Value = statistics.Top10PercentPlacements.ToLocalisableString(@"N0");
topTen.ValueColour = colourProvider.Content2;
@@ -27,9 +27,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
private OsuSpriteText levelText = null!;
private Sprite sprite = null!;
[Resolved]
private OsuColour osuColour { get; set; } = null!;
public LevelBadge()
{
TooltipText = UsersStrings.ShowStatsLevel("0");
@@ -91,7 +88,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
tier = RankingTier.Lustrous;
}
return osuColour.ForRankingTier(tier);
return OsuColour.ForRankingTier(tier);
}
}
}
@@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@@ -11,6 +12,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osuTK;
namespace osu.Game.Overlays.Settings
@@ -94,7 +96,7 @@ namespace osu.Game.Overlays.Settings
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 16, weight: FontWeight.Regular),
Text = @"back",
Text = CommonStrings.Back.ToLower(),
},
}
}
+5 -3
View File
@@ -3,12 +3,14 @@
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Utils;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
@@ -21,7 +23,7 @@ namespace osu.Game.Overlays
{
public partial class SettingsToolboxGroup : Container, IExpandable
{
private readonly string title;
private readonly LocalisableString title;
public const int CONTAINER_WIDTH = 270;
private const float transition_duration = 250;
@@ -60,7 +62,7 @@ namespace osu.Game.Overlays
/// Create a new instance.
/// </summary>
/// <param name="title">The title to be displayed in the header of this group.</param>
public SettingsToolboxGroup(string title)
public SettingsToolboxGroup(LocalisableString title)
{
this.title = title;
@@ -102,7 +104,7 @@ namespace osu.Game.Overlays
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Text = title.ToUpperInvariant(),
Text = title.ToUpper(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17),
Padding = new MarginPadding { Left = 10, Right = 30 },
},
@@ -82,7 +82,7 @@ namespace osu.Game.Overlays.SkinEditor
foreach (var drawableItem in objectsInRotation)
{
var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], actualOrigin, rotation);
var rotatedPosition = GeometryUtils.RotatePointAroundOrigin(originalPositions[drawableItem], ToScreenSpace(actualOrigin), rotation);
UpdatePosition(drawableItem, rotatedPosition);
drawableItem.Rotation = originalRotations[drawableItem] + rotation;
+12 -1
View File
@@ -1,6 +1,7 @@
// 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 System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@@ -77,6 +78,8 @@ namespace osu.Game.Overlays.Toolbar
protected readonly Container BackgroundContent;
private IDisposable? realmSubscription;
[Resolved]
private RealmAccess realm { get; set; } = null!;
@@ -184,7 +187,8 @@ namespace osu.Game.Overlays.Toolbar
{
if (Hotkey != null)
{
realm.SubscribeToPropertyChanged(r => r.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value), kb => kb.KeyCombinationString, updateKeyBindingTooltip);
realmSubscription = realm.SubscribeToPropertyChanged(r => r.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetName == null && rkb.ActionInt == (int)Hotkey.Value),
kb => kb.KeyCombinationString, updateKeyBindingTooltip);
}
}
@@ -234,6 +238,13 @@ namespace osu.Game.Overlays.Toolbar
? $" ({keyBindingString})"
: string.Empty;
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
realmSubscription?.Dispose();
}
}
public partial class OpaqueBackground : Container
@@ -476,12 +476,30 @@ namespace osu.Game.Rulesets.Objects.Legacy
private ConvertHitObject createSlider(Vector2 position, bool newCombo, int comboOffset, PathControlPoint[] controlPoints, double? length, int repeatCount,
IList<IList<HitSampleInfo>> nodeSamples)
{
var path = new SliderPath(controlPoints, length);
// there are known instances of beatmaps (https://osu.ppy.sh/beatmapsets/594828#osu/1258033) which contain zero-length sliders with non-zero numbers of repeats.
// this was exploiting a bug in stable in which the slider repeats would be generated as objects but never actually judged as a hit *or* miss during gameplay,
// therefore increasing the theoretical possible max combo to be gained from a slider while in practice never giving that extra combo.
// due to lazer ensuring that an object has its nested part fully judged, this would result in broken behaviours
// (either the zero-length slider giving hundreds of combo for nothing if the repeats are judged as hit, or insta-failing the player due to HP if judged as miss).
// to remedy this in a way that seems least damaging, detect this situation via a heuristic and reset the number of repeats to zero.
// this technically *does not* match stable beatmap parsing or conversion, *does not* match in-gameplay behaviour of such broken sliders,
// and *will* fail conversion mapping tests, but again, this is supposed to be a least-worst measure to prevent exploits.
// it is also applied centrally to all rulesets rather than in specific ruleset converters because this failure scenario
// translates across rulesets (osu! and catch are both affected).
if (Precision.AlmostEquals(path.Distance, 0))
{
repeatCount = 0;
nodeSamples = [nodeSamples[0], nodeSamples[^1]];
}
return lastObject = new ConvertSlider
{
Position = position,
NewCombo = firstObject || lastObject is ConvertSpinner || newCombo,
ComboOffset = newCombo ? comboOffset : 0,
Path = new SliderPath(controlPoints, length),
Path = path,
NodeSamples = nodeSamples,
RepeatCount = repeatCount
};
@@ -1,9 +1,10 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
@@ -14,11 +15,11 @@ using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Beatmaps.Drawables.Cards.Statistics;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet;
@@ -42,11 +43,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private BeatmapCardThumbnail thumbnail = null!;
private CollapsibleButtonContainer buttonContainer = null!;
private FillFlowContainer<BeatmapCardStatistic> statisticsContainer = null!;
private FillFlowContainer idleBottomContent = null!;
private BeatmapCardDownloadProgressBar downloadProgressBar = null!;
public AvatarOverlay SelectionOverlay = null!;
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
@@ -193,16 +194,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
AlwaysPresent = true,
Children = new Drawable[]
{
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(8, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
},
new Container
{
Masking = true,
@@ -218,23 +209,23 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
},
new FillFlowContainer
{
Padding = new MarginPadding(2),
Padding = new MarginPadding(4),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Spacing = new Vector2(6, 0),
Children = new Drawable[]
{
new StarRatingDisplay(new StarDifficulty(beatmap.StarRating, 0), StarRatingDisplaySize.Small, animated: true)
{
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Scale = new Vector2(0.875f),
Scale = new Vector2(0.9f),
},
new TruncatingSpriteText
{
Text = beatmap.DifficultyName,
Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold),
Font = OsuFont.Style.Caption1.With(weight: FontWeight.Bold),
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
}
@@ -254,6 +245,11 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
Progress = { BindTarget = DownloadTracker.Progress }
}
}
},
SelectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
}
@@ -305,24 +301,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
return BeatmapsetsStrings.ShowDetailsByArtist(romanisableArtist);
}
private IEnumerable<BeatmapCardStatistic> createStatistics()
{
var hypesStatistic = HypesStatistic.CreateFor(BeatmapSet);
if (hypesStatistic != null)
yield return hypesStatistic;
var nominationsStatistic = NominationsStatistic.CreateFor(BeatmapSet);
if (nominationsStatistic != null)
yield return nominationsStatistic;
yield return new FavouritesStatistic(BeatmapSet) { Current = FavouriteState };
yield return new PlayCountStatistic(BeatmapSet);
var dateStatistic = BeatmapCardDateStatistic.CreateFor(BeatmapSet);
if (dateStatistic != null)
yield return dateStatistic;
}
protected override void UpdateState()
{
base.UpdateState();
@@ -331,8 +309,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
buttonContainer.ShowDetails.Value = showDetails;
thumbnail.Dimmed.Value = showDetails;
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
}
public override MenuItem[] ContextMenuItems
@@ -350,5 +326,133 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
return items.ToArray();
}
}
public partial class AvatarOverlay : CompositeDrawable
{
private readonly Container<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
[Resolved]
private IAPIProvider api { get; set; } = null!;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
AutoSizeAxes = Axes.X,
Height = SelectionAvatar.AVATAR_SIZE,
};
Padding = new MarginPadding { Vertical = 5 };
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user)
{
if (avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, user.Equals(api.LocalUser.Value));
avatars.Add(avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateAvatarLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
return false;
avatar.PopOutAndExpire();
avatars.ChangeChildDepth(avatar, float.MaxValue);
updateAvatarLayout();
return true;
}
private void updateAvatarLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatars.Count - 1; i >= 0; i--)
{
var avatar = avatars[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public const float AVATAR_SIZE = 30;
public APIUser User { get; }
public bool Expired { get; private set; }
private readonly MatchmakingAvatar avatar;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
User = user;
Size = new Vector2(AVATAR_SIZE);
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
avatar.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Transforms;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osuTK;
@@ -34,9 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
public event Action<MultiplayerPlaylistItem>? ItemSelected;
[Resolved]
private IAPIProvider api { get; set; } = null!;
private readonly Dictionary<long, BeatmapSelectPanel> panelLookup = new Dictionary<long, BeatmapSelectPanel>();
private readonly PanelGridContainer panelGridContainer;
@@ -134,7 +130,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
return;
if (selected)
panel.AddUser(user, user.Equals(api.LocalUser.Value));
panel.AddUser(user);
else
panel.RemoveUser(user);
}
@@ -2,10 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using System.Collections.Generic;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
@@ -37,12 +36,15 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
private const float border_width = 3;
private Container scaleContainer = null!;
private AvatarOverlay selectionOverlay = null!;
private Drawable lighting = null!;
private Container border = null!;
private Container mainContent = null!;
private readonly List<APIUser> users = new List<APIUser>();
private BeatmapCardMatchmaking? card;
public override bool PropagatePositionalInputSubTree => AllowSelection;
public BeatmapSelectPanel(MultiplayerPlaylistItem item)
@@ -75,11 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
RelativeSizeAxes = Axes.Both,
Alpha = 0,
},
selectionOverlay = new AvatarOverlay
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
}
}
},
border = new Container
@@ -114,19 +111,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
};
lookupCache.GetBeatmapAsync(Item.BeatmapID).ContinueWith(b => Schedule(() =>
{
Debug.Assert(card == null);
var beatmap = b.GetResultSafely()!;
beatmap.StarRating = Item.StarRating;
mainContent.Add(new BeatmapCardMatchmaking(beatmap)
mainContent.Add(card = new BeatmapCardMatchmaking(beatmap)
{
Depth = float.MaxValue,
Action = () => Action?.Invoke(Item),
});
foreach (var user in users)
card.SelectionOverlay.AddUser(user);
}));
}
public bool AddUser(APIUser user, bool isOwnUser = false) => selectionOverlay.AddUser(user, isOwnUser);
public bool RemoveUser(APIUser user) => selectionOverlay.RemoveUser(user.Id);
public void AddUser(APIUser user)
{
users.Add(user);
card?.SelectionOverlay.AddUser(user);
}
public void RemoveUser(APIUser user)
{
users.Remove(user);
card?.SelectionOverlay.RemoveUser(user.Id);
}
protected override bool OnHover(HoverEvent e)
{
@@ -212,130 +223,5 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.BeatmapSelect
this.Delay(delay + duration).FadeOut().Expire();
}
private partial class AvatarOverlay : CompositeDrawable
{
private readonly Container<SelectionAvatar> avatars;
private Sample? userAddedSample;
private double? lastSamplePlayback;
public AvatarOverlay()
{
AutoSizeAxes = Axes.Both;
InternalChild = avatars = new Container<SelectionAvatar>
{
AutoSizeAxes = Axes.X,
Height = SelectionAvatar.AVATAR_SIZE,
};
Padding = new MarginPadding(5);
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
userAddedSample = audio.Samples.Get(@"Multiplayer/player-ready");
}
public bool AddUser(APIUser user, bool isOwnUser)
{
if (avatars.Any(a => a.User.Id == user.Id))
return false;
var avatar = new SelectionAvatar(user, isOwnUser);
avatars.Add(avatar);
if (lastSamplePlayback == null || Time.Current - lastSamplePlayback > OsuGameBase.SAMPLE_DEBOUNCE_TIME)
{
userAddedSample?.Play();
lastSamplePlayback = Time.Current;
}
updateAvatarLayout();
avatar.FinishTransforms();
return true;
}
public bool RemoveUser(int id)
{
if (avatars.SingleOrDefault(a => a.User.Id == id) is not SelectionAvatar avatar)
return false;
avatar.PopOutAndExpire();
avatars.ChangeChildDepth(avatar, float.MaxValue);
updateAvatarLayout();
return true;
}
private void updateAvatarLayout()
{
const double stagger = 30;
const float spacing = 4;
double delay = 0;
float x = 0;
for (int i = avatars.Count - 1; i >= 0; i--)
{
var avatar = avatars[i];
if (avatar.Expired)
continue;
avatar.Delay(delay).MoveToX(x, 500, Easing.OutElasticQuarter);
x -= avatar.LayoutSize.X + spacing;
delay += stagger;
}
}
public partial class SelectionAvatar : CompositeDrawable
{
public const float AVATAR_SIZE = 30;
public APIUser User { get; }
public bool Expired { get; private set; }
private readonly MatchmakingAvatar avatar;
public SelectionAvatar(APIUser user, bool isOwnUser)
{
User = user;
Size = new Vector2(AVATAR_SIZE);
InternalChild = avatar = new MatchmakingAvatar(user, isOwnUser)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
};
}
protected override void LoadComplete()
{
base.LoadComplete();
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
public void PopOutAndExpire()
{
avatar.ScaleTo(0, 400, Easing.OutExpo);
this.FadeOut(100).Expire();
Expired = true;
}
}
}
}
}
@@ -1,17 +1,36 @@
// 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 System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
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.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Localisation;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Online.Matchmaking.Events;
using osu.Game.Online.Metadata;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Overlays;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results;
using osu.Game.Screens.Play;
using osu.Game.Users;
using osuTK;
@@ -21,10 +40,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
/// A panel used throughout matchmaking to represent a user, including local information like their
/// rank and high level statistics in the matchmaking system.
/// </summary>
public partial class PlayerPanel : UserPanel
public partial class PlayerPanel : OsuClickableContainer, IHasContextMenu
{
public static readonly Vector2 SIZE_HORIZONTAL = new Vector2(250, 100);
public static readonly Vector2 SIZE_VERTICAL = new Vector2(150, 200);
private static readonly Vector2 size_horizontal = new Vector2(250, 100);
private static readonly Vector2 size_vertical = new Vector2(150, 200);
private static readonly Vector2 avatar_size = new Vector2(80);
public readonly MultiplayerRoomUser RoomUser;
@@ -35,6 +54,33 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
[Resolved]
private IAPIProvider api { get; set; } = null!;
[Resolved]
private UserProfileOverlay? profileOverlay { get; set; }
[Resolved]
private ChannelManager? channelManager { get; set; }
[Resolved]
private ChatOverlay? chatOverlay { get; set; }
[Resolved]
private IDialogOverlay? dialogOverlay { get; set; }
[Resolved]
protected OverlayColourProvider? ColourProvider { get; private set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved]
protected OsuColour Colours { get; private set; } = null!;
[Resolved]
private MultiplayerClient? multiplayerClient { get; set; }
[Resolved]
private MetadataClient? metadataClient { get; set; }
private OsuSpriteText rankText = null!;
private OsuSpriteText scoreText = null!;
@@ -43,36 +89,76 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
private MatchmakingAvatar avatar = null!;
private OsuSpriteText username = null!;
private Container scaleContainer = null!;
private Container mainContent = null!;
public bool Horizontal
private PlayerPanelDisplayMode displayMode = PlayerPanelDisplayMode.Horizontal;
public PlayerPanelDisplayMode DisplayMode
{
get => horizontal;
get => displayMode;
set
{
horizontal = value;
displayMode = value;
if (IsLoaded)
updateLayout(false);
}
}
private bool horizontal;
public readonly APIUser User;
/// <summary>
/// Perform an action in addition to showing the user's profile.
/// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX).
/// </summary>
public new Action? Action;
protected Action ViewProfile { get; private set; } = null!;
public Box SolidBackgroundLayer { get; private set; } = null!;
protected Drawable? Background { get; private set; }
public PlayerPanel(MultiplayerRoomUser user)
: base(user.User!)
: base(HoverSampleSet.Button)
{
ArgumentNullException.ThrowIfNull(user.User);
User = user.User;
RoomUser = user;
}
[BackgroundDependencyLoader]
private void load()
{
Masking = true;
CornerRadius = 10;
CornerExponent = 10;
Add(SolidBackgroundLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = ColourProvider?.Background5 ?? Colours.Gray1
});
Add(scaleContainer = new Container
Background = new UserCoverBackground
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
User = User
};
if (Background != null)
Add(Background);
base.Action = ViewProfile = () =>
{
Action?.Invoke();
profileOverlay?.ShowUser(User);
};
Content.Masking = true;
Content.CornerRadius = 10;
Content.CornerExponent = 10;
Content.Anchor = Anchor.Centre;
Content.Origin = Anchor.Centre;
Add(new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -104,14 +190,17 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
},
rankText = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomCentre,
Blending = BlendingParameters.Additive,
Margin = new MarginPadding(4),
Font = OsuFont.Style.Title.With(size: 70),
Text = "-",
Font = OsuFont.Style.Title.With(size: 55),
},
username = new OsuSpriteText
{
Alpha = 0,
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Text = User.Username,
@@ -119,6 +208,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
},
scoreText = new OsuSpriteText
{
Alpha = 0,
Margin = new MarginPadding(10),
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
@@ -128,9 +218,10 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
}
}
});
}
protected override Drawable CreateLayout() => Empty();
// Allow avatar to exist outside of masking for when it jumps around and stuff.
AddInternal(avatar.CreateProxy());
}
protected override void LoadComplete()
{
@@ -146,51 +237,92 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
avatar.ScaleTo(0)
.ScaleTo(1, 500, Easing.OutElasticHalf)
.FadeIn(200);
}
rankText.Hide();
scoreText.Hide();
username.Hide();
private bool horizontal => displayMode == PlayerPanelDisplayMode.Horizontal;
using (BeginDelayedSequence(100))
private Vector2 avatarPosition
{
get
{
username.FadeInFromZero(600);
using (BeginDelayedSequence(100))
switch (displayMode)
{
scoreText.FadeInFromZero(600);
case PlayerPanelDisplayMode.AvatarOnly:
return avatar_size / 2;
using (BeginDelayedSequence(100))
{
rankText.FadeTo(0.6f, 600);
}
case PlayerPanelDisplayMode.Horizontal:
return new Vector2(50);
case PlayerPanelDisplayMode.Vertical:
return new Vector2(75, 50);
default:
throw new ArgumentOutOfRangeException();
}
}
}
private Vector2 avatarPosition => horizontal ? new Vector2(50) : new Vector2(75, 50);
private void updateLayout(bool instant)
{
double duration = instant ? 0 : 1000;
avatarPositionTarget.MoveTo(avatarPosition, duration, Easing.OutPow10);
this.ResizeTo(horizontal ? SIZE_HORIZONTAL : SIZE_VERTICAL, duration, Easing.OutPow10);
rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10);
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
switch (displayMode)
{
case PlayerPanelDisplayMode.AvatarOnly:
rankText.Hide();
scoreText.Hide();
username.Hide();
Background.FadeOut(200, Easing.OutQuint);
SolidBackgroundLayer.FadeOut(200, Easing.OutQuint);
this.ResizeTo(avatar_size, duration, Easing.OutPow10);
break;
case PlayerPanelDisplayMode.Horizontal:
case PlayerPanelDisplayMode.Vertical:
Background.FadeIn(200);
SolidBackgroundLayer.FadeIn(200);
using (BeginDelayedSequence(100))
{
username.FadeIn(600);
using (BeginDelayedSequence(100))
{
scoreText.FadeIn(600);
using (BeginDelayedSequence(100))
{
rankText.FadeTo(1, 600);
}
}
}
this.ResizeTo(horizontal ? size_horizontal : size_vertical, duration, Easing.OutPow10);
rankText.MoveTo(horizontal ? new Vector2(-40, -10) : new Vector2(-70, 0), duration, Easing.OutPow10);
username.MoveTo(horizontal ? new Vector2(0, -46) : new Vector2(0, -86), duration, Easing.OutPow10);
scoreText.MoveTo(horizontal ? new Vector2(0, -16) : new Vector2(0, -56), duration, Easing.OutPow10);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
protected override bool OnHover(HoverEvent e)
{
scaleContainer.ScaleTo(1.03f, 750, Easing.OutPow10);
mainContent.ScaleTo(1.03f, 750, Easing.OutPow10);
Content.ScaleTo(1.03f, 2000, Easing.OutPow10);
mainContent.ScaleTo(1.03f, 2000, Easing.OutPow10);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
scaleContainer.ScaleTo(1f, 750, Easing.OutPow10);
Content.ScaleTo(1f, 750, Easing.OutPow10);
mainContent.ScaleTo(1, 750, Easing.OutPow10);
mainContent.MoveTo(Vector2.Zero, 1250, Easing.OutPow10);
@@ -202,8 +334,8 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
{
var offset = (avatarPositionTarget.ToLocalSpace(e.ScreenSpaceMousePosition) - avatarPositionTarget.DrawSize / 2) * 0.02f;
mainContent.MoveTo(offset * 0.5f, 1000, Easing.OutPow10);
avatarPositionTarget.MoveTo(avatarPosition + offset, 400, Easing.OutPow10);
mainContent.MoveTo(offset * 0.5f, 2000, Easing.OutPow10);
avatarPositionTarget.MoveTo(avatarPosition + offset, 2000, Easing.OutPow10);
return base.OnMouseMove(e);
}
@@ -215,10 +347,13 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (!matchmakingState.Users.UserDictionary.TryGetValue(User.Id, out MatchmakingUser? userScore))
return;
rankText.Text = $"#{userScore.Placement}";
rankText.Text = userScore.Placement.Ordinalize(CultureInfo.CurrentCulture);
rankText.FadeColour(SubScreenResults.ColourForPlacement(userScore.Placement));
scoreText.Text = $"{userScore.Points} pts";
});
private int consecutiveJumps;
private void onMatchEvent(MatchServerEvent e)
{
switch (e)
@@ -230,11 +365,36 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
switch (action.Action)
{
case MatchmakingAvatarAction.Jump:
avatarJumpTarget.MoveToY(-10, 200, Easing.Out)
.Then().MoveToY(0, 200, Easing.In);
avatarJumpTarget.ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
var movement = avatarJumpTarget.Delay(0);
var scale = avatarJumpTarget.Delay(0);
// only increase height if the user jumps again while in a "jumped" state.
// this avoids building up large jumps from very quick spam, and adds a timing game.
bool isConsecutive = avatarJumpTarget.Y < 0;
if (isConsecutive)
{
consecutiveJumps++;
if (avatarJumpTarget.Y > 0)
movement = movement.MoveToY(0);
movement = movement.MoveToY(5, 100, Easing.Out);
scale = scale.ScaleTo(new Vector2(1, 0.95f), 100, Easing.Out);
}
else
{
consecutiveJumps = 0;
}
float multiplier = 1 + 0.3f * Math.Min(10, consecutiveJumps);
movement.Then().MoveToY(-10 * multiplier, 200, Easing.Out)
.Then().MoveToY(0, 200, Easing.In);
scale.Then().ScaleTo(new Vector2(1, 1.05f), 200, Easing.Out)
.Then().ScaleTo(new Vector2(1, 0.95f), 200, Easing.In)
.Then().ScaleTo(Vector2.One, 800, Easing.OutElastic);
break;
}
@@ -252,5 +412,60 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
client.MatchEvent -= onMatchEvent;
}
}
public MenuItem[] ContextMenuItems
{
get
{
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, ViewProfile)
};
if (User.Equals(api.LocalUser.Value))
return items.ToArray();
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, () =>
{
channelManager?.OpenPrivateChannel(User);
chatOverlay?.Show();
}));
items.Add(!isUserBlocked()
? new OsuMenuItem(UsersStrings.BlocksButtonBlock, MenuItemType.Destructive, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Block(User)))
: new OsuMenuItem(UsersStrings.BlocksButtonUnblock, MenuItemType.Standard, () => dialogOverlay?.Push(ConfirmBlockActionDialog.Unblock(User))));
if (isUserOnline())
{
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
{
if (isUserOnline())
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(User)));
}));
if (canInviteUser())
{
items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () =>
{
if (canInviteUser())
multiplayerClient!.InvitePlayer(User.Id);
}));
}
}
return items.ToArray();
bool isUserOnline() => metadataClient?.GetPresence(User.OnlineID) != null;
bool canInviteUser() => isUserOnline() && multiplayerClient?.Room?.Users.All(u => u.UserID != User.Id) == true;
bool isUserBlocked() => api.Blocks.Any(b => b.TargetID == User.OnlineID);
}
}
}
public enum PlayerPanelDisplayMode
{
AvatarOnly,
Horizontal,
Vertical
}
}
@@ -140,7 +140,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
foreach (var panel in panels)
{
panel.FadeTo(1, 200);
panel.Horizontal = false;
panel.DisplayMode = PlayerPanelDisplayMode.Vertical;
}
gridLayout.AcquirePanels(panels.ToArray());
@@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
foreach (var panel in panels)
{
panel.FadeTo(1, 200);
panel.Horizontal = true;
panel.DisplayMode = PlayerPanelDisplayMode.Horizontal;
}
int leftCount = (int)Math.Ceiling(panels.Count / 2f);
@@ -280,8 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
if (panel?.Parent == null)
return;
Size = panel.Horizontal ? PlayerPanel.SIZE_HORIZONTAL : PlayerPanel.SIZE_VERTICAL;
Size *= panel.Scale;
Size = panel.Size * panel.Scale;
var targetPos = getFinalPosition();
@@ -3,55 +3,135 @@
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Database;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osuTK.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
{
public partial class PanelRoomAward : CompositeDrawable
public partial class PanelRoomAward : OsuClickableContainer
{
private readonly Color4 backgroundColour = Color4.SaddleBrown;
private readonly string text;
private readonly string description;
private readonly int userId;
public PanelRoomAward(string text, int userId)
private Box glossLayer = null!;
private Container scaleContainer = null!;
public PanelRoomAward(string text, string description, int userId)
{
this.text = text;
this.description = description;
this.userId = userId;
AutoSizeAxes = Axes.Both;
Height = 40;
RelativeSizeAxes = Axes.X;
// Just make hover sounds work for now.
Action = () => { };
}
[BackgroundDependencyLoader]
private void load(UserLookupCache userLookupCache)
private void load(UserLookupCache userLookupCache, OverlayColourProvider colourProvider)
{
// Should be cached by this point.
APIUser? user = userLookupCache.GetUserAsync(userId).GetResultSafely();
APIUser user = userLookupCache.GetUserAsync(userId).GetResultSafely()!;
InternalChild = new CircularContainer
Child = scaleContainer = new Container
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = 5,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
Colour = colourProvider.Background3,
},
new OsuSpriteText
new FillFlowContainer
{
Margin = new MarginPadding(10),
Text = $"{text}: {user?.Username}"
}
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding(10),
Spacing = new Vector2(10),
Children = new Drawable[]
{
new MatchmakingAvatar(user)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
},
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.Style.Caption1,
Text = user.Username
},
new OsuSpriteText
{
Font = OsuFont.Style.Caption2.With(weight: FontWeight.Bold),
Text = text
}
}
},
}
},
glossLayer = new Box
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.CentreRight,
Rotation = 30,
Scale = new Vector2(0.1f, 3),
Colour = ColourInfo.GradientHorizontal(
colourProvider.Background2.Opacity(0),
colourProvider.Background2),
Alpha = 0.1f,
Blending = BlendingParameters.Additive,
},
}
};
}
protected override bool OnHover(HoverEvent e)
{
scaleContainer.ScaleTo(1.15f, 2000, Easing.OutPow10);
glossLayer
.FadeTo(0.05f, 2000, Easing.OutPow10)
.MoveToX(-8, 2000, Easing.OutPow10);
return base.OnHover(e);
}
protected override void OnHoverLost(HoverLostEvent e)
{
scaleContainer.ScaleTo(1f, 500, Easing.OutQuint);
glossLayer
.FadeTo(0.1f, 500, Easing.OutQuint)
.MoveToX(0, 500, Easing.OutQuint);
base.OnHoverLost(e);
}
public override LocalisableString TooltipText => description;
}
}
@@ -1,28 +1,35 @@
// 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.Globalization;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osuTK.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
{
public partial class PanelUserStatistic : CompositeDrawable
{
private readonly Color4 backgroundColour = Color4.SaddleBrown;
private readonly int position;
private readonly string text;
public PanelUserStatistic(string text)
public PanelUserStatistic(int position, string text)
{
this.position = position;
this.text = text;
AutoSizeAxes = Axes.Both;
}
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
@@ -32,16 +39,48 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
Masking = true,
Children = new Drawable[]
{
new Box
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
new Container
{
Width = 30,
Masking = true,
CornerRadius = 6,
CornerExponent = 10,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = SubScreenResults.ColourForPlacement(position),
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Default.With(weight: FontWeight.Bold),
Text = position.Ordinalize(CultureInfo.CurrentCulture),
Colour = colourProvider.Background4,
},
}
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Style.Caption2,
Text = text
}
}
},
new OsuSpriteText
{
Margin = new MarginPadding(10),
Text = text
}
}
};
}
@@ -1,16 +1,22 @@
// 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.Globalization;
using System.Linq;
using Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Multiplayer.MatchTypes.Matchmaking;
using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Utils;
using osuTK;
@@ -24,133 +30,144 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
private const float grid_spacing = 5;
public override PanelDisplayStyle PlayersDisplayStyle => PanelDisplayStyle.Grid;
public override Drawable PlayersDisplayArea { get; }
public override Drawable PlayersDisplayArea { get; } = new Container { RelativeSizeAxes = Axes.Both };
[Resolved]
private MultiplayerClient client { get; set; } = null!;
private readonly OsuSpriteText placementText;
private readonly FillFlowContainer<PanelUserStatistic> userStatistics;
private readonly FillFlowContainer<PanelRoomAward> roomStatistics;
private OsuSpriteText placementText = null!;
private FillFlowContainer<PanelUserStatistic> userStatistics = null!;
private FillFlowContainer<PanelRoomAward> roomAwards = null!;
public SubScreenResults()
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new GridContainer
{
Padding = new MarginPadding(5),
RelativeSizeAxes = Axes.Both,
RowDimensions =
[
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, grid_spacing),
new Dimension(),
new Dimension(GridSizeMode.Absolute, grid_spacing),
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, 75)
],
Content = new Drawable[]?[]
},
Content = new[]
{
[
new FillFlowContainer
new[]
{
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(grid_spacing),
Children = new[]
AutoSizeAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Children = new Drawable[]
{
new OsuSpriteText
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Placement",
Font = OsuFont.Default.With(size: 12)
Masking = true,
CornerRadius = 5,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = colourProvider.Background4,
RelativeSizeAxes = Axes.Both,
},
}
},
placementText = new OsuSpriteText
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Default.With(size: 72),
UseFullGlyphHeight = false
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Padding = new MarginPadding(6),
Spacing = new Vector2(grid_spacing),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "How you played",
Font = OsuFont.Style.Heading2,
Margin = new MarginPadding { Vertical = 15 },
},
userStatistics = new FillFlowContainer<PanelUserStatistic>
{
Anchor = Anchor.TopLeft,
Origin = Anchor.TopLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(grid_spacing)
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Room Awards",
Font = OsuFont.Style.Heading2,
Margin = new MarginPadding { Vertical = 15 },
},
roomAwards = new FillFlowContainer<PanelRoomAward>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(grid_spacing)
}
}
}
}
}
],
null,
[
},
},
Empty(),
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions =
RowDimensions =
[
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.Absolute, grid_spacing),
new Dimension()
new Dimension(),
],
Content = new Drawable?[][]
Content = new Drawable[]?[]
{
[
new FillFlowContainer
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(grid_spacing),
Children = new Drawable[]
Spacing = new Vector2(16),
Children = new[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Breakdown",
Font = OsuFont.Default.With(size: 12)
Text = "Your final placement",
Font = OsuFont.Style.Heading2.With(size: 36),
},
userStatistics = new FillFlowContainer<PanelUserStatistic>
placementText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(grid_spacing)
Font = OsuFont.Style.Heading1.With(size: 72),
UseFullGlyphHeight = false
}
}
},
null,
PlayersDisplayArea = Empty().With(d =>
{
d.RelativeSizeAxes = Axes.Both;
})
]
}
}
],
null,
[
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(grid_spacing),
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Statistics",
Font = OsuFont.Default.With(size: 12)
},
roomStatistics = new FillFlowContainer<PanelRoomAward>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(grid_spacing)
}
}
],
null,
[
PlayersDisplayArea,
],
}
},
],
},
}
};
}
@@ -180,36 +197,62 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
if (state.Users[client.LocalUser!.UserID].Rounds.Count == 0)
{
placementText.Text = "-";
addStatistic("No rounds played");
placementText.Colour = OsuColour.Gray(1f);
return;
}
int overallPlacement = state.Users[client.LocalUser!.UserID].Placement;
placementText.Text = overallPlacement.Ordinalize(CultureInfo.CurrentCulture);
placementText.Colour = ColourForPlacement(overallPlacement);
int overallPoints = state.Users[client.LocalUser!.UserID].Points;
int bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.Min(r => r.Placement);
var accuracyPlacement = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
.OrderByDescending(t => t.avgAcc)
.Select((t, i) => (info: t, index: i))
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
addStatistic(overallPlacement, $"Overall position ({overallPoints} points)");
placementText.Text = $"#{state.Users[client.LocalUser!.UserID].Placement}";
addStatistic($"#{overallPlacement} overall ({overallPoints}pts)");
addStatistic($"#{bestPlacement} best placement");
addStatistic($"#{accuracyPlacement.index + 1} accuracy ({accuracyPlacement.info.avgAcc.FormatAccuracy()})");
var accuracyOrderedUsers = state.Users.Select(u => (user: u, avgAcc: u.Rounds.Select(r => r.Accuracy).DefaultIfEmpty(0).Average()))
.OrderByDescending(t => t.avgAcc)
.Select((t, i) => (info: t, index: i))
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
int accuracyPlacement = accuracyOrderedUsers.index + 1;
addStatistic(accuracyPlacement, $"Overall accuracy ({accuracyOrderedUsers.info.avgAcc.FormatAccuracy()})");
void addStatistic(string text)
var maxComboOrderedUsers = state.Users.Select(u => (user: u, maxCombo: u.Rounds.Max(r => r.MaxCombo)))
.OrderByDescending(t => t.maxCombo)
.Select((t, i) => (info: t, index: i))
.Single(t => t.info.user.UserId == client.LocalUser!.UserID);
int maxComboPlacement = maxComboOrderedUsers.index + 1;
addStatistic(maxComboPlacement, $"Best max combo ({maxComboOrderedUsers.info.maxCombo}x)");
var bestPlacement = state.Users[client.LocalUser!.UserID].Rounds.MinBy(r => r.Placement);
addStatistic(bestPlacement!.Placement, $"Best round placement (round {bestPlacement.Round})");
void addStatistic(int position, string text) => userStatistics.Add(new PanelUserStatistic(position, text));
}
public static ColourInfo ColourForPlacement(int overallPlacement)
{
// for top 3 placements use special colours.
// don't for the rest.
switch (overallPlacement)
{
userStatistics.Add(new PanelUserStatistic(text)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
});
case 1:
return OsuColour.ForRankingTier(RankingTier.Gold);
case 2:
return OsuColour.ForRankingTier(RankingTier.Silver);
case 3:
return OsuColour.ForRankingTier(RankingTier.Bronze);
default:
return OsuColour.ForRankingTier(RankingTier.Iron);
}
}
private void populateRoomStatistics(MatchmakingRoomState state)
{
roomStatistics.Clear();
roomAwards.Clear();
long maxScore = long.MinValue;
int maxScoreUserId = 0;
@@ -301,35 +344,22 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match.Results
}
}
// Highest score - highest score across all rounds.
addStatistic(maxScoreUserId, "Highest score");
addAward(maxScoreUserId, "Score champ", "Highest score in a single round");
// Most accurate - highest accuracy across all rounds.
addStatistic(maxAccuracyUserId, "Most accurate");
addAward(maxAccuracyUserId, "Most accurate", "Highest accuracy in a single round");
// Most combo - highest combo across all rounds.
addStatistic(maxComboUserId, "Most combo");
addAward(maxComboUserId, "Top combo", "Highest combo in a single round");
// Most bonus - most bonus score across all rounds.
if (maxBonusScoreUserId > 0)
addStatistic(maxBonusScoreUserId, "Most bonus");
addAward(maxBonusScoreUserId, "Biggest bonus", "Biggest bonus score across all rounds");
// Most clutch - smallest victory in any round.
if (smallestScoreDifferenceUserId > 0)
addStatistic(smallestScoreDifferenceUserId, "Most clutch");
addAward(smallestScoreDifferenceUserId, "Most clutch", "Smallest winning score difference in a single round");
// Best finish - largest victory in any round.
if (largestScoreDifferenceUserId > 0)
addStatistic(largestScoreDifferenceUserId, "Best finish");
addAward(largestScoreDifferenceUserId, "Best finish", "Largest score difference in a single round");
void addStatistic(int userId, string text)
{
roomStatistics.Add(new PanelRoomAward(text, userId)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
});
}
void addAward(int userId, string text, string description) => roomAwards.Add(new PanelRoomAward(text, description, userId));
}
protected override void Dispose(bool isDisposing)
@@ -36,9 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(10)
Padding = new MarginPadding(6)
{
Bottom = StageDisplay.HEIGHT,
Bottom = StageDisplay.HEIGHT + 6,
},
Children = new Drawable[]
{
@@ -312,11 +312,14 @@ namespace osu.Game.Screens.OnlinePlay.Matchmaking.Match
public override bool OnExiting(ScreenExitEvent e)
{
if (base.OnExiting(e))
return true;
if (exitConfirmed)
{
if (base.OnExiting(e))
{
exitConfirmed = false;
return true;
}
client.LeaveRoom().FireAndForget();
return false;
}
@@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
{
@@ -25,6 +26,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
private readonly AudioAdjustments clockAdjustmentsFromMods = new AudioAdjustments();
private readonly SpectatorPlayerClock spectatorPlayerClock;
// purposefully cached as empty - the multi spectator screen already has one leaderboard, on the left of all the player instances
[Cached(typeof(IGameplayLeaderboardProvider))]
private readonly EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider();
/// <summary>
/// Creates a new <see cref="MultiSpectatorPlayer"/>.
/// </summary>
@@ -106,6 +106,8 @@ namespace osu.Game.Screens.OnlinePlay
public override void OnEntering(ScreenTransitionEvent e)
{
base.OnEntering(e);
this.FadeIn();
waves.Show();
@@ -119,6 +121,8 @@ namespace osu.Game.Screens.OnlinePlay
public override void OnResuming(ScreenTransitionEvent e)
{
base.OnResuming(e);
this.FadeIn(250);
this.ScaleTo(1, 250, Easing.OutSine);
@@ -129,12 +133,12 @@ namespace osu.Game.Screens.OnlinePlay
// to work around this, do not proxy resume to screens that haven't loaded yet.
if ((screenStack.CurrentScreen as Drawable)?.IsLoaded == true)
screenStack.CurrentScreen.OnResuming(e);
base.OnResuming(e);
}
public override void OnSuspending(ScreenTransitionEvent e)
{
base.OnSuspending(e);
this.ScaleTo(1.1f, 250, Easing.InSine);
this.FadeOut(250);
@@ -152,18 +156,19 @@ namespace osu.Game.Screens.OnlinePlay
while (screenStack.CurrentScreen != null && screenStack.CurrentScreen is not LoungeSubScreen)
{
var subScreen = (Screen)screenStack.CurrentScreen;
if (subScreen.IsLoaded && subScreen.OnExiting(e))
return true;
subScreen.Exit();
// If it's still current after calling Exit(), it must have blocked OnExiting().
if (subScreen.IsCurrentScreen())
return true;
}
waves.Hide();
this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut();
base.OnExiting(e);
return false;
return base.OnExiting(e);
}
public override bool OnBackButton()
@@ -19,6 +19,7 @@ using osu.Game.Resources.Localisation.Web;
using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play.HUD;
using osuTK;
using CommonStrings = osu.Game.Localisation.CommonStrings;
namespace osu.Game.Screens.Play
{
@@ -165,7 +166,7 @@ namespace osu.Game.Screens.Play
},
new Drawable[]
{
new MetadataLineLabel("Mapper"),
new MetadataLineLabel(CommonStrings.Mapper),
new MetadataLineInfo(metadata.Author.Username)
}
}
+4 -2
View File
@@ -1,10 +1,12 @@
// 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.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Localisation;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Scoring;
using osuTK;
@@ -32,7 +34,7 @@ namespace osu.Game.Screens.Play.Break
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "current progress".ToUpperInvariant(),
Text = BreakInfoStrings.CurrentProgressTitle.ToUpper(),
Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 15),
},
new FillFlowContainer
@@ -46,7 +48,7 @@ namespace osu.Game.Screens.Play.Break
AccuracyDisplay = new PercentageBreakInfoLine(BeatmapsetsStrings.ShowScoreboardHeadersAccuracy),
// See https://github.com/ppy/osu/discussions/15185
// RankDisplay = new BreakInfoLine<int>("Rank"),
GradeDisplay = new BreakInfoLine<ScoreRank>("Grade"),
GradeDisplay = new BreakInfoLine<ScoreRank>(BreakInfoStrings.ShowInfoGrade),
},
}
},
@@ -19,7 +19,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
private readonly PlayerCheckbox beatmapHitsoundsToggle;
public AudioSettings()
: base("Audio Settings")
: base(PlayerSettingsOverlayStrings.AudioSettingsTitle)
{
Children = new Drawable[]
{
@@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
public partial class InputSettings : PlayerSettingsGroup
{
public InputSettings()
: base("Input Settings")
: base(PlayerSettingsOverlayStrings.InputSettingsTitle)
{
}
@@ -41,7 +41,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
private IconButton pausePlay = null!;
public PlaybackSettings()
: base("playback")
: base(PlayerSettingsOverlayStrings.PlaybackTitle)
{
}
@@ -138,7 +138,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
{
rateSlider = new PlayerSliderBar<double>
{
LabelText = "Playback speed",
LabelText = PlayerSettingsOverlayStrings.PlaybackSpeed,
Current = UserPlaybackRate,
},
multiplierText = new OsuSpriteText
@@ -2,13 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Overlays;
namespace osu.Game.Screens.Play.PlayerSettings
{
public partial class PlayerSettingsGroup : SettingsToolboxGroup
{
public PlayerSettingsGroup(string title)
public PlayerSettingsGroup(LocalisableString title)
: base(title)
{
}
@@ -18,7 +18,7 @@ namespace osu.Game.Screens.Play.PlayerSettings
private readonly PlayerCheckbox beatmapColorsToggle;
public VisualSettings()
: base("Visual Settings")
: base(PlayerSettingsOverlayStrings.VisualSettingsTitle)
{
Children = new Drawable[]
{
+7 -1
View File
@@ -6,6 +6,7 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Screens;
using osu.Game.Online.Spectator;
using osu.Game.Scoring;
using osu.Game.Screens.Select.Leaderboards;
using osu.Game.Users;
namespace osu.Game.Screens.Play
@@ -14,10 +15,13 @@ namespace osu.Game.Screens.Play
{
private readonly Score score;
[Cached(typeof(IGameplayLeaderboardProvider))]
private SoloGameplayLeaderboardProvider leaderboardProvider = new SoloGameplayLeaderboardProvider();
protected override UserActivity InitialActivity => new UserActivity.SpectatingUser(Score.ScoreInfo);
public SoloSpectatorPlayer(Score score)
: base(score, new PlayerConfiguration { AllowUserInteraction = false })
: base(score, new PlayerConfiguration { AllowUserInteraction = false, ShowLeaderboard = true })
{
this.score = score;
}
@@ -26,6 +30,8 @@ namespace osu.Game.Screens.Play
private void load()
{
SpectatorClient.OnUserBeganPlaying += userBeganPlaying;
AddInternal(leaderboardProvider);
}
public override bool OnExiting(ScreenExitEvent e)
-6
View File
@@ -13,17 +13,11 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select.Leaderboards;
namespace osu.Game.Screens.Play
{
public abstract partial class SpectatorPlayer : Player
{
// TODO: maybe consider giving this proper scores.
// `SoloGameplayLeaderboardProvider` doesn't immediately work because there's no guarantee that `LeaderboardManager` global state matches the currently spectated beatmap.
[Cached(typeof(IGameplayLeaderboardProvider))]
private readonly EmptyGameplayLeaderboardProvider leaderboardProvider = new EmptyGameplayLeaderboardProvider();
[Resolved]
protected SpectatorClient SpectatorClient { get; private set; } = null!;
+9 -6
View File
@@ -237,26 +237,29 @@ namespace osu.Game.Screens.Select
private void beatmapSetsChanged(object? beatmaps, NotifyCollectionChangedEventArgs changed)
{
IEnumerable<BeatmapSetInfo>? oldBeatmapSets = changed.OldItems?.Cast<BeatmapSetInfo>();
HashSet<Guid> oldBeatmapSetIDs = oldBeatmapSets?.Select(s => s.ID).ToHashSet() ?? [];
IEnumerable<BeatmapSetInfo>? newBeatmapSets = changed.NewItems?.Cast<BeatmapSetInfo>();
HashSet<Guid> newBeatmapSetIDs = newBeatmapSets?.Select(s => s.ID).ToHashSet() ?? [];
switch (changed.Action)
{
case NotifyCollectionChangedAction.Add:
HashSet<Guid> newBeatmapSetIDs = newBeatmapSets!.Select(s => s.ID).ToHashSet();
setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID));
setsRequiringUpdate.AddRange(newBeatmapSets!);
break;
case NotifyCollectionChangedAction.Remove:
IEnumerable<BeatmapSetInfo> oldBeatmapSets = changed.OldItems!.Cast<BeatmapSetInfo>();
HashSet<Guid> oldBeatmapSetIDs = oldBeatmapSets.Select(s => s.ID).ToHashSet();
setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID));
setsRequiringRemoval.AddRange(oldBeatmapSets);
setsRequiringRemoval.AddRange(oldBeatmapSets!);
break;
case NotifyCollectionChangedAction.Replace:
setsRequiringUpdate.RemoveWhere(s => oldBeatmapSetIDs.Contains(s.ID));
setsRequiringRemoval.AddRange(oldBeatmapSets!);
setsRequiringRemoval.RemoveWhere(s => newBeatmapSetIDs.Contains(s.ID));
setsRequiringUpdate.AddRange(newBeatmapSets!);
break;
@@ -77,10 +77,23 @@ namespace osu.Game.Screens.Select.Carousel
if (!match) return false;
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(BeatmapInfo.Metadata.Author.Username);
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) ||
criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
match &= !criteria.Title.HasFilter || criteria.Title.Matches(BeatmapInfo.Metadata.Title) ||
criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
if (criteria.Artist.HasFilter)
{
if (criteria.Artist.ExcludeTerm)
match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) && criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
else
match &= criteria.Artist.Matches(BeatmapInfo.Metadata.Artist) || criteria.Artist.Matches(BeatmapInfo.Metadata.ArtistUnicode);
}
if (criteria.Title.HasFilter)
{
if (criteria.Title.ExcludeTerm)
match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) && criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
else
match &= criteria.Title.Matches(BeatmapInfo.Metadata.Title) || criteria.Title.Matches(BeatmapInfo.Metadata.TitleUnicode);
}
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(BeatmapInfo.DifficultyName);
match &= !criteria.Source.HasFilter || criteria.Source.Matches(BeatmapInfo.Metadata.Source);
@@ -88,12 +101,24 @@ namespace osu.Game.Screens.Select.Carousel
{
foreach (var tagFilter in criteria.UserTags)
{
bool anyTagMatched = false;
if (tagFilter.ExcludeTerm)
{
// if `ExcludeTerm` is true, `Matches()` will return true if a user tag *doesn't match* the excluded term.
// thus, every user tag must pass this filter.
foreach (string tag in BeatmapInfo.Metadata.UserTags)
match &= tagFilter.Matches(tag);
}
else
{
// if `ExcludeTerm` is false, `Matches()` will return true if a user tag *matches* the expected term.
// the expected behaviour is that a beatmap should be displayed if at least one of the user tags passes the filter.
bool anyTagMatched = false;
foreach (string tag in BeatmapInfo.Metadata.UserTags)
anyTagMatched |= tagFilter.Matches(tag);
foreach (string tag in BeatmapInfo.Metadata.UserTags)
anyTagMatched |= tagFilter.Matches(tag);
match &= anyTagMatched;
match &= anyTagMatched;
}
}
}
+1 -1
View File
@@ -205,7 +205,7 @@ namespace osu.Game.Screens.Select
// search term is guaranteed to be non-empty, so if the string we're comparing is empty, it's not matching
if (string.IsNullOrEmpty(value))
return false;
return ExcludeTerm;
bool result;
+16 -3
View File
@@ -371,7 +371,7 @@ namespace osu.Game.Screens.SelectV2
if (userCollapsedGroup)
{
if (grouping.BeatmapSetsGroupedTogether && CurrentGroupedBeatmap != null)
if (grouping.BeatmapSetsGroupedTogether && CurrentGroupedBeatmap != null && CheckModelEquality(group, CurrentGroupedBeatmap.Group))
setExpandedSet(new GroupedBeatmapSet(CurrentGroupedBeatmap.Group, CurrentGroupedBeatmap.Beatmap.BeatmapSet!));
userCollapsedGroup = false;
}
@@ -840,9 +840,11 @@ namespace osu.Game.Screens.SelectV2
private readonly DrawablePool<PanelGroup> groupPanelPool = new DrawablePool<PanelGroup>(100);
private readonly DrawablePool<PanelGroupStarDifficulty> starsGroupPanelPool = new DrawablePool<PanelGroupStarDifficulty>(11);
private readonly DrawablePool<PanelGroupRankDisplay> ranksGroupPanelPool = new DrawablePool<PanelGroupRankDisplay>(9);
private readonly DrawablePool<PanelGroupRankedStatus> statusGroupPanelPool = new DrawablePool<PanelGroupRankedStatus>(8);
private void setupPools()
{
AddInternal(statusGroupPanelPool);
AddInternal(ranksGroupPanelPool);
AddInternal(starsGroupPanelPool);
AddInternal(groupPanelPool);
@@ -880,6 +882,9 @@ namespace osu.Game.Screens.SelectV2
if (x is RankDisplayGroupDefinition rankX && y is RankDisplayGroupDefinition rankY)
return rankX.Equals(rankY);
if (x is RankedStatusGroupDefinition statusX && y is RankedStatusGroupDefinition statusY)
return statusX.Equals(statusY);
return base.CheckModelEquality(x, y);
}
@@ -887,6 +892,9 @@ namespace osu.Game.Screens.SelectV2
{
switch (item.Model)
{
case RankedStatusGroupDefinition:
return statusGroupPanelPool.Get();
case StarDifficultyGroupDefinition:
return starsGroupPanelPool.Get();
@@ -1012,13 +1020,13 @@ namespace osu.Game.Screens.SelectV2
private bool nextRandomSet()
{
ICollection<GroupedBeatmapSet> visibleGroupedSets = ExpandedGroup != null
ICollection<GroupedBeatmapSet> visibleGroupedSets = ExpandedGroup != null && grouping.GroupItems.TryGetValue(ExpandedGroup, out var groupItems)
// In the case of grouping, users expect random to only operate on the expanded group.
// This is going to incur some overhead as we don't have a group-beatmapset mapping currently.
//
// If this becomes an issue, we could either store a mapping, or run the random algorithm many times
// using the `SetItems` method until we get a group HIT.
? grouping.GroupItems[ExpandedGroup].Select(i => i.Model).OfType<GroupedBeatmapSet>().ToArray()
? groupItems.Select(i => i.Model).OfType<GroupedBeatmapSet>().ToArray()
// This is the fastest way to retrieve sets for randomisation.
: grouping.SetItems.Keys;
@@ -1154,6 +1162,11 @@ namespace osu.Game.Screens.SelectV2
/// </summary>
public record RankDisplayGroupDefinition(ScoreRank Rank) : GroupDefinition(-(int)Rank, Rank.GetLocalisableDescription());
/// <summary>
/// Defines a grouping header for a set of carousel items grouped by ranked status.
/// </summary>
public record RankedStatusGroupDefinition(int Order, BeatmapOnlineStatus Status) : GroupDefinition(Order, Status.GetLocalisableDescription());
/// <summary>
/// Used to represent a portion of a <see cref="BeatmapSetInfo"/> under a <see cref="GroupDefinition"/>.
/// The purpose of this model is to support splitting beatmap sets apart when the active grouping mode demands it.
@@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Beatmaps;
using osu.Game.Collections;
@@ -193,7 +192,7 @@ namespace osu.Game.Screens.SelectV2
{
var date = b.LastPlayed;
if (date == null || date == DateTimeOffset.MinValue)
if (date == null)
return new GroupDefinition(int.MaxValue, "Never").Yield();
return defineGroupByDate(date.Value);
@@ -315,28 +314,28 @@ namespace osu.Game.Screens.SelectV2
{
case BeatmapOnlineStatus.Ranked:
case BeatmapOnlineStatus.Approved:
return new GroupDefinition(0, BeatmapOnlineStatus.Ranked.GetDescription()).Yield();
return new RankedStatusGroupDefinition(0, BeatmapOnlineStatus.Ranked).Yield();
case BeatmapOnlineStatus.Qualified:
return new GroupDefinition(1, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(1, status).Yield();
case BeatmapOnlineStatus.WIP:
return new GroupDefinition(2, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(2, status).Yield();
case BeatmapOnlineStatus.Pending:
return new GroupDefinition(3, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(3, status).Yield();
case BeatmapOnlineStatus.Graveyard:
return new GroupDefinition(4, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(4, status).Yield();
case BeatmapOnlineStatus.LocallyModified:
return new GroupDefinition(5, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(5, status).Yield();
case BeatmapOnlineStatus.None:
return new GroupDefinition(6, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(6, status).Yield();
case BeatmapOnlineStatus.Loved:
return new GroupDefinition(7, status.GetDescription()).Yield();
return new RankedStatusGroupDefinition(7, status).Yield();
default:
throw new ArgumentOutOfRangeException(nameof(status), status, null);
@@ -96,10 +96,23 @@ namespace osu.Game.Screens.SelectV2
if (!match) return false;
match &= !criteria.Creator.HasFilter || criteria.Creator.Matches(beatmap.Metadata.Author.Username);
match &= !criteria.Artist.HasFilter || criteria.Artist.Matches(beatmap.Metadata.Artist) ||
criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode);
match &= !criteria.Title.HasFilter || criteria.Title.Matches(beatmap.Metadata.Title) ||
criteria.Title.Matches(beatmap.Metadata.TitleUnicode);
if (criteria.Artist.HasFilter)
{
if (criteria.Artist.ExcludeTerm)
match &= criteria.Artist.Matches(beatmap.Metadata.Artist) && criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode);
else
match &= criteria.Artist.Matches(beatmap.Metadata.Artist) || criteria.Artist.Matches(beatmap.Metadata.ArtistUnicode);
}
if (criteria.Title.HasFilter)
{
if (criteria.Title.ExcludeTerm)
match &= criteria.Title.Matches(beatmap.Metadata.Title) && criteria.Title.Matches(beatmap.Metadata.TitleUnicode);
else
match &= criteria.Title.Matches(beatmap.Metadata.Title) || criteria.Title.Matches(beatmap.Metadata.TitleUnicode);
}
match &= !criteria.DifficultyName.HasFilter || criteria.DifficultyName.Matches(beatmap.DifficultyName);
match &= !criteria.Source.HasFilter || criteria.Source.Matches(beatmap.Metadata.Source);
@@ -107,12 +120,24 @@ namespace osu.Game.Screens.SelectV2
{
foreach (var tagFilter in criteria.UserTags)
{
bool anyTagMatched = false;
if (tagFilter.ExcludeTerm)
{
// if `ExcludeTerm` is true, `Matches()` will return true if a user tag *doesn't match* the excluded term.
// thus, every user tag must pass this filter.
foreach (string tag in beatmap.Metadata.UserTags)
match &= tagFilter.Matches(tag);
}
else
{
// if `ExcludeTerm` is false, `Matches()` will return true if a user tag *matches* the expected term.
// the expected behaviour is that a beatmap should be displayed if at least one of the user tags passes the filter.
bool anyTagMatched = false;
foreach (string tag in beatmap.Metadata.UserTags)
anyTagMatched |= tagFilter.Matches(tag);
foreach (string tag in beatmap.Metadata.UserTags)
anyTagMatched |= tagFilter.Matches(tag);
match &= anyTagMatched;
match &= anyTagMatched;
}
}
}
@@ -3,10 +3,12 @@
using System;
using System.Linq;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
@@ -402,24 +404,46 @@ namespace osu.Game.Screens.SelectV2
updateSubWedgeVisibility();
}
private CancellationTokenSource? userTagsCancellationSource;
private void updateUserTags()
{
string[] tags = realm.Run(r =>
userTagsCancellationSource?.Cancel();
userTagsCancellationSource = new CancellationTokenSource();
var token = userTagsCancellationSource.Token;
realm.RunAsync(r =>
{
// need to refetch because `beatmap.Value.BeatmapInfo` is not going to have the latest tags
r.Refresh();
var refetchedBeatmap = r.Find<BeatmapInfo>(beatmap.Value.BeatmapInfo.ID);
return refetchedBeatmap?.Metadata.UserTags.ToArray() ?? [];
});
if (tags.Length == 0)
}, token).ContinueWith(t =>
{
userTags.FadeOut(transition_duration, Easing.OutQuint);
return;
}
string[] tags = t.GetResultSafely();
userTags.FadeIn(transition_duration, Easing.OutQuint);
userTags.Tags = (tags, t => songSelect?.Search($@"tag=""{t}""!"));
Schedule(() =>
{
if (token.IsCancellationRequested)
return;
if (tags.Length == 0)
{
userTags.FadeOut(transition_duration, Easing.OutQuint);
return;
}
userTags.FadeIn(transition_duration, Easing.OutQuint);
userTags.Tags = (tags, tag => songSelect?.Search($@"tag=""{tag}""!"));
});
}, token);
}
protected override void Dispose(bool isDisposing)
{
userTagsCancellationSource?.Cancel();
userTagsCancellationSource = null;
base.Dispose(isDisposing);
}
}
}

Some files were not shown because too many files have changed in this diff Show More