1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-17 20:02:58 +08:00

Merge branch 'master' into no-combo-scaling

This commit is contained in:
apollo 2022-03-17 21:13:07 +00:00 committed by GitHub
commit 0d4fe96ddf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
206 changed files with 4834 additions and 1497 deletions

View File

@ -50,7 +50,7 @@ Please make sure you have the following prerequisites:
- A desktop platform with the [.NET 6.0 SDK](https://dotnet.microsoft.com/download) installed.
- When developing with mobile, [Xamarin](https://docs.microsoft.com/en-us/xamarin/) is required, which is shipped together with Visual Studio or [Visual Studio for Mac](https://visualstudio.microsoft.com/vs/mac/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as [Visual Studio 2019+](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When working with the codebase, we recommend using an IDE with intelligent code completion and syntax highlighting, such as the latest version of [Visual Studio](https://visualstudio.microsoft.com/vs/), [JetBrains Rider](https://www.jetbrains.com/rider/) or [Visual Studio Code](https://code.visualstudio.com/).
- When running on Linux, please have a system-wide FFmpeg installation available to support video decoding.
### Downloading the source code
@ -72,7 +72,7 @@ git pull
Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing).
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations.
- Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln`. This will allow access to template run configurations.
You can also build and run *osu!* from the command-line with a single command:

View File

@ -1,11 +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 osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Pippidon.Scoring
{
public class PippidonScoreProcessor : ScoreProcessor
{
}
}

View File

@ -51,11 +51,11 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.211.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.223.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.304.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.314.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.9.0" />
<PackageReference Include="Realm" Version="10.10.0" />
</ItemGroup>
</Project>

View File

@ -10,6 +10,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Game.Configuration;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
@ -108,10 +109,7 @@ namespace osu.Desktop
presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty);
// update ruleset
int onlineID = ruleset.Value.OnlineID;
bool isLegacyRuleset = onlineID >= 0 && onlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
presence.Assets.SmallImageKey = isLegacyRuleset ? $"mode_{onlineID}" : "mode_custom";
presence.Assets.SmallImageKey = ruleset.Value.IsLegacyRuleset() ? $"mode_{ruleset.Value.OnlineID}" : "mode_custom";
presence.Assets.SmallImageText = ruleset.Value.Name;
client.SetPresence(presence);

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
@ -96,6 +97,8 @@ namespace osu.Desktop
switch (RuntimeInfo.OS)
{
case RuntimeInfo.Platform.Windows:
Debug.Assert(OperatingSystem.IsWindows());
return new SquirrelUpdateManager();
default:

View File

@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Runtime.Versioning;
using System.Threading;
using System.Threading.Tasks;
using osu.Desktop.LegacyIpc;
@ -12,6 +13,7 @@ using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.IPC;
using osu.Game.Tournament;
using Squirrel;
namespace osu.Desktop
{
@ -24,6 +26,10 @@ namespace osu.Desktop
[STAThread]
public static void Main(string[] args)
{
// run Squirrel first, as the app may exit after these run
if (OperatingSystem.IsWindows())
setupSquirrel();
// Back up the cwd before DesktopGameHost changes it
string cwd = Environment.CurrentDirectory;
@ -104,6 +110,23 @@ namespace osu.Desktop
}
}
[SupportedOSPlatform("windows")]
private static void setupSquirrel()
{
SquirrelAwareApp.HandleEvents(onInitialInstall: (version, tools) =>
{
tools.CreateShortcutForThisExe();
tools.CreateUninstallerRegistryEntry();
}, onAppUninstall: (version, tools) =>
{
tools.RemoveShortcutForThisExe();
tools.RemoveUninstallerRegistryEntry();
}, onEveryRun: (version, tools, firstRun) =>
{
tools.SetProcessAppUserModelId();
});
}
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary>

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
@ -16,10 +17,11 @@ using osu.Game.Overlays.Notifications;
using osuTK;
using osuTK.Graphics;
using Squirrel;
using LogLevel = Splat.LogLevel;
using Squirrel.SimpleSplat;
namespace osu.Desktop.Updater
{
[SupportedOSPlatform("windows")]
public class SquirrelUpdateManager : osu.Game.Updater.UpdateManager
{
private UpdateManager updateManager;
@ -34,12 +36,14 @@ namespace osu.Desktop.Updater
/// </summary>
private bool updatePending;
private readonly SquirrelLogger squirrelLogger = new SquirrelLogger();
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
notificationOverlay = notification;
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
SquirrelLocator.CurrentMutable.Register(() => squirrelLogger, typeof(ILogger));
}
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false);
@ -49,9 +53,11 @@ namespace osu.Desktop.Updater
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
const string github_token = null; // TODO: populate.
try
{
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true).ConfigureAwait(false);
updateManager ??= new GithubUpdateManager(@"https://github.com/ppy/osu", false, github_token, @"osulazer");
var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false);
@ -201,11 +207,11 @@ namespace osu.Desktop.Updater
}
}
private class SquirrelLogger : Splat.ILogger, IDisposable
private class SquirrelLogger : ILogger, IDisposable
{
public LogLevel Level { get; set; } = LogLevel.Info;
public Squirrel.SimpleSplat.LogLevel Level { get; set; } = Squirrel.SimpleSplat.LogLevel.Info;
public void Write(string message, LogLevel logLevel)
public void Write(string message, Squirrel.SimpleSplat.LogLevel logLevel)
{
if (logLevel < Level)
return;

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="1.0.0.0" name="osu!" />
<SquirrelAwareVersion xmlns="urn:schema-squirrel-com:asm.v1">1</SquirrelAwareVersion>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
@ -17,4 +18,4 @@
<dpiAware>true</dpiAware>
</asmv3:windowsSettings>
</asmv3:application>
</asmv1:assembly>
</asmv1:assembly>

View File

@ -24,10 +24,10 @@
<ProjectReference Include="..\osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="Clowd.Squirrel" Version="2.8.28-pre" />
<PackageReference Include="Microsoft.NETCore.Targets" Version="5.0.0" />
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
<PackageReference Include="System.IO.Packaging" Version="6.0.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.9.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="5.0.14">
<PrivateAssets>all</PrivateAssets>

View File

@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.Tests
[SetUp]
public void SetUp() => Schedule(() =>
{
scoreProcessor = new ScoreProcessor();
scoreProcessor = new ScoreProcessor(new CatchRuleset());
SetContents(_ => new CatchComboDisplay
{

View File

@ -19,7 +19,6 @@ using osu.Game.Rulesets.Catch.Difficulty;
using osu.Game.Rulesets.Catch.Scoring;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Extensions.EnumExtensions;
using osu.Game.Rulesets.Catch.Edit;
@ -182,7 +181,7 @@ namespace osu.Game.Rulesets.Catch
public override ISkin CreateLegacySkinProvider(ISkin skin, IBeatmap beatmap) => new CatchLegacySkinTransformer(skin);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public override PerformanceCalculator CreatePerformanceCalculator() => new CatchPerformanceCalculator();
public int LegacyID => 2;

View File

@ -13,33 +13,29 @@ namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchPerformanceCalculator : PerformanceCalculator
{
protected new CatchDifficultyAttributes Attributes => (CatchDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int fruitsHit;
private int ticksHit;
private int tinyTicksHit;
private int tinyTicksMissed;
private int misses;
public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
public CatchPerformanceCalculator()
: base(new CatchRuleset())
{
}
public override PerformanceAttributes Calculate()
protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
mods = Score.Mods;
var catchAttributes = (CatchDifficultyAttributes)attributes;
fruitsHit = Score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = Score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = Score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = Score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = Score.Statistics.GetValueOrDefault(HitResult.Miss);
fruitsHit = score.Statistics.GetValueOrDefault(HitResult.Great);
ticksHit = score.Statistics.GetValueOrDefault(HitResult.LargeTickHit);
tinyTicksHit = score.Statistics.GetValueOrDefault(HitResult.SmallTickHit);
tinyTicksMissed = score.Statistics.GetValueOrDefault(HitResult.SmallTickMiss);
misses = score.Statistics.GetValueOrDefault(HitResult.Miss);
// We are heavily relying on aim in catch the beat
double value = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
double value = Math.Pow(5.0 * Math.Max(1.0, catchAttributes.StarRating / 0.0049) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more. "Longer" means how many hits there are which can contribute to combo
int numTotalHits = totalComboHits();
@ -52,10 +48,10 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= Math.Pow(0.97, misses);
// Combo scaling
if (Attributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(Score.MaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
if (catchAttributes.MaxCombo > 0)
value *= Math.Min(Math.Pow(score.MaxCombo, 0.8) / Math.Pow(catchAttributes.MaxCombo, 0.8), 1.0);
double approachRate = Attributes.ApproachRate;
double approachRate = catchAttributes.ApproachRate;
double approachRateFactor = 1.0;
if (approachRate > 9.0)
approachRateFactor += 0.1 * (approachRate - 9.0); // 10% for each AR above 9
@ -66,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= approachRateFactor;
if (mods.Any(m => m is ModHidden))
if (score.Mods.Any(m => m is ModHidden))
{
// Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0)
@ -75,12 +71,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
value *= 1.01 + 0.04 * (11.0 - Math.Min(11.0, approachRate)); // 5% at AR 10, 1% at AR 11
}
if (mods.Any(m => m is ModFlashlight))
if (score.Mods.Any(m => m is ModFlashlight))
value *= 1.35 * lengthBonus;
value *= Math.Pow(accuracy(), 5.5);
if (mods.Any(m => m is ModNoFail))
if (score.Mods.Any(m => m is ModNoFail))
value *= 0.90;
return new CatchPerformanceAttributes

View File

@ -7,5 +7,11 @@ namespace osu.Game.Rulesets.Catch.Scoring
{
public class CatchScoreProcessor : ScoreProcessor
{
public CatchScoreProcessor()
: base(new CatchRuleset())
{
}
protected override double ClassicScoreMultiplier => 28;
}
}

View File

@ -13,10 +13,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
{
public class ManiaPerformanceCalculator : PerformanceCalculator
{
protected new ManiaDifficultyAttributes Attributes => (ManiaDifficultyAttributes)base.Attributes;
private Mod[] mods;
// Score after being scaled by non-difficulty-increasing mods
private double scaledScore;
@ -27,39 +23,40 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh;
private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
public ManiaPerformanceCalculator()
: base(new ManiaRuleset())
{
}
public override PerformanceAttributes Calculate()
protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
mods = Score.Mods;
scaledScore = Score.TotalScore;
countPerfect = Score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = Score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
var maniaAttributes = (ManiaDifficultyAttributes)attributes;
if (Attributes.ScoreMultiplier > 0)
scaledScore = score.TotalScore;
countPerfect = score.Statistics.GetValueOrDefault(HitResult.Perfect);
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countGood = score.Statistics.GetValueOrDefault(HitResult.Good);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
if (maniaAttributes.ScoreMultiplier > 0)
{
// Scale score up, so it's comparable to other keymods
scaledScore *= 1.0 / Attributes.ScoreMultiplier;
scaledScore *= 1.0 / maniaAttributes.ScoreMultiplier;
}
// Arbitrary initial value for scaling pp in order to standardize distributions across game modes.
// The specific number has no intrinsic meaning and can be adjusted as needed.
double multiplier = 0.8;
if (mods.Any(m => m is ModNoFail))
if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.9;
if (mods.Any(m => m is ModEasy))
if (score.Mods.Any(m => m is ModEasy))
multiplier *= 0.5;
double difficultyValue = computeDifficultyValue();
double accValue = computeAccuracyValue(difficultyValue);
double difficultyValue = computeDifficultyValue(maniaAttributes);
double accValue = computeAccuracyValue(difficultyValue, maniaAttributes);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
@ -75,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty
};
}
private double computeDifficultyValue()
private double computeDifficultyValue(ManiaDifficultyAttributes attributes)
{
double difficultyValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
double difficultyValue = Math.Pow(5 * Math.Max(1, attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
difficultyValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
@ -97,14 +94,14 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return difficultyValue;
}
private double computeAccuracyValue(double difficultyValue)
private double computeAccuracyValue(double difficultyValue, ManiaDifficultyAttributes attributes)
{
if (Attributes.GreatHitWindow <= 0)
if (attributes.GreatHitWindow <= 0)
return 0;
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
double accuracyValue = Math.Max(0.0, 0.2 - (Attributes.GreatHitWindow - 34) * 0.006667)
double accuracyValue = Math.Max(0.0, 0.2 - (attributes.GreatHitWindow - 34) * 0.006667)
* difficultyValue
* Math.Pow(Math.Max(0.0, scaledScore - 960000) / 40000, 1.1);

View File

@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
public override PerformanceCalculator CreatePerformanceCalculator() => new ManiaPerformanceCalculator();
public const string SHORT_NAME = "mania";
@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Mania
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new ManiaModMuted(),
new ModAdaptiveSpeed()
};
default:

View File

@ -45,10 +45,5 @@ namespace osu.Game.Rulesets.Mania
}
};
}
private class TimeSlider : OsuSliderBar<double>
{
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
}
}
}

View File

@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Mania.Scoring
{
internal class ManiaScoreProcessor : ScoreProcessor
{
public ManiaScoreProcessor()
: base(new ManiaRuleset())
{
}
protected override double DefaultAccuracyPortion => 0.99;
protected override double DefaultComboPortion => 0.01;
protected override double ClassicScoreMultiplier => 16;
}
}

View File

@ -3,7 +3,7 @@
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -14,10 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
private Mod[] mods;
private double accuracy;
private int scoreMaxCombo;
private int countGreat;
@ -27,31 +22,32 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double effectiveMissCount;
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
public OsuPerformanceCalculator()
: base(new OsuRuleset())
{
}
public override PerformanceAttributes Calculate()
protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
mods = Score.Mods;
accuracy = Score.Accuracy;
scoreMaxCombo = Score.MaxCombo;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount();
var osuAttributes = (OsuDifficultyAttributes)attributes;
accuracy = score.Accuracy;
scoreMaxCombo = score.MaxCombo;
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
effectiveMissCount = calculateEffectiveMissCount(osuAttributes);
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
if (mods.Any(m => m is OsuModNoFail))
if (score.Mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * effectiveMissCount);
if (mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (score.Mods.Any(m => m is OsuModSpunOut) && totalHits > 0)
multiplier *= 1.0 - Math.Pow((double)osuAttributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
if (score.Mods.Any(h => h is OsuModRelax))
{
// As we're adding Oks and Mehs to an approximated number of combo breaks the result can be higher than total hits in specific scenarios (which breaks some calculations) so we need to clamp it.
effectiveMissCount = Math.Min(effectiveMissCount + countOk + countMeh, totalHits);
@ -59,10 +55,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty
multiplier *= 0.6;
}
double aimValue = computeAimValue();
double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue();
double flashlightValue = computeFlashlightValue();
double aimValue = computeAimValue(score, osuAttributes);
double speedValue = computeSpeedValue(score, osuAttributes);
double accuracyValue = computeAccuracyValue(score, osuAttributes);
double flashlightValue = computeFlashlightValue(score, osuAttributes);
double totalValue =
Math.Pow(
Math.Pow(aimValue, 1.1) +
@ -82,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
};
}
private double computeAimValue()
private double computeAimValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double rawAim = Attributes.AimDifficulty;
double rawAim = attributes.AimDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
if (score.Mods.Any(m => m is OsuModTouchDevice))
rawAim = Math.Pow(rawAim, 0.8);
double aimValue = Math.Pow(5.0 * Math.Max(1.0, rawAim / 0.0675) - 4.0, 3.0) / 100000.0;
@ -99,41 +95,41 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= calculateMissPenalty(effectiveMissCount, Attributes.AimDifficultStrainCount);
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
else if (Attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - Attributes.ApproachRate);
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
else if (attributes.ApproachRate < 8.0)
approachRateFactor = 0.1 * (8.0 - attributes.ApproachRate);
aimValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden))
if (score.Mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * effectiveMissCount)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * attributes.DrainRate * attributes.DrainRate);
else if (score.Mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
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;
double estimateDifficultSliders = attributes.SliderCount * 0.15;
if (Attributes.SliderCount > 0)
if (attributes.SliderCount > 0)
{
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, Attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - Attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + Attributes.SliderFactor;
double estimateSliderEndsDropped = Math.Clamp(Math.Min(countOk + countMeh + countMiss, attributes.MaxCombo - scoreMaxCombo), 0, estimateDifficultSliders);
double sliderNerfFactor = (1 - attributes.SliderFactor) * Math.Pow(1 - estimateSliderEndsDropped / estimateDifficultSliders, 3) + attributes.SliderFactor;
aimValue *= sliderNerfFactor;
}
aimValue *= 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;
return aimValue;
}
private double computeSpeedValue()
private double computeSpeedValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.0;
double speedValue = Math.Pow(5.0 * Math.Max(1.0, attributes.SpeedDifficulty / 0.0675) - 4.0, 3.0) / 100000.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);
@ -143,24 +139,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= calculateMissPenalty(effectiveMissCount, Attributes.SpeedDifficultStrainCount);
double approachRateFactor = 0.0;
if (Attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (Attributes.ApproachRate - 10.33);
if (attributes.ApproachRate > 10.33)
approachRateFactor = 0.3 * (attributes.ApproachRate - 10.33);
speedValue *= 1.0 + approachRateFactor * lengthBonus; // Buff for longer maps with high AR.
if (mods.Any(m => m is OsuModBlinds))
if (score.Mods.Any(m => m is OsuModBlinds))
{
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
else if (mods.Any(m => m is OsuModHidden))
else if (score.Mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
speedValue *= 1.0 + 0.04 * (12.0 - attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
speedValue *= (0.95 + Math.Pow(attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -168,14 +164,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
return speedValue;
}
private double computeAccuracyValue()
private double computeAccuracyValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (mods.Any(h => h is OsuModRelax))
if (score.Mods.Any(h => h is OsuModRelax))
return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
int amountHitObjectsWithAccuracy = attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);
@ -188,43 +184,43 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Lots of arbitrary values from testing.
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution.
double accuracyValue = Math.Pow(1.52163, Attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
double accuracyValue = Math.Pow(1.52163, attributes.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83;
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given.
if (mods.Any(m => m is OsuModBlinds))
if (score.Mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
else if (score.Mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight))
if (score.Mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02;
return accuracyValue;
}
private double computeFlashlightValue()
private double computeFlashlightValue(ScoreInfo score, OsuDifficultyAttributes attributes)
{
if (!mods.Any(h => h is OsuModFlashlight))
if (!score.Mods.Any(h => h is OsuModFlashlight))
return 0.0;
double rawFlashlight = Attributes.FlashlightDifficulty;
double rawFlashlight = attributes.FlashlightDifficulty;
if (mods.Any(m => m is OsuModTouchDevice))
if (score.Mods.Any(m => m is OsuModTouchDevice))
rawFlashlight = Math.Pow(rawFlashlight, 0.8);
double flashlightValue = Math.Pow(rawFlashlight, 2.0) * 25.0;
if (mods.Any(h => h is OsuModHidden))
if (score.Mods.Any(h => h is OsuModHidden))
flashlightValue *= 1.3;
// Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses.
if (effectiveMissCount > 0)
flashlightValue *= 0.97 * Math.Pow(1 - Math.Pow(effectiveMissCount / totalHits, 0.775), Math.Pow(effectiveMissCount, .875));
flashlightValue *= getComboScalingFactor();
flashlightValue *= getComboScalingFactor(attributes);
// Account for shorter maps having a higher ratio of 0 combo/100 combo flashlight radius.
flashlightValue *= 0.7 + 0.1 * Math.Min(1.0, totalHits / 200.0) +
@ -233,19 +229,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Scale the flashlight value with accuracy _slightly_.
flashlightValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that.
flashlightValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
flashlightValue *= 0.98 + Math.Pow(attributes.OverallDifficulty, 2) / 2500;
return flashlightValue;
}
private double calculateEffectiveMissCount()
private double calculateEffectiveMissCount(OsuDifficultyAttributes attributes)
{
// Guess the number of misses + slider breaks from combo
double comboBasedMissCount = 0.0;
if (Attributes.SliderCount > 0)
if (attributes.SliderCount > 0)
{
double fullComboThreshold = Attributes.MaxCombo - 0.1 * Attributes.SliderCount;
double fullComboThreshold = attributes.MaxCombo - 0.1 * attributes.SliderCount;
if (scoreMaxCombo < fullComboThreshold)
comboBasedMissCount = fullComboThreshold / Math.Max(1.0, scoreMaxCombo);
}
@ -260,7 +256,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// so we use the amount of relatively difficult sections to adjust miss penalty
// to make it more punishing on maps with lower amount of hard sections.
private double calculateMissPenalty(double missCount, double difficultStrainCount) => 0.94 / ((missCount / (2 * Math.Sqrt(difficultStrainCount))) + 1);
private double getComboScalingFactor() => Attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
private double getComboScalingFactor(OsuDifficultyAttributes attributes) => attributes.MaxCombo <= 0 ? 1.0 : Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(attributes.MaxCombo, 0.8), 1.0);
private int totalHits => countGreat + countOk + countMeh + countMiss;
private int totalSuccessfulHits => countGreat + countOk + countMeh;
}

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
@ -25,13 +28,14 @@ namespace osu.Game.Rulesets.Osu.Mods
public override string Description => "It never gets boring!";
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE / 2;
/// <summary>
/// Number of previous hitobjects to be shifted together when another object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private Random rng;
private Random? rng;
public void ApplyToBeatmap(IBeatmap beatmap)
{
@ -44,28 +48,79 @@ namespace osu.Game.Rulesets.Osu.Mods
rng = new Random((int)Seed.Value);
RandomObjectInfo previous = null;
var randomObjects = randomiseObjects(hitObjects);
applyRandomisation(hitObjects, randomObjects);
}
/// <summary>
/// Randomise the position of each hit object and return a list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.
/// </summary>
/// <param name="hitObjects">A list of <see cref="OsuHitObject"/>s to have their positions randomised.</param>
/// <returns>A list of <see cref="RandomObjectInfo"/>s describing how each hit object should be placed.</returns>
private List<RandomObjectInfo> randomiseObjects(IEnumerable<OsuHitObject> hitObjects)
{
Debug.Assert(rng != null, $"{nameof(ApplyToBeatmap)} was not called before randomising objects");
var randomObjects = new List<RandomObjectInfo>();
RandomObjectInfo? previous = null;
float rateOfChangeMultiplier = 0;
for (int i = 0; i < hitObjects.Count; i++)
foreach (OsuHitObject hitObject in hitObjects)
{
var hitObject = hitObjects[i];
var current = new RandomObjectInfo(hitObject);
randomObjects.Add(current);
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
if (hitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (previous == null)
{
current.DistanceFromPrevious = (float)(rng.NextDouble() * OsuPlayfield.BASE_SIZE.X / 2);
current.RelativeAngle = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
}
else
{
current.DistanceFromPrevious = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
current.RelativeAngle = rateOfChangeMultiplier * 2 * (float)Math.PI * Math.Min(1f, current.DistanceFromPrevious / (playfield_diagonal * 0.5f));
}
previous = current;
}
return randomObjects;
}
/// <summary>
/// Reposition the hit objects according to the information in <paramref name="randomObjects"/>.
/// </summary>
/// <param name="hitObjects">The hit objects to be repositioned.</param>
/// <param name="randomObjects">A list of <see cref="RandomObjectInfo"/> describing how each hit object should be placed.</param>
private void applyRandomisation(IReadOnlyList<OsuHitObject> hitObjects, IReadOnlyList<RandomObjectInfo> randomObjects)
{
RandomObjectInfo? previous = null;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = randomObjects[i];
if (hitObject is Spinner)
{
previous = null;
continue;
}
applyRandomisation(rateOfChangeMultiplier, previous, current);
computeRandomisedPosition(current, previous, i > 1 ? randomObjects[i - 2] : null);
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
@ -102,44 +157,34 @@ namespace osu.Game.Rulesets.Osu.Mods
}
/// <summary>
/// Returns the final position of the hit object
/// Compute the randomised position of a hit object while attempting to keep it inside the playfield.
/// </summary>
/// <returns>Final position of the hit object</returns>
private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current)
/// <param name="current">The <see cref="RandomObjectInfo"/> representing the hit object to have the randomised position computed for.</param>
/// <param name="previous">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the current one.</param>
/// <param name="beforePrevious">The <see cref="RandomObjectInfo"/> representing the hit object immediately preceding the <paramref name="previous"/> one.</param>
private void computeRandomisedPosition(RandomObjectInfo current, RandomObjectInfo? previous, RandomObjectInfo? beforePrevious)
{
if (previous == null)
float previousAbsoluteAngle = 0f;
if (previous != null)
{
var playfieldSize = OsuPlayfield.BASE_SIZE;
current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y);
return;
Vector2 earliestPosition = beforePrevious?.HitObject.EndPosition ?? playfield_centre;
Vector2 relativePosition = previous.HitObject.Position - earliestPosition;
previousAbsoluteAngle = (float)Math.Atan2(relativePosition.Y, relativePosition.X);
}
float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
double randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
if (current.AngleRad < 0)
current.AngleRad += 2 * (float)Math.PI;
float absoluteAngle = previousAbsoluteAngle + current.RelativeAngle;
var posRelativeToPrev = new Vector2(
distanceToPrev * (float)Math.Cos(current.AngleRad),
distanceToPrev * (float)Math.Sin(current.AngleRad)
current.DistanceFromPrevious * (float)Math.Cos(absoluteAngle),
current.DistanceFromPrevious * (float)Math.Sin(absoluteAngle)
);
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
Vector2 lastEndPosition = previous?.EndPositionRandomised ?? playfield_centre;
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastEndPosition, posRelativeToPrev);
current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev;
current.PositionRandomised = lastEndPosition + posRelativeToPrev;
}
/// <summary>
@ -287,7 +332,25 @@ namespace osu.Game.Rulesets.Osu.Mods
private class RandomObjectInfo
{
public float AngleRad { get; set; }
/// <summary>
/// The jump angle from the previous hit object to this one, relative to the previous hit object's jump angle.
/// </summary>
/// <remarks>
/// <see cref="RelativeAngle"/> of the first hit object in a beatmap represents the absolute angle from playfield center to the object.
/// </remarks>
/// <example>
/// If <see cref="RelativeAngle"/> is 0, the player's cursor doesn't need to change its direction of movement when passing
/// the previous object to reach this one.
/// </example>
public float RelativeAngle { get; set; }
/// <summary>
/// The jump distance from the previous hit object to this one.
/// </summary>
/// <remarks>
/// <see cref="DistanceFromPrevious"/> of the first hit object in a beatmap is relative to the playfield center.
/// </remarks>
public float DistanceFromPrevious { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
@ -295,11 +358,13 @@ namespace osu.Game.Rulesets.Osu.Mods
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public OsuHitObject HitObject { get; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
AngleRad = 0;
HitObject = hitObject;
}
}
}

View File

@ -2,23 +2,22 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using osuTK;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring;
using osuTK.Graphics;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
@ -126,18 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
var slidingSamples = new List<ISampleInfo>();
var normalSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
if (normalSample != null)
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(normalSample).With("sliderslide"));
var whistleSample = HitObject.Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
if (whistleSample != null)
slidingSamples.Add(HitObject.SampleControlPoint.ApplyTo(whistleSample).With("sliderwhistle"));
slidingSample.Samples = slidingSamples.ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
}
public override void StopAllSamples()

View File

@ -74,6 +74,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue));
}
protected override void LoadSamples()
{
// Tail models don't actually get samples, as the playback is handled by DrawableSlider.
// This override is only here for visibility in explaining this weird flow.
}
public override void PlaySamples()
{
// Tail models don't actually get samples, as the playback is handled by DrawableSlider.
// This override is only here for visibility in explaining this weird flow.
}
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();

View File

@ -121,15 +121,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
base.LoadSamples();
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin");
spinningSample.Samples = new ISampleInfo[] { clone };
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
spinningSample.Samples = HitObject.CreateSpinningSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
spinningSample.Frequency.Value = spinning_sample_initial_frequency;
}
private void updateSpinningSample(ValueChangedEvent<bool> tracking)

View File

@ -29,6 +29,23 @@ namespace osu.Game.Rulesets.Osu.Objects
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public override IList<HitSampleInfo> AuxiliarySamples => CreateSlidingSamples().Concat(TailSamples).ToArray();
public IList<HitSampleInfo> CreateSlidingSamples()
{
var slidingSamples = new List<HitSampleInfo>();
var normalSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL);
if (normalSample != null)
slidingSamples.Add(normalSample.With("sliderslide"));
var whistleSample = Samples.FirstOrDefault(s => s.Name == HitSampleInfo.HIT_WHISTLE);
if (whistleSample != null)
slidingSamples.Add(whistleSample.With("sliderwhistle"));
return slidingSamples;
}
private readonly Cached<Vector2> endPositionCache = new Cached<Vector2>();
public override Vector2 EndPosition => endPositionCache.IsValid ? endPositionCache.Value : endPositionCache.Value = Position + this.CurvePositionAt(1);

View File

@ -1,7 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
@ -73,5 +77,20 @@ namespace osu.Game.Rulesets.Osu.Objects
public override Judgement CreateJudgement() => new OsuJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override IList<HitSampleInfo> AuxiliarySamples => CreateSpinningSamples();
public HitSampleInfo[] CreateSpinningSamples()
{
var referenceSample = Samples.FirstOrDefault();
if (referenceSample == null)
return Array.Empty<HitSampleInfo>();
return new[]
{
SampleControlPoint.ApplyTo(referenceSample).With("spinnerspin")
};
}
}
}

View File

@ -195,6 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModMuted(),
new OsuModNoScope(),
new OsuModAimAssist(),
new ModAdaptiveSpeed()
};
case ModType.System:
@ -212,7 +213,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
public override PerformanceCalculator CreatePerformanceCalculator() => new OsuPerformanceCalculator();
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);

View File

@ -11,6 +11,13 @@ namespace osu.Game.Rulesets.Osu.Scoring
{
public class OsuScoreProcessor : ScoreProcessor
{
public OsuScoreProcessor()
: base(new OsuRuleset())
{
}
protected override double ClassicScoreMultiplier => 36;
protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then()
.FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine);
.FadeOut(Math.Max(fade_length, timingPoint.BeatLength - fade_length), Easing.OutSine);
}
}
}

View File

@ -14,37 +14,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
public class TaikoPerformanceCalculator : PerformanceCalculator
{
protected new TaikoDifficultyAttributes Attributes => (TaikoDifficultyAttributes)base.Attributes;
private Mod[] mods;
private int countGreat;
private int countOk;
private int countMeh;
private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
public TaikoPerformanceCalculator()
: base(new TaikoRuleset())
{
}
public override PerformanceAttributes Calculate()
protected override PerformanceAttributes CreatePerformanceAttributes(ScoreInfo score, DifficultyAttributes attributes)
{
mods = Score.Mods;
countGreat = Score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = Score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
var taikoAttributes = (TaikoDifficultyAttributes)attributes;
countGreat = score.Statistics.GetValueOrDefault(HitResult.Great);
countOk = score.Statistics.GetValueOrDefault(HitResult.Ok);
countMeh = score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = score.Statistics.GetValueOrDefault(HitResult.Miss);
double multiplier = 1.1; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
if (mods.Any(m => m is ModNoFail))
if (score.Mods.Any(m => m is ModNoFail))
multiplier *= 0.90;
if (mods.Any(m => m is ModHidden))
if (score.Mods.Any(m => m is ModHidden))
multiplier *= 1.10;
double difficultyValue = computeDifficultyValue();
double accuracyValue = computeAccuracyValue();
double difficultyValue = computeDifficultyValue(score, taikoAttributes);
double accuracyValue = computeAccuracyValue(score, taikoAttributes);
double totalValue =
Math.Pow(
Math.Pow(difficultyValue, 1.1) +
@ -59,30 +57,30 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
};
}
private double computeDifficultyValue()
private double computeDifficultyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
double difficultyValue = Math.Pow(5.0 * Math.Max(1.0, attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0);
difficultyValue *= lengthBonus;
difficultyValue *= Math.Pow(0.985, countMiss);
if (mods.Any(m => m is ModHidden))
if (score.Mods.Any(m => m is ModHidden))
difficultyValue *= 1.025;
if (mods.Any(m => m is ModFlashlight<TaikoHitObject>))
if (score.Mods.Any(m => m is ModFlashlight<TaikoHitObject>))
difficultyValue *= 1.05 * lengthBonus;
return difficultyValue * Score.Accuracy;
return difficultyValue * score.Accuracy;
}
private double computeAccuracyValue()
private double computeAccuracyValue(ScoreInfo score, TaikoDifficultyAttributes attributes)
{
if (Attributes.GreatHitWindow <= 0)
if (attributes.GreatHitWindow <= 0)
return 0;
double accValue = Math.Pow(150.0 / Attributes.GreatHitWindow, 1.1) * Math.Pow(Score.Accuracy, 15) * 22.0;
double accValue = Math.Pow(150.0 / attributes.GreatHitWindow, 1.1) * Math.Pow(score.Accuracy, 15) * 22.0;
// Bonus for many objects - it's harder to keep good accuracy up for longer
return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3));

View File

@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspect.Value = false;
}
public void Update(Playfield playfield)

View File

@ -7,8 +7,15 @@ namespace osu.Game.Rulesets.Taiko.Scoring
{
internal class TaikoScoreProcessor : ScoreProcessor
{
public TaikoScoreProcessor()
: base(new TaikoRuleset())
{
}
protected override double DefaultAccuracyPortion => 0.75;
protected override double DefaultComboPortion => 0.25;
protected override double ClassicScoreMultiplier => 22;
}
}

View File

@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Taiko
{
new MultiMod(new ModWindUp(), new ModWindDown()),
new TaikoModMuted(),
new ModAdaptiveSpeed()
};
default:
@ -170,7 +171,7 @@ namespace osu.Game.Rulesets.Taiko
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score);
public override PerformanceCalculator CreatePerformanceCalculator() => new TaikoPerformanceCalculator();
public int LegacyID => 1;

View File

@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldAspect = new BindableBool(true);
protected override ScrollVisualisationMethod VisualisationMethod => ScrollVisualisationMethod.Overlapping;
protected override bool UserScrollSpeedAdjustment => false;
@ -70,7 +72,10 @@ namespace osu.Game.Rulesets.Taiko.UI
return ControlPoints[result];
}
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspect = { BindTarget = LockPlayfieldAspect }
};
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.UI
/// <summary>
/// Default height of a <see cref="TaikoPlayfield"/> when inside a <see cref="DrawableTaikoRuleset"/>.
/// </summary>
public const float DEFAULT_HEIGHT = 212;
public const float DEFAULT_HEIGHT = 200;
private Container<HitExplosion> hitExplosionContainer;
private Container<KiaiHitExplosion> kiaiExplosionContainer;

View File

@ -2,9 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -13,16 +13,22 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
public readonly IBindable<bool> LockPlayfieldAspect = new BindableBool(true);
protected override void Update()
{
base.Update();
float aspectAdjust = Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust);
float height = default_relative_height;
if (LockPlayfieldAspect.Value)
height *= Math.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Height = height;
// Position the taiko playfield exactly one playfield from the top of the screen.
RelativePositionAxes = Axes.Y;
Y = Size.Y;
Y = height;
}
}
}

View File

@ -79,7 +79,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="System.Formats.Asn1">
<Version>5.0.0</Version>
</PackageReference>

View File

@ -48,7 +48,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="DeepEqual" Version="2.0.0" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\iOS\Xamarin.iOS.CSharp.targets" />
</Project>

View File

@ -1,25 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using Realms;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public class RealmSubscriptionRegistrationTests : RealmTest
{
[Test]
public void TestSubscriptionCollectionAndPropertyChanges()
{
int collectionChanges = 0;
int propertyChanges = 0;
ChangeSet? lastChanges = null;
RunTestWithRealm((realm, _) =>
{
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
realm.Run(r => r.Refresh());
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1));
Assert.That(propertyChanges, Is.EqualTo(0));
Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
Assert.That(lastChanges?.ModifiedIndices, Is.Empty);
Assert.That(lastChanges?.NewModifiedIndices, Is.Empty);
realm.Write(r => r.All<BeatmapSetInfo>().First().Beatmaps.First().CountdownOffset = 5);
realm.Run(r => r.Refresh());
Assert.That(collectionChanges, Is.EqualTo(1));
Assert.That(propertyChanges, Is.EqualTo(1));
Assert.That(lastChanges?.InsertedIndices, Is.Empty);
Assert.That(lastChanges?.ModifiedIndices, Has.One.Items);
Assert.That(lastChanges?.NewModifiedIndices, Has.One.Items);
registration.Dispose();
});
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error)
{
lastChanges = changes;
if (changes == null)
return;
if (changes.HasCollectionChanges())
{
Interlocked.Increment(ref collectionChanges);
}
else
{
Interlocked.Increment(ref propertyChanges);
}
}
}
[Test]
public void TestSubscriptionWithAsyncWrite()
{
@ -47,6 +102,28 @@ namespace osu.Game.Tests.Database
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes, Exception error) => lastChanges = changes;
}
[Test]
public void TestPropertyChangedSubscription()
{
RunTestWithRealm((realm, _) =>
{
bool? receivedValue = null;
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
using (realm.SubscribeToPropertyChanged(r => r.All<BeatmapSetInfo>().First(), setInfo => setInfo.Protected, val => receivedValue = val))
{
Assert.That(receivedValue, Is.False);
realm.Write(r => r.All<BeatmapSetInfo>().First().Protected = true);
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.True);
}
});
}
[Test]
public void TestSubscriptionWithContextLoss()
{
@ -163,5 +240,41 @@ namespace osu.Game.Tests.Database
Assert.That(beatmapSetInfo, Is.Null);
});
}
[Test]
public void TestPropertyChangedSubscriptionWithContextLoss()
{
RunTestWithRealm((realm, _) =>
{
bool? receivedValue = null;
realm.Write(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
var subscription = realm.SubscribeToPropertyChanged(
r => r.All<BeatmapSetInfo>().First(),
setInfo => setInfo.Protected,
val => receivedValue = val);
Assert.That(receivedValue, Is.Not.Null);
receivedValue = null;
using (realm.BlockAllOperations())
{
}
// re-registration after context restore.
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.Not.Null);
subscription.Dispose();
receivedValue = null;
using (realm.BlockAllOperations())
Assert.That(receivedValue, Is.Null);
realm.Run(r => r.Refresh());
Assert.That(receivedValue, Is.Null);
});
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor();
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a miss judgement
@ -39,7 +39,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitObject() } };
var scoreProcessor = new ScoreProcessor();
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
// Apply a judgement
@ -53,7 +53,7 @@ namespace osu.Game.Tests.Gameplay
{
var beatmap = new Beatmap<HitObject> { HitObjects = { new HitCircle() } };
var scoreProcessor = new ScoreProcessor();
var scoreProcessor = new ScoreProcessor(new OsuRuleset());
scoreProcessor.ApplyBeatmap(beatmap);
scoreProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TestJudgement(HitResult.Great)) { Type = HitResult.Great });

View File

@ -1,29 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Configuration;
using osu.Game.Database;
using osu.Game.IO;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@ -118,59 +112,6 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample has lifetime end", () => sample.LifetimeEnd < double.MaxValue);
}
[TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(ModWindUp), 1.5)]
[TestCase(typeof(ModWindDown), 0.75)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
[TestCase(typeof(ModWindUp), 2)]
[TestCase(typeof(ModWindDown), 0.5)]
public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
{
GameplayClockContainer gameplayContainer = null;
StoryboardSampleInfo sampleInfo = null;
TestDrawableStoryboardSample sample = null;
Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
switch (testedMod)
{
case ModRateAdjust m:
m.SpeedChange.Value = expectedRate;
break;
case ModTimeRamp m:
m.FinalRate.Value = m.InitialRate.Value = expectedRate;
break;
}
AddStep("setup storyboard sample", () =>
{
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this);
SelectedMods.Value = new[] { testedMod };
var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
Add(gameplayContainer = new MasterGameplayClockContainer(Beatmap.Value, 0)
{
Child = beatmapSkinSourceContainer
});
beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
});
AddStep("start", () => gameplayContainer.Start());
AddAssert("sample playback rate matches mod rates", () =>
testedMod != null && Precision.AlmostEquals(
sample.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value,
((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime)));
}
[Test]
public void TestSamplePlaybackWithBeatmapHitsoundsOff()
{

View File

@ -24,6 +24,20 @@ namespace osu.Game.Tests.Online
[TestFixture]
public class TestAPIModJsonSerialization
{
[Test]
public void TestUnknownMod()
{
var apiMod = new APIMod { Acronym = "WNG" };
var deserialized = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
var converted = deserialized?.ToMod(new TestRuleset());
Assert.That(converted, Is.TypeOf(typeof(UnknownMod)));
Assert.That(converted?.Type, Is.EqualTo(ModType.System));
Assert.That(converted?.Acronym, Is.EqualTo("WNG??"));
}
[Test]
public void TestAcronymIsPreserved()
{

View File

@ -118,7 +118,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestBeatmapDownloadingFlow()
{
AddAssert("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet));
AddUntilStep("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded);
AddStep("start downloading", () => beatmapDownloader.Download(testBeatmapSet));
@ -132,7 +132,7 @@ namespace osu.Game.Tests.Online
AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true));
AddUntilStep("wait for import", () => beatmaps.CurrentImport != null);
AddAssert("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
AddUntilStep("ensure beatmap available", () => beatmaps.IsAvailableLocally(testBeatmapSet));
addAvailabilityCheckStep("state is locally available", BeatmapAvailability.LocallyAvailable);
}

View File

@ -0,0 +1,40 @@
// 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 Newtonsoft.Json;
using NUnit.Framework;
using osu.Game.IO.Serialization;
using osu.Game.Online.Solo;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Online
{
/// <summary>
/// Basic testing to ensure our attribute-based naming is correctly working.
/// </summary>
[TestFixture]
public class TestSubmittableScoreJsonSerialization
{
[Test]
public void TestScoreSerialisationViaExtensionMethod()
{
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
string serialised = score.Serialize();
Assert.That(serialised, Contains.Substring("large_tick_hit"));
Assert.That(serialised, Contains.Substring("\"rank\": \"S\""));
}
[Test]
public void TestScoreSerialisationWithoutSettings()
{
var score = new SubmittableScore(TestResources.CreateTestScoreInfo());
string serialised = JsonConvert.SerializeObject(score);
Assert.That(serialised, Contains.Substring("large_tick_hit"));
Assert.That(serialised, Contains.Substring("\"rank\":\"S\""));
}
}
}

View File

@ -6,11 +6,15 @@ using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Tests.Rulesets.Scoring
@ -23,7 +27,7 @@ namespace osu.Game.Tests.Rulesets.Scoring
[SetUp]
public void SetUp()
{
scoreProcessor = new ScoreProcessor();
scoreProcessor = new ScoreProcessor(new TestRuleset());
beatmap = new TestBeatmap(new RulesetInfo())
{
HitObjects = new List<HitObject>
@ -300,7 +304,26 @@ namespace osu.Game.Tests.Rulesets.Scoring
HitObjects = { new TestHitObject(result) }
});
Assert.That(scoreProcessor.GetImmediateScore(ScoringMode.Standardised, result.AffectsCombo() ? 1 : 0, statistic), Is.EqualTo(expectedScore).Within(0.5d));
Assert.That(scoreProcessor.ComputeFinalScore(ScoringMode.Standardised, new ScoreInfo
{
Ruleset = new TestRuleset().RulesetInfo,
MaxCombo = result.AffectsCombo() ? 1 : 0,
Statistics = statistic
}), Is.EqualTo(expectedScore).Within(0.5d));
}
private class TestRuleset : Ruleset
{
public override IEnumerable<Mod> GetModsFor(ModType type) => throw new System.NotImplementedException();
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => throw new System.NotImplementedException();
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => throw new System.NotImplementedException();
public override string Description => string.Empty;
public override string ShortName => string.Empty;
}
private class TestJudgement : Judgement

View File

@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay
}
[Test]
public void TestDisplay()
public void TestCalibrationFromZero()
{
const double average_error = -4.5;
@ -62,11 +62,39 @@ namespace osu.Game.Tests.Visual.Gameplay
};
});
AddAssert("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == -average_error);
AddAssert("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}
/// <summary>
/// When a beatmap offset was already set, the calibration should take it into account.
/// </summary>
[Test]
public void TestCalibrationFromNonZero()
{
const double average_error = -4.5;
const double initial_offset = -2;
AddStep("Set offset non-neutral", () => offsetControl.Current.Value = initial_offset);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Set reference score", () =>
{
offsetControl.ReferenceScore.Value = new ScoreInfo
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(average_error)
};
});
AddUntilStep("Has calibration button", () => offsetControl.ChildrenOfType<SettingsButton>().Any());
AddStep("Press button", () => offsetControl.ChildrenOfType<SettingsButton>().Single().TriggerClick());
AddAssert("Offset is adjusted", () => offsetControl.Current.Value == initial_offset - average_error);
AddUntilStep("Button is disabled", () => !offsetControl.ChildrenOfType<SettingsButton>().Single().Enabled.Value);
AddStep("Remove reference score", () => offsetControl.ReferenceScore.Value = null);
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
}

View File

@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private SkinManager skinManager { get; set; }
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -1,11 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
@ -22,6 +25,8 @@ namespace osu.Game.Tests.Visual.Gameplay
[Cached]
private Storyboard storyboard { get; set; } = new Storyboard();
private IEnumerable<DrawableStoryboardSprite> sprites => this.ChildrenOfType<DrawableStoryboardSprite>();
[Test]
public void TestSkinSpriteDisallowedByDefault()
{
@ -41,9 +46,54 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.Centre, Vector2.Zero)));
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
assertSpritesFromSkin(true);
AddAssert("skinnable sprite has correct size", () => sprites.Any(s => Precision.AlmostEquals(s.ChildrenOfType<SkinnableSprite>().Single().Size, new Vector2(128, 128))));
}
[Test]
public void TestFlippedSprite()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("flip sprites", () => sprites.ForEach(s =>
{
s.FlipH = true;
s.FlipV = true;
}));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
}
[Test]
public void TestNegativeScale()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
}
[Test]
public void TestNegativeScaleWithFlippedSprite()
{
const string lookup_name = "hitcircleoverlay";
AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true);
AddStep("create sprites", () => SetContents(_ => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero)));
AddStep("scale sprite", () => sprites.ForEach(s => s.VectorScale = new Vector2(-1)));
AddAssert("origin flipped", () => sprites.All(s => s.Origin == Anchor.BottomRight));
AddStep("flip sprites", () => sprites.ForEach(s =>
{
s.FlipH = true;
s.FlipV = true;
}));
AddAssert("origin back", () => sprites.All(s => s.Origin == Anchor.TopLeft));
}
private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition)
@ -57,7 +107,6 @@ namespace osu.Game.Tests.Visual.Gameplay
private void assertSpritesFromSkin(bool fromSkin) =>
AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}",
() => this.ChildrenOfType<DrawableStoryboardSprite>()
.All(sprite => sprite.ChildrenOfType<SkinnableSprite>().Any() == fromSkin));
() => sprites.All(sprite => sprite.ChildrenOfType<SkinnableSprite>().Any() == fromSkin));
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1);
AddAssert("total number of results == 1", () =>
{
var score = new ScoreInfo();
var score = new ScoreInfo { Ruleset = Ruleset.Value };
((FailPlayer)Player).ScoreProcessor.PopulateScore(score);

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play;
using osu.Game.Skinning;
@ -24,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -48,7 +48,11 @@ namespace osu.Game.Tests.Visual.Gameplay
{
AddStep("create display", () => recreateDisplay(new OsuHitWindows(), 5));
AddRepeatStep("New random judgement", () => newJudgement(), 40);
AddRepeatStep("New random judgement", () =>
{
double offset = RNG.Next(-150, 150);
newJudgement(offset, drawableRuleset.HitWindows.ResultFor(offset));
}, 400);
AddRepeatStep("New max negative", () => newJudgement(-drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
AddRepeatStep("New max positive", () => newJudgement(drawableRuleset.HitWindows.WindowFor(HitResult.Meh)), 20);
@ -273,6 +277,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestScoreProcessor : ScoreProcessor
{
public TestScoreProcessor()
: base(new OsuRuleset())
{
}
public void Reset() => base.Reset(false);
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay
gameplayState = new GameplayState(beatmap, ruleset);
gameplayState.LastJudgementResult.BindTo(lastJudgementResult);
scoreProcessor = new ScoreProcessor();
scoreProcessor = new ScoreProcessor(ruleset);
Child = dependencyContainer = new DependencyProvidingContainer
{

View File

@ -2,8 +2,10 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
@ -16,6 +18,9 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool Autoplay => true;
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[SetUpSteps]
public override void SetUpSteps()
{
@ -24,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddStep("reload skin editor", () =>
{
skinEditor?.Expire();
Player.ScaleTo(SkinEditorOverlay.VISIBLE_TARGET_SCALE);
Player.ScaleTo(0.8f);
LoadComponentAsync(skinEditor = new SkinEditor(Player), Add);
});
}

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Skinning.Editor;
@ -11,13 +13,17 @@ namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSkinEditorComponentsList : SkinnableTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test]
public void TestToggleEditor()
{
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox(300)
AddStep("show available components", () => SetContents(_ => new SkinComponentToolbox
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 0.6f,
}));
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinEditorMultipleSkins : SkinnableTestScene
{
[Cached]
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor();
private readonly ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@ -14,7 +15,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableAccuracyCounter : SkinnableHUDComponentTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();

View File

@ -5,6 +5,7 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
@ -13,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableComboCounter : SkinnableHUDComponentTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter();

View File

@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private HUDOverlay hudOverlay;
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

View File

@ -4,6 +4,7 @@
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@ -13,7 +14,7 @@ namespace osu.Game.Tests.Visual.Gameplay
public class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestScene
{
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor();
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

View File

@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
var decoupledClock = new DecoupleableInterpolatingFramedClock { IsCoupled = true };
storyboardContainer.Clock = decoupledClock;
storyboard = working.Storyboard.CreateDrawable(Beatmap.Value);
storyboard = working.Storyboard.CreateDrawable(SelectedMods.Value);
storyboard.Passing = false;
storyboardContainer.Add(storyboard);
@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay
sb = decoder.Decode(bfr);
}
storyboard = sb.CreateDrawable(Beatmap.Value);
storyboard = sb.CreateDrawable(SelectedMods.Value);
storyboardContainer.Add(storyboard);
decoupledClock.ChangeSource(Beatmap.Value.Track);

View File

@ -1,17 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Audio;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Storyboards;
using osu.Game.Storyboards.Drawables;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Gameplay
{
@ -19,6 +25,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{
private Storyboard storyboard;
private IReadOnlyList<Mod> storyboardMods;
protected override bool HasCustomSteps => true;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
@ -31,42 +41,107 @@ namespace osu.Game.Tests.Visual.Gameplay
backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20));
}
[SetUp]
public void SetUp() => storyboardMods = Array.Empty<Mod>();
[Test]
public void TestStoryboardSamplesStopDuringPause()
{
checkForFirstSamplePlayback();
createPlayerTest();
AddStep("player paused", () => Player.Pause());
AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value);
AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
allStoryboardSamplesStopped();
AddStep("player resume", () => Player.Resume());
AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
waitUntilStoryboardSamplesPlay();
}
[Test]
public void TestStoryboardSamplesStopOnSkip()
{
checkForFirstSamplePlayback();
createPlayerTest();
AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space));
AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
skipIntro();
allStoryboardSamplesStopped();
AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
waitUntilStoryboardSamplesPlay();
}
private void checkForFirstSamplePlayback()
[TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
public void TestStoryboardSamplesPlaybackWithRateAdjustMods(Type expectedMod, double expectedRate)
{
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
AddStep("setup mod", () =>
{
ModRateAdjust testedMod = (ModRateAdjust)Activator.CreateInstance(expectedMod).AsNonNull();
testedMod.SpeedChange.Value = expectedRate;
storyboardMods = new[] { testedMod };
});
createPlayerTest();
skipIntro();
AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == expectedRate));
}
[TestCase(typeof(ModWindUp), 0.5, 2)]
[TestCase(typeof(ModWindUp), 1.51, 2)]
[TestCase(typeof(ModWindDown), 2, 0.5)]
[TestCase(typeof(ModWindDown), 0.99, 0.5)]
public void TestStoryboardSamplesPlaybackWithTimeRampMods(Type expectedMod, double initialRate, double finalRate)
{
AddStep("setup mod", () =>
{
ModTimeRamp testedMod = (ModTimeRamp)Activator.CreateInstance(expectedMod).AsNonNull();
testedMod.InitialRate.Value = initialRate;
testedMod.FinalRate.Value = finalRate;
storyboardMods = new[] { testedMod };
});
createPlayerTest();
skipIntro();
ModTimeRamp gameplayMod = null;
AddUntilStep("mod speed change updated", () =>
{
gameplayMod = Player.GameplayState.Mods.OfType<ModTimeRamp>().Single();
return gameplayMod.SpeedChange.Value != initialRate;
});
AddAssert("sample playback rate matches mod rates", () => allStoryboardSamples.All(sound =>
sound.ChildrenOfType<DrawableSample>().First().AggregateFrequency.Value == gameplayMod.SpeedChange.Value));
}
private void createPlayerTest()
{
CreateTest(null);
AddAssert("storyboard loaded", () => Player.Beatmap.Value.Storyboard != null);
waitUntilStoryboardSamplesPlay();
}
private void waitUntilStoryboardSamplesPlay() => AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying));
private void allStoryboardSamplesStopped() => AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying));
private void skipIntro() => AddStep("skip intro", () => InputManager.Key(Key.Space));
private IEnumerable<DrawableStoryboardSample> allStoryboardSamples => Player.ChildrenOfType<DrawableStoryboardSample>();
protected override bool AllowFail => false;
protected override TestPlayer CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = SelectedMods.Value.Concat(storyboardMods).ToArray();
return new TestPlayer(true, false);
}
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false);
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) =>
new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio);

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneUnknownMod : ModTestScene
{
protected override Ruleset CreatePlayerRuleset() => new OsuRuleset();
/// <summary>
/// This test also covers the scenario of exiting Player after an unsuccessful beatmap load.
/// </summary>
[Test]
public void TestUnknownModDoesntEnterGameplay()
{
CreateModTest(new ModTestData
{
Beatmap = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo).Beatmap,
Mod = new UnknownMod("WNG"),
PassCondition = () => Player.IsLoaded && !Player.LoadedBeatmapSuccessfully
});
}
}
}

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Scoring;
using osu.Game.Rulesets.Scoring;
@ -102,6 +103,11 @@ namespace osu.Game.Tests.Visual.Gameplay
private class TestScoreProcessor : ScoreProcessor
{
public TestScoreProcessor()
: base(new OsuRuleset())
{
}
public void Reset() => base.Reset(false);
}
}

View File

@ -46,7 +46,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
var scoreProcessor = new OsuScoreProcessor();
scoreProcessor.ApplyBeatmap(playable);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray()) { Expanded = { Value = true } }, Add);
LoadComponentAsync(leaderboard = new MultiSpectatorLeaderboard(Ruleset.Value, scoreProcessor, clocks.Keys.Select(id => new MultiplayerRoomUser(id)).ToArray())
{
Expanded = { Value = true }
}, Add);
});
AddUntilStep("wait for load", () => leaderboard.IsLoaded);

View File

@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
scoreProcessor.ApplyBeatmap(playableBeatmap);
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, multiplayerUsers.ToArray())
LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(Ruleset.Value, scoreProcessor, multiplayerUsers.ToArray())
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,

View File

@ -43,8 +43,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
Dependencies.Cache(rulesets = new RealmRulesetStore(Realm));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, Realm, rulesets, null, audio, Resources, host, Beatmap.Default));
Dependencies.Cache(Realm);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
}
[SetUp]
@ -52,8 +50,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AvailabilityTracker.SelectedItem.BindTo(selectedItem);
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).WaitSafely();
importedSet = beatmaps.GetAllUsableBeatmapSets().First();
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First());
selectedItem.Value = new PlaylistItem(Beatmap.Value.BeatmapInfo)
{
RulesetID = Beatmap.Value.BeatmapInfo.Ruleset.OnlineID
@ -92,16 +92,16 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
OsuButton readyButton = null;
AddAssert("ensure ready button enabled", () =>
AddUntilStep("ensure ready button enabled", () =>
{
readyButton = button.ChildrenOfType<OsuButton>().Single();
return readyButton.Enabled.Value;
});
AddStep("delete beatmap", () => beatmaps.Delete(importedSet));
AddAssert("ready button disabled", () => !readyButton.Enabled.Value);
AddUntilStep("ready button disabled", () => !readyButton.Enabled.Value);
AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet));
AddAssert("ready button enabled back", () => readyButton.Enabled.Value);
AddUntilStep("ready button enabled back", () => readyButton.Enabled.Value);
}
[Test]

View File

@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);
AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null);
}
@ -178,7 +178,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);
AddAssert("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("ensure score is pending deletion", () => Game.Realm.Run(r => r.Find<ScoreInfo>(score.ID)?.DeletePending == true));
AddUntilStep("wait for score panel removal", () => scorePanel.Parent == null);
}

View File

@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("ensure manager loaded", () => beatmaps != null);
ensureSoleilyRemoved();
createButtonWithBeatmap(createSoleily());
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddUntilStep("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()));
AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineID == 241526));
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online
createButtonWithBeatmap(createSoleily());
AddUntilStep("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable);
ensureSoleilyRemoved();
AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
AddUntilStep("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded);
}
private void ensureSoleilyRemoved()

View File

@ -0,0 +1,90 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat.Listing;
using osuTK;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChannelListing : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider overlayColours = new OverlayColourProvider(OverlayColourScheme.Pink);
private SearchTextBox search;
private ChannelListing listing;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Children = new Drawable[]
{
search = new SearchTextBox
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 300,
Margin = new MarginPadding { Top = 100 },
},
listing = new ChannelListing
{
Size = new Vector2(800, 400),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
listing.Show();
search.Current.ValueChanged += term => listing.SearchTerm = term.NewValue;
});
}
[SetUpSteps]
public void SetUpSteps()
{
AddStep("Add Join/Leave callbacks", () =>
{
listing.OnRequestJoin += channel => channel.Joined.Value = true;
listing.OnRequestLeave += channel => channel.Joined.Value = false;
});
}
[Test]
public void TestAddRandomChannels()
{
AddStep("Add Random Channels", () =>
{
listing.UpdateAvailableChannels(createRandomChannels(20));
});
}
private Channel createRandomChannel()
{
int id = RNG.Next(0, 10000);
return new Channel
{
Name = $"#channel-{id}",
Topic = RNG.Next(4) < 3 ? $"We talk about the number {id} here" : null,
Type = ChannelType.Public,
Id = id,
};
}
private List<Channel> createRandomChannels(int num)
=> Enumerable.Range(0, num)
.Select(_ => createRandomChannel())
.ToList();
}
}

View File

@ -122,6 +122,8 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestHideOverlay()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddAssert("Chat overlay is visible", () => chatOverlay.State.Value == Visibility.Visible);
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
@ -134,6 +136,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestChannelSelection()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
AddStep("Setup get message response", () => onGetMessages = channel =>
{
@ -169,6 +172,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestSearchInSelector()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Search for 'no. 2'", () => chatOverlay.ChildrenOfType<SearchTextBox>().First().Text = "no. 2");
AddUntilStep("Only channel 2 visible", () =>
{
@ -180,6 +184,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestChannelShortcutKeys()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel)));
AddStep("Close channel selector", () => InputManager.Key(Key.Escape));
AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden);
@ -199,6 +204,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestCloseChannelBehaviour()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddUntilStep("Join until dropdown has channels", () =>
{
if (visibleChannels.Count() < joinedChannels.Count())
@ -269,6 +275,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestChannelCloseButton()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () =>
{
channelManager.JoinChannel(channel1);
@ -289,6 +296,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestCloseTabShortcut()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () =>
{
channelManager.JoinChannel(channel1);
@ -314,6 +322,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestNewTabShortcut()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 2 channels", () =>
{
channelManager.JoinChannel(channel1);
@ -330,6 +339,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestRestoreTabShortcut()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join 3 channels", () =>
{
channelManager.JoinChannel(channel1);
@ -375,6 +385,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestChatCommand()
{
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
@ -398,6 +409,8 @@ namespace osu.Game.Tests.Visual.Online
{
Channel multiplayerChannel = null;
AddStep("open chat overlay", () => chatOverlay.Show());
AddStep("join multiplayer channel", () => channelManager.JoinChannel(multiplayerChannel = new Channel(new APIUser())
{
Name = "#mp_1",
@ -412,6 +425,122 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
}
[Test]
public void TestHighlightOnCurrentChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Send message in channel 1", () =>
{
channel1.AddNewMessages(message = new Message
{
ChannelId = channel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel1));
}
[Test]
public void TestHighlightOnAnotherChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Send message in channel 2", () =>
{
channel2.AddNewMessages(message = new Message
{
ChannelId = channel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
}
[Test]
public void TestHighlightOnLeftChannel()
{
Message message = null;
AddStep("Open chat overlay", () => chatOverlay.Show());
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Join channel 2", () => channelManager.JoinChannel(channel2));
AddStep("Send message in channel 2", () =>
{
channel2.AddNewMessages(message = new Message
{
ChannelId = channel2.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Leave channel 2", () => channelManager.LeaveChannel(channel2));
AddStep("Highlight message", () => chatOverlay.HighlightMessage(message, channel2));
AddAssert("Switched to channel 2", () => channelManager.CurrentChannel.Value == channel2);
}
[Test]
public void TestHighlightWhileChatNeverOpen()
{
Message message = null;
AddStep("Join channel 1", () => channelManager.JoinChannel(channel1));
AddStep("Send message in channel 1", () =>
{
channel1.AddNewMessages(message = new Message
{
ChannelId = channel1.Id,
Content = "Message to highlight!",
Timestamp = DateTimeOffset.Now,
Sender = new APIUser
{
Id = 2,
Username = "Someone",
}
});
});
AddStep("Highlight message and open chat", () =>
{
chatOverlay.HighlightMessage(message, channel1);
chatOverlay.Show();
});
}
private void pressChannelHotkey(int number)
{
var channelKey = Key.Number0 + number;
@ -456,8 +585,6 @@ namespace osu.Game.Tests.Visual.Online
ChannelManager,
ChatOverlay = new TestChatOverlay { RelativeSizeAxes = Axes.Both, },
};
ChatOverlay.Show();
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online
private int messageIdCounter;
[SetUp]
public void Setup()
public void Setup() => Schedule(() =>
{
if (API is DummyAPIAccess daa)
{
@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online
testContainer.ChatOverlay.Show();
});
}
});
private bool dummyAPIHandleRequest(APIRequest request)
{

View File

@ -9,7 +9,8 @@ using System;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.Chat;
using osuTK.Input;
@ -108,49 +109,7 @@ namespace osu.Game.Tests.Visual.Online
[Test]
public void TestManyMessages()
{
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "I am a wang!"
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I am team red."
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I plan to win!"
}));
AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = blueUser,
Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand."
}));
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
AddStep("message from long username", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
AddStep("message with new date", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Message from the future!",
Timestamp = DateTimeOffset.Now
}));
sendRegularMessages();
checkScrolledToBottom();
const int messages_per_call = 10;
@ -183,6 +142,64 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
[Test]
public void TestMessageHighlighting()
{
Message highlighted = null;
sendRegularMessages();
AddStep("highlight first message", () =>
{
highlighted = testChannel.Messages[0];
testChannel.HighlightedMessage.Value = highlighted;
});
AddUntilStep("chat scrolled to first message", () =>
{
var line = chatDisplay.ChildrenOfType<ChatLine>().Single(c => c.Message == highlighted);
return chatDisplay.ScrollContainer.ScreenSpaceDrawQuad.Contains(line.ScreenSpaceDrawQuad.Centre);
});
sendMessage();
checkNotScrolledToBottom();
AddStep("highlight last message", () =>
{
highlighted = testChannel.Messages[^1];
testChannel.HighlightedMessage.Value = highlighted;
});
AddUntilStep("chat scrolled to last message", () =>
{
var line = chatDisplay.ChildrenOfType<ChatLine>().Single(c => c.Message == highlighted);
return chatDisplay.ScrollContainer.ScreenSpaceDrawQuad.Contains(line.ScreenSpaceDrawQuad.Centre);
});
sendMessage();
checkScrolledToBottom();
AddRepeatStep("highlight other random messages", () =>
{
highlighted = testChannel.Messages[RNG.Next(0, testChannel.Messages.Count - 1)];
testChannel.HighlightedMessage.Value = highlighted;
}, 10);
}
[Test]
public void TestMessageHighlightingOnFilledChat()
{
int index = 0;
fillChat(100);
AddStep("highlight first message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = 0]);
AddStep("highlight next message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = Math.Min(index + 1, testChannel.Messages.Count - 1)]);
AddStep("highlight last message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = testChannel.Messages.Count - 1]);
AddStep("highlight previous message", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = Math.Max(index - 1, 0)]);
AddRepeatStep("highlight random messages", () => testChannel.HighlightedMessage.Value = testChannel.Messages[index = RNG.Next(0, testChannel.Messages.Count - 1)], 10);
}
/// <summary>
/// Tests that when a message gets wrapped by the chat display getting contracted while scrolled to bottom, the chat will still keep scrolling down.
/// </summary>
@ -207,7 +224,28 @@ namespace osu.Game.Tests.Visual.Online
}
[Test]
public void TestUserScrollOverride()
public void TestOverrideChatScrolling()
{
fillChat();
sendMessage();
checkScrolledToBottom();
AddStep("Scroll to start", () => chatDisplay.ScrollContainer.ScrollToStart());
checkNotScrolledToBottom();
sendMessage();
checkNotScrolledToBottom();
AddStep("Scroll to bottom", () => chatDisplay.ScrollContainer.ScrollToEnd());
checkScrolledToBottom();
sendMessage();
checkScrolledToBottom();
}
[Test]
public void TestOverrideChatScrollingByUser()
{
fillChat();
@ -266,11 +304,11 @@ namespace osu.Game.Tests.Visual.Online
checkScrolledToBottom();
}
private void fillChat()
private void fillChat(int count = 10)
{
AddStep("fill chat", () =>
{
for (int i = 0; i < 10; i++)
for (int i = 0; i < count; i++)
{
testChannel.AddNewMessages(new Message(messageIdSequence++)
{
@ -301,6 +339,52 @@ namespace osu.Game.Tests.Visual.Online
}));
}
private void sendRegularMessages()
{
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "I am a wang!"
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I am team red."
}));
AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = redUser,
Content = "I plan to win!"
}));
AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = blueUser,
Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand."
}));
AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = admin,
Content = "Okay okay, calm down guys. Let's do this!"
}));
AddStep("message from long username", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Hi guys, my new username is lit!"
}));
AddStep("message with new date", () => testChannel.AddNewMessages(new Message(messageIdSequence++)
{
Sender = longUsernameUser,
Content = "Message from the future!",
Timestamp = DateTimeOffset.Now
}));
}
private void checkScrolledToBottom() =>
AddUntilStep("is scrolled to bottom", () => chatDisplay.ScrolledToBottom);
@ -314,9 +398,9 @@ namespace osu.Game.Tests.Visual.Online
{
}
protected DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
public DrawableChannel DrawableChannel => InternalChildren.OfType<DrawableChannel>().First();
protected UserTrackingScrollContainer ScrollContainer => (UserTrackingScrollContainer)((Container)DrawableChannel.Child).Child;
public ChannelScrollContainer ScrollContainer => (ChannelScrollContainer)((Container)DrawableChannel.Child).Child;
public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child;

View File

@ -33,35 +33,45 @@ namespace osu.Game.Tests.Visual.Playlists
private TestResultsScreen resultsScreen;
private int currentScoreId;
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 bool requestComplete;
private int totalCount;
private ScoreInfo userScore;
[SetUp]
public void Setup() => Schedule(() =>
[SetUpSteps]
public override void SetUpSteps()
{
currentScoreId = 1;
requestComplete = false;
totalCount = 0;
bindHandler();
base.SetUpSteps();
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
// else the tests that rely on ordering will fall over.
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
});
// Previous test instances of the results screen may still exist at this point so wait for
// those screens to be cleaned up by the base SetUpSteps before re-initialising test state.
// The the screen also holds a leased Beatmap bindable so reassigning it must happen after
// the screen as been exited.
AddStep("initialise user scores and beatmap", () =>
{
lowestScoreId = 1;
highestScoreId = 1;
requestComplete = false;
totalCount = 0;
userScore = TestResources.CreateTestScoreInfo();
userScore.TotalScore = 0;
userScore.Statistics = new Dictionary<HitResult, int>();
bindHandler();
// Beatmap is required to be an actual beatmap so the scores can get their scores correctly
// calculated for standardised scoring, else the tests that rely on ordering will fall over.
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
});
}
[Test]
public void TestShowWithUserScore()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(userScore: userScore);
});
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
@ -81,15 +91,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestShowUserScoreWithDelay()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(true, userScore);
});
AddStep("bind user score info handler", () => bindHandler(true, userScore));
createResults(() => userScore);
@ -124,7 +126,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
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);
}
}
@ -132,15 +134,7 @@ namespace osu.Game.Tests.Visual.Playlists
[Test]
public void TestFetchWhenScrolledToTheLeft()
{
ScoreInfo userScore = null;
AddStep("bind user score info handler", () =>
{
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(userScore: userScore);
});
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
createResults(() => userScore);
@ -156,7 +150,7 @@ namespace osu.Game.Tests.Visual.Playlists
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
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);
}
}
@ -245,16 +239,13 @@ namespace osu.Game.Tests.Visual.Playlists
{
var multiplayerUserScore = new MultiplayerScore
{
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
ID = highestScoreId,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = userScore.Passed,
Rank = userScore.Rank,
Position = real_user_position,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore,
User = userScore.User,
Statistics = userScore.Statistics,
ScoresAround = new MultiplayerScoresAround
{
Higher = new MultiplayerScores(),
@ -268,38 +259,32 @@ namespace osu.Game.Tests.Visual.Playlists
{
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
ID = --highestScoreId,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore - i,
User = new APIUser
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = userScore.Statistics
});
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
ID = ++lowestScoreId,
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = true,
Rank = userScore.Rank,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore + i,
User = new APIUser
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = userScore.Statistics
});
totalCount += 2;
@ -315,33 +300,23 @@ namespace osu.Game.Tests.Visual.Playlists
{
var result = new IndexedMultiplayerScores();
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
for (int i = 1; i <= scores_per_result; i++)
{
result.Scores.Add(new MultiplayerScore
{
ID = currentScoreId++,
ID = sort == "score_asc" ? --highestScoreId : ++lowestScoreId,
Accuracy = 1,
EndedAt = DateTimeOffset.Now,
Passed = true,
Rank = ScoreRank.X,
MaxCombo = 1000,
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
User = new APIUser
{
Id = 2,
Username = $"peppy{i}",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
Statistics = new Dictionary<HitResult, int>
{
{ HitResult.Miss, 1 },
{ HitResult.Meh, 50 },
{ HitResult.Good, 100 },
{ HitResult.Great, 300 }
}
});
totalCount++;
@ -367,7 +342,7 @@ namespace osu.Game.Tests.Visual.Playlists
{
Properties = new Dictionary<string, JToken>
{
{ "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
{ "sort", JToken.FromObject(scores.Scores[^1].ID > scores.Scores[^2].ID ? "score_asc" : "score_desc") }
}
};
}

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics;
@ -17,22 +18,33 @@ namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
{
private HitEventTimingDistributionGraph graph;
private static readonly HitObject placeholder_object = new HitCircle();
[Test]
public void TestManyDistributedEvents()
{
createTest(CreateDistributedHitEvents());
AddStep("add adjustment", () => graph.UpdateOffset(10));
}
[Test]
public void TestManyDistributedEventsOffset()
{
createTest(CreateDistributedHitEvents(-3.5));
}
[Test]
public void TestAroundCentre()
{
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
}
[Test]
public void TestZeroTimeOffset()
{
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null)).ToList());
}
[Test]
@ -47,9 +59,9 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(Enumerable.Range(0, 100).Select(i =>
{
if (i % 2 == 0)
return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null);
return new HitEvent(0, HitResult.Perfect, placeholder_object, placeholder_object, null);
return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null);
return new HitEvent(30, HitResult.Miss, placeholder_object, placeholder_object, null);
}).ToList());
}
@ -62,7 +74,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
new HitEventTimingDistributionGraph(events)
graph = new HitEventTimingDistributionGraph(events)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -77,10 +89,10 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < range * 2; i++)
{
int count = (int)(Math.Pow(range - Math.Abs(i - range), 2));
int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10;
for (int j = 0; j < count; j++)
hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null));
}
return hitEvents;

View File

@ -390,7 +390,7 @@ namespace osu.Game.Tests.Visual.Ranking
private class RulesetWithNoPerformanceCalculator : OsuRuleset
{
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null;
public override PerformanceCalculator CreatePerformanceCalculator() => null;
}
}
}

View File

@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Models;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
@ -208,13 +210,19 @@ namespace osu.Game.Tests.Visual.Ranking
public void TestKeyboardNavigation()
{
var lowestScore = TestResources.CreateTestScoreInfo();
lowestScore.MaxCombo = 100;
lowestScore.OnlineID = 3;
lowestScore.TotalScore = 0;
lowestScore.Statistics = new Dictionary<HitResult, int>();
var middleScore = TestResources.CreateTestScoreInfo();
middleScore.MaxCombo = 200;
middleScore.OnlineID = 2;
middleScore.TotalScore = 0;
middleScore.Statistics = new Dictionary<HitResult, int>();
var highestScore = TestResources.CreateTestScoreInfo();
highestScore.MaxCombo = 300;
highestScore.OnlineID = 1;
highestScore.TotalScore = 0;
highestScore.Statistics = new Dictionary<HitResult, int>();
createListStep(() => new ScorePanelList());

View File

@ -119,7 +119,8 @@ namespace osu.Game.Tests.Visual.SongSelect
AddStep(@"Network failure", () => leaderboard.SetErrorState(LeaderboardState.NetworkFailure));
AddStep(@"No supporter", () => leaderboard.SetErrorState(LeaderboardState.NotSupporter));
AddStep(@"Not logged in", () => leaderboard.SetErrorState(LeaderboardState.NotLoggedIn));
AddStep(@"Unavailable", () => leaderboard.SetErrorState(LeaderboardState.Unavailable));
AddStep(@"Ruleset unavailable", () => leaderboard.SetErrorState(LeaderboardState.RulesetUnavailable));
AddStep(@"Beatmap unavailable", () => leaderboard.SetErrorState(LeaderboardState.BeatmapUnavailable));
AddStep(@"None selected", () => leaderboard.SetErrorState(LeaderboardState.NoneSelected));
}

View File

@ -284,14 +284,13 @@ namespace osu.Game.Tests.Visual.SongSelect
public void TestDummy()
{
createSongSelect();
AddAssert("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
AddUntilStep("dummy selected", () => songSelect.CurrentBeatmap == defaultBeatmap);
AddUntilStep("dummy shown on wedge", () => songSelect.CurrentBeatmapDetailsBeatmap == defaultBeatmap);
addManyTestMaps();
AddWaitStep("wait for select", 3);
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
}
[Test]
@ -299,9 +298,8 @@ namespace osu.Game.Tests.Visual.SongSelect
{
createSongSelect();
addManyTestMaps();
AddWaitStep("wait for add", 3);
AddAssert("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
AddUntilStep("random map selected", () => songSelect.CurrentBeatmap != defaultBeatmap);
AddStep(@"Sort by Artist", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Artist));
AddStep(@"Sort by Title", () => config.SetValue(OsuSetting.SongSelectSortingMode, SortMode.Title));
@ -571,6 +569,8 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
AddStep("press ctrl+enter", () =>
{
InputManager.PressKey(Key.ControlLeft);
@ -605,6 +605,8 @@ namespace osu.Game.Tests.Visual.SongSelect
addRulesetImportStep(0);
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
DrawableCarouselBeatmapSet set = null;
AddStep("Find the DrawableCarouselBeatmapSet", () =>
{
@ -844,6 +846,8 @@ namespace osu.Game.Tests.Visual.SongSelect
createSongSelect();
AddUntilStep("wait for selection", () => !Beatmap.IsDefault);
AddStep("present score", () =>
{
// this beatmap change should be overridden by the present.

View File

@ -124,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect
});
AddUntilStep("Became present", () => topLocalRank.IsPresent);
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.B);
AddStep("Add higher score for current user", () =>
{
@ -137,7 +137,7 @@ namespace osu.Game.Tests.Visual.SongSelect
scoreManager.Import(testScoreInfo2);
});
AddAssert("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
AddUntilStep("Correct rank", () => topLocalRank.Rank == ScoreRank.S);
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestSceneDifficultyMultiplierDisplay : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestDifficultyMultiplierDisplay()
{
DifficultyMultiplierDisplay multiplierDisplay = null;
AddStep("create content", () => Child = multiplierDisplay = new DifficultyMultiplierDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
});
AddStep("set multiplier below 1", () => multiplierDisplay.Current.Value = 0.5);
AddStep("set multiplier to 1", () => multiplierDisplay.Current.Value = 1);
AddStep("set multiplier above 1", () => multiplierDisplay.Current.Value = 1.5);
AddSliderStep("set multiplier", 0, 2, 1d, multiplier =>
{
if (multiplierDisplay != null)
multiplierDisplay.Current.Value = multiplier;
});
}
}
}

View File

@ -0,0 +1,97 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Overlays;
using osu.Game.Screens.Edit.Components;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneEditorSidebar : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue);
[Test]
public void TestSidebars()
{
AddStep("Add sidebars", () =>
{
Children = new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new EditorSidebar
{
Children = new[]
{
new EditorSidebarSection("Section 1")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(3),
ChildrenEnumerable = Enumerable.Range(0, 10).Select(_ => new Box
{
Colour = Color4.White,
Size = new Vector2(32),
})
},
},
new EditorSidebarSection("Section with a really long section header")
{
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Full,
Spacing = new Vector2(3),
ChildrenEnumerable = Enumerable.Range(0, 400).Select(_ => new Box
{
Colour = Color4.Gray,
Size = new Vector2(32),
})
},
},
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
},
new EditorSidebar
{
Children = new[]
{
new EditorSidebarSection("Section 1"),
new EditorSidebarSection("Section 2"),
},
},
}
}
}
};
});
}
}
}

View File

@ -0,0 +1,45 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
[TestFixture]
public class TestScenePopupScreenTitle : OsuTestScene
{
[Cached]
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
[Test]
public void TestPopupScreenTitle()
{
AddStep("create content", () =>
{
Child = new PopupScreenTitle
{
Title = "Popup Screen Title",
Description = string.Join(" ", Enumerable.Repeat("This is a description.", 20)),
Close = () => { }
};
});
}
[Test]
public void TestDisabledExit()
{
AddStep("create content", () =>
{
Child = new PopupScreenTitle
{
Title = "Popup Screen Title",
Description = "This is a description."
};
});
}
}
}

View File

@ -8,7 +8,7 @@
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="NUnit3TestAdapter" Version="4.1.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
<PackageReference Include="Moq" Version="4.16.1" />
<PackageReference Include="Moq" Version="4.17.2" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -9,6 +9,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
@ -83,7 +84,7 @@ namespace osu.Game.Beatmaps
requestedUserId = api.LocalUser.Value.Id;
// only query API for built-in rulesets
rulesets.AvailableRulesets.Where(ruleset => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo =>
rulesets.AvailableRulesets.Where(ruleset => ruleset.IsLegacyRuleset()).ForEach(rulesetInfo =>
{
var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo);

View File

@ -140,7 +140,7 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.UIScale, 1f, 0.8f, 1.6f, 0.01f);
SetDefault(OsuSetting.UIHoldActivationDelay, 200f, 0f, 500f, 50f);
SetDefault(OsuSetting.UIHoldActivationDelay, 200.0, 0.0, 500.0, 50.0);
SetDefault(OsuSetting.IntroSequence, IntroSequence.Triangles);
@ -240,9 +240,9 @@ namespace osu.Game.Configuration
};
}
public Func<Guid, string> LookupSkinName { private get; set; }
public Func<Guid, string> LookupSkinName { private get; set; } = _ => @"unknown";
public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; }
public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; } = _ => @"unknown";
}
// IMPORTANT: These are used in user configuration files.
@ -270,7 +270,13 @@ namespace osu.Game.Configuration
MouseDisableButtons,
MouseDisableWheel,
ConfineMouseMode,
/// <summary>
/// Globally applied audio offset.
/// This is added to the audio track's current time. Higher values will cause gameplay to occur earlier, relative to the audio track.
/// </summary>
AudioOffset,
VolumeInactive,
MenuMusic,
MenuVoice,

View File

@ -6,6 +6,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using JetBrains.Annotations;
@ -170,6 +171,39 @@ namespace osu.Game.Configuration
private static readonly ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]> property_info_cache = new ConcurrentDictionary<Type, (SettingSourceAttribute, PropertyInfo)[]>();
/// <summary>
/// Returns the underlying value of the given mod setting object.
/// Can be used for serialization and equality comparison purposes.
/// </summary>
/// <param name="setting">A <see cref="SettingSourceAttribute"/> bindable.</param>
public static object GetUnderlyingSettingValue(this object setting)
{
switch (setting)
{
case Bindable<double> d:
return d.Value;
case Bindable<int> i:
return i.Value;
case Bindable<float> f:
return f.Value;
case Bindable<bool> b:
return b.Value;
case IBindable u:
// An unknown (e.g. enum) generic type.
var valueMethod = u.GetType().GetProperty(nameof(IBindable<int>.Value));
Debug.Assert(valueMethod != null);
return valueMethod.GetValue(u);
default:
// fall back for non-bindable cases.
return setting;
}
}
public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetSettingsSourceProperties(this object obj)
{
var type = obj.GetType();

View File

@ -3,9 +3,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@ -103,6 +105,8 @@ namespace osu.Game.Database
public Realm Realm => ensureUpdateRealm();
private const string realm_extension = @".realm";
private Realm ensureUpdateRealm()
{
if (isSendingNotificationResetEvents)
@ -147,11 +151,18 @@ namespace osu.Game.Database
Filename = filename;
const string realm_extension = @".realm";
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension;
string newerVersionFilename = $"{Filename.Replace(realm_extension, string.Empty)}_newer_version{realm_extension}";
// Attempt to recover a newer database version if available.
if (storage.Exists(newerVersionFilename))
{
Logger.Log(@"A newer realm database has been found, attempting recovery...", LoggingTarget.Database);
attemptRecoverFromFile(newerVersionFilename);
}
try
{
// This method triggers the first `getRealmInstance` call, which will implicitly run realm migrations and bring the schema up-to-date.
@ -159,15 +170,78 @@ namespace osu.Game.Database
}
catch (Exception e)
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
// See https://github.com/realm/realm-core/blob/master/src%2Frealm%2Fobject-store%2Fobject_store.cpp#L1016-L1022
// This is the best way we can detect a schema version downgrade.
if (e.Message.StartsWith(@"Provided schema version", StringComparison.Ordinal))
{
Logger.Error(e, "Your local database is too new to work with this version of osu!. Please close osu! and install the latest release to recover your data.");
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
storage.Delete(Filename);
// If a newer version database already exists, don't backup again. We can presume that the first backup is the one we care about.
if (!storage.Exists(newerVersionFilename))
CreateBackup(newerVersionFilename);
storage.Delete(Filename);
}
else
{
Logger.Error(e, "Realm startup failed with unrecoverable error; starting with a fresh database. A backup of your database has been made.");
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_corrupt{realm_extension}");
storage.Delete(Filename);
}
cleanupPendingDeletions();
}
}
private void attemptRecoverFromFile(string recoveryFilename)
{
Logger.Log($@"Performing recovery from {recoveryFilename}", LoggingTarget.Database);
// First check the user hasn't started to use the database that is in place..
try
{
using (var realm = Realm.GetInstance(getConfiguration()))
{
if (realm.All<ScoreInfo>().Any())
{
Logger.Log(@"Recovery aborted as the existing database has scores set already.", LoggingTarget.Database);
Logger.Log(@"To perform recovery, delete client.realm while osu! is not running.", LoggingTarget.Database);
return;
}
}
}
catch
{
// Even if reading the in place database fails, still attempt to recover.
}
// Then check that the database we are about to attempt recovery can actually be recovered on this version..
try
{
using (Realm.GetInstance(getConfiguration(recoveryFilename)))
{
// Don't need to do anything, just check that opening the realm works correctly.
}
}
catch
{
Logger.Log(@"Recovery aborted as the newer version could not be loaded by this osu! version.", LoggingTarget.Database);
return;
}
// For extra safety, also store the temporarily-used database which we are about to replace.
CreateBackup($"{Filename.Replace(realm_extension, string.Empty)}_{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}_newer_version_before_recovery{realm_extension}");
storage.Delete(Filename);
using (var inputStream = storage.GetStream(recoveryFilename))
using (var outputStream = storage.GetStream(Filename, FileAccess.Write, FileMode.Create))
inputStream.CopyTo(outputStream);
storage.Delete(recoveryFilename);
Logger.Log(@"Recovery complete!", LoggingTarget.Database);
}
private void cleanupPendingDeletions()
{
using (var realm = getRealmInstance())
@ -318,6 +392,66 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Subscribe to the property of a realm object to watch for changes.
/// </summary>
/// <remarks>
/// On subscribing, unless the <paramref name="modelAccessor"/> does not match an object, an initial invocation of <paramref name="onChanged"/> will occur immediately.
/// Further invocations will occur when the value changes, but may also fire on a realm recycle with no actual value change.
/// </remarks>
/// <param name="modelAccessor">A function to retrieve the relevant model from realm.</param>
/// <param name="propertyLookup">A function to traverse to the relevant property from the model.</param>
/// <param name="onChanged">A function to be invoked when a change of value occurs.</param>
/// <typeparam name="TModel">The type of the model.</typeparam>
/// <typeparam name="TProperty">The type of the property to be watched.</typeparam>
/// <returns>
/// A subscription token. It must be kept alive for as long as you want to receive change notifications.
/// To stop receiving notifications, call <see cref="IDisposable.Dispose"/>.
/// </returns>
public IDisposable SubscribeToPropertyChanged<TModel, TProperty>(Func<Realm, TModel?> modelAccessor, Expression<Func<TModel, TProperty>> propertyLookup, Action<TProperty> onChanged)
where TModel : RealmObjectBase
{
return RegisterCustomSubscription(r =>
{
string propertyName = getMemberName(propertyLookup);
var model = Run(modelAccessor);
var propLookupCompiled = propertyLookup.Compile();
if (model == null)
return null;
model.PropertyChanged += onPropertyChanged;
// Update initial value immediately.
onChanged(propLookupCompiled(model));
return new InvokeOnDisposal(() => model.PropertyChanged -= onPropertyChanged);
void onPropertyChanged(object sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == propertyName)
onChanged(propLookupCompiled(model));
}
});
static string getMemberName(Expression<Func<TModel, TProperty>> expression)
{
if (!(expression is LambdaExpression lambda))
throw new ArgumentException("Outermost expression must be a lambda expression", nameof(expression));
if (!(lambda.Body is MemberExpression memberExpression))
throw new ArgumentException("Lambda body must be a member access expression", nameof(expression));
// TODO: nested access can be supported, with more iteration here
// (need to iteratively soft-cast `memberExpression.Expression` into `MemberExpression`s until `lambda.Parameters[0]` is hit)
if (memberExpression.Expression != lambda.Parameters[0])
throw new ArgumentException("Nested access expressions are not supported", nameof(expression));
return memberExpression.Member.Name;
}
}
/// <summary>
/// Run work on realm that will be run every time the update thread realm instance gets recycled.
/// </summary>
@ -414,7 +548,7 @@ namespace osu.Game.Database
}
}
private RealmConfiguration getConfiguration()
private RealmConfiguration getConfiguration(string? filename = null)
{
// This is currently the only usage of temporary files at the osu! side.
// If we use the temporary folder in more situations in the future, this should be moved to a higher level (helper method or OsuGameBase).
@ -422,7 +556,7 @@ namespace osu.Game.Database
if (!Directory.Exists(tempPathLocation))
Directory.CreateDirectory(tempPathLocation);
return new RealmConfiguration(storage.GetFullPath(Filename, true))
return new RealmConfiguration(storage.GetFullPath(filename ?? Filename, true))
{
SchemaVersion = schema_version,
MigrationCallback = onMigration,

View File

@ -4,6 +4,8 @@
using System;
using Realms;
#nullable enable
namespace osu.Game.Database
{
public static class RealmExtensions
@ -22,5 +24,14 @@ namespace osu.Game.Database
transaction.Commit();
return result;
}
/// <summary>
/// Whether the provided change set has changes to the top level collection.
/// </summary>
/// <remarks>
/// Realm subscriptions fire on both collection and property changes (including *all* nested properties).
/// Quite often we only care about changes at a collection level. This can be used to guard and early-return when no such changes are in a callback.
/// </remarks>
public static bool HasCollectionChanges(this ChangeSet changes) => changes.InsertedIndices.Length > 0 || changes.DeletedIndices.Length > 0 || changes.Moves.Length > 0;
}
}

View File

@ -1,8 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Humanizer;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
using osuTK;
@ -59,8 +62,18 @@ namespace osu.Game.Extensions
component.Origin = info.Origin;
if (component is ISkinnableDrawable skinnable)
{
skinnable.UsesFixedAnchor = info.UsesFixedAnchor;
foreach (var (_, property) in component.GetSettingsSourceProperties())
{
if (!info.Settings.TryGetValue(property.Name.Underscore(), out object settingValue))
continue;
skinnable.CopyAdjustedSetting((IBindable)property.GetValue(component), settingValue);
}
}
if (component is Container container)
{
foreach (var child in info.Children)

View File

@ -72,6 +72,11 @@ namespace osu.Game.Extensions
return result;
}
/// <summary>
/// Check whether this <see cref="IRulesetInfo"/>'s online ID is within the range that defines it as a legacy ruleset (ie. either osu!, osu!taiko, osu!catch or osu!mania).
/// </summary>
public static bool IsLegacyRuleset(this IRulesetInfo ruleset) => ruleset.OnlineID >= 0 && ruleset.OnlineID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID;
/// <summary>
/// Check whether the online ID of two <see cref="IBeatmapSetInfo"/>s match.
/// </summary>

View File

@ -3,12 +3,15 @@
#nullable enable
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Mods;
using osu.Game.Storyboards.Drawables;
namespace osu.Game.Graphics.Backgrounds
@ -20,6 +23,9 @@ namespace osu.Game.Graphics.Backgrounds
[Resolved(CanBeNull = true)]
private MusicController? musicController { get; set; }
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; } = null!;
public BeatmapBackgroundWithStoryboard(WorkingBeatmap beatmap, string fallbackTextureName = "Backgrounds/bg1")
: base(beatmap, fallbackTextureName)
{
@ -39,7 +45,7 @@ namespace osu.Game.Graphics.Backgrounds
{
RelativeSizeAxes = Axes.Both,
Volume = { Value = 0 },
Child = new DrawableStoryboard(Beatmap.Storyboard) { Clock = storyboardClock }
Child = new DrawableStoryboard(Beatmap.Storyboard, mods.Value) { Clock = storyboardClock }
}, AddInternal);
}

View File

@ -30,12 +30,12 @@ namespace osu.Game.Graphics.Containers
public Bindable<double> Progress = new BindableDouble();
private Bindable<float> holdActivationDelay;
private Bindable<double> holdActivationDelay;
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
holdActivationDelay = config.GetBindable<float>(OsuSetting.UIHoldActivationDelay);
holdActivationDelay = config.GetBindable<double>(OsuSetting.UIHoldActivationDelay);
}
protected void BeginConfirm()

View File

@ -5,6 +5,9 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Layout;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Screens;
@ -38,24 +41,24 @@ namespace osu.Game.Graphics.Containers
private BackgroundScreenStack backgroundStack;
private bool allowScaling = true;
private RectangleF? customRect;
private bool customRectIsRelativePosition;
/// <summary>
/// Whether user scaling preferences should be applied. Enabled by default.
/// Set a custom position and scale which overrides any user specification.
/// </summary>
public bool AllowScaling
/// <param name="rect">A rectangle with positional and sizing information for this container to conform to. <c>null</c> will clear the custom rect and revert to user settings.</param>
/// <param name="relativePosition">Whether the position portion of the provided rect is in relative coordinate space or not.</param>
public void SetCustomRect(RectangleF? rect, bool relativePosition = false)
{
get => allowScaling;
set
{
if (value == allowScaling)
return;
customRect = rect;
customRectIsRelativePosition = relativePosition;
allowScaling = value;
if (IsLoaded) Scheduler.AddOnce(updateSize);
}
if (IsLoaded) Scheduler.AddOnce(updateSize);
}
private const float corner_radius = 10;
/// <summary>
/// Create a new instance.
/// </summary>
@ -65,11 +68,11 @@ namespace osu.Game.Graphics.Containers
this.targetMode = targetMode;
RelativeSizeAxes = Axes.Both;
InternalChild = sizableContainer = new AlwaysInputContainer
InternalChild = sizableContainer = new SizeableAlwaysInputContainer(targetMode == ScalingMode.Everything)
{
RelativeSizeAxes = Axes.Both,
RelativePositionAxes = Axes.Both,
CornerRadius = 10,
CornerRadius = corner_radius,
Child = content = new ScalingDrawSizePreservingFillContainer(targetMode != ScalingMode.Gameplay)
};
}
@ -137,7 +140,7 @@ namespace osu.Game.Graphics.Containers
private void updateSize()
{
const float fade_time = 500;
const float duration = 500;
if (targetMode == ScalingMode.Everything)
{
@ -156,17 +159,31 @@ namespace osu.Game.Graphics.Containers
backgroundStack.Push(new ScalingBackgroundScreen());
}
backgroundStack.FadeIn(fade_time);
backgroundStack.FadeIn(duration);
}
else
backgroundStack?.FadeOut(fade_time);
backgroundStack?.FadeOut(duration);
}
bool scaling = AllowScaling && (targetMode == null || scalingMode.Value == targetMode);
RectangleF targetRect = new RectangleF(Vector2.Zero, Vector2.One);
var targetSize = scaling ? new Vector2(sizeX.Value, sizeY.Value) : Vector2.One;
var targetPosition = scaling ? new Vector2(posX.Value, posY.Value) * (Vector2.One - targetSize) : Vector2.Zero;
bool requiresMasking = (scaling && targetSize != Vector2.One)
if (customRect != null)
{
sizableContainer.RelativePositionAxes = customRectIsRelativePosition ? Axes.Both : Axes.None;
targetRect = customRect.Value;
}
else if (targetMode == null || scalingMode.Value == targetMode)
{
sizableContainer.RelativePositionAxes = Axes.Both;
Vector2 scale = new Vector2(sizeX.Value, sizeY.Value);
Vector2 pos = new Vector2(posX.Value, posY.Value) * (Vector2.One - scale);
targetRect = new RectangleF(pos, scale);
}
bool requiresMasking = targetRect.Size != Vector2.One
// For the top level scaling container, for now we apply masking if safe areas are in use.
// In the future this can likely be removed as more of the actual UI supports overflowing into the safe areas.
|| (targetMode == ScalingMode.Everything && safeAreaPadding.Value.Total != Vector2.Zero);
@ -174,8 +191,14 @@ namespace osu.Game.Graphics.Containers
if (requiresMasking)
sizableContainer.Masking = true;
sizableContainer.MoveTo(targetPosition, 500, Easing.OutQuart);
sizableContainer.ResizeTo(targetSize, 500, Easing.OutQuart).OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
sizableContainer.MoveTo(targetRect.Location, duration, Easing.OutQuart);
sizableContainer.ResizeTo(targetRect.Size, duration, Easing.OutQuart);
// Of note, this will not work great in the case of nested ScalingContainers where multiple are applying corner radius.
// Masking and corner radius should likely only be applied at one point in the full game stack to fix this.
// An example of how this can occur is when the skin editor is visible and the game screen scaling is set to "Everything".
sizableContainer.TransformTo(nameof(CornerRadius), requiresMasking ? corner_radius : 0, duration, requiresMasking ? Easing.OutQuart : Easing.None)
.OnComplete(_ => { sizableContainer.Masking = requiresMasking; });
}
private class ScalingBackgroundScreen : BackgroundScreenDefault
@ -188,13 +211,50 @@ namespace osu.Game.Graphics.Containers
}
}
private class AlwaysInputContainer : Container
private class SizeableAlwaysInputContainer : Container
{
[Resolved]
private GameHost host { get; set; }
[Resolved]
private ISafeArea safeArea { get; set; }
private readonly bool confineHostCursor;
private readonly LayoutValue cursorRectCache = new LayoutValue(Invalidation.RequiredParentSizeToFit);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
public AlwaysInputContainer()
/// <summary>
/// Container used for sizing/positioning purposes in <see cref="ScalingContainer"/>. Always receives mouse input.
/// </summary>
/// <param name="confineHostCursor">Whether to confine the host cursor to the draw area of this container.</param>
/// <remarks>Cursor confinement will abide by the <see cref="OsuSetting.ConfineMouseMode"/> setting.</remarks>
public SizeableAlwaysInputContainer(bool confineHostCursor)
{
RelativeSizeAxes = Axes.Both;
this.confineHostCursor = confineHostCursor;
if (confineHostCursor)
AddLayout(cursorRectCache);
}
protected override void Update()
{
base.Update();
if (confineHostCursor && !cursorRectCache.IsValid)
{
updateHostCursorConfineRect();
cursorRectCache.Validate();
}
}
private void updateHostCursorConfineRect()
{
if (host.Window == null) return;
bool coversWholeScreen = Size == Vector2.One && safeArea.SafeAreaPadding.Value.Total == Vector2.Zero;
host.Window.CursorConfineRect = coversWholeScreen ? (RectangleF?)null : ToScreenSpace(DrawRectangle).AABBFloat;
}
}
}

View File

@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
/// </summary>
public bool UserScrolling { get; private set; }
public void CancelUserScroll() => UserScrolling = false;
public UserTrackingScrollContainer()
{
}

View File

@ -140,6 +140,7 @@ namespace osu.Game.Graphics.Cursor
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Volume.Value = baseFrequency;
channel.Play();
}

View File

@ -148,7 +148,7 @@ namespace osu.Game.Graphics.UserInterface
protected override void LoadComplete()
{
base.LoadComplete();
CurrentNumber.BindValueChanged(current => updateTooltipText(current.NewValue), true);
CurrentNumber.BindValueChanged(current => TooltipText = getTooltipText(current.NewValue), true);
}
protected override bool OnHover(HoverEvent e)
@ -178,7 +178,7 @@ namespace osu.Game.Graphics.UserInterface
{
base.OnUserChange(value);
playSample(value);
updateTooltipText(value);
TooltipText = getTooltipText(value);
}
private void playSample(T value)
@ -203,28 +203,22 @@ namespace osu.Game.Graphics.UserInterface
channel.Play();
}
private void updateTooltipText(T value)
private LocalisableString getTooltipText(T value)
{
if (CurrentNumber.IsInteger)
TooltipText = value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0");
else
{
double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo);
return value.ToInt32(NumberFormatInfo.InvariantInfo).ToString("N0");
if (DisplayAsPercentage)
{
TooltipText = floatValue.ToString("0%");
}
else
{
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
double floatValue = value.ToDouble(NumberFormatInfo.InvariantInfo);
// Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
if (DisplayAsPercentage)
return floatValue.ToString("0%");
TooltipText = floatValue.ToString($"N{significantDigits}");
}
}
decimal decimalPrecision = normalise(CurrentNumber.Precision.ToDecimal(NumberFormatInfo.InvariantInfo), max_decimal_digits);
// Find the number of significant digits (we could have less than 5 after normalize())
int significantDigits = FormatUtils.FindPrecision(decimalPrecision);
return floatValue.ToString($"N{significantDigits}");
}
protected override void UpdateAfterChildren()

View File

@ -0,0 +1,154 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Graphics.UserInterface
{
public class PopupScreenTitle : CompositeDrawable
{
public LocalisableString Title
{
set => titleSpriteText.Text = value;
}
public LocalisableString Description
{
set => descriptionText.Text = value;
}
public Action? Close
{
get => closeButton.Action;
set => closeButton.Action = value;
}
private const float corner_radius = 14;
private const float main_area_height = 70;
private readonly Container underlayContainer;
private readonly Box underlayBackground;
private readonly Container contentContainer;
private readonly Box contentBackground;
private readonly OsuSpriteText titleSpriteText;
private readonly OsuTextFlowContainer descriptionText;
private readonly IconButton closeButton;
public PopupScreenTitle()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = 70,
Top = -corner_radius
},
Children = new Drawable[]
{
underlayContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = main_area_height + 2 * corner_radius,
CornerRadius = corner_radius,
Masking = true,
BorderThickness = 2,
Child = underlayBackground = new Box
{
RelativeSizeAxes = Axes.Both
}
},
contentContainer = new Container
{
RelativeSizeAxes = Axes.X,
Height = main_area_height + corner_radius,
CornerRadius = corner_radius,
Masking = true,
BorderThickness = 2,
EdgeEffect = new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Colour = Colour4.Black.Opacity(0.1f),
Offset = new Vector2(0, 1),
Radius = 3
},
Children = new Drawable[]
{
contentBackground = new Box
{
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Margin = new MarginPadding { Top = corner_radius },
Padding = new MarginPadding { Horizontal = 100 },
Children = new Drawable[]
{
titleSpriteText = new OsuSpriteText
{
Font = OsuFont.TorusAlternate.With(size: 20)
},
descriptionText = new OsuTextFlowContainer(t =>
{
t.Font = OsuFont.Default.With(size: 12);
})
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
},
closeButton = new IconButton
{
Icon = FontAwesome.Solid.Times,
Scale = new Vector2(0.6f),
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Margin = new MarginPadding
{
Right = 21,
Top = corner_radius
}
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
underlayContainer.BorderColour = ColourInfo.GradientVertical(Colour4.Black, colourProvider.Dark4);
underlayBackground.Colour = colourProvider.Dark4;
contentContainer.BorderColour = ColourInfo.GradientVertical(colourProvider.Dark3, colourProvider.Dark1);
contentBackground.Colour = colourProvider.Dark3;
closeButton.IconHoverColour = colourProvider.Highlight1;
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Graphics.UserInterface
{
/// <summary>
/// A slider bar which displays a millisecond time value.
/// </summary>
public class TimeSlider : OsuSliderBar<double>
{
public override LocalisableString TooltipText => $"{Current.Value:N0} ms";
}
}

View File

@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.Text = value;
}
public Container TabbableContentContainer
public CompositeDrawable TabbableContentContainer
{
set => Component.TabbableContentContainer = value;
}

View File

@ -29,6 +29,16 @@ namespace osu.Game.Localisation
/// </summary>
public static LocalisableString CalibrateUsingLastPlay => new TranslatableString(getKey(@"calibrate_using_last_play"), @"Calibrate using last play");
/// <summary>
/// "(hit objects appear later)"
/// </summary>
public static LocalisableString HitObjectsAppearLater => new TranslatableString(getKey(@"hit_objects_appear_later"), @"(hit objects appear later)");
/// <summary>
/// "(hit objects appear earlier)"
/// </summary>
public static LocalisableString HitObjectsAppearEarlier => new TranslatableString(getKey(@"hit_objects_appear_earlier"), @"(hit objects appear earlier)");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}
}

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