1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-17 21:13:01 +08:00

Compare commits

...

220 Commits

193 changed files with 3347 additions and 1425 deletions
+13 -3
View File
@@ -82,8 +82,18 @@ jobs:
run: dotnet build -c Debug -warnaserror osu.Desktop.slnf
- name: Test
run: dotnet test $pwd/**/*.Tests/bin/Debug/*/*.Tests.dll --logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx" -- NUnit.ConsoleOut=0
shell: pwsh
run: >
dotnet test
osu.Game.Tests/bin/Debug/**/osu.Game.Tests.dll
osu.Game.Rulesets.Osu.Tests/bin/Debug/**/osu.Game.Rulesets.Osu.Tests.dll
osu.Game.Rulesets.Taiko.Tests/bin/Debug/**/osu.Game.Rulesets.Taiko.Tests.dll
osu.Game.Rulesets.Catch.Tests/bin/Debug/**/osu.Game.Rulesets.Catch.Tests.dll
osu.Game.Rulesets.Mania.Tests/bin/Debug/**/osu.Game.Rulesets.Mania.Tests.dll
osu.Game.Tournament.Tests/bin/Debug/**/osu.Game.Tournament.Tests.dll
Templates/**/*.Tests/bin/Debug/**/*.Tests.dll
--logger "trx;LogFileName=TestResults-${{matrix.os.prettyname}}-${{matrix.threadingMode}}.trx"
--
NUnit.ConsoleOut=0
# Attempt to upload results even if test fails.
# https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions#always
@@ -136,4 +146,4 @@ jobs:
run: dotnet workload install ios --from-rollback-file https://raw.githubusercontent.com/ppy/osu-framework/refs/heads/master/workloads.json
- name: Build
run: dotnet build -c Debug osu.iOS
run: dotnet build -c Debug osu.iOS.slnf
+1 -1
View File
@@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.313.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2025.321.0" />
</ItemGroup>
<PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged.
@@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
LegacyConvertedY = yPositionData?.Y ?? CatchHitObject.DEFAULT_LEGACY_CONVERT_Y,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(obj.StartTime).SliderVelocity : 1,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1
}.Yield();
@@ -1,8 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Beatmaps;
@@ -35,21 +36,21 @@ namespace osu.Game.Rulesets.Catch.Mods
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
public BindableBool HardRockOffsets { get; } = new BindableBool();
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
if (!CircleSize.IsDefault)
yield return ("Circle size", $"{CircleSize.Value:N1}");
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate,
spicyPatterns,
}.Where(s => !string.IsNullOrEmpty(s)));
foreach (var setting in base.SettingDescription)
yield return setting;
if (!ApproachRate.IsDefault)
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
if (!HardRockOffsets.IsDefault)
yield return ("Spicy patterns", "On");
}
}
@@ -759,9 +759,9 @@ namespace osu.Game.Rulesets.Osu.Tests
BeatmapInfo =
{
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
},
ControlPointInfo = cpi
ControlPointInfo = cpi,
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
});
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
});
@@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
ComboOffset = comboData?.ComboOffset ?? 0,
// prior to v8, speed multipliers don't adjust for how many ticks are generated over the same distance.
// this results in more (or less) ticks being generated in <v8 maps for the same time duration.
TickDistanceMultiplier = beatmap.BeatmapInfo.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
TickDistanceMultiplier = beatmap.BeatmapVersion < 8 ? 1f / ((LegacyControlPointInfo)beatmap.ControlPointInfo).DifficultyPointAt(original.StartTime).SliderVelocity : 1,
GenerateTicks = generateTicksData?.GenerateTicks ?? true,
SliderVelocityMultiplier = sliderVelocityData?.SliderVelocityMultiplier ?? 1,
}.Yield();
@@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
foreach (var h in hitObjects)
h.StackHeight = 0;
if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
if (beatmap.BeatmapVersion >= 6)
applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
else
applyStackingOld(beatmap, hitObjects);
@@ -1,7 +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.Linq;
using System.Collections.Generic;
using osu.Framework.Bindables;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
@@ -36,19 +36,18 @@ namespace osu.Game.Rulesets.Osu.Mods
ReadCurrentFromDifficulty = diff => diff.ApproachRate,
};
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
if (!CircleSize.IsDefault)
yield return ("Circle size", $"{CircleSize.Value:N1}");
return string.Join(", ", new[]
{
circleSize,
base.SettingDescription,
approachRate
}.Where(s => !string.IsNullOrEmpty(s)));
foreach (var setting in base.SettingDescription)
yield return setting;
if (!ApproachRate.IsDefault)
yield return ("Approach rate", $"{ApproachRate.Value:N1}");
}
}
@@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
double osuVelocity = taikoVelocity * (1000f / beatLength);
// osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8
if (beatmap.BeatmapInfo.BeatmapVersion >= 8)
if (beatmap.BeatmapVersion >= 8)
beatLength = timingPoint.BeatLength;
// If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat
@@ -1,7 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
@@ -19,17 +20,15 @@ namespace osu.Game.Rulesets.Taiko.Mods
ReadCurrentFromDifficulty = _ => 1,
};
public override string SettingDescription
public override IEnumerable<(LocalisableString setting, LocalisableString value)> SettingDescription
{
get
{
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
foreach (var setting in base.SettingDescription)
yield return setting;
return string.Join(", ", new[]
{
base.SettingDescription,
scrollSpeed
}.Where(s => !string.IsNullOrEmpty(s)));
if (!ScrollSpeed.IsDefault)
yield return ("Scroll speed", $"x{ScrollSpeed.Value:N2}");
}
}
@@ -60,11 +60,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
{
const double animation_time = 120;
(sprite as IFramedAnimation)?.GotoFrame(0);
var animation = sprite as IFramedAnimation;
animation?.GotoFrame(0);
(strongSprite as IFramedAnimation)?.GotoFrame(0);
this.FadeInFromZero(animation_time).Then().FadeOut(animation_time * 1.5);
// legacy judgements don't play any transforms if they are an animation.
if (animation?.FrameCount > 1)
return;
this.ScaleTo(0.6f)
.Then().ScaleTo(1.1f, animation_time * 0.8)
.Then().ScaleTo(0.9f, animation_time * 0.4)
+10 -1
View File
@@ -1,4 +1,5 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0-ios</TargetFramework>
@@ -6,11 +7,19 @@
<RootNamespace>osu.Game.Tests</RootNamespace>
<AssemblyName>osu.Game.Tests.iOS</AssemblyName>
</PropertyGroup>
<Import Project="..\osu.iOS.props" />
<PropertyGroup>
<NoWarn>$(NoWarn);CA2007</NoWarn>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\osu.Game.Tests\**\*.cs" Exclude="**\obj\**">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
</Compile>
<!-- TargetPath is relative to RootNamespace,
and DllResourceStore is relative to AssemblyName. -->
<EmbeddedResource Include="..\osu.Game.Tests\**\Resources\**\*">
<Link>%(RecursiveDir)%(Filename)%(Extension)</Link>
<TargetPath>iOS\%(RecursiveDir)%(Filename)%(Extension)</TargetPath>
</EmbeddedResource>
</ItemGroup>
<ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game\osu.Game.csproj" />
@@ -42,9 +42,9 @@ namespace osu.Game.Tests.Beatmaps.Formats
var decoder = Decoder.GetDecoder<Beatmap>(stream);
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(6, working.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.Beatmap.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
Assert.AreEqual(6, working.Beatmap.BeatmapVersion);
Assert.That(working.Beatmap.BeatmapInfo.Ruleset.Name, Is.Not.EqualTo("null placeholder ruleset"));
Assert.AreEqual(6, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
}
}
@@ -59,9 +59,8 @@ namespace osu.Game.Tests.Beatmaps.Formats
((LegacyBeatmapDecoder)decoder).ApplyOffsets = applyOffsets;
var working = new TestWorkingBeatmap(decoder.Decode(stream));
Assert.AreEqual(4, working.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.Beatmap.BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapInfo.BeatmapVersion);
Assert.AreEqual(4, working.Beatmap.BeatmapVersion);
Assert.AreEqual(4, working.GetPlayableBeatmap(new OsuRuleset().RulesetInfo, Array.Empty<Mod>()).BeatmapVersion);
Assert.AreEqual(-1, working.BeatmapInfo.Metadata.PreviewTime);
}
@@ -155,10 +155,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset)
{
BeatmapInfo =
{
BeatmapVersion = beatmapVersion
}
BeatmapVersion = beatmapVersion
};
var score = new Score
@@ -633,14 +630,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
MD5Hash = md5Hash,
Ruleset = new OsuRuleset().RulesetInfo,
Difficulty = new BeatmapDifficulty(),
BeatmapVersion = beatmapVersion,
},
// needs to have at least one objects so that `StandardisedScoreMigrationTools` doesn't die
// needs to have at least one object so that `StandardisedScoreMigrationTools` doesn't die
// when trying to recompute total score.
HitObjects =
{
new HitCircle()
}
},
BeatmapVersion = beatmapVersion,
});
}
}
@@ -62,12 +62,11 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundDataStoreProcessor());
});
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("wait for difficulties repopulated", () =>
AddAssert("Difficulties repopulated", () =>
{
return Realm.Run(r =>
{
@@ -101,13 +100,10 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () =>
{
Add(new TestBackgroundDataStoreProcessor());
});
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddWaitStep("wait some", 500);
AddAssert("Difficulty still not populated", () =>
{
return Realm.Run(r =>
@@ -118,8 +114,9 @@ namespace osu.Game.Tests.Database
});
AddStep("Set not playing", () => isPlaying.Value = LocalUserPlayingState.NotPlaying);
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("wait for difficulties repopulated", () =>
AddAssert("Difficulties repopulated", () =>
{
return Realm.Run(r =>
{
@@ -151,9 +148,11 @@ namespace osu.Game.Tests.Database
});
});
AddStep("Run background processor", () => Add(new TestBackgroundDataStoreProcessor()));
TestBackgroundDataStoreProcessor processor = null!;
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score version upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(LegacyScoreEncoder.LATEST_VERSION));
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
}
@@ -183,7 +182,7 @@ namespace osu.Game.Tests.Database
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
AddUntilStep("Wait for completion", () => processor.Completed);
AddUntilStep("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.True);
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(scoreVersion));
}
@@ -0,0 +1,185 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Multiplayer;
using osu.Game.Utils;
namespace osu.Game.Tests.Online
{
[HeadlessTest]
public partial class TestSceneMultiplayerBeatmapAvailabilityTracker : MultiplayerTestScene
{
private BeatmapManager beatmapManager = null!;
private BeatmapInfo availableBeatmap = null!;
private BeatmapInfo unavailableBeatmap = null!;
private MultiplayerBeatmapAvailabilityTracker tracker = null!;
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, Realm, null, Audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
var importedSet = beatmapManager.GetAllUsableBeatmapSets().First();
availableBeatmap = importedSet.Beatmaps[0];
unavailableBeatmap = importedSet.Beatmaps[1];
Realm.Write(r => r.Remove(r.Find<BeatmapInfo>(unavailableBeatmap.ID)!));
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("setup tracker", () =>
{
DummyAPIAccess api = (DummyAPIAccess)API;
Func<APIRequest, bool>? defaultRequestHandler = api.HandleRequest;
api.HandleRequest = req =>
{
switch (req)
{
case GetBeatmapsRequest beatmapsReq:
var availableApiBeatmap = CreateAPIBeatmap();
availableApiBeatmap.OnlineID = availableBeatmap.OnlineID;
availableApiBeatmap.OnlineBeatmapSetID = availableBeatmap.BeatmapSet!.OnlineID;
availableApiBeatmap.Checksum = availableBeatmap.MD5Hash;
availableApiBeatmap.BeatmapSet!.OnlineID = availableBeatmap.BeatmapSet!.OnlineID;
var unavailableApiBeatmap = CreateAPIBeatmap();
unavailableApiBeatmap.OnlineID = unavailableBeatmap.OnlineID;
unavailableApiBeatmap.OnlineBeatmapSetID = unavailableBeatmap.BeatmapSet!.OnlineID;
unavailableApiBeatmap.Checksum = unavailableBeatmap.MD5Hash;
unavailableApiBeatmap.BeatmapSet!.OnlineID = unavailableBeatmap.BeatmapSet!.OnlineID;
beatmapsReq.TriggerSuccess(new GetBeatmapsResponse
{
Beatmaps = new List<APIBeatmap>
{
availableApiBeatmap,
unavailableApiBeatmap
}
});
return true;
default:
return defaultRequestHandler?.Invoke(req) ?? false;
}
};
Child = tracker = new MultiplayerBeatmapAvailabilityTracker();
});
}
[Test]
public void TestEnterRoomWithNotDownloadedBeatmap()
{
AddStep("join room", () =>
{
var room = CreateDefaultRoom();
room.Playlist = [new PlaylistItem(unavailableBeatmap)];
JoinRoom(room);
});
WaitForJoined();
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
}
[Test]
public void TestEnterRoomWithLocallyAvailableBeatmap()
{
AddStep("join room", () =>
{
var room = CreateDefaultRoom();
room.Playlist = [new PlaylistItem(availableBeatmap)];
JoinRoom(room);
});
WaitForJoined();
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
}
[Test]
public void TestAvailabilityUpdatesOnItemEdit()
{
AddStep("join room", () =>
{
var room = CreateDefaultRoom();
room.Playlist = [new PlaylistItem(availableBeatmap)];
JoinRoom(room);
});
WaitForJoined();
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
AddStep("change item to not downloaded beatmap", () =>
{
PlaylistItem newItem = new PlaylistItem(MultiplayerClient.ClientRoom!.CurrentPlaylistItem).With(beatmap: new Optional<IBeatmapInfo>(unavailableBeatmap));
MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(newItem)).WaitSafely();
});
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
AddStep("change item to downloaded beatmap", () =>
{
PlaylistItem newItem = new PlaylistItem(MultiplayerClient.ClientRoom!.CurrentPlaylistItem).With(beatmap: new Optional<IBeatmapInfo>(availableBeatmap));
MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem(newItem)).WaitSafely();
});
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
}
[Test]
public void TestAvailabilityUpdatesOnSettingsChange()
{
AddStep("join room", () =>
{
var room = CreateDefaultRoom();
room.Playlist = [new PlaylistItem(availableBeatmap), new PlaylistItem(unavailableBeatmap)];
JoinRoom(room);
});
WaitForJoined();
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
AddStep("change settings to not downloaded beatmap", () => MultiplayerClient.ChangeServerRoomSettings(new MultiplayerRoomSettings(MultiplayerClient.ClientAPIRoom!)
{
PlaylistItemId = MultiplayerClient.ServerRoom!.Playlist[1].ID
}).WaitSafely());
AddUntilStep("beatmap is not available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.NotDownloaded));
AddStep("change settings to downloaded beatmap", () => MultiplayerClient.ChangeServerRoomSettings(new MultiplayerRoomSettings(MultiplayerClient.ClientAPIRoom!)
{
PlaylistItemId = MultiplayerClient.ServerRoom!.Playlist[0].ID
}).WaitSafely());
AddUntilStep("beatmap is available", () => tracker.Availability.Value.State, () => Is.EqualTo(DownloadState.LocallyAvailable));
}
}
}
@@ -1,18 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using JetBrains.Annotations;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
@@ -27,31 +23,29 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual;
namespace osu.Game.Tests.Online
{
[HeadlessTest]
public partial class TestSceneOnlinePlayBeatmapAvailabilityTracker : OsuTestScene
public partial class TestScenePlaylistsBeatmapAvailabilityTracker : OsuTestScene
{
private RulesetStore rulesets;
private TestBeatmapManager beatmaps;
private TestBeatmapModelDownloader beatmapDownloader;
private TestBeatmapManager beatmaps = null!;
private TestBeatmapModelDownloader beatmapDownloader = null!;
private string testBeatmapFile;
private BeatmapInfo testBeatmapInfo;
private BeatmapSetInfo testBeatmapSet;
private string testBeatmapFile = null!;
private BeatmapInfo testBeatmapInfo = null!;
private BeatmapSetInfo testBeatmapSet = null!;
private readonly Bindable<PlaylistItem> selectedItem = new Bindable<PlaylistItem>();
private OnlinePlayBeatmapAvailabilityTracker availabilityTracker;
private OnlinePlayBeatmapAvailabilityTracker availabilityTracker = null!;
[BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host)
{
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, rulesets, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs<BeatmapManager>(beatmaps = new TestBeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestBeatmapModelDownloader(beatmaps, API));
}
@@ -82,16 +76,11 @@ namespace osu.Game.Tests.Online
testBeatmapFile = TestResources.GetQuickTestBeatmapForImport();
testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile);
testBeatmapSet = testBeatmapInfo.BeatmapSet;
testBeatmapSet = testBeatmapInfo.BeatmapSet!;
Realm.Write(r => r.RemoveAll<BeatmapSetInfo>());
Realm.Write(r => r.RemoveAll<BeatmapInfo>());
selectedItem.Value = new PlaylistItem(testBeatmapInfo)
{
RulesetID = testBeatmapInfo.Ruleset.OnlineID,
};
recreateChildren();
});
@@ -108,9 +97,15 @@ namespace osu.Game.Tests.Online
Children = new Drawable[]
{
beatmapLookupCache,
availabilityTracker = new OnlinePlayBeatmapAvailabilityTracker
availabilityTracker = new PlaylistsBeatmapAvailabilityTracker
{
SelectedItem = { BindTarget = selectedItem, }
PlaylistItem =
{
Value = new PlaylistItem(testBeatmapInfo)
{
RulesetID = testBeatmapInfo.Ruleset.OnlineID,
},
}
}
}
};
@@ -125,10 +120,10 @@ namespace osu.Game.Tests.Online
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f));
AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.SetProgress(0.4f));
AddStep("set progress 40%", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)!).SetProgress(0.4f));
addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f));
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet))!.TriggerSuccess(testBeatmapFile));
AddStep("finish download", () => ((TestDownloadRequest)beatmapDownloader.GetExistingDownload(testBeatmapSet)!).TriggerSuccess(testBeatmapFile));
addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing);
AddStep("allow importing", () => beatmaps.AllowImport.Set());
@@ -203,10 +198,10 @@ namespace osu.Game.Tests.Online
{
public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim();
public Live<BeatmapSetInfo> CurrentImport { get; private set; }
public Live<BeatmapSetInfo>? CurrentImport { get; private set; }
public TestBeatmapManager(Storage storage, RealmAccess realm, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, IResourceStore<byte[]> resources,
GameHost host = null, WorkingBeatmap defaultBeatmap = null)
public TestBeatmapManager(Storage storage, RealmAccess realm, IAPIProvider api, AudioManager audioManager, IResourceStore<byte[]> resources,
GameHost? host = null, WorkingBeatmap? defaultBeatmap = null)
: base(storage, realm, api, audioManager, resources, host, defaultBeatmap)
{
}
@@ -226,12 +221,13 @@ namespace osu.Game.Tests.Online
this.testBeatmapManager = testBeatmapManager;
}
public override Live<BeatmapSetInfo> ImportModel(BeatmapSetInfo item, ArchiveReader archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default)
public override Live<BeatmapSetInfo>? ImportModel(BeatmapSetInfo item, ArchiveReader? archive = null, ImportParameters parameters = default,
CancellationToken cancellationToken = default)
{
if (!testBeatmapManager.AllowImport.Wait(TimeSpan.FromSeconds(10), cancellationToken))
throw new TimeoutException("Timeout waiting for import to be allowed.");
return (testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken));
return testBeatmapManager.CurrentImport = base.ImportModel(item, archive, parameters, cancellationToken);
}
}
}
@@ -91,6 +91,6 @@ namespace osu.Game.Tests.Visual.Beatmaps
}
private void assertCorrectIcon(bool favourited) => AddAssert("icon correct",
() => this.ChildrenOfType<SpriteIcon>().Single().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
() => this.ChildrenOfType<SpriteIcon>().First().Icon.Equals(favourited ? FontAwesome.Solid.Heart : FontAwesome.Regular.Heart));
}
}
@@ -1,12 +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.
#nullable disable
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
@@ -15,16 +13,18 @@ namespace osu.Game.Tests.Visual.Beatmaps
{
public partial class TestSceneDifficultySpectrumDisplay : OsuTestScene
{
private DifficultySpectrumDisplay display;
private DifficultySpectrumDisplay display = null!;
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
[SetUpSteps]
public void SetUpSteps()
{
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay
{
RulesetID = difficulty.rulesetId,
StarRating = difficulty.stars
}).ToArray()
};
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3)
});
}
[Test]
public void TestSingleRuleset()
@@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 0, stars: 3.2),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
@@ -47,7 +47,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 1, stars: 4.3),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
@@ -61,52 +61,30 @@ namespace osu.Game.Tests.Visual.Beatmaps
(rulesetId: 0, stars: 5.6),
(rulesetId: 15, stars: 7.8));
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestMaximumUncollapsed()
{
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 12).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestMinimumCollapsed()
{
var beatmapSet = createBeatmapSetWith(Enumerable.Range(0, 13).Select(i => (rulesetId: i % 4, stars: 2.5 + i * 0.25)).ToArray());
createDisplay(beatmapSet);
AddStep("set beatmap to display", () => display.BeatmapSet = beatmapSet);
}
[Test]
public void TestAdjustableDotSize()
private static APIBeatmapSet createBeatmapSetWith(params (int rulesetId, double stars)[] difficulties) => new APIBeatmapSet
{
var beatmapSet = createBeatmapSetWith(
(rulesetId: 0, stars: 2.0),
(rulesetId: 3, stars: 2.3),
(rulesetId: 0, stars: 3.2),
(rulesetId: 1, stars: 4.3),
(rulesetId: 0, stars: 5.6));
createDisplay(beatmapSet);
AddStep("change dot dimensions", () =>
Beatmaps = difficulties.Select(difficulty => new APIBeatmap
{
display.DotSize = new Vector2(8, 12);
display.DotSpacing = 2;
});
AddStep("change dot dimensions back", () =>
{
display.DotSize = new Vector2(4, 8);
display.DotSpacing = 1;
});
}
private void createDisplay(IBeatmapSetInfo beatmapSetInfo) => AddStep("create spectrum display", () => Child = display = new DifficultySpectrumDisplay(beatmapSetInfo)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(3)
});
RulesetID = difficulty.rulesetId,
StarRating = difficulty.stars
}).ToArray()
};
}
}
@@ -43,7 +43,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -66,7 +65,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -99,7 +97,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -114,7 +111,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
@@ -128,7 +125,6 @@ namespace osu.Game.Tests.Visual.DailyChallenge
{
var room = new Room
{
RoomID = 1234,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -143,7 +139,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
};
AddStep("add room", () => API.Perform(new CreateRoomRequest(room)));
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = 1234 });
AddStep("set daily challenge info", () => metadataClient.DailyChallengeInfo.Value = new DailyChallengeInfo { RoomID = room.RoomID!.Value });
Screens.OnlinePlay.DailyChallenge.DailyChallenge screen = null!;
AddStep("push screen", () => LoadScreen(screen = new Screens.OnlinePlay.DailyChallenge.DailyChallenge(room)));
@@ -44,17 +44,17 @@ namespace osu.Game.Tests.Visual.DailyChallenge
[Test]
public void TestDailyChallenge()
{
startChallenge(1234);
startChallenge();
AddStep("push screen", () => LoadScreen(new DailyChallengeIntro(room)));
}
[Test]
public void TestPlayIntroOnceFlag()
{
startChallenge(1234);
startChallenge();
AddStep("set intro played flag", () => Dependencies.Get<SessionStatics>().SetValue(Static.DailyChallengeIntroPlayed, true));
startChallenge(1235);
startChallenge();
AddAssert("intro played flag reset", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.False);
@@ -62,13 +62,12 @@ namespace osu.Game.Tests.Visual.DailyChallenge
AddUntilStep("intro played flag set", () => Dependencies.Get<SessionStatics>().Get<bool>(Static.DailyChallengeIntroPlayed), () => Is.True);
}
private void startChallenge(int roomId)
private void startChallenge()
{
AddStep("add room", () =>
{
API.Perform(new CreateRoomRequest(room = new Room
{
RoomID = roomId,
Name = "Daily Challenge: June 4, 2024",
Playlist =
[
@@ -83,7 +82,7 @@ namespace osu.Game.Tests.Visual.DailyChallenge
Category = RoomCategory.DailyChallenge
}));
});
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = roomId }));
AddStep("signal client", () => metadataClient.DailyChallengeUpdated(new DailyChallengeInfo { RoomID = room.RoomID!.Value }));
}
}
}
@@ -208,5 +208,11 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor, () => Is.EqualTo(7));
AddAssert("Correct beat divisor actually active", () => Editor.BeatDivisor, () => Is.EqualTo(7));
}
[Test]
public void TestBeatmapVersionPopulatedCorrectly()
{
AddAssert("beatmap version is populated", () => EditorBeatmap.BeatmapVersion > 0);
}
}
}
@@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays.SkinEditor;
@@ -458,6 +459,62 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("combo placed in ruleset target", () => rulesetHUDTarget.Components.OfType<LegacyDefaultComboCounter>().Count() == 1);
}
[Test]
public void TestAnchorRadioButtonBehavior()
{
ISerialisableDrawable? selectedComponent = null;
AddStep("Select first component", () =>
{
var blueprint = skinEditor.ChildrenOfType<SkinBlueprint>().First();
skinEditor.SelectedComponents.Clear();
skinEditor.SelectedComponents.Add(blueprint.Item);
selectedComponent = blueprint.Item;
});
AddStep("Right-click to open context menu", () =>
{
if (selectedComponent != null)
InputManager.MoveMouseTo(((Drawable)selectedComponent).ScreenSpaceDrawQuad.Centre);
InputManager.Click(MouseButton.Right);
});
AddStep("Click on Anchor menu", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Anchor"));
InputManager.Click(MouseButton.Left);
});
AddStep("Right-click TopLeft anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("TopLeft"));
InputManager.Click(MouseButton.Right);
});
AddAssert("TopLeft item checked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddStep("Right-click Centre anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Centre"));
InputManager.Click(MouseButton.Right);
});
AddAssert("Centre item checked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddAssert("TopLeft item unchecked", () => (getMenuItemByText("TopLeft").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
AddStep("Right-click Closest anchor", () =>
{
InputManager.MoveMouseTo(getMenuItemByText("Closest"));
InputManager.Click(MouseButton.Right);
});
AddAssert("Closest item checked", () => (getMenuItemByText("Closest").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.True);
AddAssert("Centre item unchecked", () => (getMenuItemByText("Centre").Item as TernaryStateRadioMenuItem)?.State.Value == TernaryState.False);
Menu.DrawableMenuItem getMenuItemByText(string text)
=> this.ChildrenOfType<Menu.DrawableMenuItem>().First(m => m.Item.Text.ToString() == text);
}
private Skin importSkinFromArchives(string filename)
{
var imported = skins.Import(new ImportTask(TestResources.OpenResource($@"Archives/{filename}"), filename)).GetResultSafely();
@@ -50,30 +50,17 @@ namespace osu.Game.Tests.Visual.Menus
[Test]
public void TestGameplay()
{
KiaiGameplayFountains fountains = null!;
AddStep("make fountains", () =>
{
Children = new[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
fountains = new KiaiGameplayFountains(),
};
});
AddStep("activate fountains", () =>
{
((StarFountain)Children[0]).Shoot(1);
((StarFountain)Children[1]).Shoot(-1);
});
AddStep("activate fountains", () => fountains.Shoot());
}
[Test]
@@ -61,6 +61,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
InitialBeatmap = importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0);
OtherBeatmap = importedSet.Beatmaps.Last(b => b.Ruleset.OnlineID == 0);
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Overlays;
using osu.Game.Screens.OnlinePlay.Lounge;
@@ -30,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
private DrawableLoungeRoom drawableRoom = null!;
private LoungeRoomPanel panel = null!;
private SearchTextBox searchTextBox = null!;
private readonly ManualResetEventSlim allowResponseCallback = new ManualResetEventSlim();
@@ -40,14 +41,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var mockLounge = new Mock<IOnlinePlayLounge>();
mockLounge
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string>>()))
.Callback<Room, string, Action<Room>, Action<string>>((_, _, _, d) =>
.Setup(l => l.Join(It.IsAny<Room>(), It.IsAny<string>(), It.IsAny<Action<Room>>(), It.IsAny<Action<string, Exception?>>()))
.Callback<Room, string, Action<Room>, Action<string, Exception?>>((_, _, _, d) =>
{
Task.Run(() =>
{
allowResponseCallback.Wait(10000);
allowResponseCallback.Reset();
Schedule(() => d?.Invoke("Incorrect password"));
Schedule(() => d?.Invoke("Incorrect password", new InvalidPasswordException()));
});
});
@@ -73,7 +74,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
Width = 500,
Depth = float.MaxValue
},
drawableRoom = new DrawableLoungeRoom(room)
panel = new LoungeRoomPanel(room)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@@ -87,16 +88,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFocusViaKeyboardCommit()
{
DrawableLoungeRoom.PasswordEntryPopover? popover = null;
LoungeRoomPanel.PasswordEntryPopover? popover = null;
AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
AddStep("click room twice", () =>
{
InputManager.MoveMouseTo(drawableRoom);
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
@@ -122,16 +123,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestFocusViaMouseCommit()
{
DrawableLoungeRoom.PasswordEntryPopover? popover = null;
LoungeRoomPanel.PasswordEntryPopover? popover = null;
AddAssert("search textbox has focus", () => checkFocus(searchTextBox));
AddStep("click room twice", () =>
{
InputManager.MoveMouseTo(drawableRoom);
InputManager.MoveMouseTo(panel);
InputManager.Click(MouseButton.Left);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().SingleOrDefault()) != null);
AddUntilStep("wait for popover", () => (popover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().SingleOrDefault()) != null);
AddAssert("textbox has focus", () => checkFocus(popover.ChildrenOfType<OsuPasswordTextBox>().Single()));
@@ -101,15 +101,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
[SetUpSteps]
public void SetUpSteps()
{
PlaylistItem item = null!;
AddStep("reset state", () =>
{
multiplayerClient.Invocations.Clear();
beatmapAvailability.Value = BeatmapAvailability.LocallyAvailable();
item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
PlaylistItem item = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
};
@@ -127,7 +125,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
multiplayerRoom = new MultiplayerRoom(0)
{
Playlist = { TestMultiplayerClient.CreateMultiplayerPlaylistItem(item) },
Playlist = { new MultiplayerPlaylistItem(item) },
Users = { localUser },
Host = localUser,
};
@@ -139,8 +137,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(250, 50),
SelectedItem = new Bindable<PlaylistItem?>(item)
Size = new Vector2(250, 50)
};
});
}
@@ -81,6 +81,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
});
@@ -269,7 +274,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room and immediately exit select", () =>
@@ -298,7 +303,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
@@ -349,13 +354,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("join room", () => InputManager.Key(Key.Enter));
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -802,7 +807,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
});
AddStep("refresh rooms", () => this.ChildrenOfType<LoungeSubScreen>().Single().UpdateFilter());
AddUntilStep("wait for room", () => this.ChildrenOfType<DrawableRoom>().Any());
AddUntilStep("wait for room", () => this.ChildrenOfType<RoomPanel>().Any());
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("disable polling", () => this.ChildrenOfType<LoungeListingPoller>().Single().TimeBetweenPolls.Value = 0);
@@ -924,7 +929,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -956,7 +961,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
enterGameplay();
AddStep("join other user", () => multiplayerClient.AddUser(new APIUser { Id = 1234 }));
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, TestMultiplayerClient.CreateMultiplayerPlaylistItem(
AddStep("add item as other user", () => multiplayerClient.AddUserPlaylistItem(1234, new MultiplayerPlaylistItem(
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
@@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("textbox has focus", () => InputManager.FocusedDrawable is OsuPasswordTextBox);
@@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("textbox lost focus", () => InputManager.FocusedDrawable is SearchTextBox);
AddStep("hit escape", () => InputManager.Key(Key.Escape));
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
@@ -65,9 +65,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt appeared", () => InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddStep("exit screen", () => Stack.Exit());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().Any());
AddUntilStep("password prompt hidden", () => !InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().Any());
AddAssert("room not joined", () => !MultiplayerClient.RoomJoined);
}
@@ -75,12 +75,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithIncorrectPasswordViaButton()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -94,12 +94,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithIncorrectPasswordViaEnter()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "wrong");
AddStep("press enter", () => InputManager.Key(Key.Enter));
@@ -113,12 +113,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithCorrectPassword()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press join room button", () => passwordEntryPopover.ChildrenOfType<OsuButton>().First().TriggerClick());
@@ -128,12 +128,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestJoinRoomWithPasswordViaKeyboardOnly()
{
DrawableLoungeRoom.PasswordEntryPopover? passwordEntryPopover = null;
LoungeRoomPanel.PasswordEntryPopover? passwordEntryPopover = null;
createRooms(GenerateRooms(1, withPassword: true));
AddStep("select room", () => InputManager.Key(Key.Down));
AddStep("attempt join room", () => InputManager.Key(Key.Enter));
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<DrawableLoungeRoom.PasswordEntryPopover>().FirstOrDefault()) != null);
AddUntilStep("password prompt appeared", () => (passwordEntryPopover = InputManager.ChildrenOfType<LoungeRoomPanel.PasswordEntryPopover>().FirstOrDefault()) != null);
AddStep("enter password in text box", () => passwordEntryPopover.ChildrenOfType<TextBox>().First().Text = "password");
AddStep("press enter", () => InputManager.Key(Key.Enter));
@@ -1,11 +1,11 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Online.Rooms;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
namespace osu.Game.Tests.Visual.Multiplayer
@@ -18,22 +18,33 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create footer", () =>
{
Child = new PopoverContainer
MultiplayerBeatmapAvailabilityTracker tracker = new MultiplayerBeatmapAvailabilityTracker();
Child = new DependencyProvidingContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter
CachedDependencies =
[
(typeof(OnlinePlayBeatmapAvailabilityTracker), tracker)
],
Children =
[
tracker,
new PopoverContainer
{
SelectedItem = new Bindable<PlaylistItem?>()
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Child = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = 50,
Child = new MultiplayerMatchFooter()
}
}
}
]
};
});
}
@@ -330,10 +330,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
AddUntilStep("button visible", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.GreaterThan(0));
AddUntilStep("button visible", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.GreaterThan(0));
AddStep("join other user", void () => MultiplayerClient.AddUser(new APIUser { Id = PLAYER_1_ID }));
AddStep("make other user host", () => MultiplayerClient.TransferHost(PLAYER_1_ID));
AddAssert("button hidden", () => this.ChildrenOfType<DrawableMatchRoom>().Single().ChangeSettingsButton?.Alpha, () => Is.EqualTo(0));
AddAssert("button hidden", () => this.ChildrenOfType<MultiplayerRoomPanel>().Single().ChangeSettingsButton.Alpha, () => Is.EqualTo(0));
}
private partial class TestMultiplayerMatchSubScreen : MultiplayerMatchSubScreen
@@ -6,7 +6,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Platform;
@@ -32,7 +31,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private BeatmapManager beatmaps = null!;
private BeatmapSetInfo importedSet = null!;
private BeatmapInfo importedBeatmap = null!;
private Room room = null!;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
@@ -47,19 +45,17 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
base.SetUpSteps();
AddStep("create room", () => room = CreateDefaultRoom());
AddStep("join room", () => JoinRoom(room));
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("create list", () =>
{
Child = list = new MultiplayerPlaylist(room)
Child = list = new MultiplayerPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.4f, 0.8f),
SelectedItem = new Bindable<PlaylistItem?>()
Size = new Vector2(0.4f, 0.8f)
};
});
@@ -158,37 +154,36 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertQueueTabCount(0);
}
[Ignore("Expired items are initially removed from the room.")]
[Test]
public void TestJoinRoomWithMixedItemsAddedInCorrectLists()
{
AddStep("leave room", () => MultiplayerClient.LeaveRoom());
AddUntilStep("wait for room part", () => !RoomJoined);
AddStep("join room with items", () =>
AddStep("join room with expired items", () =>
{
API.Queue(new CreateRoomRequest(new Room
{
Name = "test name",
Playlist =
[
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID
},
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
Expired = true
}
]
}));
Room room = CreateDefaultRoom();
room.Playlist =
[
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID
},
new PlaylistItem(new TestBeatmap(Ruleset.Value).BeatmapInfo)
{
RulesetID = Ruleset.Value.OnlineID,
Expired = true
}
];
JoinRoom(room);
});
AddUntilStep("wait for room join", () => RoomJoined);
WaitForJoined();
assertItemInQueueListStep(1, 0);
assertItemInHistoryListStep(2, 0);
// IDs are offset by 1 because we've joined two rooms in this test.
assertItemInQueueListStep(2, 0);
assertItemInHistoryListStep(3, 0);
}
[Test]
@@ -220,7 +215,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary>
private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{
Expired = expired,
PlayedAt = DateTimeOffset.Now
@@ -49,13 +49,15 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create playlist", () =>
{
Child = playlist = new MultiplayerQueueList(room)
Child = playlist = new MultiplayerQueueList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
Size = new Vector2(500, 300)
};
playlist.Items.ReplaceRange(0, playlist.Items.Count, MultiplayerClient.ClientAPIRoom!.Playlist);
MultiplayerClient.ClientAPIRoom!.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(Room.Playlist))
@@ -132,13 +134,25 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertDeleteButtonVisibility(1, false);
}
[Test]
public void TestChangeExistingItem()
{
AddStep("change beatmap", () => MultiplayerClient.EditPlaylistItem(new MultiplayerPlaylistItem
{
ID = playlist.Items[0].ID,
BeatmapID = 1337
}).WaitSafely());
AddUntilStep("first playlist item has new beatmap", () => playlist.Items[0].Beatmap.OnlineID, () => Is.EqualTo(1337));
}
private void addPlaylistItem(Func<int> userId)
{
long itemId = -1;
AddStep("add playlist item", () =>
{
MultiplayerPlaylistItem item = TestMultiplayerClient.CreateMultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap));
MultiplayerClient.AddUserPlaylistItem(userId(), item).WaitSafely();
@@ -5,7 +5,6 @@ using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -18,6 +17,8 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Tests.Resources;
using osuTK;
@@ -53,36 +54,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create button", () =>
{
AvailabilityTracker.SelectedItem.Value = room.Playlist.First();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
Child = new PopoverContainer
MultiplayerBeatmapAvailabilityTracker tracker = new MultiplayerBeatmapAvailabilityTracker();
Child = new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
CachedDependencies =
[
(typeof(OnlinePlayBeatmapAvailabilityTracker), tracker)
],
Children =
[
tracker,
new PopoverContainer
{
spectateButton = new MultiplayerSpectateButton
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50),
SelectedItem = new Bindable<PlaylistItem?>(room.Playlist.First())
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
spectateButton = new MultiplayerSpectateButton
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50)
},
startControl = new MatchStartControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(200, 50)
}
}
}
}
}
]
};
});
}
@@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneMultiplayerUserModDisplay : MultiplayerTestScene
{
private MultiplayerUserModDisplay modDisplay = null!;
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("join room", () => JoinRoom(CreateDefaultRoom()));
WaitForJoined();
AddStep("add display", () => Child = modDisplay = new MultiplayerUserModDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
}
[Test]
public void TestChangeMods()
{
AddStep("set DT", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime()]).WaitSafely());
AddUntilStep("mod displayed", () => modDisplay.ChildrenOfType<ModIcon>().Count() == 1);
AddStep("set DT, HR", () => MultiplayerClient.ChangeUserMods([new OsuModDoubleTime(), new OsuModHardRock()]).WaitSafely());
AddUntilStep("mods displayed", () => modDisplay.ChildrenOfType<ModIcon>().Count() == 2);
AddStep("set no mods", () => MultiplayerClient.ChangeUserMods(Enumerable.Empty<APIMod>()).WaitSafely());
AddUntilStep("no mods displayed", () => !modDisplay.ChildrenOfType<ModIcon>().Any());
}
}
}
@@ -11,7 +11,6 @@ using osu.Framework.Testing;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Tests.Visual.OnlinePlay;
@@ -199,25 +198,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("add rooms", () => rooms.AddRange(GenerateRooms(3, withPassword: true)));
}
[Test]
public void TestFreestyleBypassesRulesetFilter()
{
AddStep("apply taiko filter", () => container.Filter.Value = new FilterCriteria { Ruleset = new TaikoRuleset().RulesetInfo });
AddStep("add osu + freestyle room", () =>
{
var room = GenerateRooms(1, new OsuRuleset().RulesetInfo)[0];
room.Playlist[0].Freestyle = true;
room.CurrentPlaylistItem = room.Playlist[0];
rooms.Add(room);
});
AddAssert("room visible", () => container.DrawableRooms.Any());
}
private bool checkRoomSelected(Room? room) => selectedRoom.Value == room;
private Room? getRoomInFlow(int index) =>
(container.ChildrenOfType<FillFlowContainer<DrawableLoungeRoom>>().First().FlowingChildren.ElementAt(index) as DrawableRoom)?.Room;
(container.ChildrenOfType<FillFlowContainer<LoungeRoomPanel>>().First().FlowingChildren.ElementAt(index) as RoomPanel)?.Room;
}
}
@@ -18,13 +18,13 @@ using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Visual.Multiplayer
{
public partial class TestSceneDrawableRoom : OsuTestScene
public partial class TestSceneRoomPanel : OsuTestScene
{
[Cached]
protected readonly OverlayColourProvider ColourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
@@ -129,24 +129,24 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEnableAndDisablePassword()
{
DrawableRoom drawableRoom = null!;
RoomPanel panel = null!;
Room room = null!;
AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room
AddStep("create room", () => Child = panel = createLoungeRoom(room = new Room
{
Name = "Room with password",
Type = MatchType.HeadToHead,
}));
AddUntilStep("wait for panel load", () => drawableRoom.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddUntilStep("wait for panel load", () => panel.ChildrenOfType<DrawableRoomParticipantsList>().Any());
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
AddStep("set password", () => room.Password = "password");
AddAssert("password icon visible", () => Precision.AlmostEquals(1, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon visible", () => Precision.AlmostEquals(1, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
AddStep("unset password", () => room.Password = string.Empty);
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, panel.ChildrenOfType<RoomPanel.PasswordProtectedIcon>().Single().Alpha));
}
[Test]
@@ -160,38 +160,29 @@ namespace osu.Game.Tests.Visual.Multiplayer
Spacing = new Vector2(5),
Children = new[]
{
new DrawableMatchRoom(new Room
new MultiplayerRoomPanel(new Room
{
Name = "A host-only room",
QueueMode = QueueMode.HostOnly,
Type = MatchType.HeadToHead,
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
new DrawableMatchRoom(new Room
}),
new MultiplayerRoomPanel(new Room
{
Name = "An all-players, team-versus room",
QueueMode = QueueMode.AllPlayers,
Type = MatchType.TeamVersus
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
new DrawableMatchRoom(new Room
}),
new MultiplayerRoomPanel(new Room
{
Name = "A round-robin room",
QueueMode = QueueMode.AllPlayersRoundRobin,
Type = MatchType.HeadToHead
})
{
SelectedItem = new Bindable<PlaylistItem?>()
},
}),
}
});
}
private DrawableRoom createLoungeRoom(Room room)
private RoomPanel createLoungeRoom(Room room)
{
room.Host ??= new APIUser { Username = "peppy", Id = 2 };
@@ -204,7 +195,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}).ToArray();
}
return new DrawableLoungeRoom(room)
return new LoungeRoomPanel(room)
{
MatchingFilter = true,
SelectedRoom = selectedRoom
@@ -99,8 +99,35 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(i => i % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(i => i % 12 - 6).ToArray(),
},
TopTags =
[
new APIBeatmapTag { TagId = 4, VoteCount = 1 },
new APIBeatmapTag { TagId = 2, VoteCount = 1 },
new APIBeatmapTag { TagId = 23, VoteCount = 5 },
],
},
},
RelatedTags =
[
new APITag
{
Id = 2,
Name = "song representation/simple",
Description = "Accessible and straightforward map design."
},
new APITag
{
Id = 4,
Name = "style/clean",
Description = "Visually uncluttered and organised patterns, often involving few overlaps and equal visual spacing between objects."
},
new APITag
{
Id = 23,
Name = "aim/aim control",
Description = "Patterns with velocity or direction changes which strongly go against a player's natural movement pattern."
}
]
});
});
@@ -289,12 +316,37 @@ namespace osu.Game.Tests.Visual.Online
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(0));
});
AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().All(s => s.Text != "BanchoBot"));
AddAssert("guest mapper information not shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().All(s => s.Text != "BanchoBot0"));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddAssert("guest mapper information shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().Any(s => s.Text == "BanchoBot0"));
}
[Test]
public void TestBeatmapsetWithALotGuestOwner()
{
AddStep("show map with 2 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(2)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 3 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(3)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 10 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(10)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddStep("show map with 20 mapper", () => overlay.ShowBeatmapSet(createBeatmapSetWithGuestDifficulty(20)));
AddStep("move mouse to guest difficulty", () =>
{
InputManager.MoveMouseTo(overlay.ChildrenOfType<DifficultyIcon>().ElementAt(1));
});
AddAssert("guest mapper information shown", () => overlay.ChildrenOfType<BeatmapPicker>().Single().ChildrenOfType<OsuSpriteText>().Any(s => s.Text == "BanchoBot"));
}
private APIBeatmapSet createManyDifficultiesBeatmapSet()
@@ -336,22 +388,31 @@ namespace osu.Game.Tests.Visual.Online
return beatmapSet;
}
private APIBeatmapSet createBeatmapSetWithGuestDifficulty()
private APIBeatmapSet createBeatmapSetWithGuestDifficulty(int guestCount = 1)
{
var set = getBeatmapSet();
var beatmaps = new List<APIBeatmap>();
var beatmapOwners = new List<APIBeatmap.BeatmapOwner>();
var ownersAPIUser = new List<APIUser>();
var guestUser = new APIUser
for (int i = 0; i < guestCount; i++)
{
Username = @"BanchoBot",
Id = 3,
};
var guestUser = new APIUser
{
Username = @$"BanchoBot{i}",
Id = i + 3,
};
set.RelatedUsers = new[]
{
set.Author, guestUser
};
beatmapOwners.Add(new APIBeatmap.BeatmapOwner
{
Username = @$"BanchoBot{i}",
Id = i + 3,
});
ownersAPIUser.Add(guestUser);
}
set.RelatedUsers = new[] { set.Author }.Concat(ownersAPIUser).ToArray();
beatmaps.Add(new APIBeatmap
{
@@ -366,7 +427,7 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
Status = BeatmapOnlineStatus.Graveyard,
});
beatmaps.Add(new APIBeatmap
@@ -382,7 +443,8 @@ namespace osu.Game.Tests.Visual.Online
Fails = Enumerable.Range(1, 100).Select(j => j % 12 - 6).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(j => j % 12 - 6).ToArray(),
},
Status = BeatmapOnlineStatus.Graveyard
Status = BeatmapOnlineStatus.Graveyard,
BeatmapOwners = beatmapOwners.ToArray(),
});
set.Beatmaps = beatmaps.ToArray();
@@ -234,7 +234,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "flyte",
Id = 3103765,
IsOnline = true,
WasRecentlyOnline = true,
Statistics = new UserStatistics { GlobalRank = 1111 },
CountryCode = CountryCode.JP,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
@@ -243,7 +243,7 @@ namespace osu.Game.Tests.Visual.Online
{
Username = "peppy",
Id = 2,
IsOnline = false,
WasRecentlyOnline = false,
Statistics = new UserStatistics { GlobalRank = 2222 },
CountryCode = CountryCode.AU,
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
@@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 8195163,
CountryCode = CountryCode.BY,
CoverUrl = "https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
IsOnline = false,
WasRecentlyOnline = false,
LastVisit = DateTimeOffset.Now
}
};
@@ -0,0 +1,52 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers.Markdown;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers.Markdown;
using osu.Game.Overlays.Comments;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneImageProxying : OsuTestScene
{
[Test]
public void TestExternalImageLink()
{
MarkdownContainer markdown = null!;
// use base MarkdownContainer as a method of directly attempting to load an image without proxying logic.
AddStep("load external without proxying", () => Child = markdown = new MarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddWaitStep("wait", 5);
AddAssert("image not loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture == null);
AddStep("load external with proxying", () => Child = markdown = new OsuMarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddUntilStep("image loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture != null);
}
[Test]
public void TestExternalImageLinkInComments()
{
MarkdownContainer markdown = null!;
AddStep("load external with proxying", () => Child = markdown = new CommentMarkdownContainer
{
RelativeSizeAxes = Axes.Both,
Text = "![](https://github.com/ppy/osu-wiki/blob/master/wiki/Announcement_messages/img/notification.png?raw=true)",
});
AddUntilStep("image loaded", () => markdown.ChildrenOfType<Sprite>().SingleOrDefault()?.Texture != null);
}
}
}
@@ -62,7 +62,7 @@ namespace osu.Game.Tests.Visual.Online
CountryCode = countryCode,
CoverUrl = cover,
Colour = color ?? "000000",
IsOnline = true
WasRecentlyOnline = true
};
return new ClickableAvatar(user, showPanel)
@@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3103765,
CountryCode = CountryCode.JP,
CoverUrl = @"https://assets.ppy.sh/user-cover-presets/1/df28696b58541a9e67f6755918951d542d93bdf1da41720fcca2fd2c1ea8cf51.jpeg",
IsOnline = true
WasRecentlyOnline = true
}) { Width = 300 },
new UserGridPanel(new APIUser
{
@@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1001,
Username = "IAmOnline",
LastVisit = DateTimeOffset.Now,
IsOnline = true,
WasRecentlyOnline = true,
}, new OsuRuleset().RulesetInfo));
AddStep("Show offline user", () => header.User.Value = new UserProfileData(new APIUser
@@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 1002,
Username = "IAmOffline",
LastVisit = DateTimeOffset.Now.AddDays(-10),
IsOnline = false,
WasRecentlyOnline = false,
}, new OsuRuleset().RulesetInfo));
}
@@ -61,9 +61,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("last room is not masked", () => checkRoomVisible(roomListing.DrawableRooms[^1]));
}
private bool checkRoomVisible(DrawableRoom room) =>
private bool checkRoomVisible(RoomPanel panel) =>
loungeScreen.ChildrenOfType<OsuScrollContainer>().First().ScreenSpaceDrawQuad
.Contains(room.ScreenSpaceDrawQuad.Centre);
.Contains(panel.ScreenSpaceDrawQuad.Centre);
private void createRooms(params Room[] rooms)
{
@@ -176,6 +176,7 @@ namespace osu.Game.Tests.Visual.Playlists
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}
];
room.EndDate = DateTimeOffset.Now.AddHours(1);
});
AddAssert("match has default beatmap", () => match.Beatmap.IsDefault);
@@ -212,6 +213,11 @@ namespace osu.Game.Tests.Visual.Playlists
Debug.Assert(beatmap.BeatmapInfo.BeatmapSet != null);
importedBeatmap = manager.Import(beatmap.BeatmapInfo.BeatmapSet)!.Value.Detach();
Realm.Write(r =>
{
foreach (var beatmapInfo in r.All<BeatmapInfo>())
beatmapInfo.OnlineMD5Hash = beatmapInfo.MD5Hash;
});
});
private partial class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen
@@ -65,7 +65,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 1,
DifficultyName = "Osu 1",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "11111111",
OnlineMD5Hash = "11111111",
Ruleset = new OsuRuleset().RulesetInfo,
Metadata =
{
@@ -79,7 +80,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 2,
DifficultyName = "Osu 2",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "22222222",
OnlineMD5Hash = "22222222",
Ruleset = new OsuRuleset().RulesetInfo,
Metadata =
{
@@ -93,7 +95,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 3,
DifficultyName = "Taiko 1",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "33333333",
OnlineMD5Hash = "33333333",
Ruleset = new TaikoRuleset().RulesetInfo,
Metadata =
{
@@ -107,7 +110,8 @@ namespace osu.Game.Tests.Visual.Playlists
OnlineID = 4,
DifficultyName = "Taiko 2",
Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = Guid.NewGuid().ToString().ComputeMD5Hash(),
MD5Hash = "44444444",
OnlineMD5Hash = "44444444",
Ruleset = new TaikoRuleset().RulesetInfo,
Metadata =
{
@@ -105,6 +105,40 @@ namespace osu.Game.Tests.Visual.Ranking
displayUpdate(statistics, statistics);
}
[Test]
public void TestFromNothing()
{
createDisplay();
displayUpdate(
new UserStatistics(),
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
});
}
[Test]
public void TestToNothing()
{
createDisplay();
displayUpdate(
new UserStatistics
{
GlobalRank = 12_345,
Accuracy = 98.99,
MaxCombo = 2_322,
RankedScore = 23_123_543_456,
TotalScore = 123_123_543_456,
PP = 5_072
},
new UserStatistics());
}
private void createDisplay() => AddStep("create display", () =>
{
statisticsUpdate.Value = null;
@@ -8,11 +8,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@@ -24,8 +28,10 @@ using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
@@ -42,6 +48,22 @@ namespace osu.Game.Tests.Visual.Ranking
{
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
private ScoreManager scoreManager = null!;
private RulesetStore rulesetStore = null!;
private BeatmapManager beatmapManager = null!;
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(rulesetStore = 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(rulesetStore, () => beatmapManager, LocalStorage, Realm, API));
Dependencies.Cache(Realm);
return dependencies;
}
[Test]
public void TestScoreWithPositionStatistics()
{
@@ -162,6 +184,24 @@ namespace osu.Game.Tests.Visual.Ranking
{
var score = TestResources.CreateTestScoreInfo();
setUpTaggingRequests(() => score.BeatmapInfo);
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
private void setUpTaggingRequests(Func<BeatmapInfo> beatmap) =>
AddStep("set up network requests", () =>
{
dummyAPI.HandleRequest = request =>
@@ -175,7 +215,11 @@ namespace osu.Game.Tests.Visual.Ranking
Tags =
[
new APITag { Id = 1, Name = "tech", Description = "Tests uncommon skills.", },
new APITag { Id = 2, Name = "alt", Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.", },
new APITag
{
Id = 2, Name = "alt",
Description = "Colloquial term for maps which use rhythms that encourage the player to alternate notes. Typically distinct from burst or stream maps.",
},
new APITag { Id = 3, Name = "aim", Description = "Category for difficulty relating to cursor movement.", },
new APITag { Id = 4, Name = "tap", Description = "Category for difficulty relating to tapping input.", },
]
@@ -185,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking
case GetBeatmapSetRequest getBeatmapSetRequest:
{
var beatmapSet = CreateAPIBeatmapSet(score.BeatmapInfo);
var beatmapSet = CreateAPIBeatmapSet(beatmap.Invoke());
beatmapSet.Beatmaps.Single().TopTags =
[
new APIBeatmapTag { TagId = 3, VoteCount = 9 },
@@ -205,21 +249,6 @@ namespace osu.Game.Tests.Visual.Ranking
return false;
};
});
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
[Test]
public void TestTaggingWhenRankTooLow()
@@ -243,6 +272,100 @@ namespace osu.Game.Tests.Visual.Ranking
});
}
[Test]
public void TestTaggingConvert()
{
var score = TestResources.CreateTestScoreInfo();
score.Ruleset = new ManiaRuleset().RulesetInfo;
AddStep("load panel", () =>
{
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
AchievedScore = score,
}
};
});
}
[Test]
public void TestTaggingInteractionWithLocalScores()
{
BeatmapInfo beatmapInfo = null!;
AddStep(@"Import beatmap", () =>
{
beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
beatmapInfo = beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps.First();
});
AddStep("import bad score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.Rank = ScoreRank.D;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
AddStep("import score by another user", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.Rank = ScoreRank.D;
score.User = new APIUser { Username = "notme", Id = 5678 };
scoreManager.Import(score);
});
AddStep("import convert score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = new OsuRuleset().RulesetInfo;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
AddStep("import correct score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
score.BeatmapHash = beatmapInfo.Hash;
score.Ruleset = beatmapInfo.Ruleset;
score.User = API.LocalUser.Value;
scoreManager.Import(score);
});
setUpTaggingRequests(() => beatmapInfo);
AddStep("load panel", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.BeatmapInfo = beatmapInfo;
Child = new PopoverContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score },
}
};
});
}
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
@@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
private BeatmapCarousel carousel = null!;
private TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader beatmapDownloader = null!;
private TestScenePlaylistsBeatmapAvailabilityTracker.TestBeatmapModelDownloader beatmapDownloader = null!;
private BeatmapSetInfo testBeatmapSetInfo = null!;
@@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.SongSelect
var importer = parent.Get<BeatmapManager>();
dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestSceneOnlinePlayBeatmapAvailabilityTracker.TestBeatmapModelDownloader(importer, API));
dependencies.CacheAs<BeatmapModelDownloader>(beatmapDownloader = new TestScenePlaylistsBeatmapAvailabilityTracker.TestBeatmapModelDownloader(importer, API));
return dependencies;
}
@@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("progress download to completion", () =>
{
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
{
testRequest.SetProgress(testRequest.Progress + 0.1f);
@@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("progress download to failure", () =>
{
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
{
testRequest.SetProgress(testRequest.Progress + 0.1f);
@@ -226,7 +226,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("progress download to completion", () =>
{
if (downloadRequest is TestSceneOnlinePlayBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
if (downloadRequest is TestScenePlaylistsBeatmapAvailabilityTracker.TestDownloadRequest testRequest)
{
testRequest.SetProgress(testRequest.Progress + 0.1f);
@@ -993,7 +993,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("column not scrolled", () => modSelectOverlay.ChildrenOfType<ModSelectOverlay.ColumnScrollContainer>().Single().IsScrolledToStart());
AddStep("move mouse away", () => InputManager.MoveMouseTo(Vector2.Zero));
AddAssert("customisation panel closed",
AddUntilStep("customisation panel closed",
() => this.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
@@ -1003,6 +1003,35 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("search still not focused", () => !this.ChildrenOfType<ShearedSearchTextBox>().Single().HasFocus);
}
/// <summary>
/// Tests that recreating the mod panels (by setting the global available mods) also refreshes the active states.
/// </summary>
[Test]
public void TestActiveStatesRefreshedOnPanelsCreated()
{
createScreen();
changeRuleset(0);
Bindable<IReadOnlyList<Mod>> selectedMods = null!;
AddStep("bind mods to local bindable", () =>
{
selectedMods = new Bindable<IReadOnlyList<Mod>>([]);
modSelectOverlay.SelectedMods.UnbindFrom(SelectedMods);
modSelectOverlay.SelectedMods.BindTo(selectedMods);
});
AddStep("activate PF", () => selectedMods.Value = [new OsuModPerfect()]);
AddAssert("OsuModPerfect panel active", () => getPanelForMod(typeof(OsuModPerfect)).Active.Value);
changeRuleset(1);
AddAssert("TaikoModPerfect panel not active", () => !getPanelForMod(typeof(TaikoModPerfect)).Active.Value);
changeRuleset(0);
AddAssert("OsuModPerfect panel active", () => getPanelForMod(typeof(OsuModPerfect)).Active.Value);
}
private void waitForColumnLoad() => AddUntilStep("all column content loaded", () =>
modSelectOverlay.ChildrenOfType<ModColumn>().Any()
&& modSelectOverlay.ChildrenOfType<ModColumn>().All(column => column.IsLoaded && column.ItemsLoaded)
@@ -1018,7 +1047,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private void assertCustomisationToggleState(bool disabled, bool active)
{
AddUntilStep($"customisation panel is {(disabled ? "" : "not ")}disabled", () => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().Enabled.Value == !disabled);
AddAssert($"customisation panel is {(active ? "" : "not ")}active",
AddUntilStep($"customisation panel is {(active ? "" : "not ")}active",
() => modSelectOverlay.ChildrenOfType<ModCustomisationPanel>().Single().ExpandedState.Value,
() => active ? Is.Not.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed) : Is.EqualTo(ModCustomisationPanel.ModCustomisationPanelState.Collapsed));
}
@@ -196,6 +196,37 @@ namespace osu.Game.Tests.Visual.UserInterface
AddAssert("external overlay content still not shown", () => this.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent, () => Is.Not.True);
}
[Test]
public void TestButtonResizedAfterFooterIsDisplayed()
{
TestShearedOverlayContainer externalOverlay = null!;
AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
AddStep("set buttons", () => screenFooter.SetButtons(new[]
{
new ScreenFooterButton(externalOverlay)
{
AccentColour = Dependencies.Get<OsuColour>().Orange1,
Icon = FontAwesome.Solid.Toolbox,
Text = "One",
},
new ScreenFooterButton { Text = "Two", Action = () => { } },
new ScreenFooterButton { Text = "Three", Action = () => { } },
}));
AddWaitStep("wait for transition", 3);
AddStep("show overlay", () => externalOverlay.Show());
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
AddStep("resize active button", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(240, 300, Easing.OutQuint));
AddStep("resize active button back", () => this.ChildrenOfType<ScreenFooterButton>().First().ResizeWidthTo(116, 300, Easing.OutQuint));
AddStep("hide overlay", () => externalOverlay.Hide());
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
}
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
{
public TestShearedOverlayContainer()
@@ -0,0 +1,43 @@
// 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.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.UserInterfaceV2;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public partial class TestSceneShearedDropdown : ThemeComparisonTestScene
{
public TestSceneShearedDropdown()
: base(false)
{
}
protected override Drawable CreateContent() => new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
Colour = Color4.Black.Opacity(0.75f),
RelativeSizeAxes = Axes.Both,
},
new ShearedDropdown<string>("Test")
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 300f,
Width = 140,
Current = new Bindable<string>(),
Items = new[] { "Global", "Friends", "Local", "Really lonnnnnnng option" },
}
}
};
}
}
+2 -2
View File
@@ -6,9 +6,9 @@ using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Online;
namespace osu.Game.Audio
{
@@ -30,7 +30,7 @@ namespace osu.Game.Audio
[BackgroundDependencyLoader]
private void load(AudioManager audioManager)
{
trackStore = audioManager.GetTrackStore(new OnlineStore());
trackStore = audioManager.GetTrackStore(new TrustedDomainOnlineStore());
}
/// <summary>
+3
View File
@@ -9,6 +9,7 @@ using System.Linq;
using osu.Game.Beatmaps.ControlPoints;
using Newtonsoft.Json;
using osu.Framework.Lists;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Serialization.Converters;
namespace osu.Game.Beatmaps
@@ -141,6 +142,8 @@ namespace osu.Game.Beatmaps
public int[] Bookmarks { get; set; } = Array.Empty<int>();
public int BeatmapVersion { get; set; } = LegacyBeatmapEncoder.FIRST_LAZER_VERSION;
IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();
+1
View File
@@ -86,6 +86,7 @@ namespace osu.Game.Beatmaps
beatmap.Countdown = original.Countdown;
beatmap.CountdownOffset = original.CountdownOffset;
beatmap.Bookmarks = original.Bookmarks;
beatmap.BeatmapVersion = original.BeatmapVersion;
return beatmap;
}
+3 -4
View File
@@ -125,9 +125,10 @@ namespace osu.Game.Beatmaps
/// <summary>
/// Reset any fetched online linking information (and history).
/// </summary>
public void ResetOnlineInfo()
public void ResetOnlineInfo(bool resetOnlineId = true)
{
OnlineID = -1;
if (resetOnlineId)
OnlineID = -1;
LastOnlineUpdate = null;
OnlineMD5Hash = string.Empty;
if (Status != BeatmapOnlineStatus.LocallyModified)
@@ -231,8 +232,6 @@ namespace osu.Game.Beatmaps
[Obsolete("Use ScoreManager.GetMaximumAchievableComboAsync instead.")]
public int? MaxCombo { get; set; }
public int BeatmapVersion;
public BeatmapInfo Clone() => (BeatmapInfo)this.Detach().MemberwiseClone();
public override string ToString() => this.GetDisplayTitle();
+15 -1
View File
@@ -298,7 +298,21 @@ namespace osu.Game.Beatmaps
/// <param name="query">The query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(Expression<Func<BeatmapInfo, bool>> query) => Realm.Run(r =>
r.All<BeatmapInfo>().Filter($"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
r.All<BeatmapInfo>().Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false").FirstOrDefault(query)?.Detach());
/// <summary>
/// Perform a lookup query on available <see cref="BeatmapInfo"/>s.
/// Use this overload instead of <see cref="QueryBeatmap(System.Linq.Expressions.Expression{System.Func{osu.Game.Beatmaps.BeatmapInfo,bool}})"/>
/// when Realm is unable to transform an expression to the internal Realm query syntax.
/// </summary>
/// <param name="query">The query.</param>
/// <param name="arguments">The arguments for the query.</param>
/// <returns>The first result for the provided query, or null if no results were found.</returns>
public BeatmapInfo? QueryBeatmap(string query, params QueryArgument[] arguments) => Realm.Run(r =>
r.All<BeatmapInfo>()
.Filter($@"{nameof(BeatmapInfo.BeatmapSet)}.{nameof(BeatmapSetInfo.DeletePending)} == false")
.Filter(query, arguments)
.FirstOrDefault()?.Detach());
/// <summary>
/// A default representation of a WorkingBeatmap to use when no beatmap is available.
@@ -36,11 +36,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Origin = Anchor.CentreLeft,
TextSize = 13f
},
new DifficultySpectrumDisplay(beatmapSet)
new DifficultySpectrumDisplay
{
BeatmapSet = beatmapSet,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DotSize = new Vector2(5, 10)
}
}
};
@@ -43,61 +43,43 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
}
}
private float iconSize;
protected SpriteIcon Icon { get; private set; } = null!;
public float IconSize
private Container content = null!;
private Container hover = null!;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
get => iconSize;
set
{
iconSize = value;
Icon.Size = new Vector2(iconSize);
}
}
RelativeSizeAxes = Axes.Both;
protected readonly SpriteIcon Icon;
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Box hover;
protected BeatmapCardIconButton()
{
Origin = Anchor.Centre;
Anchor = Anchor.Centre;
base.Content.Add(content = new Container
Add(content = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Scale = new Vector2(0.8f),
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
hover = new Box
hover = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
Colour = Color4.White.Opacity(0.1f),
Blending = BlendingParameters.Additive,
Child = new Box { RelativeSizeAxes = Axes.Both, }
},
Icon = new SpriteIcon
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Scale = new Vector2(1.2f),
Size = new Vector2(14),
},
}
});
IconSize = 12;
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
IdleColour = colourProvider.Light1;
HoverColour = colourProvider.Content1;
}
@@ -127,8 +109,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
bool isHovered = IsHovered && Enabled.Value;
hover.FadeTo(isHovered ? 1f : 0f, 500, Easing.OutQuint);
content.ScaleTo(isHovered ? 1 : 0.8f, 500, Easing.OutQuint);
content.ScaleTo(isHovered ? 0.9f : 0.8f, 500, Easing.OutQuint);
Icon.FadeColour(isHovered ? HoverColour : IdleColour, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
protected void SetLoading(bool isLoading)
{
Icon.FadeTo(isLoading ? 0.2f : 1, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
Enabled.Value = !isLoading;
}
}
}
@@ -8,10 +8,8 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Resources.Localisation.Web;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
{
@@ -23,17 +21,11 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
private Bindable<bool> preferNoVideo = null!;
private readonly LoadingSpinner spinner;
[Resolved]
private BeatmapModelDownloader beatmaps { get; set; } = null!;
public DownloadButton(APIBeatmapSet beatmapSet)
{
Icon.Icon = FontAwesome.Solid.Download;
Content.Add(spinner = new LoadingSpinner { Size = new Vector2(IconSize) });
this.beatmapSet = beatmapSet;
}
@@ -41,6 +33,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
private void load(OsuConfigManager config)
{
preferNoVideo = config.GetBindable<bool>(OsuSetting.PreferNoVideo);
Icon.Icon = FontAwesome.Solid.Download;
}
protected override void LoadComplete()
@@ -64,8 +57,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
case DownloadState.Importing:
Action = null;
TooltipText = string.Empty;
spinner.Show();
Icon.Hide();
SetLoading(true);
break;
case DownloadState.LocallyAvailable:
@@ -84,8 +76,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
Action = () => beatmaps.Download(beatmapSet, preferNoVideo.Value);
this.FadeIn(BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
spinner.Hide();
Icon.Show();
SetLoading(false);
if (!beatmapSet.HasVideo)
TooltipText = BeatmapsetsStrings.PanelDownloadAll;
@@ -53,19 +53,20 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
favouriteRequest?.Cancel();
favouriteRequest = new PostBeatmapFavouriteRequest(beatmapSet.OnlineID, actionType);
Enabled.Value = false;
SetLoading(true);
favouriteRequest.Success += () =>
{
bool favourited = actionType == BeatmapFavouriteAction.Favourite;
current.Value = new BeatmapSetFavouriteState(favourited, current.Value.FavouriteCount + (favourited ? 1 : -1));
Enabled.Value = true;
SetLoading(false);
};
favouriteRequest.Failure += e =>
{
Logger.Error(e, $"Failed to {actionType.ToString().ToLowerInvariant()} beatmap: {e.Message}");
Enabled.Value = true;
SetLoading(false);
};
api.Queue(favouriteRequest);
@@ -20,15 +20,14 @@ namespace osu.Game.Beatmaps.Drawables.Cards.Buttons
public GoToBeatmapButton(APIBeatmapSet beatmapSet)
{
this.beatmapSet = beatmapSet;
Icon.Icon = FontAwesome.Solid.AngleDoubleRight;
TooltipText = "Go to beatmap";
}
[BackgroundDependencyLoader(true)]
private void load(OsuGame? game)
{
Action = () => game?.PresentBeatmap(beatmapSet);
Icon.Icon = FontAwesome.Solid.AngleDoubleRight;
TooltipText = "Go to beatmap";
}
protected override void LoadComplete()
@@ -95,9 +95,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Child = buttons = new Container<BeatmapCardIconButton>
{
RelativeSizeAxes = Axes.Both,
// Padding of 4 avoids touching the card borders when in the expanded (ie. showing difficulties) state.
// Left override allows the buttons to visually be wider and look better.
Padding = new MarginPadding(4) { Left = 2 },
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet)
@@ -106,7 +103,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both,
Height = 0.48f,
Height = 0.5f,
},
new DownloadButton(beatmapSet)
{
@@ -114,7 +111,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State },
RelativeSizeAxes = Axes.Both,
Height = 0.48f,
Height = 0.5f,
},
new GoToBeatmapButton(beatmapSet)
{
@@ -122,7 +119,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State },
RelativeSizeAxes = Axes.Both,
Height = 0.48f,
Height = 0.5f,
}
}
}
@@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions;
@@ -18,34 +17,6 @@ namespace osu.Game.Beatmaps.Drawables
{
public partial class DifficultySpectrumDisplay : CompositeDrawable
{
private Vector2 dotSize = new Vector2(4, 8);
public Vector2 DotSize
{
get => dotSize;
set
{
dotSize = value;
if (IsLoaded)
updateDisplay();
}
}
private float dotSpacing = 1;
public float DotSpacing
{
get => dotSpacing;
set
{
dotSpacing = value;
if (IsLoaded)
updateDisplay();
}
}
private IBeatmapSetInfo? beatmapSet;
public IBeatmapSetInfo? BeatmapSet
@@ -60,9 +31,12 @@ namespace osu.Game.Beatmaps.Drawables
}
}
private readonly FillFlowContainer<RulesetDifficultyGroup> flow;
private FillFlowContainer<RulesetDifficultyGroup> flow = null!;
public DifficultySpectrumDisplay(IBeatmapSetInfo? beatmapSet = null)
private const int max_difficulties_before_collapsing = 12;
[BackgroundDependencyLoader]
private void load()
{
AutoSizeAxes = Axes.Both;
@@ -72,8 +46,6 @@ namespace osu.Game.Beatmaps.Drawables
Spacing = new Vector2(10, 0),
Direction = FillDirection.Horizontal,
};
BeatmapSet = beatmapSet;
}
protected override void LoadComplete()
@@ -84,36 +56,70 @@ namespace osu.Game.Beatmaps.Drawables
private void updateDisplay()
{
flow.Clear();
foreach (var group in flow)
group.Alpha = 0;
if (beatmapSet == null)
{
foreach (var group in flow)
group.Beatmaps = [];
return;
}
// matching web: https://github.com/ppy/osu-web/blob/d06d8c5e735eb1f48799b1654b528e9a7afb0a35/resources/assets/lib/beatmapset-panel.tsx#L127
bool collapsed = beatmapSet.Beatmaps.Count() > 12;
bool collapsed = beatmapSet.Beatmaps.Count() > max_difficulties_before_collapsing;
foreach (var rulesetGrouping in beatmapSet.Beatmaps.GroupBy(beatmap => beatmap.Ruleset).OrderBy(group => group.Key))
{
flow.Add(new RulesetDifficultyGroup(rulesetGrouping.Key.OnlineID, rulesetGrouping, collapsed, dotSize)
int rulesetId = rulesetGrouping.Key.OnlineID;
var group = flow.SingleOrDefault(rg => rg.RulesetId == rulesetId);
if (group == null)
{
Spacing = new Vector2(DotSpacing, 0f),
});
group = new RulesetDifficultyGroup(rulesetId);
flow.Add(group);
flow.SetLayoutPosition(group, rulesetId);
}
group.Alpha = 1;
group.Beatmaps = rulesetGrouping.ToArray();
group.Collapsed = collapsed;
}
}
private partial class RulesetDifficultyGroup : FillFlowContainer
{
private readonly int rulesetId;
private readonly IEnumerable<IBeatmapInfo> beatmapInfos;
private readonly bool collapsed;
private readonly Vector2 dotSize;
public readonly int RulesetId;
public RulesetDifficultyGroup(int rulesetId, IEnumerable<IBeatmapInfo> beatmapInfos, bool collapsed, Vector2 dotSize)
private IBeatmapInfo[] beatmaps = [];
public IBeatmapInfo[] Beatmaps
{
this.rulesetId = rulesetId;
this.beatmapInfos = beatmapInfos;
this.collapsed = collapsed;
this.dotSize = dotSize;
set
{
beatmaps = value.OrderBy(bi => bi.StarRating).ToArray();
updateDisplay();
}
}
private bool collapsed;
public bool Collapsed
{
get => collapsed;
set
{
collapsed = value;
updateDisplay();
}
}
private OsuSpriteText countText = null!;
public RulesetDifficultyGroup(int rulesetId)
{
RulesetId = rulesetId;
}
[BackgroundDependencyLoader]
@@ -123,53 +129,83 @@ namespace osu.Game.Beatmaps.Drawables
Spacing = new Vector2(1, 0);
Direction = FillDirection.Horizontal;
var icon = rulesets.GetRuleset(rulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
var icon = rulesets.GetRuleset(RulesetId)?.CreateInstance().CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle };
Add(icon.With(i =>
{
i.Size = new Vector2(14);
i.Anchor = i.Origin = Anchor.Centre;
}));
if (!collapsed)
for (int i = 0; i < max_difficulties_before_collapsing; i++)
Add(new DifficultyDot());
Add(countText = new OsuSpriteText
{
foreach (var beatmapInfo in beatmapInfos.OrderBy(bi => bi.StarRating))
Add(new DifficultyDot(beatmapInfo.StarRating, dotSize));
}
else
Font = OsuFont.Default.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 1 }
});
}
protected override void LoadComplete()
{
base.LoadComplete();
updateDisplay();
}
private void updateDisplay()
{
countText.Alpha = collapsed ? 1 : 0;
countText.Text = beatmaps.Length.ToLocalisableString(@"N0");
var dots = this.OfType<DifficultyDot>().ToArray();
for (int i = 0; i < max_difficulties_before_collapsing; i++)
{
Add(new OsuSpriteText
var dot = dots[i];
if (collapsed || i >= beatmaps.Length)
{
Text = beatmapInfos.Count().ToLocalisableString(@"N0"),
Font = OsuFont.Default.With(size: 12),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Padding = new MarginPadding { Bottom = 1 }
});
dot.Alpha = 0;
continue;
}
dot.Alpha = 1;
dot.StarDifficulty = beatmaps[i].StarRating;
}
}
}
private partial class DifficultyDot : CircularContainer
private partial class DifficultyDot : Circle
{
private readonly double starDifficulty;
private double starDifficulty;
public DifficultyDot(double starDifficulty, Vector2 dotSize)
public double StarDifficulty
{
this.starDifficulty = starDifficulty;
Size = dotSize;
get => starDifficulty;
set
{
starDifficulty = value;
updateColour();
}
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Anchor = Origin = Anchor.Centre;
Masking = true;
[Resolved]
private OsuColour colours { get; set; } = null!;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.ForStarDifficulty(starDifficulty)
};
[BackgroundDependencyLoader]
private void load()
{
Size = new Vector2(5, 10);
Anchor = Origin = Anchor.Centre;
updateColour();
}
private void updateColour()
{
Colour = colours.ForStarDifficulty(starDifficulty);
}
}
}
@@ -79,7 +79,7 @@ namespace osu.Game.Beatmaps.Formats
protected override void ParseStreamInto(LineBufferedReader stream, Beatmap beatmap)
{
this.beatmap = beatmap;
this.beatmap.BeatmapInfo.BeatmapVersion = FormatVersion;
this.beatmap.BeatmapVersion = FormatVersion;
parser = new ConvertHitObjectParser(getOffsetTime(), FormatVersion);
ApplyLegacyDefaults(this.beatmap);
@@ -193,6 +193,10 @@ namespace osu.Game.Beatmaps.Formats
internal static void ApplyLegacyDefaults(Beatmap beatmap)
{
beatmap.WidescreenStoryboard = false;
// in a perfect world this would throw if osu! ruleset couldn't be found,
// but unfortunately there are "legitimate" cases where it's not there (i.e. ruleset test projects),
// so attempt to trudge on with whatever it is that's in `BeatmapInfo` if the lookup fails.
beatmap.BeatmapInfo.Ruleset = RulesetStore?.GetRuleset(0) ?? beatmap.BeatmapInfo.Ruleset;
}
protected override void ParseLine(Beatmap beatmap, Section section, string line)
+2
View File
@@ -109,6 +109,8 @@ namespace osu.Game.Beatmaps
int[] Bookmarks { get; internal set; }
int BeatmapVersion { get; }
/// <summary>
/// Creates a shallow-clone of this beatmap and returns it.
/// </summary>
+11 -1
View File
@@ -97,8 +97,9 @@ namespace osu.Game.Database
/// 45 2024-12-23 Change beat snap divisor adjust defaults to be Ctrl+Scroll instead of Ctrl+Shift+Scroll, if not already changed by user.
/// 46 2024-12-26 Change beat snap divisor bindings to match stable directionality ¯\_(ツ)_/¯.
/// 47 2025-01-21 Remove right mouse button binding for absolute scroll. Never use mouse buttons (or scroll) for global actions.
/// 48 2025-03-19 Clear online status for all qualified beatmaps (some were stuck in a qualified state due to local caching issues).
/// </summary>
private const int schema_version = 47;
private const int schema_version = 48;
/// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
@@ -1245,6 +1246,15 @@ namespace osu.Game.Database
break;
}
case 48:
const int qualified = (int)BeatmapOnlineStatus.Qualified;
var beatmaps = migration.NewRealm.All<BeatmapInfo>().Where(b => b.StatusInt == qualified);
foreach (var beatmap in beatmaps)
beatmap.ResetOnlineInfo(resetOnlineId: false);
break;
}
Logger.Log($"Migration completed in {stopwatch.ElapsedMilliseconds}ms");
+11 -2
View File
@@ -127,8 +127,6 @@ namespace osu.Game.Graphics.Backgrounds
{
base.Update();
Invalidate(Invalidation.DrawNode);
if (CreateNewTriangles)
addTriangles(false);
@@ -138,6 +136,10 @@ namespace osu.Game.Graphics.Backgrounds
: 1;
float elapsedSeconds = (float)Time.Elapsed / 1000;
if (elapsedSeconds == 0)
return;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
// Since we will later multiply by the scale of individual triangles we normalize by
// dividing by triangleScale.
@@ -157,6 +159,8 @@ namespace osu.Game.Graphics.Backgrounds
if (bottomPos < 0)
parts.RemoveAt(i);
}
Invalidate(Invalidation.DrawNode);
}
/// <summary>
@@ -183,8 +187,13 @@ namespace osu.Game.Graphics.Backgrounds
int currentCount = parts.Count;
if (AimCount - currentCount == 0)
return;
for (int i = 0; i < AimCount - currentCount; i++)
parts.Add(createTriangle(randomY));
Invalidate(Invalidation.DrawNode);
}
private TriangleParticle createTriangle(bool randomY)
+11 -2
View File
@@ -91,12 +91,14 @@ namespace osu.Game.Graphics.Backgrounds
{
base.Update();
Invalidate(Invalidation.DrawNode);
if (CreateNewTriangles)
addTriangles(false);
float elapsedSeconds = (float)Time.Elapsed / 1000;
if (elapsedSeconds == 0)
return;
// Since position is relative, the velocity needs to scale inversely with DrawHeight.
float movedDistance = -elapsedSeconds * Velocity * base_velocity / DrawHeight;
@@ -112,6 +114,8 @@ namespace osu.Game.Graphics.Backgrounds
if (bottomPos < 0)
parts.RemoveAt(i);
}
Invalidate(Invalidation.DrawNode);
}
/// <summary>
@@ -138,8 +142,13 @@ namespace osu.Game.Graphics.Backgrounds
int currentCount = parts.Count;
if (AimCount - currentCount == 0)
return;
for (int i = 0; i < AimCount - currentCount; i++)
parts.Add(createTriangle(randomY));
Invalidate(Invalidation.DrawNode);
}
private TriangleParticle createTriangle(bool randomY)
@@ -13,7 +13,7 @@ namespace osu.Game.Graphics.Containers.Markdown
public LocalisableString TooltipText { get; }
public OsuMarkdownImage(LinkInline linkInline)
: base(linkInline.Url)
: base($"https://osu.ppy.sh/media-url?url={linkInline.Url}")
{
TooltipText = linkInline.Title;
}
@@ -27,7 +27,7 @@ namespace osu.Game.Graphics.Containers
private partial class ArbitraryDrawableWrapper : Container, IHasLineBaseHeight
{
public float LineBaseHeight => DrawHeight;
public float LineBaseHeight => (Child as IHasLineBaseHeight)?.LineBaseHeight ?? DrawHeight;
public ArbitraryDrawableWrapper()
{
@@ -89,7 +89,7 @@ namespace osu.Game.Graphics.UserInterface
{
if (Link == null) return;
game?.CopyUrlToClipboard(Link);
game?.CopyToClipboard(Link);
}
}
}
@@ -53,7 +53,7 @@ namespace osu.Game.Graphics.UserInterface
#region OsuDropdownMenu
protected partial class OsuDropdownMenu : DropdownMenu
public partial class OsuDropdownMenu : DropdownMenu
{
public override bool HandleNonPositionalInput => State == MenuState.Open;
@@ -252,6 +252,7 @@ namespace osu.Game.Graphics.UserInterface
Size = new Vector2(8),
Alpha = 0,
X = chevron_offset,
Y = 1,
Margin = new MarginPadding { Left = 3, Right = 3 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
@@ -0,0 +1,308 @@
// 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.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterfaceV2
{
public partial class ShearedDropdown<T> : Dropdown<T>, IKeyBindingHandler<GlobalAction>
{
protected override DropdownHeader CreateHeader() => new ShearedDropdownHeader();
protected override DropdownMenu CreateMenu() => new ShearedDropdownMenu();
public ShearedDropdown(LocalisableString label)
{
if (Header is ShearedDropdownHeader osuHeader)
{
osuHeader.Dropdown = this;
osuHeader.LeftSideLabel = label;
}
}
protected override void Update()
{
base.Update();
var header = (ShearedDropdownHeader)Header;
var menu = (ShearedDropdownMenu)Menu;
menu.Padding = new MarginPadding { Left = header.LabelContainer.DrawWidth - 10f, Right = 6f };
}
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{
if (e.Repeat) return false;
if (e.Action == GlobalAction.Back)
return Back();
return false;
}
public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e)
{
}
protected partial class ShearedDropdownMenu : OsuDropdown<T>.OsuDropdownMenu
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public new MarginPadding Padding
{
get => base.Padding;
set => base.Padding = value;
}
public ShearedDropdownMenu()
{
Shear = shear;
Margin = new MarginPadding { Top = 5f };
}
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new ShearedMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
public partial class ShearedMenuItem : DrawableOsuDropdownMenuItem
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
public ShearedMenuItem(MenuItem item)
: base(item)
{
Foreground.Shear = -shear;
}
}
}
public partial class ShearedDropdownHeader : DropdownHeader
{
private const float corner_radius = 5f;
private LocalisableString label;
protected override LocalisableString Label
{
get => label;
set
{
label = value;
valueText.Text = value;
}
}
public LocalisableString LeftSideLabel
{
set => labelText.Text = value;
}
private readonly OsuSpriteText labelText;
private readonly OsuSpriteText valueText;
private readonly Box labelBox;
private readonly SpriteIcon chevron;
public Container LabelContainer { get; }
public ShearedDropdown<T> Dropdown = null!;
private ShearedDropdownSearchBar searchBar = null!;
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[Resolved]
private OverlayColourProvider colourProvider { get; set; } = null!;
public ShearedDropdownHeader()
{
Shear = shear;
CornerRadius = corner_radius;
Masking = true;
Foreground.Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension()
},
Content = new[]
{
new[]
{
LabelContainer = new Container
{
CornerRadius = corner_radius,
Masking = true,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
labelBox = new Box
{
RelativeSizeAxes = Axes.Both
},
labelText = new OsuSpriteText
{
Margin = new MarginPadding { Horizontal = 10f, Vertical = 8f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
Shear = -shear,
},
},
},
new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10f },
Shear = -shear,
Children = new Drawable[]
{
valueText = new TruncatingSpriteText
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Padding = new MarginPadding { Right = 15f },
Font = OsuFont.Torus.With(size: 16.8f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
},
chevron = new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Y = 1f,
Icon = FontAwesome.Solid.ChevronDown,
Size = new Vector2(10f),
}
},
},
}
}
},
};
AddInternal(LabelContainer.CreateProxy());
}
[BackgroundDependencyLoader]
private void load()
{
labelBox.Colour = colourProvider.Background3;
}
protected override void LoadComplete()
{
base.LoadComplete();
Dropdown.Menu.StateChanged += _ => updateChevron();
SearchBar.State.ValueChanged += _ => updateColour();
Enabled.BindValueChanged(_ => updateColour());
updateColour();
}
protected override void Update()
{
base.Update();
searchBar.Padding = new MarginPadding { Left = LabelContainer.DrawWidth };
// By limiting the width we avoid this box showing up as an outline around the drawables that are on top of it.
Background.Padding = new MarginPadding { Left = LabelContainer.DrawWidth - corner_radius };
}
protected override bool OnHover(HoverEvent e)
{
updateColour();
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
updateColour();
}
private void updateColour()
{
bool hovered = Enabled.Value && IsHovered;
var hoveredColour = colourProvider.Light4;
var unhoveredColour = colourProvider.Background5;
Colour = Color4.White;
Alpha = Enabled.Value ? 1 : 0.3f;
if (SearchBar.State.Value == Visibility.Visible)
{
chevron.Colour = hovered ? hoveredColour.Lighten(0.5f) : Colour4.White;
Background.Colour = unhoveredColour;
}
else
{
chevron.Colour = Color4.White;
Background.Colour = hovered ? hoveredColour : unhoveredColour;
}
}
private void updateChevron()
{
Debug.Assert(Dropdown != null);
bool open = Dropdown.Menu.State == MenuState.Open;
chevron.ScaleTo(open ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
}
protected override DropdownSearchBar CreateSearchBar() => searchBar = new ShearedDropdownSearchBar();
private partial class ShearedDropdownSearchBar : DropdownSearchBar
{
protected override void PopIn() => this.FadeIn();
protected override void PopOut() => this.FadeOut();
protected override TextBox CreateTextBox() => new DropdownSearchTextBox
{
FontSize = OsuFont.Default.Size,
};
private partial class DropdownSearchTextBox : OsuTextBox
{
private readonly Vector2 shear = new Vector2(OsuGame.SHEAR, 0);
[BackgroundDependencyLoader]
private void load(OverlayColourProvider? colourProvider)
{
TextContainer.Shear = -shear;
BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
}
protected override void OnFocus(FocusEvent e)
{
base.OnFocus(e);
BorderThickness = 0;
}
}
}
}
}
}
@@ -17,7 +17,13 @@ namespace osu.Game.Localisation.SkinComponents
/// <summary>
/// "Whether to show extended information for each mod."
/// </summary>
public static LocalisableString ShowExtendedInformationDescription => new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
public static LocalisableString ShowExtendedInformationDescription =>
new TranslatableString(getKey(@"whether_to_show_extended_information"), @"Whether to show extended information for each mod.");
/// <summary>
/// "Display direction"
/// </summary>
public static LocalisableString DisplayDirection => new TranslatableString(getKey(@"display_direction"), "Display direction");
/// <summary>
/// "Expansion mode"
+2 -2
View File
@@ -45,9 +45,9 @@ namespace osu.Game.Localisation
public static LocalisableString SkinSaved => new TranslatableString(getKey(@"skin_saved"), @"Skin saved");
/// <summary>
/// "Link copied to clipboard"
/// "Copied to clipboard"
/// </summary>
public static LocalisableString UrlCopied => new TranslatableString(getKey(@"url_copied"), @"Link copied to clipboard");
public static LocalisableString CopiedToClipboard => new TranslatableString(getKey(@"copied_to_clipboard"), @"Copied to clipboard");
/// <summary>
/// "Speed changed to {0:N2}x"
@@ -20,7 +20,7 @@ namespace osu.Game.Online.API.Requests
protected override WebRequest CreateWebRequest()
{
var req = base.CreateWebRequest();
req.Method = HttpMethod.Post;
req.Method = HttpMethod.Put;
return req;
}
@@ -109,6 +109,9 @@ namespace osu.Game.Online.API.Requests.Responses
public double BPM { get; set; }
[JsonProperty(@"owners")]
public BeatmapOwner[] BeatmapOwners { get; set; } = Array.Empty<BeatmapOwner>();
#region Implementation of IBeatmapInfo
public IBeatmapMetadataInfo Metadata => (BeatmapSet as IBeatmapSetInfo)?.Metadata ?? new BeatmapMetadata();
@@ -177,5 +180,14 @@ namespace osu.Game.Online.API.Requests.Responses
// ReSharper disable once NonReadonlyMemberInGetHashCode
public override int GetHashCode() => OnlineID;
}
public class BeatmapOwner
{
[JsonProperty(@"id")]
public int Id { get; set; }
[JsonProperty(@"username")]
public string Username { get; set; } = string.Empty;
}
}
}
@@ -128,6 +128,9 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"converts")]
public APIBeatmap[]? Converts { get; set; }
[JsonProperty(@"related_tags")]
public APITag[]? RelatedTags { get; set; }
private BeatmapMetadata metadata => new BeatmapMetadata
{
Title = Title,
@@ -9,6 +9,7 @@ using System.Linq;
using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Game.Extensions;
using osu.Game.Online.Metadata;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
@@ -111,8 +112,13 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"is_active")]
public bool Active;
/// <summary>
/// From osu-web's perspective, whether a user was recently online.
/// This doesn't imply the user is online in a lazer client (may be updated from stable or web browser).
/// Use <see cref="MetadataClient.GetPresence"/> for real-time lazer online status checks.
/// </summary>
[JsonProperty(@"is_online")]
public bool IsOnline;
public bool WasRecentlyOnline;
[JsonProperty(@"pm_friends_only")]
public bool PMFriendsOnly;
@@ -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.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Containers;
@@ -219,7 +220,7 @@ namespace osu.Game.Online.Leaderboards
}
};
string description = mod.SettingDescription;
string description = string.Join(", ", mod.SettingDescription.Select(svp => $"{svp.setting}: {svp.value}"));
if (!string.IsNullOrEmpty(description))
{
@@ -227,7 +228,7 @@ namespace osu.Game.Online.Leaderboards
{
RelativeSizeAxes = Axes.Y,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = mod.SettingDescription,
Text = description,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Top = 1 },
@@ -57,6 +57,9 @@ namespace osu.Game.Online.Metadata
/// <summary>
/// Attempts to retrieve the presence of a user.
/// </summary>
/// <remarks>
/// This will return data if the client is currently receiving presence data. See <see cref="BeginWatchingUserPresence"/>.
/// </remarks>
/// <param name="userId">The user ID.</param>
/// <returns>The user presence, or null if not available or the user's offline.</returns>
public UserPresence? GetPresence(int userId)
@@ -81,6 +81,18 @@ namespace osu.Game.Online.Multiplayer
Playlist = room.Playlist.Select(p => new MultiplayerPlaylistItem(p)).ToArray();
}
/// <summary>
/// Retrieves the active <see cref="MultiplayerPlaylistItem"/> as determined by the room's current settings.
/// </summary>
[IgnoreMember]
public MultiplayerPlaylistItem CurrentPlaylistItem => Playlist.Single(item => item.ID == Settings.PlaylistItemId);
/// <summary>
/// Determines whether a user is able to add playlist items to this room.
/// </summary>
/// <param name="user">The user to check.</param>
public bool CanAddPlaylistItems(MultiplayerRoomUser user) => user.Equals(Host) || Settings.QueueMode != QueueMode.HostOnly;
public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]";
}
}
@@ -44,6 +44,8 @@ namespace osu.Game.Online
(typeof(UserActivity.EditingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.ModdingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.TestingBeatmap), typeof(UserActivity)),
(typeof(UserActivity.InDailyChallengeLobby), typeof(UserActivity)),
(typeof(UserActivity.PlayingDailyChallenge), typeof(UserActivity)),
};
}
}
@@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
namespace osu.Game.Online
{
public sealed class TrustedDomainOnlineStore : OnlineStore
{
protected override string GetLookupUrl(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out Uri? uri) || !uri.Host.EndsWith(@".ppy.sh", StringComparison.OrdinalIgnoreCase))
{
Logger.Log($@"Blocking resource lookup from external website: {url}", LoggingTarget.Network, LogLevel.Important);
return string.Empty;
}
return url;
}
}
}
+3 -3
View File
@@ -519,10 +519,10 @@ namespace osu.Game
}
});
public void CopyUrlToClipboard(string url) => waitForReady(() => onScreenDisplay, _ =>
public void CopyToClipboard(string value) => waitForReady(() => onScreenDisplay, _ =>
{
dependencies.Get<Clipboard>().SetText(url);
onScreenDisplay.Display(new CopyUrlToast());
dependencies.Get<Clipboard>().SetText(value);
onScreenDisplay.Display(new CopiedToClipboardToast());
});
public void OpenUrlExternally(string url, LinkWarnMode warnMode = LinkWarnMode.Default) => waitForReady(() => externalLinkOpener, _ => externalLinkOpener.OpenUrlExternally(url, warnMode));
+3 -1
View File
@@ -108,6 +108,8 @@ namespace osu.Game
public virtual EndpointConfiguration CreateEndpoints() =>
UseDevelopmentServer ? new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration();
protected override OnlineStore CreateOnlineStore() => new TrustedDomainOnlineStore();
public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version();
/// <summary>
@@ -278,7 +280,7 @@ namespace osu.Game
dependencies.CacheAs(Storage);
var largeStore = new LargeTextureStore(Host.Renderer, Host.CreateTextureLoaderStore(new NamespacedResourceStore<byte[]>(Resources, @"Textures")));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(new OnlineStore()));
largeStore.AddTextureSource(Host.CreateTextureLoaderStore(CreateOnlineStore()));
dependencies.Cache(largeStore);
dependencies.CacheAs(LocalConfig);
+67 -71
View File
@@ -12,6 +12,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Extensions;
@@ -31,9 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet
private const float tile_icon_padding = 7;
private const float tile_spacing = 2;
private readonly OsuSpriteText version, starRating, starRatingText;
private readonly LinkFlowContainer guestMapperContainer;
private readonly FillFlowContainer starRatingContainer;
private readonly LinkFlowContainer infoContainer;
private readonly Statistic plays, favourites;
public readonly DifficultiesContainer Difficulties;
@@ -53,6 +52,9 @@ namespace osu.Game.Overlays.BeatmapSet
}
}
[Resolved]
private OsuColour colours { get; set; } = null!;
public BeatmapPicker()
{
RelativeSizeAxes = Axes.X;
@@ -72,59 +74,13 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = -(tile_icon_padding + tile_spacing / 2), Bottom = 10 },
OnLostHover = () =>
{
showBeatmap(Beatmap.Value);
starRatingContainer.FadeOut(100);
},
OnLostHover = () => showBeatmap(Beatmap.Value, withStarRating: false),
},
new FillFlowContainer
infoContainer = new LinkFlowContainer(t => t.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
{
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5f),
Children = new Drawable[]
{
version = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold)
},
guestMapperContainer = new LinkFlowContainer(s =>
s.Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 11))
{
AutoSizeAxes = Axes.Both,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Margin = new MarginPadding { Bottom = 1 },
},
starRatingContainer = new FillFlowContainer
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Alpha = 0,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0),
Margin = new MarginPadding { Bottom = 1 },
Children = new[]
{
starRatingText = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
Text = BeatmapsetsStrings.ShowStatsStars,
},
starRating = new OsuSpriteText
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold),
Text = string.Empty,
},
}
},
},
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
TextAnchor = Anchor.BottomLeft,
},
new FillFlowContainer
{
@@ -144,7 +100,7 @@ namespace osu.Game.Overlays.BeatmapSet
Beatmap.ValueChanged += b =>
{
showBeatmap(b.NewValue);
showBeatmap(b.NewValue, withStarRating: Difficulties.Any(d => d.IsHovered));
updateDifficultyButtons();
};
}
@@ -153,10 +109,8 @@ namespace osu.Game.Overlays.BeatmapSet
private IBindable<RulesetInfo> ruleset { get; set; } = null!;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
private void load()
{
starRating.Colour = colours.Yellow;
starRatingText.Colour = colours.Yellow;
updateDisplay();
}
@@ -185,16 +139,12 @@ namespace osu.Game.Overlays.BeatmapSet
State = DifficultySelectorState.NotSelected,
OnHovered = beatmap =>
{
showBeatmap(beatmap);
starRating.Text = beatmap.StarRating.FormatStarRating();
starRatingContainer.FadeIn(100);
showBeatmap(beatmap, withStarRating: true);
},
OnClicked = beatmap => { Beatmap.Value = beatmap; },
});
}
starRatingContainer.FadeOut(100);
// If a selection is already made, try and maintain it.
if (Beatmap.Value != null)
Beatmap.Value = Difficulties.FirstOrDefault(b => b.Beatmap.OnlineID == Beatmap.Value.OnlineID)?.Beatmap;
@@ -208,22 +158,68 @@ namespace osu.Game.Overlays.BeatmapSet
updateDifficultyButtons();
}
private void showBeatmap(APIBeatmap? beatmapInfo)
private void showBeatmap(APIBeatmap? beatmapInfo, bool withStarRating)
{
guestMapperContainer.Clear();
infoContainer.Clear();
if (beatmapInfo?.AuthorID != BeatmapSet?.AuthorID)
infoContainer.AddText(beatmapInfo?.DifficultyName ?? string.Empty, s => s.Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold));
infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5));
var beatmapOwners = beatmapInfo?.BeatmapOwners;
bool isHostDifficulty = beatmapOwners?.Length == 1 && beatmapOwners.First().Id == beatmapSet?.AuthorID;
if (beatmapOwners != null && !isHostDifficulty)
{
APIUser? user = BeatmapSet?.RelatedUsers?.SingleOrDefault(u => u.OnlineID == beatmapInfo?.AuthorID);
APIUser[] users = BeatmapSet?.RelatedUsers?.Where(u => beatmapOwners.Any(o => o.Id == u.OnlineID)).ToArray() ?? [];
int count = users.Length;
if (user != null)
switch (count)
{
guestMapperContainer.AddText("mapped by ");
guestMapperContainer.AddUserLink(user);
case 0:
break;
case 1:
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
infoContainer.AddUserLink(users[0]);
break;
case 2:
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
infoContainer.AddUserLink(users[0]);
infoContainer.AddText(CommonStrings.ArrayAndTwoWordsConnector);
infoContainer.AddUserLink(users[1]);
break;
default:
{
infoContainer.AddText(BeatmapsetsStrings.ShowDetailsMappedBy(string.Empty));
for (int i = 0; i < count; i++)
{
infoContainer.AddUserLink(users[i]);
if (i < count - 2)
infoContainer.AddText(CommonStrings.ArrayAndWordsConnector);
else if (i == count - 2)
infoContainer.AddText(CommonStrings.ArrayAndLastWordConnector);
}
break;
}
}
}
version.Text = beatmapInfo?.DifficultyName ?? string.Empty;
if (withStarRating)
{
infoContainer.AddArbitraryDrawable(Empty().With(e => e.Width = 5));
infoContainer.AddText(
LocalisableString.Interpolate($"{BeatmapsetsStrings.ShowStatsStars} {beatmapInfo?.StarRating.FormatStarRating()}"),
t =>
{
t.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Bold);
t.Colour = colours.Yellow;
});
}
}
private void updateDifficultyButtons()
@@ -98,7 +98,7 @@ namespace osu.Game.Overlays.BeatmapSet
{
Vertical = BeatmapSetOverlay.Y_PADDING,
Left = WaveOverlayContainer.HORIZONTAL_PADDING,
Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH,
Right = WaveOverlayContainer.HORIZONTAL_PADDING + BeatmapSetOverlay.RIGHT_WIDTH + 10,
},
Children = new Drawable[]
{
+43 -17
View File
@@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -9,6 +11,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing;
@@ -17,26 +20,22 @@ namespace osu.Game.Overlays.BeatmapSet
{
public partial class Info : Container
{
private const float metadata_width = 175;
private const float metadata_width = 185;
private const float spacing = 20;
private const float base_height = 220;
private const float base_height = 300;
private readonly Box successRateBackground;
private readonly Box background;
private readonly SuccessRate successRate;
private readonly MetadataSection<string[]?> userTags;
public readonly Bindable<APIBeatmapSet> BeatmapSet = new Bindable<APIBeatmapSet>();
public APIBeatmap? BeatmapInfo
{
get => successRate.Beatmap;
set => successRate.Beatmap = value;
}
public readonly Bindable<APIBeatmap> Beatmap = new Bindable<APIBeatmap>();
public Info()
{
SuccessRate successRate;
MetadataSectionNominators nominators;
MetadataSection source, tags;
MetadataSection source, mapperTags;
MetadataSectionGenre genre;
MetadataSectionLanguage language;
OsuSpriteText notRankedPlaceholder;
@@ -66,27 +65,30 @@ namespace osu.Game.Overlays.BeatmapSet
Child = new MetadataSectionDescription(),
},
},
new Container
new OsuScrollContainer
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = metadata_width,
Padding = new MarginPadding { Horizontal = 10 },
Padding = new MarginPadding { Left = 10 },
Margin = new MarginPadding { Right = BeatmapSetOverlay.RIGHT_WIDTH + spacing },
Masking = true,
ScrollbarOverlapsContent = false,
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Padding = new MarginPadding { Right = 5 },
Children = new Drawable[]
{
nominators = new MetadataSectionNominators(),
source = new MetadataSectionSource(),
genre = new MetadataSectionGenre { Width = 0.5f },
language = new MetadataSectionLanguage { Width = 0.5f },
tags = new MetadataSectionTags(),
userTags = new MetadataSectionUserTags(),
mapperTags = new MetadataSectionMapperTags(),
},
},
},
@@ -121,18 +123,42 @@ namespace osu.Game.Overlays.BeatmapSet
},
};
BeatmapSet.ValueChanged += b =>
BeatmapSet.BindValueChanged(b =>
{
nominators.Metadata = (b.NewValue?.CurrentNominations ?? Array.Empty<BeatmapSetOnlineNomination>(), b.NewValue?.RelatedUsers ?? Array.Empty<APIUser>());
source.Metadata = b.NewValue?.Source ?? string.Empty;
tags.Metadata = b.NewValue?.Tags ?? string.Empty;
mapperTags.Metadata = b.NewValue?.Tags ?? string.Empty;
updateUserTags();
genre.Metadata = b.NewValue?.Genre ?? new BeatmapSetOnlineGenre { Id = (int)SearchGenre.Unspecified };
language.Metadata = b.NewValue?.Language ?? new BeatmapSetOnlineLanguage { Id = (int)SearchLanguage.Unspecified };
bool setHasLeaderboard = b.NewValue?.Status > 0;
successRate.Alpha = setHasLeaderboard ? 1 : 0;
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;
Height = setHasLeaderboard ? 270 : base_height;
};
});
Beatmap.BindValueChanged(b =>
{
successRate.Beatmap = b.NewValue;
updateUserTags();
});
}
private void updateUserTags()
{
if (Beatmap.Value?.TopTags == null || Beatmap.Value.TopTags.Length == 0 || BeatmapSet.Value?.RelatedTags == null)
{
userTags.Metadata = null;
return;
}
var tagsById = BeatmapSet.Value.RelatedTags.ToDictionary(t => t.Id);
userTags.Metadata = Beatmap.Value.TopTags
.Select(t => (topTag: t, relatedTag: tagsById.GetValueOrDefault(t.TagId)))
.Where(t => t.relatedTag != null)
// see https://github.com/ppy/osu-web/blob/bb3bd2e7c6f84f26066df5ea20a81c77ec9bb60a/resources/js/beatmapsets-show/controller.ts#L103-L106 for sort criteria
.OrderByDescending(t => t.topTag.VoteCount)
.ThenBy(t => t.relatedTag!.Name)
.Select(t => t.relatedTag!.Name)
.ToArray();
}
[BackgroundDependencyLoader]
@@ -7,10 +7,10 @@ using osu.Game.Online.Chat;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionTags : MetadataSection
public partial class MetadataSectionMapperTags : MetadataSection
{
public MetadataSectionTags(Action<string>? searchAction = null)
: base(MetadataType.Tags, searchAction)
public MetadataSectionMapperTags(Action<string>? searchAction = null)
: base(MetadataType.MapperTags, searchAction)
{
}
@@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
namespace osu.Game.Overlays.BeatmapSet
{
public partial class MetadataSectionUserTags : MetadataSection<string[]?>
{
private readonly Action<string>? searchAction;
public MetadataSectionUserTags(Action<string>? searchAction = null)
: base(MetadataType.UserTags, null)
{
this.searchAction = searchAction;
}
protected override void AddMetadata(string[]? tags, LinkFlowContainer loaded)
{
if (tags == null)
return;
for (int i = 0; i <= tags.Length - 1; i++)
{
string tag = tags[i];
if (searchAction != null)
loaded.AddLink(tag, () => searchAction(tag));
else
loaded.AddLink(tag, LinkAction.SearchBeatmapSet, $@"tag=""""{tag}""""");
if (i != tags.Length - 1)
loaded.AddText(" ");
}
}
}
}
+6 -2
View File
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.ComponentModel;
using osu.Framework.Localisation;
using osu.Game.Resources.Localisation.Web;
@@ -8,8 +9,11 @@ namespace osu.Game.Overlays.BeatmapSet
{
public enum MetadataType
{
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoTags))]
Tags,
[Description("User Tags")] // TODO: use translated string after osu-resources update
UserTags,
[Description("Mapper Tags")] // TODO: use translated string after osu-resources update
MapperTags,
[LocalisableDescription(typeof(BeatmapsetsStrings), nameof(BeatmapsetsStrings.ShowInfoSource))]
Source,
@@ -249,6 +249,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
getScoresRequest = null;
noScoresPlaceholder.Hide();
noTeamPlaceholder.Hide();
notSupporterPlaceholder.Hide();
if (Beatmap.Value == null || Beatmap.Value.OnlineID <= 0 || (Beatmap.Value.Status <= BeatmapOnlineStatus.Pending))
{
@@ -271,9 +273,6 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
return;
}
noTeamPlaceholder.Hide();
notSupporterPlaceholder.Hide();
Show();
loading.Show();
+5 -6
View File
@@ -47,7 +47,10 @@ namespace osu.Game.Overlays
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{
info = new Info(),
info = new Info
{
Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
},
new ScoresContainer
{
Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap }
@@ -60,11 +63,7 @@ namespace osu.Game.Overlays
info.BeatmapSet.BindTo(beatmapSet);
comments.BeatmapSet.BindTo(beatmapSet);
Header.HeaderContent.Picker.Beatmap.ValueChanged += b =>
{
info.BeatmapInfo = b.NewValue;
ScrollFlow.ScrollToStart();
};
Header.HeaderContent.Picker.Beatmap.ValueChanged += b => ScrollFlow.ScrollToStart();
}
[BackgroundDependencyLoader]
+37 -8
View File
@@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
@@ -14,6 +15,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Framework.Screens;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
@@ -22,7 +24,10 @@ 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.Multiplayer;
using osu.Game.Resources.Localisation.Web;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osuTK;
using osuTK.Graphics;
using ChatStrings = osu.Game.Localisation.ChatStrings;
@@ -69,6 +74,12 @@ namespace osu.Game.Overlays.Chat
[Resolved]
private OsuColour colours { get; set; } = null!;
[Resolved]
private MultiplayerClient? multiplayerClient { get; set; }
[Resolved]
private IPerformFromScreenRunner? performer { get; set; }
[Resolved(canBeNull: true)]
private ChannelManager? chatManager { get; set; }
@@ -161,13 +172,10 @@ namespace osu.Game.Overlays.Chat
if (user.Equals(APIUser.SYSTEM_USER))
return Array.Empty<MenuItem>();
List<MenuItem> items = new List<MenuItem>
{
new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile)
};
if (user.Equals(api.LocalUser.Value))
return Array.Empty<MenuItem>();
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
List<MenuItem> items = new List<MenuItem>();
if (currentChannel?.Value != null)
{
@@ -177,8 +185,29 @@ namespace osu.Game.Overlays.Chat
}));
}
if (!user.Equals(api.LocalUser.Value))
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
items.Add(new OsuMenuItem(ContextMenuStrings.ViewProfile, MenuItemType.Highlighted, openUserProfile));
items.Add(new OsuMenuItem(UsersStrings.CardSendMessage, MenuItemType.Standard, openUserChannel));
// We should probably be checking against an online state here.
// But we can't use MetadataClient.GetPresence because we may not be requesting/receiving presences.
// This isn't really too bad worst case scenario the client will open spectator view and show the user as "offline".
{
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(ContextMenuStrings.SpectatePlayer, MenuItemType.Standard, () =>
{
performer?.PerformFromScreen(s => s.Push(new SoloSpectatorScreen(user)));
}));
if (multiplayerClient?.Room?.Users.All(u => u.UserID != user.Id) == true)
{
items.Add(new OsuMenuItem(ContextMenuStrings.InvitePlayer, MenuItemType.Standard, () => multiplayerClient.InvitePlayer(user.Id)));
}
}
items.Add(new OsuMenuItemSpacer());
items.Add(new OsuMenuItem(UsersStrings.ReportButtonText, MenuItemType.Destructive, ReportRequested));
return items.ToArray();
}
@@ -1,13 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -19,7 +22,7 @@ using osuTK;
namespace osu.Game.Overlays.Comments
{
public partial class CommentReportButton : CompositeDrawable, IHasPopover
public partial class CommentReportButton : CompositeDrawable, IHasPopover, IHasLineBaseHeight
{
private readonly Comment comment;
@@ -88,5 +91,7 @@ namespace osu.Game.Overlays.Comments
api.Queue(request);
}
public float LineBaseHeight => link.ChildrenOfType<IHasLineBaseHeight>().FirstOrDefault()?.LineBaseHeight ?? DrawHeight;
}
}
@@ -420,7 +420,7 @@ namespace osu.Game.Overlays.Comments
private void copyUrl()
{
clipboard.SetText($@"{api.Endpoints.APIUrl}/comments/{Comment.Id}");
onScreenDisplay?.Display(new CopyUrlToast());
onScreenDisplay?.Display(new CopiedToClipboardToast());
}
private void toggleReply()
@@ -107,12 +107,7 @@ namespace osu.Game.Overlays.MedalSplash
},
};
description.AddText(medal.Description, s =>
{
s.Anchor = Anchor.TopCentre;
s.Origin = Anchor.TopCentre;
s.Font = s.Font.With(size: 16);
});
description.AddText(medal.Description, s => s.Font = s.Font.With(size: 16));
medalContainer.OnLoadComplete += _ =>
{
@@ -177,15 +177,18 @@ namespace osu.Game.Overlays.Mods
bpmDisplay.Current.Value = FormatUtils.RoundBPM(BeatmapInfo.Value.BPM, rate);
BeatmapDifficulty originalDifficulty = new BeatmapDifficulty(BeatmapInfo.Value.Difficulty);
BeatmapDifficulty adjustedDifficulty = new BeatmapDifficulty(originalDifficulty);
foreach (var mod in Mods.Value.OfType<IApplicableToDifficulty>())
mod.ApplyToDifficulty(originalDifficulty);
mod.ApplyToDifficulty(adjustedDifficulty);
Ruleset ruleset = GameRuleset.Value.CreateInstance();
BeatmapDifficulty adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(originalDifficulty, rate);
adjustedDifficulty = ruleset.GetRateAdjustedDisplayDifficulty(adjustedDifficulty, rate);
TooltipContent = new AdjustedAttributesTooltip.Data(originalDifficulty, adjustedDifficulty);
circleSizeDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.CircleSize, adjustedDifficulty.CircleSize);
drainRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.DrainRate, adjustedDifficulty.DrainRate);
approachRateDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.ApproachRate, adjustedDifficulty.ApproachRate);
overallDifficultyDisplay.AdjustType.Value = VerticalAttributeDisplay.CalculateEffect(originalDifficulty.OverallDifficulty, adjustedDifficulty.OverallDifficulty);

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