1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-12 01:22:59 +08:00

Merge branch 'pp-dev' into match-my-freak-sliders

This commit is contained in:
danielthirtle 2024-12-22 17:01:11 +13:00
commit b0059d6f7a
289 changed files with 4955 additions and 1942 deletions

View File

@ -114,7 +114,10 @@ jobs:
dotnet-version: "8.0.x" dotnet-version: "8.0.x"
- name: Install .NET workloads - name: Install .NET workloads
run: dotnet workload install android # since windows image 20241113.3.0, not specifying a version here
# installs the .NET 7 version of android workload for very unknown reasons.
# revisit once we upgrade to .NET 9, it's probably fixed there.
run: dotnet workload install android --version (dotnet --version)
- name: Compile - name: Compile
run: dotnet build -c Debug osu.Android.slnf run: dotnet build -c Debug osu.Android.slnf

View File

@ -115,7 +115,7 @@ jobs:
steps: steps:
- name: Check permissions - name: Check permissions
run: | run: |
ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte) ALLOWED_USERS=(smoogipoo peppy bdach frenzibyte tsunyoku stanriders)
for i in "${ALLOWED_USERS[@]}"; do for i in "${ALLOWED_USERS[@]}"; do
if [[ "${{ github.actor }}" == "$i" ]]; then if [[ "${{ github.actor }}" == "$i" ]]; then
exit 0 exit 0

View File

@ -1,57 +0,0 @@
# .NET Code Style
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
# IDE0001: Simplify names
dotnet_diagnostic.IDE0001.severity = warning
# IDE0002: Simplify member access
dotnet_diagnostic.IDE0002.severity = warning
# IDE0003: Remove qualification
dotnet_diagnostic.IDE0003.severity = warning
# IDE0004: Remove unnecessary cast
dotnet_diagnostic.IDE0004.severity = warning
# IDE0005: Remove unnecessary imports
dotnet_diagnostic.IDE0005.severity = warning
# IDE0034: Simplify default literal
dotnet_diagnostic.IDE0034.severity = warning
# IDE0036: Sort modifiers
dotnet_diagnostic.IDE0036.severity = warning
# IDE0040: Add accessibility modifier
dotnet_diagnostic.IDE0040.severity = warning
# IDE0049: Use keyword for type name
dotnet_diagnostic.IDE0040.severity = warning
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning
# IDE0051: Private method is unused
dotnet_diagnostic.IDE0051.severity = silent
# IDE0052: Private member is unused
dotnet_diagnostic.IDE0052.severity = silent
# IDE0073: File header
dotnet_diagnostic.IDE0073.severity = warning
# IDE0130: Namespace mismatch with folder
dotnet_diagnostic.IDE0130.severity = warning
# IDE1006: Naming style
dotnet_diagnostic.IDE1006.severity = warning
#Disable operator overloads requiring alternate named methods
dotnet_diagnostic.CA2225.severity = none
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
dotnet_diagnostic.OSUF001.severity = none

View File

@ -14,10 +14,6 @@ M:Realms.CollectionExtensions.SubscribeForNotifications`1(System.Collections.Gen
M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks. M:System.Threading.Tasks.Task.Wait();Don't use Task.Wait. Use Task.WaitSafely() to ensure we avoid deadlocks.
P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks. P:System.Threading.Tasks.Task`1.Result;Don't use Task.Result. Use Task.GetResultSafely() to ensure we avoid deadlocks.
M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever. M:System.Threading.ManualResetEventSlim.Wait();Specify a timeout to avoid waiting forever.
M:System.Char.ToLower(System.Char);char.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.Char.ToUpper(System.Char);char.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use char.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture.
M:System.String.ToLower();string.ToLower() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToLowerInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:System.String.ToUpper();string.ToUpper() changes behaviour depending on CultureInfo.CurrentCulture. Use string.ToUpperInvariant() instead. If wanting culture-sensitive behaviour, explicitly provide CultureInfo.CurrentCulture or use LocalisableString.
M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead. M:Humanizer.InflectorExtensions.Pascalize(System.String);Humanizer's .Pascalize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToPascalCase() instead.
M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead. M:Humanizer.InflectorExtensions.Camelize(System.String);Humanizer's .Camelize() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToCamelCase() instead.
M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead. M:Humanizer.InflectorExtensions.Underscore(System.String);Humanizer's .Underscore() extension method changes behaviour depending on CultureInfo.CurrentCulture. Use StringDehumanizeExtensions.ToSnakeCase() instead.

View File

@ -0,0 +1,109 @@
# .NET Code Style
# IDE styles reference: https://docs.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/
is_global = true
# IDE0001: Simplify names
dotnet_diagnostic.IDE0001.severity = warning
# IDE0002: Simplify member access
dotnet_diagnostic.IDE0002.severity = warning
# IDE0003: Remove qualification
dotnet_diagnostic.IDE0003.severity = warning
# IDE0004: Remove unnecessary cast
dotnet_diagnostic.IDE0004.severity = warning
# IDE0005: Remove unnecessary imports
dotnet_diagnostic.IDE0005.severity = warning
# IDE0034: Simplify default literal
dotnet_diagnostic.IDE0034.severity = warning
# IDE0036: Sort modifiers
dotnet_diagnostic.IDE0036.severity = warning
# IDE0040: Add accessibility modifier
dotnet_diagnostic.IDE0040.severity = warning
# IDE0049: Use keyword for type name
dotnet_diagnostic.IDE0040.severity = warning
# IDE0055: Fix formatting
dotnet_diagnostic.IDE0055.severity = warning
# IDE0051: Private method is unused
dotnet_diagnostic.IDE0051.severity = silent
# IDE0052: Private member is unused
dotnet_diagnostic.IDE0052.severity = silent
# IDE0073: File header
dotnet_diagnostic.IDE0073.severity = warning
# IDE0130: Namespace mismatch with folder
dotnet_diagnostic.IDE0130.severity = warning
# IDE1006: Naming style
dotnet_diagnostic.IDE1006.severity = warning
# CA1305: Specify IFormatProvider
# Too many noisy warnings for parsing/formatting numbers
dotnet_diagnostic.CA1305.severity = none
# CA1507: Use nameof to express symbol names
# Flaggs serialization name attributes
dotnet_diagnostic.CA1507.severity = suggestion
# CA1806: Do not ignore method results
# The usages for numeric parsing are explicitly optional
dotnet_diagnostic.CA1806.severity = suggestion
# CA1822: Mark members as static
# Potential false positive around reflection/too much noise
dotnet_diagnostic.CA1822.severity = none
# CA1826: Do not use Enumerable method on indexable collections
dotnet_diagnostic.CA1826.severity = suggestion
# CA1859: Use concrete types when possible for improved performance
# Involves design considerations
dotnet_diagnostic.CA1859.severity = suggestion
# CA1860: Avoid using 'Enumerable.Any()' extension method
dotnet_diagnostic.CA1860.severity = suggestion
# CA1861: Avoid constant arrays as arguments
# Outdated with collection expressions
dotnet_diagnostic.CA1861.severity = suggestion
# CA2007: Consider calling ConfigureAwait on the awaited task
dotnet_diagnostic.CA2007.severity = warning
# CA2016: Forward the 'CancellationToken' parameter to methods
# Some overloads are having special handling for debugger
dotnet_diagnostic.CA2016.severity = suggestion
# CA2021: Do not call Enumerable.Cast<T> or Enumerable.OfType<T> with incompatible types
# Causing a lot of false positives with generics
dotnet_diagnostic.CA2021.severity = none
# CA2101: Specify marshaling for P/Invoke string arguments
# Reports warning for all non-UTF16 usages on DllImport; consider migrating to LibraryImport
dotnet_diagnostic.CA2101.severity = none
# CA2201: Do not raise reserved exception types
dotnet_diagnostic.CA2201.severity = warning
# CA2208: Instantiate argument exceptions correctly
dotnet_diagnostic.CA2208.severity = suggestion
# CA2242: Test for NaN correctly
dotnet_diagnostic.CA2242.severity = warning
# Banned APIs
dotnet_diagnostic.RS0030.severity = error
# Temporarily disable analysing CanBeNull = true in NRT contexts due to mobile issues.
# See: https://github.com/ppy/osu/pull/19677
dotnet_diagnostic.OSUF001.severity = none

View File

@ -1,58 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0">
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
<Rule Id="CA1016" Action="None" />
<Rule Id="CA1028" Action="None" />
<Rule Id="CA1031" Action="None" />
<Rule Id="CA1034" Action="None" />
<Rule Id="CA1036" Action="None" />
<Rule Id="CA1040" Action="None" />
<Rule Id="CA1044" Action="None" />
<Rule Id="CA1051" Action="None" />
<Rule Id="CA1054" Action="None" />
<Rule Id="CA1056" Action="None" />
<Rule Id="CA1062" Action="None" />
<Rule Id="CA1063" Action="None" />
<Rule Id="CA1067" Action="None" />
<Rule Id="CA1707" Action="None" />
<Rule Id="CA1710" Action="None" />
<Rule Id="CA1714" Action="None" />
<Rule Id="CA1716" Action="None" />
<Rule Id="CA1717" Action="None" />
<Rule Id="CA1720" Action="None" />
<Rule Id="CA1721" Action="None" />
<Rule Id="CA1724" Action="None" />
<Rule Id="CA1801" Action="None" />
<Rule Id="CA1806" Action="None" />
<Rule Id="CA1812" Action="None" />
<Rule Id="CA1814" Action="None" />
<Rule Id="CA1815" Action="None" />
<Rule Id="CA1819" Action="None" />
<Rule Id="CA1822" Action="None" />
<Rule Id="CA1823" Action="None" />
<Rule Id="CA2007" Action="Warning" />
<Rule Id="CA2214" Action="None" />
<Rule Id="CA2227" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.CodeQuality.CSharp.Analyzers" RuleNamespace="Microsoft.CodeQuality.CSharp.Analyzers">
<Rule Id="CA1001" Action="None" />
<Rule Id="CA1032" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.NetCore.Analyzers" RuleNamespace="Microsoft.NetCore.Analyzers">
<Rule Id="CA1303" Action="None" />
<Rule Id="CA1304" Action="None" />
<Rule Id="CA1305" Action="None" />
<Rule Id="CA1307" Action="None" />
<Rule Id="CA1308" Action="None" />
<Rule Id="CA1816" Action="None" />
<Rule Id="CA1826" Action="None" />
<Rule Id="CA2000" Action="None" />
<Rule Id="CA2008" Action="None" />
<Rule Id="CA2213" Action="None" />
<Rule Id="CA2235" Action="None" />
</Rules>
<Rules AnalyzerId="Microsoft.NetCore.CSharp.Analyzers" RuleNamespace="Microsoft.NetCore.CSharp.Analyzers">
<Rule Id="CA1309" Action="Warning" />
<Rule Id="CA2201" Action="Warning" />
</Rules>
</RuleSet>

View File

@ -18,9 +18,21 @@
<ItemGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" /> <PackageReference Include="Microsoft.CodeAnalysis.BannedApiAnalyzers" Version="3.3.4" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" /> <AdditionalFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\BannedSymbols.txt" />
<!-- Rider compatibility: .globalconfig needs to be explicitly referenced instead of using the global file name. -->
<GlobalAnalyzerConfigFiles Include="$(MSBuildThisFileDirectory)CodeAnalysis\osu.globalconfig" />
</ItemGroup> </ItemGroup>
<PropertyGroup Label="Code Analysis"> <PropertyGroup Label="Code Analysis">
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset</CodeAnalysisRuleSet> <AnalysisMode>Default</AnalysisMode>
<AnalysisModeDesign>Default</AnalysisModeDesign>
<AnalysisModeDocumentation>Recommended</AnalysisModeDocumentation>
<AnalysisModeGlobalization>Recommended</AnalysisModeGlobalization>
<AnalysisModeInteroperability>Recommended</AnalysisModeInteroperability>
<AnalysisModeMaintainability>Recommended</AnalysisModeMaintainability>
<AnalysisModeNaming>Default</AnalysisModeNaming>
<AnalysisModePerformance>Minimum</AnalysisModePerformance>
<AnalysisModeReliability>Recommended</AnalysisModeReliability>
<AnalysisModeSecurity>Default</AnalysisModeSecurity>
<AnalysisModeUsage>Default</AnalysisModeUsage>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Documentation"> <PropertyGroup Label="Documentation">
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>

View File

@ -10,7 +10,7 @@
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk> <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.1118.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2024.1206.0" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
<!-- Fody does not handle Android build well, and warns when unchanged. <!-- Fody does not handle Android build well, and warns when unchanged.

View File

@ -15,6 +15,7 @@ using osu.Framework.Threading;
using osu.Game; using osu.Game;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
@ -47,6 +48,9 @@ namespace osu.Desktop
[Resolved] [Resolved]
private MultiplayerClient multiplayerClient { get; set; } = null!; private MultiplayerClient multiplayerClient { get; set; } = null!;
[Resolved]
private LocalUserStatisticsProvider statisticsProvider { get; set; } = null!;
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } = null!; private OsuConfigManager config { get; set; } = null!;
@ -117,7 +121,9 @@ namespace osu.Desktop
status.BindValueChanged(_ => schedulePresenceUpdate()); status.BindValueChanged(_ => schedulePresenceUpdate());
activity.BindValueChanged(_ => schedulePresenceUpdate()); activity.BindValueChanged(_ => schedulePresenceUpdate());
privacyMode.BindValueChanged(_ => schedulePresenceUpdate()); privacyMode.BindValueChanged(_ => schedulePresenceUpdate());
multiplayerClient.RoomUpdated += onRoomUpdated; multiplayerClient.RoomUpdated += onRoomUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
} }
private void onReady(object _, ReadyMessage __) private void onReady(object _, ReadyMessage __)
@ -133,6 +139,8 @@ namespace osu.Desktop
private void onRoomUpdated() => schedulePresenceUpdate(); private void onRoomUpdated() => schedulePresenceUpdate();
private void onStatisticsUpdated(UserStatisticsUpdate _) => schedulePresenceUpdate();
private ScheduledDelegate? presenceUpdateDelegate; private ScheduledDelegate? presenceUpdateDelegate;
private void schedulePresenceUpdate() private void schedulePresenceUpdate()
@ -167,7 +175,7 @@ namespace osu.Desktop
presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation)); presence.State = clampLength(activity.Value.GetStatus(hideIdentifiableInformation));
presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty); presence.Details = clampLength(activity.Value.GetDetails(hideIdentifiableInformation) ?? string.Empty);
if (getBeatmapID(activity.Value) is int beatmapId && beatmapId > 0) if (activity.Value.GetBeatmapID(hideIdentifiableInformation) is int beatmapId && beatmapId > 0)
{ {
presence.Buttons = new[] presence.Buttons = new[]
{ {
@ -229,10 +237,8 @@ namespace osu.Desktop
presence.Assets.LargeImageText = string.Empty; presence.Assets.LargeImageText = string.Empty;
else else
{ {
if (user.Value.RulesetsStatistics != null && user.Value.RulesetsStatistics.TryGetValue(ruleset.Value.ShortName, out UserStatistics? statistics)) var statistics = statisticsProvider.GetStatisticsFor(ruleset.Value);
presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty); presence.Assets.LargeImageText = $"{user.Value.Username}" + (statistics?.GlobalRank > 0 ? $" (rank #{statistics.GlobalRank:N0})" : string.Empty);
else
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
} }
// small image // small image
@ -327,25 +333,14 @@ namespace osu.Desktop
return true; return true;
} }
private static int? getBeatmapID(UserActivity activity)
{
switch (activity)
{
case UserActivity.InGame game:
return game.BeatmapID;
case UserActivity.EditingBeatmap edit:
return edit.BeatmapID;
}
return null;
}
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)
{ {
if (multiplayerClient.IsNotNull()) if (multiplayerClient.IsNotNull())
multiplayerClient.RoomUpdated -= onRoomUpdated; multiplayerClient.RoomUpdated -= onRoomUpdated;
if (statisticsProvider.IsNotNull())
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
client.Dispose(); client.Dispose();
base.Dispose(isDisposing); base.Dispose(isDisposing);
} }

View File

@ -99,7 +99,7 @@ namespace osu.Desktop
var hostOptions = new HostOptions var hostOptions = new HostOptions
{ {
IPCPort = !tournamentClient ? OsuGame.IPC_PORT : null, IPCPipeName = !tournamentClient ? OsuGame.IPC_PIPE_NAME : null,
FriendlyGameName = OsuGameBase.GAME_NAME, FriendlyGameName = OsuGameBase.GAME_NAME,
}; };

View File

@ -148,15 +148,7 @@ namespace osu.Desktop.Windows
foreach (var association in uri_associations) foreach (var association in uri_associations)
association.UpdateDescription(getLocalisedString(association.Description)); association.UpdateDescription(getLocalisedString(association.Description));
string getLocalisedString(LocalisableString s) string getLocalisedString(LocalisableString s) => localisation?.GetLocalisedString(s) ?? s.ToString();
{
if (localisation == null)
return s.ToString();
var b = localisation.GetLocalisedBindableString(s);
b.UnbindAll();
return b.Value;
}
} }
#region Native interop #region Native interop

View File

@ -4,28 +4,54 @@
using System.Collections.Generic; using System.Collections.Generic;
using BenchmarkDotNet.Attributes; using BenchmarkDotNet.Attributes;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Objects; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Benchmarks namespace osu.Game.Benchmarks
{ {
public class BenchmarkUnstableRate : BenchmarkTest public class BenchmarkUnstableRate : BenchmarkTest
{ {
private List<HitEvent> events = null!; private readonly List<List<HitEvent>> incrementalEventLists = new List<List<HitEvent>>();
public override void SetUp() public override void SetUp()
{ {
base.SetUp(); base.SetUp();
events = new List<HitEvent>();
for (int i = 0; i < 1000; i++) var events = new List<HitEvent>();
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, new HitObject(), null, null));
for (int i = 0; i < 2048; i++)
{
// Ensure the object has hit windows populated.
var hitObject = new HitCircle();
hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
events.Add(new HitEvent(RNG.NextDouble(-200.0, 200.0), RNG.NextDouble(1.0, 2.0), HitResult.Great, hitObject, null, null));
incrementalEventLists.Add(new List<HitEvent>(events));
}
} }
[Benchmark] [Benchmark]
public void CalculateUnstableRate() public void CalculateUnstableRate()
{ {
_ = events.CalculateUnstableRate(); for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
_ = events.CalculateUnstableRate();
}
}
[Benchmark]
public void CalculateUnstableRateUsingIncrementalCalculation()
{
HitEventExtensions.UnstableRateCalculationResult? last = null;
for (int i = 0; i < 2048; i++)
{
var events = incrementalEventLists[i];
last = events.CalculateUnstableRate(last);
}
} }
} }
} }

View File

@ -10,10 +10,12 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components; using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -54,6 +56,12 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
[Resolved] [Resolved]
private EditorBeatmap? editorBeatmap { get; set; } private EditorBeatmap? editorBeatmap { get; set; }
[Resolved]
private IEditorChangeHandler? changeHandler { get; set; }
[Resolved]
private BindableBeatDivisor? beatDivisor { get; set; }
public JuiceStreamSelectionBlueprint(JuiceStream hitObject) public JuiceStreamSelectionBlueprint(JuiceStream hitObject)
: base(hitObject) : base(hitObject)
{ {
@ -119,6 +127,20 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
return base.OnMouseDown(e); return base.OnMouseDown(e);
} }
protected override bool OnKeyDown(KeyDownEvent e)
{
if (!IsSelected)
return false;
if (e.Key == Key.F && e.ControlPressed && e.ShiftPressed)
{
convertToStream();
return true;
}
return false;
}
private void onDefaultsApplied(HitObject _) private void onDefaultsApplied(HitObject _)
{ {
computeObjectBounds(); computeObjectBounds();
@ -168,6 +190,50 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
lastSliderPathVersion = HitObject.Path.Version.Value; lastSliderPathVersion = HitObject.Path.Version.Value;
} }
// duplicated in `SliderSelectionBlueprint.convertToStream()`
// consider extracting common helper when applying changes here
private void convertToStream()
{
if (editorBeatmap == null || beatDivisor == null)
return;
var timingPoint = editorBeatmap.ControlPointInfo.TimingPointAt(HitObject.StartTime);
double streamSpacing = timingPoint.BeatLength / beatDivisor.Value;
changeHandler?.BeginChange();
int i = 0;
double time = HitObject.StartTime;
while (!Precision.DefinitelyBigger(time, HitObject.GetEndTime(), 1))
{
// positionWithRepeats is a fractional number in the range of [0, HitObject.SpanCount()]
// and indicates how many fractional spans of a slider have passed up to time.
double positionWithRepeats = (time - HitObject.StartTime) / HitObject.Duration * HitObject.SpanCount();
double pathPosition = positionWithRepeats - (int)positionWithRepeats;
// every second span is in the reverse direction - need to reverse the path position.
if (positionWithRepeats % 2 >= 1)
pathPosition = 1 - pathPosition;
float fruitXValue = HitObject.OriginalX + HitObject.Path.PositionAt(pathPosition).X;
editorBeatmap.Add(new Fruit
{
StartTime = time,
OriginalX = fruitXValue,
NewCombo = i == 0 && HitObject.NewCombo,
Samples = HitObject.Samples.Select(s => s.With()).ToList()
});
i += 1;
time = HitObject.StartTime + i * streamSpacing;
}
editorBeatmap.Remove(HitObject);
changeHandler?.EndChange();
}
private IEnumerable<MenuItem> getContextMenuItems() private IEnumerable<MenuItem> getContextMenuItems()
{ {
yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () => yield return new OsuMenuItem("Add vertex", MenuItemType.Standard, () =>
@ -177,6 +243,11 @@ namespace osu.Game.Rulesets.Catch.Edit.Blueprints
{ {
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft)) Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.MouseLeft))
}; };
yield return new OsuMenuItem("Convert to stream", MenuItemType.Destructive, convertToStream)
{
Hotkey = new Hotkey(new KeyCombination(InputKey.Control, InputKey.Shift, InputKey.F))
};
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
{ {
Caption = "Use special (N+1) style", Caption = "Use special (N+1) style",
HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.", HintText = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle } Current = { Value = Beatmap.SpecialStyle }
}, },
healthDrainSlider = new FormSliderBar<float> healthDrainSlider = new FormSliderBar<float>
{ {
@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
// for now, update these on commit rather than making BeatmapMetadata bindables. // for now, update these on commit rather than making BeatmapMetadata bindables.
// after switching database engines we can reconsider if switching to bindables is a good direction. // after switching database engines we can reconsider if switching to bindables is a good direction.
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value; Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value; Beatmap.SpecialStyle = specialStyle.Current.Value;
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;

View File

@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 1: return colour_cyan; case 1: return colour_cyan;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 3: case 3:
@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 2: return colour_cyan; case 2: return colour_cyan;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 4: case 4:
@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 3: return colour_purple; case 3: return colour_purple;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 5: case 5:
@ -206,7 +206,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 4: return colour_cyan; case 4: return colour_cyan;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 6: case 6:
@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 5: return colour_pink; case 5: return colour_pink;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 7: case 7:
@ -244,7 +244,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 6: return colour_pink; case 6: return colour_pink;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 8: case 8:
@ -266,7 +266,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 7: return colour_purple; case 7: return colour_purple;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 9: case 9:
@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 8: return colour_purple; case 8: return colour_purple;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
case 10: case 10:
@ -316,7 +316,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 9: return colour_purple; case 9: return colour_purple;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
} }
@ -339,7 +339,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Argon
case 5: return colour_green; case 5: return colour_green;
default: throw new ArgumentOutOfRangeException(); default: throw new ArgumentOutOfRangeException(nameof(columnIndex));
} }
} }
} }

View File

@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy
private Drawable getResult(HitResult result) private Drawable getResult(HitResult result)
{ {
if (!hit_result_mapping.ContainsKey(result)) if (!hit_result_mapping.TryGetValue(result, out var value))
return null; return null;
string filename = this.GetManiaSkinConfig<string>(hit_result_mapping[result])?.Value string filename = this.GetManiaSkinConfig<string>(value)?.Value
?? default_hit_result_skin_filenames[result]; ?? default_hit_result_skin_filenames[result];
var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d); var animation = this.GetAnimation(filename, true, true, frameLength: 1000 / 20d);

View File

@ -231,6 +231,36 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2)); AddAssert("slider still has 2 anchors", () => secondSlider.Path.ControlPoints.Count, () => Is.EqualTo(2));
} }
[Test]
public void TestControlClickDoesNotDiscardExistingSelectionEvenIfNothingHit()
{
var firstSlider = new Slider
{
StartTime = 0,
Position = new Vector2(0, 0),
Path = new SliderPath
{
ControlPoints =
{
new PathControlPoint(),
new PathControlPoint(new Vector2(100))
}
}
};
AddStep("add object", () => EditorBeatmap.AddRange([firstSlider]));
AddStep("select first slider", () => EditorBeatmap.SelectedHitObjects.AddRange([firstSlider]));
AddStep("move mouse to middle of playfield", () => InputManager.MoveMouseTo(blueprintContainer.ScreenSpaceDrawQuad.Centre));
AddStep("control-click left mouse", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.Click(MouseButton.Left);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddAssert("selection preserved", () => EditorBeatmap.SelectedHitObjects.Count, () => Is.EqualTo(1));
}
private ComposeBlueprintContainer blueprintContainer private ComposeBlueprintContainer blueprintContainer
=> Editor.ChildrenOfType<ComposeBlueprintContainer>().First(); => Editor.ChildrenOfType<ComposeBlueprintContainer>().First();

View File

@ -191,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
private void gridSizeIs(int size) private void gridSizeIs(int size)
=> AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size) => AddAssert($"grid size is {size}", () => this.ChildrenOfType<RectangularPositionSnapGrid>().Single().Spacing.Value == new Vector2(size)
&& EditorBeatmap.BeatmapInfo.GridSize == size); && EditorBeatmap.GridSize == size);
[Test] [Test]
public void TestGridTypeToggling() public void TestGridTypeToggling()

View File

@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
}) })
} }
}, },
BeatmapInfo = StackLeniency = 0,
{
StackLeniency = 0,
}
}, },
ReplayFrames = new List<ReplayFrame> ReplayFrames = new List<ReplayFrame>
{ {

View File

@ -74,12 +74,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
BeatmapInfo = new BeatmapInfo BeatmapInfo = new BeatmapInfo
{ {
StackLeniency = 0,
Difficulty = new BeatmapDifficulty Difficulty = new BeatmapDifficulty
{ {
ApproachRate = 8.5f ApproachRate = 8.5f
} }
}, },
StackLeniency = 0,
ControlPointInfo = controlPointInfo ControlPointInfo = controlPointInfo
}; };

View File

@ -15,20 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu.Tests";
[TestCase(6.7171144000821119d, 239, "diffcalc-test")] [TestCase(6.718709884850683d, 239, "diffcalc-test")]
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
[TestCase(0.14143808967817237d, 2, "nan-slider")] [TestCase(0.14143808967817237d, 2, "nan-slider")]
public void Test(double expectedStarRating, int expectedMaxCombo, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expectedStarRating, expectedMaxCombo, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9825709931204205d, 239, "diffcalc-test")] [TestCase(9.4310274277499619d, 239, "diffcalc-test")]
[TestCase(1.7550169162648608d, 54, "zero-length-sliders")] [TestCase(1.7550169162648608d, 54, "zero-length-sliders")]
[TestCase(0.55231632896800109d, 4, "very-fast-slider")] [TestCase(0.55231632896800109d, 4, "very-fast-slider")]
public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.7171144000821119d, 239, "diffcalc-test")] [TestCase(6.718709884850683d, 239, "diffcalc-test")]
[TestCase(1.4485749025771304d, 54, "zero-length-sliders")] [TestCase(1.4485749025771304d, 54, "zero-length-sliders")]
[TestCase(0.42630400627180914d, 4, "very-fast-slider")] [TestCase(0.42630400627180914d, 4, "very-fast-slider")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name) public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)

View File

@ -465,7 +465,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap) private void performTest(List<ReplayFrame> frames, Beatmap<OsuHitObject> beatmap)
{ {
beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo; beatmap.BeatmapInfo.Ruleset = new OsuRuleset().RulesetInfo;
beatmap.BeatmapInfo.StackLeniency = 0; beatmap.StackLeniency = 0;
beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty beatmap.BeatmapInfo.Difficulty = new BeatmapDifficulty
{ {
SliderMultiplier = 4, SliderMultiplier = 4,

View File

@ -56,13 +56,13 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
h.StackHeight = 0; h.StackHeight = 0;
if (beatmap.BeatmapInfo.BeatmapVersion >= 6) if (beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(beatmap.BeatmapInfo, hitObjects, 0, hitObjects.Count - 1); applyStacking(beatmap, hitObjects, 0, hitObjects.Count - 1);
else else
applyStackingOld(beatmap.BeatmapInfo, hitObjects); applyStackingOld(beatmap, hitObjects);
} }
} }
private static void applyStacking(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects, int startIndex, int endIndex) private static void applyStacking(IBeatmap beatmap, List<OsuHitObject> hitObjects, int startIndex, int endIndex)
{ {
ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex); ArgumentOutOfRangeException.ThrowIfGreaterThan(startIndex, endIndex);
ArgumentOutOfRangeException.ThrowIfNegative(startIndex); ArgumentOutOfRangeException.ThrowIfNegative(startIndex);
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
continue; continue;
double endTime = stackBaseObject.GetEndTime(); double endTime = stackBaseObject.GetEndTime();
double stackThreshold = objectN.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectN.TimePreempt * beatmap.StackLeniency;
if (objectN.StartTime - endTime > stackThreshold) if (objectN.StartTime - endTime > stackThreshold)
// We are no longer within stacking range of the next object. // We are no longer within stacking range of the next object.
@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
OsuHitObject objectI = hitObjects[i]; OsuHitObject objectI = hitObjects[i];
if (objectI.StackHeight != 0 || objectI is Spinner) continue; if (objectI.StackHeight != 0 || objectI is Spinner) continue;
double stackThreshold = objectI.TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = objectI.TimePreempt * beatmap.StackLeniency;
/* If this object is a hitcircle, then we enter this "special" case. /* If this object is a hitcircle, then we enter this "special" case.
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider. * It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
@ -214,7 +214,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
} }
} }
private static void applyStackingOld(BeatmapInfo beatmapInfo, List<OsuHitObject> hitObjects) private static void applyStackingOld(IBeatmap beatmap, List<OsuHitObject> hitObjects)
{ {
for (int i = 0; i < hitObjects.Count; i++) for (int i = 0; i < hitObjects.Count; i++)
{ {
@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
for (int j = i + 1; j < hitObjects.Count; j++) for (int j = i + 1; j < hitObjects.Count; j++)
{ {
double stackThreshold = hitObjects[i].TimePreempt * beatmapInfo.StackLeniency; double stackThreshold = hitObjects[i].TimePreempt * beatmap.StackLeniency;
if (hitObjects[j].StartTime - stackThreshold > startTime) if (hitObjects[j].StartTime - stackThreshold > startTime)
break; break;

View File

@ -12,9 +12,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
public static class AimEvaluator public static class AimEvaluator
{ {
private const double wide_angle_multiplier = 1.5; private const double wide_angle_multiplier = 1.5;
private const double acute_angle_multiplier = 1.95; private const double acute_angle_multiplier = 2.35;
private const double slider_multiplier = 1.35; private const double slider_multiplier = 1.35;
private const double velocity_change_multiplier = 0.75; private const double velocity_change_multiplier = 0.75;
private const double wiggle_multiplier = 1.02;
/// <summary> /// <summary>
/// Evaluates the difficulty of aiming the current object, based on: /// Evaluates the difficulty of aiming the current object, based on:
@ -64,6 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double acuteAngleBonus = 0; double acuteAngleBonus = 0;
double sliderBonus = 0; double sliderBonus = 0;
double velocityChangeBonus = 0; double velocityChangeBonus = 0;
double wiggleBonus = 0;
double aimStrain = currVelocity; // Start strain with regular velocity. double aimStrain = currVelocity; // Start strain with regular velocity.
@ -79,22 +81,27 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
double angleBonus = Math.Min(currVelocity, prevVelocity); double angleBonus = Math.Min(currVelocity, prevVelocity);
wideAngleBonus = calcWideAngleBonus(currAngle); wideAngleBonus = calcWideAngleBonus(currAngle);
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
if (DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2) < 300) // Only buff deltaTime exceeding 300 bpm 1/2. // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter
acuteAngleBonus = 0; acuteAngleBonus = calcAcuteAngleBonus(currAngle) *
else angleBonus *
{ DifficultyCalculationUtils.Smootherstep(DifficultyCalculationUtils.MillisecondsToBPM(osuCurrObj.StrainTime, 2), 300, 400) *
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern. DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, diameter, diameter * 2);
* Math.Min(angleBonus, diameter * 1.25 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, radius, diameter) - radius) / radius), 2); // Buff distance exceeding radius up to diameter.
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute. // Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3))); wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse. // Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3))); acuteAngleBonus *= 0.03 + 0.97 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
// Apply wiggle bonus for jumps that are [radius, 2*diameter] in distance, with < 110 angle and bpm > 150
// https://www.desmos.com/calculator/iis7lgbppe
wiggleBonus = angleBonus
* DifficultyCalculationUtils.Smootherstep(osuCurrObj.LazyJumpDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuCurrObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(currAngle, double.DegreesToRadians(110), double.DegreesToRadians(60))
* DifficultyCalculationUtils.Smootherstep(osuLastObj.LazyJumpDistance, radius, diameter)
* Math.Pow(DifficultyCalculationUtils.ReverseLerp(osuLastObj.LazyJumpDistance, diameter * 3, diameter), 1.8)
* DifficultyCalculationUtils.Smootherstep(lastAngle, double.DegreesToRadians(110), double.DegreesToRadians(60));
} }
} }
@ -122,6 +129,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime; sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
} }
aimStrain += wiggleBonus * wiggle_multiplier;
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier); aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);

View File

@ -8,6 +8,7 @@ using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty namespace osu.Game.Rulesets.Osu.Difficulty
{ {
@ -19,6 +20,12 @@ namespace osu.Game.Rulesets.Osu.Difficulty
[JsonProperty("aim_difficulty")] [JsonProperty("aim_difficulty")]
public double AimDifficulty { get; set; } public double AimDifficulty { get; set; }
/// <summary>
/// The number of <see cref="Slider"/>s weighted by difficulty.
/// </summary>
[JsonProperty("aim_difficult_slider_count")]
public double AimDifficultSliderCount { get; set; }
/// <summary> /// <summary>
/// The difficulty corresponding to the speed skill. /// The difficulty corresponding to the speed skill.
/// </summary> /// </summary>
@ -113,6 +120,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount); yield return (ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT, AimDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount); yield return (ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT, SpeedDifficultStrainCount);
yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount); yield return (ATTRIB_ID_SPEED_NOTE_COUNT, SpeedNoteCount);
yield return (ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT, AimDifficultSliderCount);
} }
public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo) public override void FromDatabaseAttributes(IReadOnlyDictionary<int, double> values, IBeatmapOnlineInfo onlineInfo)
@ -129,6 +137,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT]; AimDifficultStrainCount = values[ATTRIB_ID_AIM_DIFFICULT_STRAIN_COUNT];
SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT]; SpeedDifficultStrainCount = values[ATTRIB_ID_SPEED_DIFFICULT_STRAIN_COUNT];
SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT]; SpeedNoteCount = values[ATTRIB_ID_SPEED_NOTE_COUNT];
AimDifficultSliderCount = values[ATTRIB_ID_AIM_DIFFICULT_SLIDER_COUNT];
DrainRate = onlineInfo.DrainRate; DrainRate = onlineInfo.DrainRate;
HitCircleCount = onlineInfo.CircleCount; HitCircleCount = onlineInfo.CircleCount;
SliderCount = onlineInfo.SliderCount; SliderCount = onlineInfo.SliderCount;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier; double aimRatingNoSliders = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier; double speedRating = Math.Sqrt(skills[2].DifficultyValue()) * difficulty_multiplier;
double speedNotes = ((Speed)skills[2]).RelevantNoteCount(); double speedNotes = ((Speed)skills[2]).RelevantNoteCount();
double difficultSliders = ((Aim)skills[0]).GetDifficultSliders();
double flashlightRating = 0.0; double flashlightRating = 0.0;
if (mods.Any(h => h is OsuModFlashlight)) if (mods.Any(h => h is OsuModFlashlight))
@ -104,6 +104,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
StarRating = starRating, StarRating = starRating,
Mods = mods, Mods = mods,
AimDifficulty = aimRating, AimDifficulty = aimRating,
AimDifficultSliderCount = difficultSliders,
SpeedDifficulty = speedRating, SpeedDifficulty = speedRating,
SpeedNoteCount = speedNotes, SpeedNoteCount = speedNotes,
FlashlightDifficulty = flashlightRating, FlashlightDifficulty = flashlightRating,

View File

@ -135,7 +135,30 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes) private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{ {
double aimValue = OsuStrainSkill.DifficultyToPerformance(attributes.AimDifficulty); double aimDifficulty = attributes.AimDifficulty;
if (attributes.SliderCount > 0 && attributes.AimDifficultSliderCount > 0)
{
double estimateImproperlyFollowedDifficultSliders;
if (usingClassicSliderAccuracy)
{
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
int maximumPossibleDroppedSliders = totalImperfectHits;
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, attributes.AimDifficultSliderCount);
}
else
{
// We add tick misses here since they too mean that the player didn't follow the slider properly
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
estimateImproperlyFollowedDifficultSliders = Math.Min(countSliderEndsDropped + countSliderTickMiss, attributes.AimDifficultSliderCount);
}
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / attributes.AimDifficultSliderCount, 3) + attributes.SliderFactor;
aimDifficulty *= sliderNerfFactor;
}
double aimValue = OsuStrainSkill.DifficultyToPerformance(aimDifficulty);
double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) +
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0);
@ -165,30 +188,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
} }
// We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator.
double estimateDifficultSliders = attributes.SliderCount * 0.15;
if (attributes.SliderCount > 0)
{
double estimateImproperlyFollowedDifficultSliders;
if (usingClassicSliderAccuracy)
{
// When the score is considered classic (regardless if it was made on old client or not) we consider all missing combo to be dropped difficult sliders
int maximumPossibleDroppedSliders = totalImperfectHits;
estimateImproperlyFollowedDifficultSliders = Math.Clamp(Math.Min(maximumPossibleDroppedSliders, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
}
else
{
// We add tick misses here since they too mean that the player didn't follow the slider properly
// We however aren't adding misses here because missing slider heads has a harsh penalty by itself and doesn't mean that the rest of the slider wasn't followed properly
estimateImproperlyFollowedDifficultSliders = Math.Clamp(countSliderEndsDropped + countSliderTickMiss, 0, estimateDifficultSliders);
}
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateImproperlyFollowedDifficultSliders / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
aimValue *= accuracy; aimValue *= accuracy;
// It is important to consider accuracy difficulty when scaling with accuracy. // It is important to consider accuracy difficulty when scaling with accuracy.
aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;

View File

@ -2,6 +2,8 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Difficulty.Evaluators; using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
@ -27,6 +29,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
private double skillMultiplier => 25.18; private double skillMultiplier => 25.18;
private double strainDecayBase => 0.15; private double strainDecayBase => 0.15;
private readonly List<double> sliderStrains = new List<double>();
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000); private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime); protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
@ -40,5 +44,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
return currentStrain; return currentStrain;
} }
public double GetDifficultSliders()
{
if (sliderStrains.Count == 0)
return 0;
double[] sortedStrains = typedObjectStrains.Where(typedObjectStrain => typedObjectStrain.isSlider).OrderDescending().ToArray();
double maxSliderStrain = sortedStrains.Max();
if (maxSliderStrain == 0)
return 0;
return sortedStrains.Sum(strain => 1.0 / (1.0 + Math.Exp(-(strain / maxSliderStrain * 12.0 - 6.0))));
}
} }
} }

View File

@ -551,6 +551,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
HitObject.Position += first; HitObject.Position += first;
} }
// duplicated in `JuiceStreamSelectionBlueprint.convertToStream()`
// consider extracting common helper when applying changes here
private void convertToStream() private void convertToStream()
{ {
if (editorBeatmap == null || beatDivisor == null) if (editorBeatmap == null || beatDivisor == null)

View File

@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Edit
}, },
}; };
Spacing.Value = editorBeatmap.BeatmapInfo.GridSize; Spacing.Value = editorBeatmap.GridSize;
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Edit
spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}"; spacingSlider.ContractedLabelText = $"S: {spacing.NewValue:#,0.##}";
spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}"; spacingSlider.ExpandedLabelText = $"Spacing: {spacing.NewValue:#,0.##}";
SpacingVector.Value = new Vector2(spacing.NewValue); SpacingVector.Value = new Vector2(spacing.NewValue);
editorBeatmap.BeatmapInfo.GridSize = (int)spacing.NewValue; editorBeatmap.GridSize = (int)spacing.NewValue;
}, true); }, true);
GridLinesRotation.BindValueChanged(rotation => GridLinesRotation.BindValueChanged(rotation =>

View File

@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
{ {
Caption = "Stack Leniency", Caption = "Stack Leniency",
HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.", HintText = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency) Current = new BindableFloat(Beatmap.StackLeniency)
{ {
Default = 0.7f, Default = 0.7f,
MinValue = 0, MinValue = 0,
@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Setup
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value; Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value; Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value; Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value; Beatmap.StackLeniency = stackLeniency.Current.Value;
Beatmap.UpdateAllHitObjects(); Beatmap.UpdateAllHitObjects();
Beatmap.SaveState(); Beatmap.SaveState();

View File

@ -57,11 +57,22 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
const string base_lookup = @"hitcircle";
var drawableOsuObject = (DrawableOsuHitObject?)drawableObject; var drawableOsuObject = (DrawableOsuHitObject?)drawableObject;
// As a precondition, ensure that any prefix lookups are run against the skin which is providing "hitcircle".
// This is to correctly handle a case such as:
//
// - Beatmap provides `hitcircle`
// - User skin provides `sliderstartcircle`
//
// In such a case, the `hitcircle` should be used for slider start circles rather than the user's skin override.
var provider = skin.FindProvider(s => s.GetTexture(base_lookup) != null) ?? skin;
// if a base texture for the specified prefix exists, continue using it for subsequent lookups. // if a base texture for the specified prefix exists, continue using it for subsequent lookups.
// otherwise fall back to the default prefix "hitcircle". // otherwise fall back to the default prefix "hitcircle".
string circleName = (priorityLookupPrefix != null && skin.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : @"hitcircle"; string circleName = (priorityLookupPrefix != null && provider.GetTexture(priorityLookupPrefix) != null) ? priorityLookupPrefix : base_lookup;
Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2; Vector2 maxSize = OsuHitObject.OBJECT_DIMENSIONS * 2;
@ -70,7 +81,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png. // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png.
InternalChildren = new[] InternalChildren = new[]
{ {
CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(circleName)?.WithMaximumSize(maxSize) }) CircleSprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(circleName)?.WithMaximumSize(maxSize) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -79,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = skin.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) }) Child = OverlaySprite = new LegacyKiaiFlashingDrawable(() => new Sprite { Texture = provider.GetTexture(@$"{circleName}overlay")?.WithMaximumSize(maxSize) })
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -63,18 +63,18 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-top"), Texture = source.GetTexture("spinner-top"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
spinningMiddle = new Sprite spinningMiddle = new Sprite
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle2"), Texture = source.GetTexture("spinner-middle2"),
}, },
fixedMiddle = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = source.GetTexture("spinner-middle"),
},
} }
}); });

View File

@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.UI
protected override ResumeOverlay CreateResumeOverlay() protected override ResumeOverlay CreateResumeOverlay()
{ {
if (Mods.Any(m => m is OsuModAutopilot)) if (Mods.Any(m => m is OsuModAutopilot or OsuModTouchDevice))
return new DelayedResumeOverlay { Scale = new Vector2(0.65f) }; return new DelayedResumeOverlay { Scale = new Vector2(0.65f) };
return new OsuResumeOverlay(); return new OsuResumeOverlay();

View File

@ -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.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
namespace osu.Game.Rulesets.Taiko.Difficulty.Evaluators
{
public static class ReadingEvaluator
{
private readonly struct VelocityRange
{
public double Min { get; }
public double Max { get; }
public double Center => (Max + Min) / 2;
public double Range => Max - Min;
public VelocityRange(double min, double max)
{
Min = min;
Max = max;
}
}
/// <summary>
/// Calculates the influence of higher slider velocities on hitobject difficulty.
/// The bonus is determined based on the EffectiveBPM, shifting within a defined range
/// between the upper and lower boundaries to reflect how increased slider velocity impacts difficulty.
/// </summary>
/// <param name="noteObject">The hit object to evaluate.</param>
/// <returns>The reading difficulty value for the given hit object.</returns>
public static double EvaluateDifficultyOf(TaikoDifficultyHitObject noteObject)
{
double effectiveBPM = noteObject.EffectiveBPM;
var highVelocity = new VelocityRange(480, 640);
var midVelocity = new VelocityRange(360, 480);
return 1.0 * DifficultyCalculationUtils.Logistic(effectiveBPM, highVelocity.Center, 1.0 / (highVelocity.Range / 10))
+ 0.5 * DifficultyCalculationUtils.Logistic(effectiveBPM, midVelocity.Center, 1.0 / (midVelocity.Range / 10));
}
}
}

View File

@ -0,0 +1,50 @@
// 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 osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading
{
public class EffectiveBPMPreprocessor
{
private readonly IList<TaikoDifficultyHitObject> noteObjects;
private readonly double globalSliderVelocity;
public EffectiveBPMPreprocessor(IBeatmap beatmap, List<TaikoDifficultyHitObject> noteObjects)
{
this.noteObjects = noteObjects;
globalSliderVelocity = beatmap.Difficulty.SliderMultiplier;
}
/// <summary>
/// Calculates and sets the effective BPM and slider velocity for each note object, considering clock rate and scroll speed.
/// </summary>
public void ProcessEffectiveBPM(ControlPointInfo controlPointInfo, double clockRate)
{
foreach (var currentNoteObject in noteObjects)
{
double startTime = currentNoteObject.StartTime * clockRate;
// Retrieve the timing point at the note's start time
TimingControlPoint currentControlPoint = controlPointInfo.TimingPointAt(startTime);
// Calculate the slider velocity at the note's start time.
double currentSliderVelocity = calculateSliderVelocity(controlPointInfo, startTime, clockRate);
currentNoteObject.CurrentSliderVelocity = currentSliderVelocity;
currentNoteObject.EffectiveBPM = currentControlPoint.BPM * currentSliderVelocity;
}
}
/// <summary>
/// Calculates the slider velocity based on control point info and clock rate.
/// </summary>
private double calculateSliderVelocity(ControlPointInfo controlPointInfo, double startTime, double clockRate)
{
var activeEffectControlPoint = controlPointInfo.EffectPointAt(startTime);
return globalSliderVelocity * (activeEffectControlPoint.ScrollSpeed) * clockRate;
}
}
}

View File

@ -48,6 +48,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
/// </summary> /// </summary>
public readonly TaikoDifficultyHitObjectColour Colour; public readonly TaikoDifficultyHitObjectColour Colour;
/// <summary>
/// The adjusted BPM of this hit object, based on its slider velocity and scroll speed.
/// </summary>
public double EffectiveBPM;
/// <summary>
/// The current slider velocity of this hit object.
/// </summary>
public double CurrentSliderVelocity;
/// <summary> /// <summary>
/// Creates a new difficulty hit object. /// Creates a new difficulty hit object.
/// </summary> /// </summary>

View File

@ -0,0 +1,44 @@
// 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.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Difficulty.Evaluators;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Objects;
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
{
/// <summary>
/// Calculates the reading coefficient of taiko difficulty.
/// </summary>
public class Reading : StrainDecaySkill
{
protected override double SkillMultiplier => 1.0;
protected override double StrainDecayBase => 0.4;
private double currentStrain;
public Reading(Mod[] mods)
: base(mods)
{
}
protected override double StrainValueOf(DifficultyHitObject current)
{
// Drum Rolls and Swells are exempt.
if (current.BaseObject is not Hit)
{
return 0.0;
}
var taikoObject = (TaikoDifficultyHitObject)current;
currentStrain *= StrainDecayBase;
currentStrain += ReadingEvaluator.EvaluateDifficultyOf(taikoObject) * SkillMultiplier;
return currentStrain;
}
}
}

View File

@ -28,17 +28,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
[JsonProperty("rhythm_difficulty")] [JsonProperty("rhythm_difficulty")]
public double RhythmDifficulty { get; set; } public double RhythmDifficulty { get; set; }
/// <summary>
/// The difficulty corresponding to the reading skill.
/// </summary>
[JsonProperty("reading_difficulty")]
public double ReadingDifficulty { get; set; }
/// <summary> /// <summary>
/// The difficulty corresponding to the colour skill. /// The difficulty corresponding to the colour skill.
/// </summary> /// </summary>
[JsonProperty("colour_difficulty")] [JsonProperty("colour_difficulty")]
public double ColourDifficulty { get; set; } public double ColourDifficulty { get; set; }
/// <summary> [JsonProperty("rhythm_difficult_strains")]
/// The difficulty corresponding to the hardest parts of the map. public double RhythmTopStrains { get; set; }
/// </summary>
[JsonProperty("peak_difficulty")] [JsonProperty("colour_difficult_strains")]
public double PeakDifficulty { get; set; } public double ColourTopStrains { get; set; }
[JsonProperty("stamina_difficult_strains")]
public double StaminaTopStrains { get; set; }
/// <summary> /// <summary>
/// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc). /// The perceived hit window for a GREAT hit inclusive of rate-adjusting mods (DT/HT/etc).

View File

@ -8,10 +8,12 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Colour;
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing.Reading;
using osu.Game.Rulesets.Taiko.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Skills;
using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
@ -21,7 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyCalculator : DifficultyCalculator public class TaikoDifficultyCalculator : DifficultyCalculator
{ {
private const double difficulty_multiplier = 0.084375; private const double difficulty_multiplier = 0.084375;
private const double rhythm_skill_multiplier = 0.2 * difficulty_multiplier; private const double rhythm_skill_multiplier = 0.200 * difficulty_multiplier;
private const double reading_skill_multiplier = 0.100 * difficulty_multiplier;
private const double colour_skill_multiplier = 0.375 * difficulty_multiplier; private const double colour_skill_multiplier = 0.375 * difficulty_multiplier;
private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier; private const double stamina_skill_multiplier = 0.375 * difficulty_multiplier;
@ -37,6 +40,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return new Skill[] return new Skill[]
{ {
new Rhythm(mods), new Rhythm(mods),
new Reading(mods),
new Colour(mods), new Colour(mods),
new Stamina(mods, false), new Stamina(mods, false),
new Stamina(mods, true) new Stamina(mods, true)
@ -53,21 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{ {
List<DifficultyHitObject> difficultyHitObjects = new List<DifficultyHitObject>(); var difficultyHitObjects = new List<DifficultyHitObject>();
List<TaikoDifficultyHitObject> centreObjects = new List<TaikoDifficultyHitObject>(); var centreObjects = new List<TaikoDifficultyHitObject>();
List<TaikoDifficultyHitObject> rimObjects = new List<TaikoDifficultyHitObject>(); var rimObjects = new List<TaikoDifficultyHitObject>();
List<TaikoDifficultyHitObject> noteObjects = new List<TaikoDifficultyHitObject>(); var noteObjects = new List<TaikoDifficultyHitObject>();
EffectiveBPMPreprocessor bpmLoader = new EffectiveBPMPreprocessor(beatmap, noteObjects);
// Generate TaikoDifficultyHitObjects from the beatmap's hit objects.
for (int i = 2; i < beatmap.HitObjects.Count; i++) for (int i = 2; i < beatmap.HitObjects.Count; i++)
{ {
difficultyHitObjects.Add( difficultyHitObjects.Add(new TaikoDifficultyHitObject(
new TaikoDifficultyHitObject( beatmap.HitObjects[i],
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, difficultyHitObjects, beatmap.HitObjects[i - 1],
centreObjects, rimObjects, noteObjects, difficultyHitObjects.Count) beatmap.HitObjects[i - 2],
); clockRate,
difficultyHitObjects,
centreObjects,
rimObjects,
noteObjects,
difficultyHitObjects.Count
));
} }
TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects); TaikoColourDifficultyPreprocessor.ProcessAndAssign(difficultyHitObjects);
bpmLoader.ProcessEffectiveBPM(beatmap.ControlPointInfo, clockRate);
return difficultyHitObjects; return difficultyHitObjects;
} }
@ -77,27 +90,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (beatmap.HitObjects.Count == 0) if (beatmap.HitObjects.Count == 0)
return new TaikoDifficultyAttributes { Mods = mods }; return new TaikoDifficultyAttributes { Mods = mods };
Colour colour = (Colour)skills.First(x => x is Colour); bool isRelax = mods.Any(h => h is TaikoModRelax);
Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm); Rhythm rhythm = (Rhythm)skills.First(x => x is Rhythm);
Reading reading = (Reading)skills.First(x => x is Reading);
Colour colour = (Colour)skills.First(x => x is Colour);
Stamina stamina = (Stamina)skills.First(x => x is Stamina); Stamina stamina = (Stamina)skills.First(x => x is Stamina);
Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina); Stamina singleColourStamina = (Stamina)skills.Last(x => x is Stamina);
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
double readingRating = reading.DifficultyValue() * reading_skill_multiplier;
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier; double staminaRating = stamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier; double monoStaminaRating = singleColourStamina.DifficultyValue() * stamina_skill_multiplier;
double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5); double monoStaminaFactor = staminaRating == 0 ? 1 : Math.Pow(monoStaminaRating / staminaRating, 5);
double combinedRating = combinedDifficultyValue(rhythm, colour, stamina); double rhythmDifficultStrains = rhythm.CountTopWeightedStrains();
double colourDifficultStrains = colour.CountTopWeightedStrains();
double staminaDifficultStrains = stamina.CountTopWeightedStrains();
double combinedRating = combinedDifficultyValue(rhythm, reading, colour, stamina, isRelax);
double starRating = rescale(combinedRating * 1.4); double starRating = rescale(combinedRating * 1.4);
// TODO: This is temporary measure as we don't detect abuse of multiple-input playstyles of converts within the current system. // Converts are penalised outside the scope of difficulty calculation, as our assumptions surrounding standard play-styles becomes out-of-scope.
if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0) if (beatmap.BeatmapInfo.Ruleset.OnlineID == 0)
{ {
starRating *= 0.925; starRating *= 0.825;
// For maps with low colour variance and high stamina requirement, multiple inputs are more likely to be abused.
if (colourRating < 2 && staminaRating > 8) // For maps with relax, multiple inputs are more likely to be abused.
starRating *= 0.80; if (isRelax)
starRating *= 0.60;
} }
HitWindows hitWindows = new TaikoHitWindows(); HitWindows hitWindows = new TaikoHitWindows();
@ -107,11 +129,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{ {
StarRating = starRating, StarRating = starRating,
Mods = mods, Mods = mods,
RhythmDifficulty = rhythmRating,
ReadingDifficulty = readingRating,
ColourDifficulty = colourRating,
StaminaDifficulty = staminaRating, StaminaDifficulty = staminaRating,
MonoStaminaFactor = monoStaminaFactor, MonoStaminaFactor = monoStaminaFactor,
RhythmDifficulty = rhythmRating, StaminaTopStrains = staminaDifficultStrains,
ColourDifficulty = colourRating, RhythmTopStrains = rhythmDifficultStrains,
PeakDifficulty = combinedRating, ColourTopStrains = colourDifficultStrains,
GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate, GreatHitWindow = hitWindows.WindowFor(HitResult.Great) / clockRate,
OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate, OkHitWindow = hitWindows.WindowFor(HitResult.Ok) / clockRate,
MaxCombo = beatmap.GetMaxCombo(), MaxCombo = beatmap.GetMaxCombo(),
@ -120,17 +145,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
return attributes; return attributes;
} }
/// <summary>
/// Applies a final re-scaling of the star rating.
/// </summary>
/// <param name="sr">The raw star rating value before re-scaling.</param>
private double rescale(double sr)
{
if (sr < 0) return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
/// <summary> /// <summary>
/// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map. /// Returns the combined star rating of the beatmap, calculated using peak strains from all sections of the map.
/// </summary> /// </summary>
@ -138,22 +152,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
/// </remarks> /// </remarks>
private double combinedDifficultyValue(Rhythm rhythm, Colour colour, Stamina stamina) private double combinedDifficultyValue(Rhythm rhythm, Reading reading, Colour colour, Stamina stamina, bool isRelax)
{ {
List<double> peaks = new List<double>(); List<double> peaks = new List<double>();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList(); var rhythmPeaks = rhythm.GetCurrentStrainPeaks().ToList();
var readingPeaks = reading.GetCurrentStrainPeaks().ToList();
var colourPeaks = colour.GetCurrentStrainPeaks().ToList();
var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList(); var staminaPeaks = stamina.GetCurrentStrainPeaks().ToList();
for (int i = 0; i < colourPeaks.Count; i++) for (int i = 0; i < colourPeaks.Count; i++)
{ {
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier; double rhythmPeak = rhythmPeaks[i] * rhythm_skill_multiplier;
double readingPeak = readingPeaks[i] * reading_skill_multiplier;
double colourPeak = colourPeaks[i] * colour_skill_multiplier;
double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier; double staminaPeak = staminaPeaks[i] * stamina_skill_multiplier;
double peak = norm(1.5, colourPeak, staminaPeak); if (isRelax)
peak = norm(2, peak, rhythmPeak); {
colourPeak = 0; // There is no colour difficulty in relax.
staminaPeak /= 1.5; // Stamina difficulty is decreased with an increased available finger count.
}
double peak = DifficultyCalculationUtils.Norm(2, DifficultyCalculationUtils.Norm(1.5, colourPeak, staminaPeak), rhythmPeak, readingPeak);
// Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871). // Sections with 0 strain are excluded to avoid worst-case time complexity of the following sort (e.g. /b/2351871).
// These sections will not contribute to the difficulty. // These sections will not contribute to the difficulty.
@ -174,10 +195,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
} }
/// <summary> /// <summary>
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector. /// Applies a final re-scaling of the star rating.
/// </summary> /// </summary>
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param> /// <param name="sr">The raw star rating value before re-scaling.</param>
/// <param name="values">The coefficients of the vector.</param> private double rescale(double sr)
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); {
if (sr < 0) return sr;
return 10.43 * Math.Log(sr / 8 + 1);
}
} }
} }

View File

@ -144,6 +144,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
foreach (var nested in hitObject.NestedHitObjects) foreach (var nested in hitObject.NestedHitObjects)
simulateHit(nested, ref attributes); simulateHit(nested, ref attributes);
return; return;
case StrongNestedHitObject:
// we never need to deal with these directly.
// the only thing strong hits do in terms of scoring is double their object's score increase,
// which is already handled at the parent object level via the `strongable.IsStrong` check lower down in this method.
// not handling these here can lead to them falsely being counted as combo-increasing when handling strong drum rolls!
return;
} }
if (hitObject is DrumRollTick tick) if (hitObject is DrumRollTick tick)

View File

@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes) private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{ {
double difficultyValue = Math.Pow(5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0, 2.25) / 1150.0; double baseDifficulty = 5 * Math.Max(1.0, attributes.StarRating / 0.115) - 4.0;
double difficultyValue = Math.Min(Math.Pow(baseDifficulty, 3) / 69052.51, Math.Pow(baseDifficulty, 2.25) / 1150.0);
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus; difficultyValue *= lengthBonus;
@ -86,9 +87,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
if (score.Mods.Any(m => m is ModHidden)) if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025; difficultyValue *= 1.025;
if (score.Mods.Any(m => m is ModHardRock))
difficultyValue *= 1.10;
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>)) if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus); difficultyValue *= Math.Max(1, 1.050 - Math.Min(attributes.MonoStaminaFactor / 50, 1) * lengthBonus);
@ -97,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps. // Scale accuracy more harshly on nearly-completely mono (single coloured) speed maps.
double accScalingExponent = 2 + attributes.MonoStaminaFactor; double accScalingExponent = 2 + attributes.MonoStaminaFactor;
double accScalingShift = 300 - 100 * attributes.MonoStaminaFactor; double accScalingShift = 400 - 100 * attributes.MonoStaminaFactor;
return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent); return difficultyValue * Math.Pow(SpecialFunctions.Erf(accScalingShift / (Math.Sqrt(2) * estimatedUnstableRate.Value)), accScalingExponent);
} }
@ -133,6 +131,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed). const double z = 2.32634787404; // 99% critical value for the normal distribution (one-tailed).
double? deviationGreatWindow = calcDeviationGreatWindow();
double? deviationGoodWindow = calcDeviationGoodWindow();
return deviationGreatWindow is null ? deviationGoodWindow : Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
// The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window. // The upper bound on deviation, calculated with the ratio of 300s to objects, and the great hit window.
double? calcDeviationGreatWindow() double? calcDeviationGreatWindow()
{ {
@ -159,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
double n = totalHits; double n = totalHits;
// Proportion of greats + goods hit. // Proportion of greats + goods hit.
double p = totalSuccessfulHits / n; double p = Math.Max(0, totalSuccessfulHits - 0.0005 * countOk) / n;
// We can be 99% confident that p is at least this value. // We can be 99% confident that p is at least this value.
double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4); double pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * Math.Sqrt(n * p * (1 - p) + z * z / 4);
@ -167,14 +170,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
// We can be 99% confident that the deviation is not higher than: // We can be 99% confident that the deviation is not higher than:
return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound)); return h100 / (Math.Sqrt(2) * SpecialFunctions.ErfInv(pLowerBound));
} }
double? deviationGreatWindow = calcDeviationGreatWindow();
double? deviationGoodWindow = calcDeviationGoodWindow();
if (deviationGreatWindow is null)
return deviationGoodWindow;
return Math.Min(deviationGreatWindow.Value, deviationGoodWindow!.Value);
} }
private int totalHits => countGreat + countOk + countMeh + countMiss; private int totalHits => countGreat + countOk + countMeh + countMiss;

View File

@ -80,16 +80,16 @@ namespace osu.Game.Tests.Beatmaps.Formats
var metadata = beatmap.Metadata; var metadata = beatmap.Metadata;
Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile); Assert.AreEqual("03. Renatus - Soleily 192kbps.mp3", metadata.AudioFile);
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(164471, metadata.PreviewTime); Assert.AreEqual(164471, metadata.PreviewTime);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.IsFalse(beatmapInfo.LetterboxInBreaks); Assert.IsFalse(beatmap.LetterboxInBreaks);
Assert.IsFalse(beatmapInfo.SpecialStyle); Assert.IsFalse(beatmap.SpecialStyle);
Assert.IsFalse(beatmapInfo.WidescreenStoryboard); Assert.IsFalse(beatmap.WidescreenStoryboard);
Assert.IsFalse(beatmapInfo.SamplesMatchPlaybackRate); Assert.IsFalse(beatmap.SamplesMatchPlaybackRate);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
} }
@ -101,7 +101,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu")) using (var resStream = TestResources.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
{ {
var beatmapInfo = decoder.Decode(stream).BeatmapInfo; var beatmap = decoder.Decode(stream);
int[] expectedBookmarks = int[] expectedBookmarks =
{ {
@ -109,13 +109,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
95901, 106450, 116999, 119637, 130186, 140735, 151285, 95901, 106450, 116999, 119637, 130186, 140735, 151285,
161834, 164471, 175020, 185570, 196119, 206669, 209306 161834, 164471, 175020, 185570, 196119, 206669, 209306
}; };
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmap.BeatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
} }
@ -993,15 +993,15 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.Multiple(() => Assert.Multiple(() =>
{ {
Assert.That(decoded.BeatmapInfo.AudioLeadIn, Is.EqualTo(0)); Assert.That(decoded.AudioLeadIn, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.StackLeniency, Is.EqualTo(0.7f)); Assert.That(decoded.StackLeniency, Is.EqualTo(0.7f));
Assert.That(decoded.BeatmapInfo.SpecialStyle, Is.False); Assert.That(decoded.SpecialStyle, Is.False);
Assert.That(decoded.BeatmapInfo.LetterboxInBreaks, Is.False); Assert.That(decoded.LetterboxInBreaks, Is.False);
Assert.That(decoded.BeatmapInfo.WidescreenStoryboard, Is.False); Assert.That(decoded.WidescreenStoryboard, Is.False);
Assert.That(decoded.BeatmapInfo.EpilepsyWarning, Is.False); Assert.That(decoded.EpilepsyWarning, Is.False);
Assert.That(decoded.BeatmapInfo.SamplesMatchPlaybackRate, Is.False); Assert.That(decoded.SamplesMatchPlaybackRate, Is.False);
Assert.That(decoded.BeatmapInfo.Countdown, Is.EqualTo(CountdownType.None)); Assert.That(decoded.Countdown, Is.EqualTo(CountdownType.None));
Assert.That(decoded.BeatmapInfo.CountdownOffset, Is.EqualTo(0)); Assert.That(decoded.CountdownOffset, Is.EqualTo(0));
Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1)); Assert.That(decoded.BeatmapInfo.Metadata.PreviewTime, Is.EqualTo(-1));
Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0)); Assert.That(decoded.BeatmapInfo.Ruleset.OnlineID, Is.EqualTo(0));
}); });

View File

@ -51,14 +51,14 @@ namespace osu.Game.Tests.Beatmaps.Formats
{ {
var beatmap = decodeAsJson(normal); var beatmap = decodeAsJson(normal);
var beatmapInfo = beatmap.BeatmapInfo; var beatmapInfo = beatmap.BeatmapInfo;
Assert.AreEqual(0, beatmapInfo.AudioLeadIn); Assert.AreEqual(0, beatmap.AudioLeadIn);
Assert.AreEqual(0.7f, beatmapInfo.StackLeniency); Assert.AreEqual(0.7f, beatmap.StackLeniency);
Assert.AreEqual(false, beatmapInfo.SpecialStyle); Assert.AreEqual(false, beatmap.SpecialStyle);
Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0); Assert.IsTrue(beatmapInfo.Ruleset.OnlineID == 0);
Assert.AreEqual(false, beatmapInfo.LetterboxInBreaks); Assert.AreEqual(false, beatmap.LetterboxInBreaks);
Assert.AreEqual(false, beatmapInfo.WidescreenStoryboard); Assert.AreEqual(false, beatmap.WidescreenStoryboard);
Assert.AreEqual(CountdownType.None, beatmapInfo.Countdown); Assert.AreEqual(CountdownType.None, beatmap.Countdown);
Assert.AreEqual(0, beatmapInfo.CountdownOffset); Assert.AreEqual(0, beatmap.CountdownOffset);
} }
[Test] [Test]
@ -73,13 +73,13 @@ namespace osu.Game.Tests.Beatmaps.Formats
95901, 106450, 116999, 119637, 130186, 140735, 151285, 95901, 106450, 116999, 119637, 130186, 140735, 151285,
161834, 164471, 175020, 185570, 196119, 206669, 209306 161834, 164471, 175020, 185570, 196119, 206669, 209306
}; };
Assert.AreEqual(expectedBookmarks.Length, beatmapInfo.Bookmarks.Length); Assert.AreEqual(expectedBookmarks.Length, beatmap.Bookmarks.Length);
for (int i = 0; i < expectedBookmarks.Length; i++) for (int i = 0; i < expectedBookmarks.Length; i++)
Assert.AreEqual(expectedBookmarks[i], beatmapInfo.Bookmarks[i]); Assert.AreEqual(expectedBookmarks[i], beatmap.Bookmarks[i]);
Assert.AreEqual(1.8, beatmapInfo.DistanceSpacing); Assert.AreEqual(1.8, beatmap.DistanceSpacing);
Assert.AreEqual(4, beatmapInfo.BeatDivisor); Assert.AreEqual(4, beatmapInfo.BeatDivisor);
Assert.AreEqual(4, beatmapInfo.GridSize); Assert.AreEqual(4, beatmap.GridSize);
Assert.AreEqual(2, beatmapInfo.TimelineZoom); Assert.AreEqual(2, beatmap.TimelineZoom);
} }
[Test] [Test]

View File

@ -0,0 +1,7 @@
# Higher global_level has higher priority, the default global_level
# is 100 for root .globalconfig and 0 for others
# https://learn.microsoft.com/dotnet/fundamentals/code-analysis/configuration-files#precedence
is_global = true
global_level = 101
dotnet_diagnostic.CA2007.severity = none

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Database
Assert.That(lastChanges?.ModifiedIndices, Is.Empty); Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty); Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5); realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().EditorTimestamp = 5);
realm.Run(r => r.Refresh()); realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1)); Assert.That(collectionChanges, Is.EqualTo(1));

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
@ -64,6 +65,10 @@ namespace osu.Game.Tests
// Beatmap must be imported before the collection manager is loaded. // Beatmap must be imported before the collection manager is loaded.
if (withBeatmap) if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely(); BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely();
// the logic for setting the initial ruleset exists in OsuGame rather than OsuGameBase.
// the ruleset bindable is not meant to be nullable, so assign any ruleset in here.
Ruleset.Value = RulesetStore.AvailableRulesets.First();
} }
} }
} }

View File

@ -20,12 +20,53 @@ namespace osu.Game.Tests.NonVisual.Ranking
public void TestDistributedHits() public void TestDistributedHits()
{ {
var events = Enumerable.Range(-5, 11) var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null)); .Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
var unstableRate = new UnstableRate(events); var unstableRate = new UnstableRate(events);
Assert.IsNotNull(unstableRate.Value); Assert.IsNotNull(unstableRate.Value);
Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value.Value, 10 * Math.Sqrt(10))); Assert.AreEqual(unstableRate.Value.Value, 10 * Math.Sqrt(10), Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncrementalRewind()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
result = events.GetRange(0, 2).CalculateUnstableRate(result);
Assert.IsNotNull(result!.Result);
Assert.AreEqual(5, result.Result, Precision.DOUBLE_EPSILON);
}
[Test]
public void TestDistributedHitsIncremental()
{
var events = Enumerable.Range(-5, 11)
.Select(t => new HitEvent(t - 5, 1.0, HitResult.Great, new HitObject(), null, null))
.ToList();
HitEventExtensions.UnstableRateCalculationResult result = null;
for (int i = 0; i < events.Count; i++)
{
result = events.GetRange(0, i + 1)
.CalculateUnstableRate(result);
}
Assert.IsNotNull(result!.Result);
Assert.AreEqual(10 * Math.Sqrt(10), result.Result, Precision.DOUBLE_EPSILON);
} }
[Test] [Test]

View File

@ -49,17 +49,17 @@ namespace osu.Game.Tests.Visual.Background
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(new OsuConfigManager(LocalStorage)); Dependencies.Cache(new OsuConfigManager(LocalStorage));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely(); manager.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
Add(detachedBeatmapStore); Add(beatmapStore);
Beatmap.SetDefault(); Beatmap.SetDefault();
} }

View File

@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false); AddStep("turn countdown off", () => designSection.EnableCountdown.Current.Value = false);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.None); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.None);
AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent); AddUntilStep("other controls hidden", () => !designSection.CountdownSettings.IsPresent);
} }
@ -65,12 +65,12 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls shown", () => designSection.CountdownSettings.IsPresent);
AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed); AddStep("change countdown speed", () => designSection.CountdownSpeed.Current.Value = CountdownType.DoubleSpeed);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.DoubleSpeed); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.DoubleSpeed);
AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent); AddUntilStep("other controls still shown", () => designSection.CountdownSettings.IsPresent);
} }
@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true); AddStep("turn countdown on", () => designSection.EnableCountdown.Current.Value = true);
AddAssert("beatmap has correct type", () => editorBeatmap.BeatmapInfo.Countdown == CountdownType.Normal); AddAssert("beatmap has correct type", () => editorBeatmap.Countdown == CountdownType.Normal);
checkOffsetAfter("1", 1); checkOffsetAfter("1", 1);
checkOffsetAfter(string.Empty, 0); checkOffsetAfter(string.Empty, 0);
@ -99,7 +99,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("commit text", () => InputManager.Key(Key.Enter)); AddStep("commit text", () => InputManager.Key(Key.Enter));
AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture)); AddAssert($"displayed value is {expectedFinalValue}", () => designSection.CountdownOffset.Current.Value == expectedFinalValue.ToString(CultureInfo.InvariantCulture));
AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.BeatmapInfo.CountdownOffset == expectedFinalValue); AddAssert($"beatmap value is {expectedFinalValue}", () => editorBeatmap.CountdownOffset == expectedFinalValue);
} }
private partial class TestDesignSection : DesignSection private partial class TestDesignSection : DesignSection

View File

@ -4,11 +4,13 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -19,6 +21,7 @@ using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
@ -27,6 +30,7 @@ using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Setup;
using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK; using osuTK;
@ -98,44 +102,15 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("enter compose mode", () => InputManager.Key(Key.F1)); AddStep("enter compose mode", () => InputManager.Key(Key.F1));
AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType<Timeline>().FirstOrDefault()?.IsLoaded == true); AddUntilStep("wait for timeline load", () => Editor.ChildrenOfType<Timeline>().FirstOrDefault()?.IsLoaded == true);
AddStep("enter setup mode", () => InputManager.Key(Key.F4)); AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual); AddAssert("track is virtual", () => Beatmap.Value.Track is TrackVirtual);
AddAssert("switch track to real track", () => AddAssert("switch track to real track", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
string temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")));
// ensure audio file is copied to beatmap as "audio.mp3" rather than original filename.
Assert.That(Beatmap.Value.Metadata.AudioFile == "audio.mp3");
return success;
}
finally
{
File.Delete(temp);
Directory.Delete(extractedFolder, true);
}
});
AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual); AddAssert("track is not virtual", () => Beatmap.Value.Track is not TrackVirtual);
AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000); AddUntilStep("track length changed", () => Beatmap.Value.Track.Length > 60000);
AddStep("test play", () => Editor.TestGameplay()); AddStep("test play", () => Editor.TestGameplay());
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog != null);
AddStep("confirm save", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for return to editor", () => Editor.IsCurrentScreen()); AddUntilStep("wait for return to editor", () => Editor.IsCurrentScreen());
AddAssert("track is still not virtual", () => Beatmap.Value.Track is not TrackVirtual); AddAssert("track is still not virtual", () => Beatmap.Value.Track is not TrackVirtual);
@ -154,6 +129,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName); AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 })); AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect point", () => EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true }));
AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[] AddStep("add hitobjects", () => EditorBeatmap.AddRange(new[]
{ {
new HitCircle new HitCircle
@ -200,6 +176,11 @@ namespace osu.Game.Tests.Visual.Editing
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single(); var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000; return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
}); });
AddAssert("created difficulty has effect points", () =>
{
var effectPoint = EditorBeatmap.ControlPointInfo.EffectPoints.Single();
return effectPoint.Time == 500 && effectPoint.KiaiMode && effectPoint.ScrollSpeedBindable.IsDefault;
});
AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0); AddAssert("created difficulty has no objects", () => EditorBeatmap.HitObjects.Count == 0);
AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified); AddAssert("status is modified", () => EditorBeatmap.BeatmapInfo.Status == BeatmapOnlineStatus.LocallyModified);
@ -219,6 +200,111 @@ namespace osu.Game.Tests.Visual.Editing
}); });
} }
[Test]
public void TestCreateNewDifficultyWithScrollSpeed_SameRuleset()
{
string previousDifficultyName = null!;
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != previousDifficultyName;
});
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = previousDifficultyName = Guid.NewGuid().ToString());
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 });
EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 });
EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 });
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != previousDifficultyName;
});
AddAssert("created difficulty has timing point", () =>
{
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has effect points", () =>
{
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
{
new EffectControlPoint { Time = 250, KiaiMode = false, ScrollSpeed = 0.05 },
new EffectControlPoint { Time = 500, KiaiMode = true, ScrollSpeed = 0.1 },
new EffectControlPoint { Time = 750, KiaiMode = true, ScrollSpeed = 0.15 },
new EffectControlPoint { Time = 1000, KiaiMode = false, ScrollSpeed = 0.2 },
new EffectControlPoint { Time = 1500, KiaiMode = false, ScrollSpeed = 0.3 },
});
});
}
[Test]
public void TestCreateNewDifficultyWithScrollSpeed_DifferentRuleset()
{
string firstDifficultyName = Guid.NewGuid().ToString();
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new ManiaRuleset().RulesetInfo));
AddStep("set unique difficulty name", () => EditorBeatmap.BeatmapInfo.DifficultyName = firstDifficultyName);
AddStep("add timing point", () => EditorBeatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }));
AddStep("add effect points", () =>
{
EditorBeatmap.ControlPointInfo.Add(250, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.05 });
EditorBeatmap.ControlPointInfo.Add(500, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.1 });
EditorBeatmap.ControlPointInfo.Add(750, new EffectControlPoint { KiaiMode = true, ScrollSpeed = 0.15 });
EditorBeatmap.ControlPointInfo.Add(1000, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.2 });
EditorBeatmap.ControlPointInfo.Add(1500, new EffectControlPoint { KiaiMode = false, ScrollSpeed = 0.3 });
});
AddStep("save beatmap", () => Editor.Save());
AddStep("create new difficulty", () => Editor.CreateNewDifficulty(new TaikoRuleset().RulesetInfo));
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != firstDifficultyName;
});
AddAssert("created difficulty has timing point", () =>
{
var timingPoint = EditorBeatmap.ControlPointInfo.TimingPoints.Single();
return timingPoint.Time == 0 && timingPoint.BeatLength == 1000;
});
AddAssert("created difficulty has effect points", () =>
{
// since this difficulty is on another ruleset, scroll speed specifications are completely reset,
// therefore discarding some effect points in the process due to being redundant.
return EditorBeatmap.ControlPointInfo.EffectPoints.SequenceEqual(new[]
{
new EffectControlPoint { Time = 500, KiaiMode = true },
new EffectControlPoint { Time = 1000, KiaiMode = false },
});
});
}
[Test] [Test]
public void TestCopyDifficulty() public void TestCopyDifficulty()
{ {
@ -530,5 +616,228 @@ namespace osu.Game.Tests.Visual.Editing
return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3); return set != null && set.PerformRead(s => s.Beatmaps.Count == 3 && s.Files.Count == 3);
}); });
} }
[Test]
public void TestSingleBackgroundFile()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
createNewDifficulty();
createNewDifficulty();
switchToDifficulty(1);
AddAssert("set background on second diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));
switchToDifficulty(0);
AddAssert("set background on first diff only", () => setBackground(applyToAllDifficulties: false, expected: "bg (2).jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (2).jpg"));
AddAssert("set background on all diff", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpg"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg" || f.Filename == "bg (2).jpg"));
}
[Test]
public void TestBackgroundFileChangesPreserveOnEncode()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set background", () => setBackground(applyToAllDifficulties: true, expected: "bg.jpg"));
createNewDifficulty();
createNewDifficulty();
switchToDifficulty(0);
AddAssert("set different background on all diff", () => setBackgroundDifferentExtension(applyToAllDifficulties: true, expected: "bg.jpeg"));
AddAssert("all diff uses one background", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.BackgroundFile == "bg.jpeg"));
AddAssert("all diff encode same background", () =>
{
return Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b =>
{
var files = new RealmFileStore(Realm, Dependencies.Get<GameHost>().Storage);
using var store = new RealmBackedResourceStore<BeatmapSetInfo>(b.BeatmapSet!.ToLive(Realm), files.Store, Realm);
string[] osu = Encoding.UTF8.GetString(store.Get(b.File!.Filename)).Split(Environment.NewLine);
Assert.That(osu, Does.Contain("0,0,\"bg.jpeg\",0,0"));
return true;
});
});
}
[Test]
public void TestSingleAudioFile()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set audio", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
createNewDifficulty();
createNewDifficulty();
switchToDifficulty(1);
AddAssert("set audio on second diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
switchToDifficulty(0);
AddAssert("set audio on first diff only", () => setAudio(applyToAllDifficulties: false, expected: "audio (2).mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (2).mp3"));
AddAssert("set audio on all diff", () => setAudio(applyToAllDifficulties: true, expected: "audio.mp3"));
AddAssert("all diff uses one audio", () => Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Metadata.AudioFile == "audio.mp3"));
AddAssert("file added", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
AddAssert("other files removed", () => !Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3" || f.Filename == "audio (2).mp3"));
}
[Test]
public void TestMultipleBackgroundFiles()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));
createNewDifficulty();
AddAssert("new difficulty uses same background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
AddAssert("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg (1).jpg"));
AddAssert("new difficulty uses new background", () => Beatmap.Value.Metadata.BackgroundFile == "bg (1).jpg");
switchToDifficulty(0);
AddAssert("old difficulty uses old background", () => Beatmap.Value.Metadata.BackgroundFile == "bg.jpg");
AddAssert("old background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg.jpg"));
AddStep("set background", () => setBackground(applyToAllDifficulties: false, expected: "bg.jpg"));
AddAssert("other background not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "bg (1).jpg"));
}
[Test]
public void TestMultipleAudioFiles()
{
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));
createNewDifficulty();
AddAssert("new difficulty uses same audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
AddAssert("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio (1).mp3"));
AddAssert("new difficulty uses new audio", () => Beatmap.Value.Metadata.AudioFile == "audio (1).mp3");
switchToDifficulty(0);
AddAssert("old difficulty uses old audio", () => Beatmap.Value.Metadata.AudioFile == "audio.mp3");
AddAssert("old audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio.mp3"));
AddStep("set audio", () => setAudio(applyToAllDifficulties: false, expected: "audio.mp3"));
AddAssert("other audio not removed", () => Beatmap.Value.BeatmapSetInfo.Files.Any(f => f.Filename == "audio (1).mp3"));
}
private void createNewDifficulty()
{
string? currentDifficulty = null;
AddStep("save", () => Editor.Save());
AddStep("create new difficulty", () =>
{
currentDifficulty = EditorBeatmap.BeatmapInfo.DifficultyName;
Editor.CreateNewDifficulty(new OsuRuleset().RulesetInfo);
});
AddUntilStep("wait for dialog", () => DialogOverlay.CurrentDialog is CreateNewDifficultyDialog);
AddStep("confirm creation with no objects", () => DialogOverlay.CurrentDialog!.PerformOkAction());
AddUntilStep("wait for created", () =>
{
string? difficultyName = Editor.ChildrenOfType<EditorBeatmap>().SingleOrDefault()?.BeatmapInfo.DifficultyName;
return difficultyName != null && difficultyName != currentDifficulty;
});
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
}
private void switchToDifficulty(int index)
{
AddStep("save", () => Editor.Save());
AddStep($"switch to difficulty #{index + 1}", () =>
Editor.SwitchToDifficulty(Beatmap.Value.BeatmapSetInfo.Beatmaps.ElementAt(index)));
AddUntilStep("wait for editor load", () => Editor.IsLoaded);
AddStep("enter setup mode", () => Editor.Mode.Value = EditorScreenMode.SongSetup);
AddUntilStep("wait for load", () => Editor.ChildrenOfType<SetupScreen>().Any());
}
private bool setBackground(bool applyToAllDifficulties, string expected)
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder =>
{
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeBackgroundImage(
new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpg")),
applyToAllDifficulties);
Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected));
return success;
});
}
private bool setBackgroundDifferentExtension(bool applyToAllDifficulties, string expected)
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
return setFile(TestResources.GetQuickTestBeatmapForImport(), extractedFolder =>
{
File.Move(
Path.Combine(extractedFolder, @"machinetop_background.jpg"),
Path.Combine(extractedFolder, @"machinetop_background.jpeg"));
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeBackgroundImage(
new FileInfo(Path.Combine(extractedFolder, @"machinetop_background.jpeg")),
applyToAllDifficulties);
Assert.That(Beatmap.Value.Metadata.BackgroundFile, Is.EqualTo(expected));
return success;
});
}
private bool setAudio(bool applyToAllDifficulties, string expected)
{
var setup = Editor.ChildrenOfType<SetupScreen>().First();
return setFile(TestResources.GetTestBeatmapForImport(), extractedFolder =>
{
bool success = setup.ChildrenOfType<ResourcesSection>().First().ChangeAudioTrack(
new FileInfo(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")),
applyToAllDifficulties);
Assert.That(Beatmap.Value.Metadata.AudioFile, Is.EqualTo(expected));
return success;
});
}
private bool setFile(string archivePath, Func<string, bool> func)
{
string temp = archivePath;
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
return func(extractedFolder);
}
finally
{
File.Delete(temp);
Directory.Delete(extractedFolder, true);
}
}
} }
} }

View File

@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16); AddStep("Set beat divisor", () => Editor.Dependencies.Get<BindableBeatDivisor>().Value = 16);
AddStep("Set timeline zoom", () => AddStep("Set timeline zoom", () =>
{ {
originalTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; originalTimelineZoom = EditorBeatmap.TimelineZoom;
var timeline = Editor.ChildrenOfType<Timeline>().Single(); var timeline = Editor.ChildrenOfType<Timeline>().Single();
InputManager.MoveMouseTo(timeline); InputManager.MoveMouseTo(timeline);
@ -81,19 +81,19 @@ namespace osu.Game.Tests.Visual.Editing
AddAssert("Ensure timeline zoom changed", () => AddAssert("Ensure timeline zoom changed", () =>
{ {
changedTimelineZoom = EditorBeatmap.BeatmapInfo.TimelineZoom; changedTimelineZoom = EditorBeatmap.TimelineZoom;
return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom); return !Precision.AlmostEquals(changedTimelineZoom, originalTimelineZoom);
}); });
SaveEditor(); SaveEditor();
AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
ReloadEditorToSameBeatmap(); ReloadEditorToSameBeatmap();
AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16); AddAssert("Beatmap still has correct beat divisor", () => EditorBeatmap.BeatmapInfo.BeatDivisor == 16);
AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.BeatmapInfo.TimelineZoom == changedTimelineZoom); AddAssert("Beatmap still has correct timeline zoom", () => EditorBeatmap.TimelineZoom == changedTimelineZoom);
} }
[Test] [Test]

View File

@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Editing
beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 }); beatmap.ControlPointInfo.Add(50000, new DifficultyControlPoint { SliderVelocity = 2 });
beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true }); beatmap.ControlPointInfo.Add(80000, new EffectControlPoint { KiaiMode = true });
beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false }); beatmap.ControlPointInfo.Add(110000, new EffectControlPoint { KiaiMode = false });
beatmap.BeatmapInfo.Bookmarks = new[] { 75000, 125000 }; beatmap.Bookmarks = new[] { 75000, 125000 };
beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000)); beatmap.Breaks.Add(new ManualBreakPeriod(90000, 120000));
editorBeatmap = new EditorBeatmap(beatmap); editorBeatmap = new EditorBeatmap(beatmap);

View File

@ -205,7 +205,7 @@ namespace osu.Game.Tests.Visual.Editing
{ {
double originalSpacing = 0; double originalSpacing = 0;
AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.BeatmapInfo.DistanceSpacing); AddStep("retrieve original spacing", () => originalSpacing = editorBeatmap.DistanceSpacing);
AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl)); AddStep("hold ctrl", () => InputManager.PressKey(Key.LControl));
AddStep("hold alt", () => InputManager.PressKey(Key.LAlt)); AddStep("hold alt", () => InputManager.PressKey(Key.LAlt));
@ -215,7 +215,7 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt)); AddStep("release alt", () => InputManager.ReleaseKey(Key.LAlt));
AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl)); AddStep("release ctrl", () => InputManager.ReleaseKey(Key.LControl));
AddAssert("distance spacing increased by 0.5", () => editorBeatmap.BeatmapInfo.DistanceSpacing == originalSpacing + 0.5); AddAssert("distance spacing increased by 0.5", () => editorBeatmap.DistanceSpacing == originalSpacing + 0.5);
} }
public partial class EditorBeatmapContainer : PopoverContainer public partial class EditorBeatmapContainer : PopoverContainer

View File

@ -527,8 +527,11 @@ namespace osu.Game.Tests.Visual.Editing
checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleBank(HitSampleInfo.BANK_NORMAL);
checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL); checkPlacementSampleAdditionBank(HitSampleInfo.BANK_NORMAL);
void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); void checkPlacementSampleBank(string expected) => AddAssert($"Placement sample is {expected}",
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}", () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected)); () => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name == HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
void checkPlacementSampleAdditionBank(string expected) => AddAssert($"Placement sample addition is {expected}",
() => EditorBeatmap.PlacementObject.Value.Samples.First(s => s.Name != HitSampleInfo.HIT_NORMAL).Bank, () => Is.EqualTo(expected));
} }
[Test] [Test]
@ -781,15 +784,39 @@ namespace osu.Game.Tests.Visual.Editing
setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT); setAdditionBankViaPopover(HitSampleInfo.BANK_SOFT);
dismissPopover(); dismissPopover();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); assertNoChanges();
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
AddStep("select first object", () => EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0])); AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH); AddStep("select second object", () =>
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL); {
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT); EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]);
});
assertNoChanges();
AddStep("select first object", () =>
{
EditorBeatmap.SelectedHitObjects.Clear();
EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[0]);
});
assertNoChanges();
void assertNoChanges()
{
hitObjectHasSamples(0, HitSampleInfo.HIT_NORMAL, HitSampleInfo.HIT_FINISH);
hitObjectHasSampleNormalBank(0, HitSampleInfo.BANK_NORMAL);
hitObjectHasSampleAdditionBank(0, HitSampleInfo.BANK_SOFT);
hitObjectHasSamples(1, HitSampleInfo.HIT_NORMAL);
hitObjectHasSampleNormalBank(1, HitSampleInfo.BANK_SOFT);
hitObjectHasSampleAdditionBank(1, HitSampleInfo.BANK_SOFT);
}
} }
private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () => private void clickSamplePiece(int objectIndex) => AddStep($"click {objectIndex.ToOrdinalWords()} sample piece", () =>
@ -883,11 +910,12 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.All(o => o.Volume == volume); return h.Samples.All(o => o.Volume == volume);
}); });
private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () => private void hitObjectNodeHasSampleVolume(int objectIndex, int nodeIndex, int volume) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has volume {volume}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Volume == volume);
});
private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () => private void setBankViaPopover(string bank) => AddStep($"set bank {bank} via popover", () =>
{ {
@ -944,29 +972,33 @@ namespace osu.Game.Tests.Visual.Editing
return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); return h.Samples.Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
}); });
private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () => private void hitObjectNodeHasSamples(int objectIndex, int nodeIndex, params string[] samples) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has samples {string.Join(',', samples)}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Select(s => s.Name).SequenceEqual(samples);
});
private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}", () => private void hitObjectNodeHasSampleBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has bank {bank}",
{ () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () => private void hitObjectNodeHasSampleNormalBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has normal bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name == HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert($"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () => private void hitObjectNodeHasSampleAdditionBank(int objectIndex, int nodeIndex, string bank) => AddAssert(
{ $"{objectIndex.ToOrdinalWords()} object {nodeIndex.ToOrdinalWords()} node has addition bank {bank}", () =>
var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats; {
return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank); var h = EditorBeatmap.HitObjects.ElementAt(objectIndex) as IHasRepeats;
}); return h is not null && h.NodeSamples[nodeIndex].Where(o => o.Name != HitSampleInfo.HIT_NORMAL).All(o => o.Bank == bank);
});
private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1)); private void editorTimeIs(double time) => AddAssert($"editor time is {time}", () => Precision.AlmostEquals(EditorClock.CurrentTimeAccurate, time, 1));
} }

View File

@ -32,6 +32,7 @@ namespace osu.Game.Tests.Visual.Editing
private TimingScreen timingScreen; private TimingScreen timingScreen;
private EditorBeatmap editorBeatmap; private EditorBeatmap editorBeatmap;
private BeatmapEditorChangeHandler changeHandler;
protected override bool ScrollUsingMouseWheel => false; protected override bool ScrollUsingMouseWheel => false;
@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.Editing
private void reloadEditorBeatmap() private void reloadEditorBeatmap()
{ {
editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value)); editorBeatmap = new EditorBeatmap(Beatmap.Value.GetPlayableBeatmap(Ruleset.Value));
changeHandler = new BeatmapEditorChangeHandler(editorBeatmap);
Child = new DependencyProvidingContainer Child = new DependencyProvidingContainer
{ {
@ -53,6 +55,7 @@ namespace osu.Game.Tests.Visual.Editing
CachedDependencies = new (Type, object)[] CachedDependencies = new (Type, object)[]
{ {
(typeof(EditorBeatmap), editorBeatmap), (typeof(EditorBeatmap), editorBeatmap),
(typeof(IEditorChangeHandler), changeHandler),
(typeof(IBeatSnapProvider), editorBeatmap) (typeof(IBeatSnapProvider), editorBeatmap)
}, },
Child = timingScreen = new TimingScreen Child = timingScreen = new TimingScreen
@ -72,8 +75,10 @@ namespace osu.Game.Tests.Visual.Editing
AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any()); AddUntilStep("Wait for rows to load", () => Child.ChildrenOfType<EffectRowAttribute>().Any());
} }
// TODO: this is best-effort for now, but the comment out test below should probably be how things should work.
// Was originally working as of https://github.com/ppy/osu/pull/26141; Regressed at some point.
[Test] [Test]
public void TestSelectedRetainedOverUndo() public void TestSelectionDismissedOnUndo()
{ {
AddStep("Select first timing point", () => AddStep("Select first timing point", () =>
{ {
@ -95,25 +100,52 @@ namespace osu.Game.Tests.Visual.Editing
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170; return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
}); });
AddStep("simulate undo", () => AddStep("undo", () => changeHandler?.RestoreState(-1));
{
var clone = editorBeatmap.ControlPointInfo.DeepClone();
editorBeatmap.ControlPointInfo.Clear(); AddUntilStep("selection dismissed", () => timingScreen.SelectedGroup.Value, () => Is.Null);
foreach (var group in clone.Groups)
{
foreach (var cp in group.ControlPoints)
editorBeatmap.ControlPointInfo.Add(group.Time, cp);
}
});
AddUntilStep("selection retained", () =>
{
return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
});
} }
// [Test]
// public void TestSelectedRetainedOverUndo()
// {
// AddStep("Select first timing point", () =>
// {
// InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("Selection changed", () => timingScreen.SelectedGroup.Value.Time == 2170);
// AddUntilStep("Ensure seeked to correct time", () => EditorClock.CurrentTimeAccurate == 2170);
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddUntilStep("wait for offset changed", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddStep("undo", () => changeHandler?.RestoreState(-1));
//
// AddUntilStep("selection retained", () =>
// {
// return timingScreen.SelectedGroup.Value.ControlPoints.Any(c => c is TimingControlPoint) && timingScreen.SelectedGroup.Value.Time > 2170;
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
//
// AddStep("Adjust offset", () =>
// {
// InputManager.MoveMouseTo(timingScreen.ChildrenOfType<TimingAdjustButton>().First().ScreenSpaceDrawQuad.Centre + new Vector2(20, 0));
// InputManager.Click(MouseButton.Left);
// });
//
// AddAssert("check group count", () => editorBeatmap.ControlPointInfo.Groups.Count, () => Is.EqualTo(10));
// }
[Test] [Test]
public void TestScrollControlGroupIntoView() public void TestScrollControlGroupIntoView()
{ {

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = leadIn } AudioLeadIn = leadIn
}); });
checkFirstFrameTime(expectedStartTime); checkFirstFrameTime(expectedStartTime);
@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} " Text = $"GameplayStartTime: {DrawableRuleset.GameplayStartTime} "
+ $"FirstHitObjectTime: {FirstHitObjectTime} " + $"FirstHitObjectTime: {FirstHitObjectTime} "
+ $"LeadInTime: {Beatmap.Value.BeatmapInfo.AudioLeadIn} " + $"LeadInTime: {Beatmap.Value.Beatmap.AudioLeadIn} "
+ $"FirstFrameClockTime: {FirstFrameClockTime}" + $"FirstFrameClockTime: {FirstFrameClockTime}"
}); });
} }

View File

@ -136,10 +136,10 @@ namespace osu.Game.Tests.Visual.Gameplay
var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); var workingBeatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
// Add intro time to test quick retry skipping (TestQuickRetry). // Add intro time to test quick retry skipping (TestQuickRetry).
workingBeatmap.BeatmapInfo.AudioLeadIn = 60000; workingBeatmap.Beatmap.AudioLeadIn = 60000;
// Set up data for testing disclaimer display. // Set up data for testing disclaimer display.
workingBeatmap.BeatmapInfo.EpilepsyWarning = epilepsyWarning ?? false; workingBeatmap.Beatmap.EpilepsyWarning = epilepsyWarning ?? false;
workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked; workingBeatmap.BeatmapInfo.Status = onlineStatus ?? BeatmapOnlineStatus.Ranked;
Beatmap.Value = workingBeatmap; Beatmap.Value = workingBeatmap;

View File

@ -221,7 +221,7 @@ namespace osu.Game.Tests.Visual.Gameplay
string? filePath = null; string? filePath = null;
// Files starting with _ are temporary, created by CreateFileSafely call. // Files starting with _ are temporary, created by CreateFileSafely call.
AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith("_", StringComparison.Ordinal)), () => Is.Not.Null); AddUntilStep("wait for export file", () => filePath = LocalStorage.GetFiles("exports").SingleOrDefault(f => !Path.GetFileName(f).StartsWith('_')), () => Is.Not.Null);
AddUntilStep("filesize is non-zero", () => AddUntilStep("filesize is non-zero", () =>
{ {
try try

View File

@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo) loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo)
{ {
BeatmapInfo = { AudioLeadIn = 60000 } AudioLeadIn = 60000
}); });
AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible); AddUntilStep("wait for skip overlay", () => Player.ChildrenOfType<SkipOverlay>().First().IsButtonVisible);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("load storyboard with only video", () => AddStep("load storyboard with only video", () =>
{ {
// LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually // LegacyStoryboardDecoder doesn't parse WidescreenStoryboard, so it is set manually
loadStoryboard("storyboard_only_video.osu", s => s.BeatmapInfo.WidescreenStoryboard = false); loadStoryboard("storyboard_only_video.osu", s => s.Beatmap.WidescreenStoryboard = false);
}); });
AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f)); AddAssert("storyboard is correct width", () => Precision.AlmostEquals(storyboard?.Width ?? 0f, 480 * 16 / 9f));

View File

@ -10,11 +10,13 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Login; using osu.Game.Overlays.Login;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Tests.Visual.Online;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Users.Drawables; using osu.Game.Users.Drawables;
using osuTK.Input; using osuTK.Input;
@ -31,6 +33,9 @@ namespace osu.Game.Tests.Visual.Menus
[Resolved] [Resolved]
private OsuConfigManager configManager { get; set; } = null!; private OsuConfigManager configManager { get; set; } = null!;
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestSceneUserPanel.TestUserStatisticsProvider statisticsProvider = new TestSceneUserPanel.TestUserStatisticsProvider();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -170,6 +175,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088"); AddStep("enter code", () => loginOverlay.ChildrenOfType<OsuTextBox>().First().Text = "88800088");
assertAPIState(APIState.Online); assertAPIState(APIState.Online);
AddStep("feed statistics", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
AddStep("click on flag", () => AddStep("click on flag", () =>
{ {
InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First()); InputManager.MoveMouseTo(loginOverlay.ChildrenOfType<UpdateableFlag>().First());

View File

@ -3,8 +3,10 @@
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -73,5 +75,57 @@ namespace osu.Game.Tests.Visual.Menus
((StarFountain)Children[1]).Shoot(-1); ((StarFountain)Children[1]).Shoot(-1);
}); });
} }
[Test]
public void TestGameplayStarFountainsSetting()
{
Bindable<bool> starFountainsEnabled = null!;
AddStep("load configuration", () =>
{
var config = new OsuConfigManager(LocalStorage);
starFountainsEnabled = config.GetBindable<bool>(OsuSetting.StarFountains);
});
AddStep("make fountains", () =>
{
Children = new Drawable[]
{
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
X = 75,
},
new KiaiGameplayFountains.GameplayStarFountain
{
Anchor = Anchor.BottomRight,
Origin = Anchor.BottomRight,
X = -75,
},
};
});
AddStep("enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("disable KiaiStarEffects", () => starFountainsEnabled.Value = false);
AddRepeatStep("attempt to activate fountains (disabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
AddStep("re-enable KiaiStarEffects", () => starFountainsEnabled.Value = true);
AddRepeatStep("activate fountains (re-enabled)", () =>
{
((KiaiGameplayFountains.GameplayStarFountain)Children[0]).Shoot(1);
((KiaiGameplayFountains.GameplayStarFountain)Children[1]).Shoot(-1);
}, 100);
}
} }
} }

View File

@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Gain", () => AddStep("Gain", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Loss", () => AddStep("Loss", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Tiny increase in PP", () => AddStep("Tiny increase in PP", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("No change 1", () => AddStep("No change 1", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -170,7 +170,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Was null", () => AddStep("Was null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {
@ -187,7 +187,7 @@ namespace osu.Game.Tests.Visual.Menus
AddStep("Became null", () => AddStep("Became null", () =>
{ {
var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single(); var transientUpdateDisplay = this.ChildrenOfType<TransientUserStatisticsUpdateDisplay>().Single();
transientUpdateDisplay.LatestUpdate.Value = new UserStatisticsUpdate( transientUpdateDisplay.LatestUpdate.Value = new ScoreBasedUserStatisticsUpdate(
new ScoreInfo(), new ScoreInfo(),
new UserStatistics new UserStatistics
{ {

View File

@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Mods
protected override TestPlayer CreateModPlayer(Ruleset ruleset) protected override TestPlayer CreateModPlayer(Ruleset ruleset)
{ {
var player = base.CreateModPlayer(ruleset); var player = base.CreateModPlayer(ruleset);
player.RestartRequested = _ => restartRequested = true; player.PrepareLoaderForRestart = _ => restartRequested = true;
return player; return player;
} }

View File

@ -44,14 +44,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
Add(detachedBeatmapStore); Add(beatmapStore);
} }
public override void SetUpSteps() public override void SetUpSteps()

View File

@ -14,7 +14,6 @@ using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
@ -76,7 +75,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Multiplayer room", Name = "Multiplayer room",
Status = new RoomStatusOpen(),
EndDate = DateTimeOffset.Now.AddDays(1), EndDate = DateTimeOffset.Now.AddDays(1),
Type = MatchType.HeadToHead, Type = MatchType.HeadToHead,
Playlist = [item1], Playlist = [item1],
@ -85,7 +83,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Private room", Name = "Private room",
Status = new RoomStatusOpenPrivate(),
Password = "*", Password = "*",
EndDate = DateTimeOffset.Now.AddDays(1), EndDate = DateTimeOffset.Now.AddDays(1),
Type = MatchType.HeadToHead, Type = MatchType.HeadToHead,
@ -95,36 +92,38 @@ namespace osu.Game.Tests.Visual.Multiplayer
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Playlist room with multiple beatmaps", Name = "Playlist room with multiple beatmaps",
Status = new RoomStatusPlaying(), Status = RoomStatus.Playing,
EndDate = DateTimeOffset.Now.AddDays(1), EndDate = DateTimeOffset.Now.AddDays(1),
Playlist = [item1, item2], Playlist = [item1, item2],
CurrentPlaylistItem = item1 CurrentPlaylistItem = item1
}), }),
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Finished room", Name = "Closing soon",
Status = new RoomStatusEnded(), EndDate = DateTimeOffset.Now.AddSeconds(5),
}),
createLoungeRoom(new Room
{
Name = "Closed room",
EndDate = DateTimeOffset.Now, EndDate = DateTimeOffset.Now,
}), }),
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Spotlight room", Name = "Spotlight room",
Status = new RoomStatusOpen(),
Category = RoomCategory.Spotlight, Category = RoomCategory.Spotlight,
}), }),
createLoungeRoom(new Room createLoungeRoom(new Room
{ {
Name = "Featured artist room", Name = "Featured artist room",
Status = new RoomStatusOpen(),
Category = RoomCategory.FeaturedArtist, Category = RoomCategory.FeaturedArtist,
}), }),
} }
}; };
}); });
AddUntilStep("wait for panel load", () => rooms.Count == 6); AddUntilStep("wait for panel load", () => rooms.Count == 7);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2); AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Currently playing", StringComparison.Ordinal)) == 2);
AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 4); AddUntilStep("correct status text", () => rooms.ChildrenOfType<OsuSpriteText>().Count(s => s.Text.ToString().StartsWith("Ready to play", StringComparison.Ordinal)) == 5);
} }
[Test] [Test]
@ -136,7 +135,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room AddStep("create room", () => Child = drawableRoom = createLoungeRoom(room = new Room
{ {
Name = "Room with password", Name = "Room with password",
Status = new RoomStatusOpen(),
Type = MatchType.HeadToHead, Type = MatchType.HeadToHead,
})); }));

View File

@ -406,13 +406,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
} }
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a high <see cref="BeatmapInfo.AudioLeadIn"/> value. /// Tests spectating with a beatmap that has a high <see cref="IBeatmap.AudioLeadIn"/> value.
/// ///
/// This test is not intended not to check the correct initial time value, but only to guard against /// This test is not intended not to check the correct initial time value, but only to guard against
/// gameplay potentially getting stuck in a stopped state due to lead in time being present. /// gameplay potentially getting stuck in a stopped state due to lead in time being present.
/// </summary> /// </summary>
[Test] [Test]
public void TestAudioLeadIn() => testLeadIn(b => b.BeatmapInfo.AudioLeadIn = 2000); public void TestAudioLeadIn() => testLeadIn(b => b.Beatmap.AudioLeadIn = 2000);
/// <summary> /// <summary>
/// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element). /// Tests spectating with a beatmap that has a storyboard element with a negative start time (i.e. intro storyboard element).

View File

@ -66,14 +66,14 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, API, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
Add(detachedBeatmapStore); Add(beatmapStore);
} }
public override void SetUpSteps() public override void SetUpSteps()

View File

@ -46,16 +46,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()))!; importedBeatmapSet = manager.Import(TestResources.CreateTestBeatmapSetInfo(8, rulesets.AvailableRulesets.ToArray()))!;
Add(detachedBeatmapStore); Add(beatmapStore);
} }
private void setUp() private void setUp()

View File

@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("make second user host", () => MultiplayerClient.TransferHost(3)); AddStep("make second user host", () => MultiplayerClient.TransferHost(3));
AddUntilStep("kick buttons not visible", () => this.ChildrenOfType<ParticipantPanel.KickButton>().Count(d => d.IsPresent) == 0); AddUntilStep("kick buttons not visible", () => !this.ChildrenOfType<ParticipantPanel.KickButton>().Any(d => d.IsPresent));
AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id)); AddStep("make local user host again", () => MultiplayerClient.TransferHost(API.LocalUser.Value.Id));

View File

@ -31,18 +31,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
Dependencies.Cache(new RealmRulesetStore(Realm)); Dependencies.Cache(new RealmRulesetStore(Realm));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
var beatmapSet = TestResources.CreateTestBeatmapSetInfo(); var beatmapSet = TestResources.CreateTestBeatmapSetInfo();
manager.Import(beatmapSet); manager.Import(beatmapSet);
Add(detachedBeatmapStore); Add(beatmapStore);
} }
public override void SetUpSteps() public override void SetUpSteps()

View File

@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.Navigation
}); });
AddStep("create IPC sender channels", () => AddStep("create IPC sender channels", () =>
{ {
ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPort = OsuGame.IPC_PORT }); ipcSenderHost = new HeadlessGameHost(gameHost.Name, new HostOptions { IPCPipeName = OsuGame.IPC_PIPE_NAME });
osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost); osuSchemeLinkIPCSender = new OsuSchemeLinkIPCChannel(ipcSenderHost);
archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost); archiveImportIPCSender = new ArchiveImportIPCChannel(ipcSenderHost);
}); });

View File

@ -354,6 +354,23 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("retry count is 1", () => player.RestartCount == 1); AddAssert("retry count is 1", () => player.RestartCount == 1);
} }
[Test]
public void TestLastScoreNullAfterExitingPlayer()
{
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
var getOriginalPlayer = playToCompletion();
AddStep("attempt to retry", () => getOriginalPlayer().ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for last play matches player", getLastPlay, () => Is.EqualTo(getOriginalPlayer().Score.ScoreInfo));
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != getOriginalPlayer() && Game.ScreenStack.CurrentScreen is Player);
AddStep("exit player", () => (Game.ScreenStack.CurrentScreen as Player)?.Exit());
AddUntilStep("wait for last play null", getLastPlay, () => Is.Null);
ScoreInfo getLastPlay() => Game.Dependencies.Get<SessionStatics>().Get<ScoreInfo>(Static.LastLocalUserScore);
}
[Test] [Test]
public void TestRetryImmediatelyAfterCompletion() public void TestRetryImmediatelyAfterCompletion()
{ {

View File

@ -23,6 +23,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Play.HUD;
using osu.Game.Screens.Play.HUD.HitErrorMeters; using osu.Game.Screens.Play.HUD.HitErrorMeters;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
@ -212,6 +213,33 @@ namespace osu.Game.Tests.Visual.Navigation
AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any()); AddAssert("no mod selected", () => !((Player)Game.ScreenStack.CurrentScreen).Mods.Value.Any());
} }
[Test]
public void TestGameplaySettingsDoesNotExpandWhenSkinOverlayPresent()
{
advanceToSongSelect();
openSkinEditor();
AddStep("select autoplay", () => Game.SelectedMods.Value = new Mod[] { new OsuModAutoplay() });
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
switchToGameplayScene();
AddUntilStep("wait for settings", () => getPlayerSettingsOverlay() != null);
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
AddStep("move cursor to right of screen", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight));
AddAssert("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
toggleSkinEditor();
AddStep("move cursor slightly", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(1)));
AddUntilStep("settings visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.GreaterThan(0));
AddStep("move cursor to right of screen too far", () => InputManager.MoveMouseTo(InputManager.ScreenSpaceDrawQuad.TopRight + new Vector2(10240, 0)));
AddUntilStep("settings not visible", () => getPlayerSettingsOverlay().DrawWidth, () => Is.EqualTo(0));
PlayerSettingsOverlay getPlayerSettingsOverlay() => ((Player)Game.ScreenStack.CurrentScreen).ChildrenOfType<PlayerSettingsOverlay>().SingleOrDefault();
}
[Test] [Test]
public void TestCinemaModRemovedOnEnteringGameplay() public void TestCinemaModRemovedOnEnteringGameplay()
{ {

View File

@ -457,6 +457,61 @@ namespace osu.Game.Tests.Visual.Online
waitForChannel1Visible(); waitForChannel1Visible();
} }
[Test]
public void TestPublicChannelsSortedByName()
{
// Intentionally join back to front.
AddStep("Show overlay with channel 2", () =>
{
channelManager.CurrentChannel.Value = channelManager.JoinChannel(testChannel2);
chatOverlay.Show();
});
AddUntilStep("second channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel2);
AddStep("Join channel 1", () => channelManager.JoinChannel(testChannel1));
AddUntilStep("first channel is at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
AddStep("message in channel 2", () =>
{
testChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("first channel still at top of list", () => getFirstVisiblePublicChannel().Channel == testChannel1);
ChannelListItem getFirstVisiblePublicChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PublicChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.Public);
}
[Test]
public void TestPrivateChannelsSortedByRecent()
{
Channel pmChannel1 = createPrivateChannel();
Channel pmChannel2 = createPrivateChannel();
joinChannel(pmChannel1);
joinChannel(pmChannel2);
AddStep("Show overlay", () => chatOverlay.Show());
AddUntilStep("first channel is at top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
AddStep("message in channel 2", () =>
{
pmChannel2.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel2);
AddStep("message in channel 1", () =>
{
pmChannel1.AddNewMessages(new Message(1) { Content = "hi!", Sender = new APIUser { Username = "person" } });
});
AddUntilStep("wait for first channel raised to top of list", () => getFirstVisiblePMChannel().Channel == pmChannel1);
ChannelListItem getFirstVisiblePMChannel() =>
chatOverlay.ChildrenOfType<ChannelList>().Single().PrivateChannelGroup.ItemFlow.FlowingChildren.OfType<ChannelListItem>().First(item => item.Channel.Type == ChannelType.PM);
}
[Test] [Test]
public void TestKeyboardNewChannel() public void TestKeyboardNewChannel()
{ {

View File

@ -0,0 +1,179 @@
// 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 NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Graphics.Containers;
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.Rulesets;
using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
public partial class TestSceneLocalUserStatisticsProvider : OsuTestScene
{
private LocalUserStatisticsProvider statisticsProvider = null!;
private readonly Dictionary<(int userId, string rulesetName), UserStatistics> serverSideStatistics = new Dictionary<(int userId, string rulesetName), UserStatistics>();
[SetUpSteps]
public void SetUpSteps()
{
AddStep("clear statistics", () => serverSideStatistics.Clear());
setUser(1000);
AddStep("setup provider", () =>
{
OsuTextFlowContainer text;
((DummyAPIAccess)API).HandleRequest = r =>
{
switch (r)
{
case GetUserRequest userRequest:
int userId = int.Parse(userRequest.Lookup);
string rulesetName = userRequest.Ruleset!.ShortName;
var response = new APIUser
{
Id = userId,
Statistics = tryGetStatistics(userId, rulesetName)
};
userRequest.TriggerSuccess(response);
return true;
default:
return false;
}
};
Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(text = new OsuTextFlowContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
});
statisticsProvider.StatisticsUpdated += update =>
{
text.Clear();
foreach (var ruleset in Dependencies.Get<RulesetStore>().AvailableRulesets)
{
text.AddText(statisticsProvider.GetStatisticsFor(ruleset) is UserStatistics statistics
? $"{ruleset.Name} statistics: (total score: {statistics.TotalScore})"
: $"{ruleset.Name} statistics: (null)");
text.NewLine();
}
text.AddText($"latest update: {update.Ruleset}"
+ $" ({(update.OldStatistics?.TotalScore.ToString() ?? "null")} -> {update.NewStatistics.TotalScore})");
};
Ruleset.Value = new OsuRuleset().RulesetInfo;
});
}
[Test]
public void TestInitialStatistics()
{
AddAssert("osu statistics populated", () => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(4_000_000));
AddAssert("taiko statistics populated", () => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(3_000_000));
AddAssert("catch statistics populated", () => statisticsProvider.GetStatisticsFor(new CatchRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(2_000_000));
AddAssert("mania statistics populated", () => statisticsProvider.GetStatisticsFor(new ManiaRuleset().RulesetInfo)!.TotalScore, () => Is.EqualTo(1_000_000));
}
[Test]
public void TestUserChanges()
{
setUser(1001);
AddStep("update statistics for user 1000", () =>
{
serverSideStatistics[(1000, "osu")] = new UserStatistics { TotalScore = 5_000_000 };
serverSideStatistics[(1000, "taiko")] = new UserStatistics { TotalScore = 6_000_000 };
});
AddAssert("statistics matches user 1001 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddAssert("statistics matches user 1001 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(3_000_000));
setUser(1000, false);
AddAssert("statistics matches user 1000 in osu",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(5_000_000));
AddAssert("statistics matches user 1000 in taiko",
() => statisticsProvider.GetStatisticsFor(new TaikoRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(6_000_000));
}
[Test]
public void TestRefetchStatistics()
{
UserStatisticsUpdate? update = null;
setUser(1001);
AddStep("update statistics server side",
() => serverSideStatistics[(1001, "osu")] = new UserStatistics { TotalScore = 9_000_000 });
AddAssert("statistics match old score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(4_000_000));
AddStep("setup event", () =>
{
update = null;
statisticsProvider.StatisticsUpdated -= onStatisticsUpdated;
statisticsProvider.StatisticsUpdated += onStatisticsUpdated;
});
AddStep("request refetch", () => statisticsProvider.RefetchStatistics(new OsuRuleset().RulesetInfo));
AddUntilStep("statistics update raised",
() => update?.NewStatistics.TotalScore,
() => Is.EqualTo(9_000_000));
AddAssert("statistics match new score",
() => statisticsProvider.GetStatisticsFor(new OsuRuleset().RulesetInfo)!.TotalScore,
() => Is.EqualTo(9_000_000));
void onStatisticsUpdated(UserStatisticsUpdate u) => update = u;
}
private UserStatistics tryGetStatistics(int userId, string rulesetName)
=> serverSideStatistics.TryGetValue((userId, rulesetName), out var stats) ? stats : new UserStatistics();
private void setUser(int userId, bool generateStatistics = true)
{
AddStep($"set local user to {userId}", () =>
{
if (generateStatistics)
{
serverSideStatistics[(userId, "osu")] = new UserStatistics { TotalScore = 4_000_000 };
serverSideStatistics[(userId, "taiko")] = new UserStatistics { TotalScore = 3_000_000 };
serverSideStatistics[(userId, "fruits")] = new UserStatistics { TotalScore = 2_000_000 };
serverSideStatistics[(userId, "mania")] = new UserStatistics { TotalScore = 1_000_000 };
}
((DummyAPIAccess)API).LocalUser.Value = new APIUser { Id = userId };
});
}
}
}

View File

@ -1,8 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
#nullable disable
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -11,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -24,17 +23,20 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public partial class TestSceneUserPanel : OsuTestScene public partial class TestSceneUserPanel : OsuTestScene
{ {
private readonly Bindable<UserActivity> activity = new Bindable<UserActivity>(); private readonly Bindable<UserActivity?> activity = new Bindable<UserActivity?>();
private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>(); private readonly Bindable<UserStatus?> status = new Bindable<UserStatus?>();
private UserGridPanel boundPanel1; private UserGridPanel boundPanel1 = null!;
private TestUserListPanel boundPanel2; private TestUserListPanel boundPanel2 = null!;
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Cached(typeof(LocalUserStatisticsProvider))]
private readonly TestUserStatisticsProvider statisticsProvider = new TestUserStatisticsProvider();
[Resolved] [Resolved]
private IRulesetStore rulesetStore { get; set; } private IRulesetStore rulesetStore { get; set; } = null!;
[SetUp] [SetUp]
public void SetUp() => Schedule(() => public void SetUp() => Schedule(() =>
@ -42,7 +44,11 @@ namespace osu.Game.Tests.Visual.Online
activity.Value = null; activity.Value = null;
status.Value = null; status.Value = null;
Child = new FillFlowContainer Remove(statisticsProvider, false);
Clear();
Add(statisticsProvider);
Add(new FillFlowContainer
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -108,7 +114,7 @@ namespace osu.Game.Tests.Visual.Online
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null } Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
}) { Width = 300 } }) { Width = 300 }
} }
}; });
boundPanel1.Status.BindTo(status); boundPanel1.Status.BindTo(status);
boundPanel1.Activity.BindTo(activity); boundPanel1.Activity.BindTo(activity);
@ -162,24 +168,21 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("update statistics", () => AddStep("update statistics", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(100000), GlobalRank = RNG.Next(100000),
CountryRank = RNG.Next(100000) CountryRank = RNG.Next(100000)
}); }, Ruleset.Value);
}); });
AddStep("set statistics to something big", () => AddStep("set statistics to something big", () =>
{ {
API.UpdateStatistics(new UserStatistics statisticsProvider.UpdateStatistics(new UserStatistics
{ {
GlobalRank = RNG.Next(1_000_000, 100_000_000), GlobalRank = RNG.Next(1_000_000, 100_000_000),
CountryRank = RNG.Next(1_000_000, 100_000_000) CountryRank = RNG.Next(1_000_000, 100_000_000)
}); }, Ruleset.Value);
});
AddStep("set statistics to empty", () =>
{
API.UpdateStatistics(new UserStatistics());
}); });
AddStep("set statistics to empty", () => statisticsProvider.UpdateStatistics(new UserStatistics(), Ruleset.Value));
} }
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!); private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
@ -201,5 +204,11 @@ namespace osu.Game.Tests.Visual.Online
public new TextFlowContainer LastVisitMessage => base.LastVisitMessage; public new TextFlowContainer LastVisitMessage => base.LastVisitMessage;
} }
public partial class TestUserStatisticsProvider : LocalUserStatisticsProvider
{
public new void UpdateStatistics(UserStatistics newStatistics, RulesetInfo ruleset, Action<UserStatisticsUpdate>? callback = null)
=> base.UpdateStatistics(newStatistics, ruleset, callback);
}
} }
} }

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Online
AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v)); AddSliderStep("weekly best", 0, 250, 1, v => update(s => s.WeeklyStreakBest = v));
AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v)); AddSliderStep("top 10%", 0, 999, 0, v => update(s => s.Top10PercentPlacements = v));
AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v)); AddSliderStep("top 50%", 0, 999, 0, v => update(s => s.Top50PercentPlacements = v));
AddSliderStep("playcount", 0, 999, 0, v => update(s => s.PlayCount = v)); AddSliderStep("playcount", 0, 1500, 1, v => update(s => s.PlayCount = v));
AddStep("create", () => AddStep("create", () =>
{ {
Clear(); Clear();
@ -66,8 +66,8 @@ namespace osu.Game.Tests.Visual.Online
[Test] [Test]
public void TestPlayCountRankingTier() public void TestPlayCountRankingTier()
{ {
AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Bronze); AddAssert("1 before silver", () => DailyChallengeStatsTooltip.TierForPlayCount(29) == RankingTier.Bronze);
AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(31) == RankingTier.Silver); AddAssert("first silver", () => DailyChallengeStatsTooltip.TierForPlayCount(30) == RankingTier.Silver);
} }
} }
} }

View File

@ -25,6 +25,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
protected override bool UseOnlineAPI => false; protected override bool UseOnlineAPI => false;
private LocalUserStatisticsProvider statisticsProvider = null!;
private UserStatisticsWatcher watcher = null!; private UserStatisticsWatcher watcher = null!;
[Resolved] [Resolved]
@ -107,7 +108,9 @@ namespace osu.Game.Tests.Visual.Online
AddStep("create watcher", () => AddStep("create watcher", () =>
{ {
Child = watcher = new UserStatisticsWatcher(); Clear();
Add(statisticsProvider = new LocalUserStatisticsProvider());
Add(watcher = new UserStatisticsWatcher(statisticsProvider));
}); });
} }
@ -123,7 +126,7 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -146,7 +149,7 @@ namespace osu.Game.Tests.Visual.Online
// note ordering - in this test processing completes *before* the registration is added. // note ordering - in this test processing completes *before* the registration is added.
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
@ -164,7 +167,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -191,7 +194,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -212,7 +215,7 @@ namespace osu.Game.Tests.Visual.Online
long scoreId = getScoreId(); long scoreId = getScoreId();
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
@ -241,7 +244,7 @@ namespace osu.Game.Tests.Visual.Online
feignScoreProcessing(userId, ruleset, 6_000_000); feignScoreProcessing(userId, ruleset, 6_000_000);
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(secondScoreId, ruleset, receivedUpdate => update = receivedUpdate);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, secondScoreId));
@ -259,15 +262,14 @@ namespace osu.Game.Tests.Visual.Online
var ruleset = new OsuRuleset().RulesetInfo; var ruleset = new OsuRuleset().RulesetInfo;
UserStatisticsUpdate? update = null; ScoreBasedUserStatisticsUpdate? update = null;
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate); registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
feignScoreProcessing(userId, ruleset, 5_000_000); feignScoreProcessing(userId, ruleset, 5_000_000);
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId)); AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
AddUntilStep("update received", () => update != null); AddUntilStep("update received", () => update != null);
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000)); AddAssert("statistics values are correct", () => statisticsProvider.GetStatisticsFor(ruleset)!.TotalScore, () => Is.EqualTo(5_000_000));
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
} }
private int nextUserId = 2000; private int nextUserId = 2000;
@ -289,7 +291,7 @@ namespace osu.Game.Tests.Visual.Online
}); });
} }
private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<UserStatisticsUpdate> onUpdateReady) => private void registerForUpdates(long scoreId, RulesetInfo rulesetInfo, Action<ScoreBasedUserStatisticsUpdate> onUpdateReady) =>
AddStep("register for updates", () => AddStep("register for updates", () =>
{ {
watcher.RegisterForStatisticsUpdateAfter( watcher.RegisterForStatisticsUpdateAfter(

View File

@ -7,6 +7,7 @@ using System.Linq;
using System.Net; using System.Net;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Playlists
private const int scores_per_result = 10; private const int scores_per_result = 10;
private const int real_user_position = 200; private const int real_user_position = 200;
private TestResultsScreen resultsScreen = null!; private ResultsScreen resultsScreen = null!;
private int lowestScoreId; // Score ID of the lowest score in the list. private int lowestScoreId; // Score ID of the lowest score in the list.
private int highestScoreId; // Score ID of the highest score in the list. private int highestScoreId; // Score ID of the highest score in the list.
@ -68,11 +69,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowWithUserScore() public void TestShowUserScore()
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded); AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
@ -81,11 +82,24 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScore() public void TestShowUserBest()
{
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createUserBestResults();
waitForDisplay();
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.UserID == userScore.UserID).ScorePosition.Value == real_user_position);
}
[Test]
public void TestShowNonUserScores()
{ {
AddStep("bind user score info handler", () => bindHandler()); AddStep("bind user score info handler", () => bindHandler());
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -96,7 +110,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(true, userScore)); AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1); AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
@ -104,11 +118,11 @@ namespace osu.Game.Tests.Visual.Playlists
} }
[Test] [Test]
public void TestShowNullUserScoreWithDelay() public void TestShowNonUserScoresWithDelay()
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
@ -119,7 +133,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
for (int i = 0; i < 2; i++) for (int i = 0; i < 2; i++)
@ -127,13 +141,16 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
} }
} }
@ -142,29 +159,36 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true)); AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
createResults(); createUserBestResults();
waitForDisplay(); waitForDisplay();
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true)); AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false)); AddStep("scroll to right", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
AddAssert("right loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Visible);
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount); AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); AddAssert("right loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreRight).State.Value == Visibility.Hidden);
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero); AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
} }
@ -173,7 +197,7 @@ namespace osu.Game.Tests.Visual.Playlists
{ {
AddStep("bind user score info handler", () => bindHandler(userScore: userScore)); AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore); createResultsWithScore(() => userScore);
waitForDisplay(); waitForDisplay();
AddStep("bind delayed handler", () => bindHandler(true)); AddStep("bind delayed handler", () => bindHandler(true));
@ -183,30 +207,36 @@ namespace osu.Game.Tests.Visual.Playlists
int beforePanelCount = 0; int beforePanelCount = 0;
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count()); AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false)); AddStep("scroll to left", () => resultsScreen.ChildrenOfType<ScorePanelList>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
AddAssert("left loading spinner shown", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Visible);
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
waitForDisplay(); waitForDisplay();
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result); AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); AddAssert("left loading spinner hidden", () =>
resultsScreen.ChildrenOfType<LoadingSpinner>().Single(l => l.Anchor == Anchor.CentreLeft).State.Value == Visibility.Hidden);
} }
} }
/// <summary>
/// Shows the <see cref="TestUserBestResultsScreen"/> with no scores provided by the API.
/// </summary>
[Test] [Test]
public void TestShowWithNoScores() public void TestShowUserBestWithNoScoresPresent()
{ {
AddStep("bind user score info handler", () => bindHandler(noScores: true)); AddStep("bind user score info handler", () => bindHandler(noScores: true));
createResults(); createUserBestResults();
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any()); AddAssert("no scores visible", () => !resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Any());
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1)); AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
} }
private void createResults(Func<ScoreInfo>? getScore = null) private void createResultsWithScore(Func<ScoreInfo> getScore)
{ {
AddStep("load results", () => AddStep("load results", () =>
{ {
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo) LoadScreen(resultsScreen = new TestScoreResultsScreen(getScore(), 1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{ {
RulesetID = new OsuRuleset().RulesetInfo.OnlineID RulesetID = new OsuRuleset().RulesetInfo.OnlineID
})); }));
@ -215,14 +245,27 @@ namespace osu.Game.Tests.Visual.Playlists
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded); AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
} }
private void createUserBestResults()
{
AddStep("load results", () =>
{
LoadScreen(resultsScreen = new TestUserBestResultsScreen(1, new PlaylistItem(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo)
{
RulesetID = new OsuRuleset().RulesetInfo.OnlineID
}, 2));
});
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
}
private void waitForDisplay() private void waitForDisplay()
{ {
AddUntilStep("wait for scores loaded", () => AddUntilStep("wait for scores loaded", () =>
requestComplete requestComplete
// request handler may need to fire more than once to get scores. // request handler may need to fire more than once to get scores.
&& totalCount > 0 && totalCount > 0
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount && resultsScreen.ChildrenOfType<ScorePanelList>().Single().GetScorePanels().Count() == totalCount
&& resultsScreen.ScorePanelList.AllPanelsVisible); && resultsScreen.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
AddWaitStep("wait for display", 5); AddWaitStep("wait for display", 5);
} }
@ -231,6 +274,7 @@ namespace osu.Game.Tests.Visual.Playlists
// pre-check for requests we should be handling (as they are scheduled below). // pre-check for requests we should be handling (as they are scheduled below).
switch (request) switch (request)
{ {
case ShowPlaylistScoreRequest:
case ShowPlaylistUserScoreRequest: case ShowPlaylistUserScoreRequest:
case IndexPlaylistScoresRequest: case IndexPlaylistScoresRequest:
break; break;
@ -253,7 +297,7 @@ namespace osu.Game.Tests.Visual.Playlists
switch (request) switch (request)
{ {
case ShowPlaylistUserScoreRequest s: case ShowPlaylistScoreRequest s:
if (userScore == null) if (userScore == null)
triggerFail(s); triggerFail(s);
else else
@ -261,6 +305,14 @@ namespace osu.Game.Tests.Visual.Playlists
break; break;
case ShowPlaylistUserScoreRequest u:
if (userScore == null)
triggerFail(u);
else
triggerSuccess(u, createUserResponse(userScore));
break;
case IndexPlaylistScoresRequest i: case IndexPlaylistScoresRequest i:
triggerSuccess(i, createIndexResponse(i, noScores)); triggerSuccess(i, createIndexResponse(i, noScores));
break; break;
@ -314,7 +366,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -329,7 +381,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = userScore.MaxCombo, MaxCombo = userScore.MaxCombo,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -363,7 +415,7 @@ namespace osu.Game.Tests.Visual.Playlists
MaxCombo = 1000, MaxCombo = 1000,
User = new APIUser User = new APIUser
{ {
Id = 2, Id = 2 + i,
Username = $"peppy{i}", Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}, },
@ -410,18 +462,22 @@ namespace osu.Game.Tests.Visual.Playlists
}; };
} }
private partial class TestResultsScreen : PlaylistItemUserResultsScreen private partial class TestScoreResultsScreen : PlaylistItemScoreResultsScreen
{ {
public new LoadingSpinner LeftSpinner => base.LeftSpinner; public TestScoreResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem)
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
public new LoadingSpinner RightSpinner => base.RightSpinner;
public new ScorePanelList ScorePanelList => base.ScorePanelList;
public TestResultsScreen(ScoreInfo? score, int roomId, PlaylistItem playlistItem)
: base(score, roomId, playlistItem) : base(score, roomId, playlistItem)
{ {
AllowRetry = true; AllowRetry = true;
} }
} }
private partial class TestUserBestResultsScreen : PlaylistItemUserBestResultsScreen
{
public TestUserBestResultsScreen(int roomId, PlaylistItem playlistItem, int userId)
: base(roomId, playlistItem, userId)
{
AllowRetry = true;
}
}
} }
} }

View File

@ -1,41 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Framework.Screens;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Visual.OnlinePlay;
namespace osu.Game.Tests.Visual.Playlists
{
public partial class TestScenePlaylistsRoomSubScreen : OnlinePlayTestScene
{
protected new TestRoomManager RoomManager => (TestRoomManager)base.RoomManager;
[Test]
public void TestStatusUpdateOnEnter()
{
Room room = null!;
PlaylistsRoomSubScreen roomScreen = null!;
AddStep("create room", () =>
{
RoomManager.AddRoom(room = new Room
{
Name = @"Test Room",
Host = new APIUser { Username = @"Host" },
Category = RoomCategory.Normal,
EndDate = DateTimeOffset.Now.AddMinutes(-1)
});
});
AddStep("push screen", () => LoadScreen(roomScreen = new PlaylistsRoomSubScreen(room)));
AddUntilStep("wait for screen load", () => roomScreen.IsCurrentScreen());
AddAssert("status is still ended", () => roomScreen.Room.Status, Is.TypeOf<RoomStatusEnded>);
}
}
}

View File

@ -112,6 +112,6 @@ namespace osu.Game.Tests.Visual.Ranking
}); });
private void displayUpdate(UserStatistics before, UserStatistics after) => private void displayUpdate(UserStatistics before, UserStatistics after) =>
AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new UserStatisticsUpdate(new ScoreInfo(), before, after)); AddStep("display update", () => overallRanking.StatisticsUpdate.Value = new ScoreBasedUserStatisticsUpdate(new ScoreInfo(), before, after));
} }
} }

View File

@ -91,12 +91,12 @@ namespace osu.Game.Tests.Visual.Ranking
UserStatisticsWatcher userStatisticsWatcher = null!; UserStatisticsWatcher userStatisticsWatcher = null!;
ScoreInfo score = null!; ScoreInfo score = null!;
AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher())); AddStep("create user statistics watcher", () => Add(userStatisticsWatcher = new UserStatisticsWatcher(new LocalUserStatisticsProvider())));
AddStep("set user statistics update", () => AddStep("set user statistics update", () =>
{ {
score = TestResources.CreateTestScoreInfo(); score = TestResources.CreateTestScoreInfo();
score.OnlineID = 1234; score.OnlineID = 1234;
((Bindable<UserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new UserStatisticsUpdate(score, ((Bindable<ScoreBasedUserStatisticsUpdate>)userStatisticsWatcher.LatestUpdate).Value = new ScoreBasedUserStatisticsUpdate(score,
new UserStatistics new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Ranking
Score = { Value = score }, Score = { Value = score },
DisplayedUserStatisticsUpdate = DisplayedUserStatisticsUpdate =
{ {
Value = new UserStatisticsUpdate(score, new UserStatistics Value = new ScoreBasedUserStatisticsUpdate(score, new UserStatistics
{ {
Level = new UserStatistics.LevelInfo Level = new UserStatistics.LevelInfo
{ {

View File

@ -16,6 +16,7 @@ using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
@ -23,6 +24,7 @@ using osu.Game.Rulesets.Taiko;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input; using osuTK.Input;
@ -42,6 +44,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private const int set_count = 5; private const int set_count = 5;
private const int diff_count = 3; private const int diff_count = 3;
[Cached(typeof(BeatmapStore))]
private TestBeatmapStore beatmaps = new TestBeatmapStore();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetStore rulesets) private void load(RulesetStore rulesets)
{ {
@ -1329,7 +1334,8 @@ namespace osu.Game.Tests.Visual.SongSelect
carouselAdjust?.Invoke(carousel); carouselAdjust?.Invoke(carousel);
carousel.BeatmapSets = beatmapSets; beatmaps.BeatmapSets.Clear();
beatmaps.BeatmapSets.AddRange(beatmapSets);
(target ?? this).Child = carousel; (target ?? this).Child = carousel;
}); });

View File

@ -8,14 +8,17 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Mania;
@ -23,30 +26,38 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osu.Game.Users; using osu.Game.Users;
using osu.Game.Utils;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
public partial class TestSceneBeatmapRecommendations : OsuGameTestScene public partial class TestSceneBeatmapRecommendations : OsuGameTestScene
{ {
[Resolved]
private IRulesetStore rulesetStore { get; set; }
[SetUpSteps] [SetUpSteps]
public override void SetUpSteps() public override void SetUpSteps()
{ {
AddStep("populate ruleset statistics", () => AddStep("populate ruleset statistics", () =>
{ {
Dictionary<string, UserStatistics> rulesetStatistics = new Dictionary<string, UserStatistics>(); ((DummyAPIAccess)API).HandleRequest = r =>
rulesetStore.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{ {
rulesetStatistics[rulesetInfo.ShortName] = new UserStatistics switch (r)
{ {
PP = getNecessaryPP(rulesetInfo.OnlineID) case GetUserRequest userRequest:
}; userRequest.TriggerSuccess(new APIUser
}); {
Id = 99,
Statistics = new UserStatistics
{
PP = getNecessaryPP(userRequest.Ruleset?.OnlineID ?? 0)
}
});
API.LocalUser.Value.RulesetsStatistics = rulesetStatistics; return true;
default:
return false;
}
};
}); });
decimal getNecessaryPP(int? rulesetID) decimal getNecessaryPP(int? rulesetID)
@ -57,7 +68,7 @@ namespace osu.Game.Tests.Visual.SongSelect
return 336; // recommended star rating of 2 return 336; // recommended star rating of 2
case 1: case 1:
return 928; // SR 3 return 973; // SR 3
case 2: case 2:
return 1905; // SR 4 return 1905; // SR 4
@ -164,6 +175,45 @@ namespace osu.Game.Tests.Visual.SongSelect
presentAndConfirm(() => maniaSet, 5); presentAndConfirm(() => maniaSet, 5);
} }
[Test]
public void TestBeatmapListingFilter()
{
AddStep("set playmode to taiko", () => ((DummyAPIAccess)API).LocalUser.Value.PlayMode = "taiko");
AddStep("open beatmap listing", () =>
{
InputManager.PressKey(Key.ControlLeft);
InputManager.PressKey(Key.B);
InputManager.ReleaseKey(Key.B);
InputManager.ReleaseKey(Key.ControlLeft);
});
AddUntilStep("wait for load", () => Game.ChildrenOfType<BeatmapListingOverlay>().SingleOrDefault()?.IsLoaded, () => Is.True);
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(1).TriggerClick());
checkRecommendedDifficulty(2);
AddStep("change mode filter to osu!taiko", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(2).TriggerClick());
checkRecommendedDifficulty(3);
AddStep("change mode filter to osu!catch", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(3).TriggerClick());
checkRecommendedDifficulty(4);
AddStep("change mode filter to osu!mania", () => Game.ChildrenOfType<BeatmapSearchRulesetFilterRow>().Single().ChildrenOfType<FilterTabItem<RulesetInfo>>().ElementAt(4).TriggerClick());
checkRecommendedDifficulty(5);
void checkRecommendedDifficulty(double starRating)
=> AddAssert($"recommended difficulty is {starRating}",
() => Game.ChildrenOfType<BeatmapSearchGeneralFilterRow>().Single().ChildrenOfType<OsuSpriteText>().ElementAt(1).Text.ToString(),
() => Is.EqualTo($"Recommended difficulty ({starRating.FormatStarRating()})"));
}
private BeatmapSetInfo importBeatmapSet(IEnumerable<RulesetInfo> difficultyRulesets) private BeatmapSetInfo importBeatmapSet(IEnumerable<RulesetInfo> difficultyRulesets)
{ {
var rulesets = difficultyRulesets.ToArray(); var rulesets = difficultyRulesets.ToArray();

View File

@ -56,20 +56,20 @@ namespace osu.Game.Tests.Visual.SongSelect
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio) private void load(GameHost host, AudioManager audio)
{ {
DetachedBeatmapStore detachedBeatmapStore; BeatmapStore beatmapStore;
// These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install. // These DI caches are required to ensure for interactive runs this test scene doesn't nuke all user beatmaps in the local install.
// At a point we have isolated interactive test runs enough, this can likely be removed. // At a point we have isolated interactive test runs enough, this can likely be removed.
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm)); Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default)); Dependencies.Cache(manager = new BeatmapManager(LocalStorage, Realm, null, audio, Resources, host, defaultBeatmap = Beatmap.Default));
Dependencies.Cache(detachedBeatmapStore = new DetachedBeatmapStore()); Dependencies.CacheAs(beatmapStore = new RealmDetachedBeatmapStore());
Dependencies.Cache(music = new MusicController()); Dependencies.Cache(music = new MusicController());
// required to get bindables attached // required to get bindables attached
Add(music); Add(music);
Add(detachedBeatmapStore); Add(beatmapStore);
Dependencies.Cache(config = new OsuConfigManager(LocalStorage)); Dependencies.Cache(config = new OsuConfigManager(LocalStorage));
} }

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -10,12 +9,14 @@ using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Online; using osu.Game.Tests.Online;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
using osuTK.Input; using osuTK.Input;
@ -31,6 +32,9 @@ namespace osu.Game.Tests.Visual.SongSelect
private BeatmapSetInfo testBeatmapSetInfo = null!; private BeatmapSetInfo testBeatmapSetInfo = null!;
[Cached(typeof(BeatmapStore))]
private TestBeatmapStore beatmaps = new TestBeatmapStore();
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{ {
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
@ -246,13 +250,12 @@ namespace osu.Game.Tests.Visual.SongSelect
private BeatmapCarousel createCarousel() private BeatmapCarousel createCarousel()
{ {
beatmaps.BeatmapSets.Clear();
beatmaps.BeatmapSets.Add(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5));
return carousel = new BeatmapCarousel(new FilterCriteria()) return carousel = new BeatmapCarousel(new FilterCriteria())
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BeatmapSets = new List<BeatmapSetInfo>
{
(testBeatmapSetInfo = TestResources.CreateTestBeatmapSetInfo(5)),
}
}; };
} }
} }

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.SelectV2;
namespace osu.Game.Tests.Visual.SongSelectV2 namespace osu.Game.Tests.Visual.SongSelectV2
{ {

View File

@ -101,7 +101,16 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
} }
} }
} },
}
},
new OsuMenuItem(@"Another nested option")
{
Items = new MenuItem[]
{
new OsuMenuItem(@"Sub-One"),
new OsuMenuItem(@"Sub-Two"),
new OsuMenuItem(@"Sub-Three"),
} }
}, },
new OsuMenuItem(@"Choose me please"), new OsuMenuItem(@"Choose me please"),

View File

@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click delete option", () => AddStep("click delete option", () =>
{ {
InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => i.Item.Text.Value.ToString().ToLowerInvariant() == "delete")); InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType<DrawableOsuMenuItem>().First(i => string.Equals(i.Item.Text.Value.ToString(), "delete", System.StringComparison.OrdinalIgnoreCase)));
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });

View File

@ -3,8 +3,10 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Database;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.UserInterface
[Cached] [Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
[Cached(typeof(BeatmapStore))]
private BeatmapStore beatmapStore = new TestBeatmapStore();
public TestSceneFirstRunScreenUIScale() public TestSceneFirstRunScreenUIScale()
{ {
AddStep("load screen", () => AddStep("load screen", () =>

View File

@ -17,12 +17,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.FirstRunSetup; using osu.Game.Overlays.FirstRunSetup;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Footer; using osu.Game.Screens.Footer;
using osu.Game.Tests.Beatmaps;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -47,6 +49,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage));
Dependencies.CacheAs<IPerformFromScreenRunner>(performer.Object); Dependencies.CacheAs<IPerformFromScreenRunner>(performer.Object);
Dependencies.CacheAs<INotificationOverlay>(notificationOverlay.Object); Dependencies.CacheAs<INotificationOverlay>(notificationOverlay.Object);
Dependencies.CacheAs<BeatmapStore>(new TestBeatmapStore());
} }
[SetUpSteps] [SetUpSteps]

View File

@ -9,6 +9,7 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Graphics.UserInterfaceV2;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Screens.Edit.Setup;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -89,8 +90,13 @@ namespace osu.Game.Tests.Visual.UserInterface
}, },
new FormFileSelector new FormFileSelector
{ {
Caption = "Audio file", Caption = "File selector",
PlaceholderText = "Select an audio file", PlaceholderText = "Select a file",
},
new FormBeatmapFileSelector(true)
{
Caption = "File selector with intermediate choice dialog",
PlaceholderText = "Select a file",
}, },
new FormColourPalette new FormColourPalette
{ {

View File

@ -12,9 +12,9 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Label="Code Analysis"> <ItemGroup Label="Code Analysis">
<CodeAnalysisRuleSet>tests.ruleset</CodeAnalysisRuleSet> <GlobalAnalyzerConfigFiles Include="CodeAnalysis.tests.globalconfig" />
</PropertyGroup> </ItemGroup>
<ItemGroup Label="Project References"> <ItemGroup Label="Project References">
<ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj" />
<ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" /> <ProjectReference Include="..\osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj" />

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RuleSet Name="osu! Rule Set" Description=" " ToolsVersion="16.0">
<Rules AnalyzerId="Microsoft.CodeQuality.Analyzers" RuleNamespace="Microsoft.CodeQuality.Analyzers">
<Rule Id="CA2007" Action="None" />
</Rules>
</RuleSet>

View File

@ -15,6 +15,7 @@ using osu.Game.Graphics;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Utils;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -207,7 +208,7 @@ namespace osu.Game.Tournament.Components
Children = new Drawable[] Children = new Drawable[]
{ {
new DiffPiece(stats), new DiffPiece(stats),
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.00}{srExtra}")) new DiffPiece(("Star Rating", $"{beatmap.StarRating.FormatStarRating()}{srExtra}"))
} }
}, },
new FillFlowContainer new FillFlowContainer

View File

@ -115,6 +115,32 @@ namespace osu.Game.Beatmaps
return mostCommon.beatLength; return mostCommon.beatLength;
} }
public double AudioLeadIn { get; set; }
public float StackLeniency { get; set; } = 0.7f;
public bool SpecialStyle { get; set; }
public bool LetterboxInBreaks { get; set; }
public bool WidescreenStoryboard { get; set; } = true;
public bool EpilepsyWarning { get; set; }
public bool SamplesMatchPlaybackRate { get; set; }
public double DistanceSpacing { get; set; } = 1.0;
public int GridSize { get; set; }
public double TimelineZoom { get; set; } = 1.0;
public CountdownType Countdown { get; set; } = CountdownType.None;
public int CountdownOffset { get; set; }
public int[] Bookmarks { get; set; } = Array.Empty<int>();
IBeatmap IBeatmap.Clone() => Clone(); IBeatmap IBeatmap.Clone() => Clone();
public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone(); public Beatmap<T> Clone() => (Beatmap<T>)MemberwiseClone();

View File

@ -73,6 +73,19 @@ namespace osu.Game.Beatmaps
beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList();
beatmap.Breaks = original.Breaks; beatmap.Breaks = original.Breaks;
beatmap.UnhandledEventLines = original.UnhandledEventLines; beatmap.UnhandledEventLines = original.UnhandledEventLines;
beatmap.AudioLeadIn = original.AudioLeadIn;
beatmap.StackLeniency = original.StackLeniency;
beatmap.SpecialStyle = original.SpecialStyle;
beatmap.LetterboxInBreaks = original.LetterboxInBreaks;
beatmap.WidescreenStoryboard = original.WidescreenStoryboard;
beatmap.EpilepsyWarning = original.EpilepsyWarning;
beatmap.SamplesMatchPlaybackRate = original.SamplesMatchPlaybackRate;
beatmap.DistanceSpacing = original.DistanceSpacing;
beatmap.GridSize = original.GridSize;
beatmap.TimelineZoom = original.TimelineZoom;
beatmap.Countdown = original.Countdown;
beatmap.CountdownOffset = original.CountdownOffset;
beatmap.Bookmarks = original.Bookmarks;
return beatmap; return beatmap;
} }

View File

@ -428,17 +428,7 @@ namespace osu.Game.Beatmaps
Hash = hash, Hash = hash,
DifficultyName = decodedInfo.DifficultyName, DifficultyName = decodedInfo.DifficultyName,
OnlineID = decodedInfo.OnlineID, OnlineID = decodedInfo.OnlineID,
AudioLeadIn = decodedInfo.AudioLeadIn,
StackLeniency = decodedInfo.StackLeniency,
SpecialStyle = decodedInfo.SpecialStyle,
LetterboxInBreaks = decodedInfo.LetterboxInBreaks,
WidescreenStoryboard = decodedInfo.WidescreenStoryboard,
EpilepsyWarning = decodedInfo.EpilepsyWarning,
SamplesMatchPlaybackRate = decodedInfo.SamplesMatchPlaybackRate,
DistanceSpacing = decodedInfo.DistanceSpacing,
BeatDivisor = decodedInfo.BeatDivisor, BeatDivisor = decodedInfo.BeatDivisor,
GridSize = decodedInfo.GridSize,
TimelineZoom = decodedInfo.TimelineZoom,
MD5Hash = memoryStream.ComputeMD5Hash(), MD5Hash = memoryStream.ComputeMD5Hash(),
EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration), EndTimeObjectCount = decoded.HitObjects.Count(h => h is IHasDuration),
TotalObjectCount = decoded.HitObjects.Count TotalObjectCount = decoded.HitObjects.Count

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