mirror of
https://github.com/ppy/osu.git
synced 2026-06-02 23:41:00 +08:00
Compare commits
74 Commits
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]
|
||||
|
||||
+3
-4
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
+142
-38
@@ -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);
|
||||
}
|
||||
|
||||
+23
-137
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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[]
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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!;
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user