mirror of
https://github.com/ppy/osu.git
synced 2026-05-17 19:04:00 +08:00
Compare commits
423 Commits
@@ -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:
|
||||
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -7,5 +7,6 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
{
|
||||
public class CatchScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override double ClassicScoreMultiplier => 28;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new ManiaModMuted(),
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -394,6 +395,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
{
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(score.HitEvents),
|
||||
new UnstableRate(score.HitEvents)
|
||||
}), true)
|
||||
}
|
||||
|
||||
@@ -45,10 +45,5 @@ namespace osu.Game.Rulesets.Mania
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private class TimeSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Mania.Scoring
|
||||
protected override double DefaultAccuracyPortion => 0.99;
|
||||
|
||||
protected override double DefaultComboPortion => 0.01;
|
||||
|
||||
protected override double ClassicScoreMultiplier => 16;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@@ -72,7 +73,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
EditorClock.Seek(slider.StartTime);
|
||||
EditorBeatmap.SelectedHitObjects.Add(slider);
|
||||
});
|
||||
AddStep("change beat divisor", () => beatDivisor.Value = 3);
|
||||
AddStep("change beat divisor", () =>
|
||||
{
|
||||
beatDivisor.ValidDivisors.Value = BeatDivisorPresetCollection.TRIPLETS;
|
||||
beatDivisor.Value = 3;
|
||||
});
|
||||
|
||||
convertToStream();
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,6 +195,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
new OsuModMuted(),
|
||||
new OsuModNoScope(),
|
||||
new OsuModAimAssist(),
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
case ModType.System:
|
||||
@@ -314,6 +315,7 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
}), true)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ namespace osu.Game.Rulesets.Osu.Scoring
|
||||
{
|
||||
public class OsuScoreProcessor : ScoreProcessor
|
||||
{
|
||||
protected override double ClassicScoreMultiplier => 36;
|
||||
|
||||
protected override HitEvent CreateHitEvent(JudgementResult result)
|
||||
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
get
|
||||
{
|
||||
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}";
|
||||
string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N2}";
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
|
||||
@@ -10,5 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
protected override double DefaultAccuracyPortion => 0.75;
|
||||
|
||||
protected override double DefaultComboPortion => 0.25;
|
||||
|
||||
protected override double ClassicScoreMultiplier => 22;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
new MultiMod(new ModWindUp(), new ModWindDown()),
|
||||
new TaikoModMuted(),
|
||||
new ModAdaptiveSpeed()
|
||||
};
|
||||
|
||||
default:
|
||||
@@ -237,6 +238,7 @@ namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
new StatisticItem(string.Empty, () => new SimpleStatisticTable(3, new SimpleStatisticItem[]
|
||||
{
|
||||
new AverageHitError(timedHitEvents),
|
||||
new UnstableRate(timedHitEvents)
|
||||
}), true)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,23 +1,129 @@
|
||||
// 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()
|
||||
{
|
||||
ChangeSet? lastChanges = null;
|
||||
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
// Without forcing the write onto its own thread, realm will internally run the operation synchronously, which can cause a deadlock with `WaitSafely`.
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo()));
|
||||
}).WaitSafely();
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(lastChanges?.InsertedIndices, Has.One.Items);
|
||||
|
||||
registration.Dispose();
|
||||
});
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -134,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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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\""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
@@ -20,30 +22,31 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
private BeatDivisorControl beatDivisorControl;
|
||||
private BindableBeatDivisor bindableBeatDivisor;
|
||||
|
||||
private SliderBar<int> tickSliderBar;
|
||||
private EquilateralTriangle tickMarkerHead;
|
||||
private SliderBar<int> tickSliderBar => beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
private EquilateralTriangle tickMarkerHead => tickSliderBar.ChildrenOfType<EquilateralTriangle>().Single();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
|
||||
Child = new PopoverContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(90, 90)
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = beatDivisorControl = new BeatDivisorControl(bindableBeatDivisor = new BindableBeatDivisor(16))
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(90, 90)
|
||||
}
|
||||
};
|
||||
|
||||
tickSliderBar = beatDivisorControl.ChildrenOfType<SliderBar<int>>().Single();
|
||||
tickMarkerHead = tickSliderBar.ChildrenOfType<EquilateralTriangle>().Single();
|
||||
});
|
||||
|
||||
[Test]
|
||||
public void TestBindableBeatDivisor()
|
||||
{
|
||||
AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 4);
|
||||
AddRepeatStep("move previous", () => bindableBeatDivisor.Previous(), 2);
|
||||
AddAssert("divisor is 4", () => bindableBeatDivisor.Value == 4);
|
||||
AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 3);
|
||||
AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 12);
|
||||
AddRepeatStep("move next", () => bindableBeatDivisor.Next(), 1);
|
||||
AddAssert("divisor is 12", () => bindableBeatDivisor.Value == 8);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@@ -79,5 +82,115 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
sliderDrawQuad.Centre.Y
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatChevronNavigation()
|
||||
{
|
||||
switchBeatSnap(1);
|
||||
assertBeatSnap(1);
|
||||
|
||||
switchBeatSnap(3);
|
||||
assertBeatSnap(8);
|
||||
|
||||
switchBeatSnap(-1);
|
||||
assertBeatSnap(4);
|
||||
|
||||
switchBeatSnap(-3);
|
||||
assertBeatSnap(16);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBeatPresetNavigation()
|
||||
{
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
switchPresets(-1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
|
||||
switchPresets(-1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
setDivisorViaInput(3);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
|
||||
setDivisorViaInput(8);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
setDivisorViaInput(15);
|
||||
assertPreset(BeatDivisorType.Custom, 15);
|
||||
|
||||
switchBeatSnap(-1);
|
||||
assertBeatSnap(5);
|
||||
|
||||
switchBeatSnap(-1);
|
||||
assertBeatSnap(3);
|
||||
|
||||
setDivisorViaInput(5);
|
||||
assertPreset(BeatDivisorType.Custom, 15);
|
||||
|
||||
switchPresets(1);
|
||||
assertPreset(BeatDivisorType.Common);
|
||||
|
||||
switchPresets(-1);
|
||||
assertPreset(BeatDivisorType.Triplets);
|
||||
}
|
||||
|
||||
private void switchBeatSnap(int direction) => AddRepeatStep($"move snap {(direction > 0 ? "forward" : "backward")}", () =>
|
||||
{
|
||||
int chevronIndex = direction > 0 ? 1 : 0;
|
||||
var chevronButton = beatDivisorControl.ChildrenOfType<BeatDivisorControl.ChevronButton>().ElementAt(chevronIndex);
|
||||
InputManager.MoveMouseTo(chevronButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}, Math.Abs(direction));
|
||||
|
||||
private void assertBeatSnap(int expected) => AddAssert($"beat snap is {expected}",
|
||||
() => bindableBeatDivisor.Value == expected);
|
||||
|
||||
private void switchPresets(int direction) => AddRepeatStep($"move presets {(direction > 0 ? "forward" : "backward")}", () =>
|
||||
{
|
||||
int chevronIndex = direction > 0 ? 3 : 2;
|
||||
var chevronButton = beatDivisorControl.ChildrenOfType<BeatDivisorControl.ChevronButton>().ElementAt(chevronIndex);
|
||||
InputManager.MoveMouseTo(chevronButton);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}, Math.Abs(direction));
|
||||
|
||||
private void assertPreset(BeatDivisorType type, int? maxDivisor = null)
|
||||
{
|
||||
AddAssert($"preset is {type}", () => bindableBeatDivisor.ValidDivisors.Value.Type == type);
|
||||
|
||||
if (type == BeatDivisorType.Custom)
|
||||
{
|
||||
Debug.Assert(maxDivisor != null);
|
||||
AddAssert($"max divisor is {maxDivisor}", () => bindableBeatDivisor.ValidDivisors.Value.Presets.Max() == maxDivisor.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private void setDivisorViaInput(int divisor)
|
||||
{
|
||||
AddStep("open divisor input popover", () =>
|
||||
{
|
||||
var button = beatDivisorControl.ChildrenOfType<BeatDivisorControl.DivisorDisplay>().Single();
|
||||
InputManager.MoveMouseTo(button);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
BeatDivisorControl.CustomDivisorPopover popover = null;
|
||||
AddUntilStep("wait for popover", () => (popover = this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().SingleOrDefault()) != null && popover.IsLoaded);
|
||||
AddStep($"set divisor to {divisor}", () =>
|
||||
{
|
||||
var textBox = popover.ChildrenOfType<TextBox>().Single();
|
||||
InputManager.MoveMouseTo(textBox);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
textBox.Text = divisor.ToString();
|
||||
InputManager.Key(Key.Enter);
|
||||
});
|
||||
AddUntilStep("wait for dismiss", () => !this.ChildrenOfType<BeatDivisorControl.CustomDivisorPopover>().Any());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
@@ -22,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddStep("switch between all screens at once", () =>
|
||||
{
|
||||
foreach (var screen in Enum.GetValues(typeof(EditorScreenMode)).Cast<EditorScreenMode>())
|
||||
Editor.ChildrenOfType<EditorMenuBar>().Single().Mode.Value = screen;
|
||||
Editor.Mode.Value = screen;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play.PlayerSettings;
|
||||
using osu.Game.Tests.Visual.Ranking;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneBeatmapOffsetControl : OsuTestScene
|
||||
{
|
||||
private BeatmapOffsetControl offsetControl;
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create control", () =>
|
||||
{
|
||||
Child = new PlayerSettingsGroup("Some settings")
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
offsetControl = new BeatmapOffsetControl()
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTooShortToDisplay()
|
||||
{
|
||||
AddStep("Set short reference score", () =>
|
||||
{
|
||||
offsetControl.ReferenceScore.Value = new ScoreInfo
|
||||
{
|
||||
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents(0, 2)
|
||||
};
|
||||
});
|
||||
|
||||
AddAssert("No calibration button", () => !offsetControl.ChildrenOfType<SettingsButton>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCalibrationFromZero()
|
||||
{
|
||||
const double average_error = -4.5;
|
||||
|
||||
AddAssert("Offset is neutral", () => offsetControl.Current.Value == 0);
|
||||
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 == -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());
|
||||
}
|
||||
|
||||
/// <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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -131,9 +131,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
public double GameplayClockTime => GameplayClockContainer.GameplayClock.CurrentTime;
|
||||
|
||||
protected override void Update()
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.Update();
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
if (!FirstFrameClockTime.HasValue)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -412,6 +412,121 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("channel left", () => !channelManager.JoinedChannels.Contains(multiplayerChannel));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHighlightOnCurrentChannel()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
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("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("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 TestHighlightWhileChatHidden()
|
||||
{
|
||||
Message message = null;
|
||||
|
||||
AddStep("hide chat", () => chatOverlay.Hide());
|
||||
|
||||
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 and show chat", () =>
|
||||
{
|
||||
chatOverlay.HighlightMessage(message, channel1);
|
||||
chatOverlay.Show();
|
||||
});
|
||||
}
|
||||
|
||||
private void pressChannelHotkey(int number)
|
||||
{
|
||||
var channelKey = Key.Number0 + number;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -33,16 +33,25 @@ 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(() =>
|
||||
{
|
||||
currentScoreId = 1;
|
||||
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.
|
||||
@@ -53,15 +62,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[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 +82,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 +117,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 +125,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 +141,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 +230,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 +250,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 +291,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 +333,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") }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -71,16 +83,16 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
};
|
||||
});
|
||||
|
||||
public static List<HitEvent> CreateDistributedHitEvents()
|
||||
public static List<HitEvent> CreateDistributedHitEvents(double centre = 0, double range = 25)
|
||||
{
|
||||
var hitEvents = new List<HitEvent>();
|
||||
|
||||
for (int i = 0; i < 50; i++)
|
||||
for (int i = 0; i < range * 2; i++)
|
||||
{
|
||||
int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
|
||||
int count = (int)(Math.Pow(range - Math.Abs(i - range), 2)) / 10;
|
||||
|
||||
for (int j = 0; j < count; j++)
|
||||
hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
|
||||
hitEvents.Add(new HitEvent(centre + i - range, HitResult.Perfect, placeholder_object, placeholder_object, null));
|
||||
}
|
||||
|
||||
return hitEvents;
|
||||
|
||||
@@ -17,10 +17,13 @@ using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Ranking.Expanded.Statistics;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Tests.Resources;
|
||||
using osuTK;
|
||||
@@ -256,6 +259,23 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRulesetWithNoPerformanceCalculator()
|
||||
{
|
||||
var ruleset = new RulesetWithNoPerformanceCalculator();
|
||||
var score = TestResources.CreateTestScoreInfo(ruleset.RulesetInfo);
|
||||
|
||||
AddStep("load results", () => Child = new TestResultsContainer(createResultsScreen(score)));
|
||||
AddUntilStep("wait for load", () => this.ChildrenOfType<ScorePanelList>().Single().AllPanelsVisible);
|
||||
|
||||
AddAssert("PP displayed as 0", () =>
|
||||
{
|
||||
var performance = this.ChildrenOfType<PerformanceStatistic>().Single();
|
||||
var counter = performance.ChildrenOfType<StatisticCounter>().Single();
|
||||
return counter.Current.Value == 0;
|
||||
});
|
||||
}
|
||||
|
||||
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo());
|
||||
|
||||
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());
|
||||
@@ -367,5 +387,10 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
|
||||
}
|
||||
}
|
||||
|
||||
private class RulesetWithNoPerformanceCalculator : OsuRuleset
|
||||
{
|
||||
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneModColumn : OsuManualInputManagerTestScene
|
||||
{
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green);
|
||||
|
||||
[TestCase(ModType.DifficultyReduction)]
|
||||
[TestCase(ModType.DifficultyIncrease)]
|
||||
[TestCase(ModType.Conversion)]
|
||||
[TestCase(ModType.Automation)]
|
||||
[TestCase(ModType.Fun)]
|
||||
public void TestBasic(ModType modType)
|
||||
{
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(30),
|
||||
Child = new ModColumn(modType, false)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("change ruleset to osu!", () => Ruleset.Value = new OsuRuleset().RulesetInfo);
|
||||
AddStep("change ruleset to taiko", () => Ruleset.Value = new TaikoRuleset().RulesetInfo);
|
||||
AddStep("change ruleset to catch", () => Ruleset.Value = new CatchRuleset().RulesetInfo);
|
||||
AddStep("change ruleset to mania", () => Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiSelection()
|
||||
{
|
||||
ModColumn column = null;
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(30),
|
||||
Child = column = new ModColumn(ModType.DifficultyIncrease, true)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
|
||||
|
||||
clickToggle();
|
||||
AddUntilStep("all panels selected", () => this.ChildrenOfType<ModPanel>().All(panel => panel.Active.Value));
|
||||
|
||||
clickToggle();
|
||||
AddUntilStep("all panels deselected", () => this.ChildrenOfType<ModPanel>().All(panel => !panel.Active.Value));
|
||||
|
||||
AddStep("manually activate all panels", () => this.ChildrenOfType<ModPanel>().ForEach(panel => panel.Active.Value = true));
|
||||
AddUntilStep("checkbox selected", () => this.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
AddStep("deselect first panel", () => this.ChildrenOfType<ModPanel>().First().Active.Value = false);
|
||||
AddUntilStep("checkbox not selected", () => !this.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
void clickToggle() => AddStep("click toggle", () =>
|
||||
{
|
||||
var checkbox = this.ChildrenOfType<OsuCheckbox>().Single();
|
||||
InputManager.MoveMouseTo(checkbox);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFiltering()
|
||||
{
|
||||
TestModColumn column = null;
|
||||
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(30),
|
||||
Child = column = new TestModColumn(ModType.Fun, true)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase));
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
|
||||
|
||||
clickToggle();
|
||||
AddUntilStep("wait for animation", () => !column.SelectionAnimationRunning);
|
||||
AddAssert("only visible items selected", () => column.ChildrenOfType<ModPanel>().Where(panel => panel.Active.Value).All(panel => !panel.Filtered.Value));
|
||||
|
||||
AddStep("unset filter", () => column.Filter = null);
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
|
||||
AddAssert("checkbox not selected", () => !column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
AddStep("set filter", () => column.Filter = mod => mod.Name.Contains("Wind", StringComparison.CurrentCultureIgnoreCase));
|
||||
AddUntilStep("two panels visible", () => column.ChildrenOfType<ModPanel>().Count(panel => !panel.Filtered.Value) == 2);
|
||||
AddAssert("checkbox selected", () => column.ChildrenOfType<OsuCheckbox>().Single().Current.Value);
|
||||
|
||||
AddStep("filter out everything", () => column.Filter = _ => false);
|
||||
AddUntilStep("no panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => panel.Filtered.Value));
|
||||
AddUntilStep("checkbox hidden", () => !column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
|
||||
|
||||
AddStep("inset filter", () => column.Filter = null);
|
||||
AddUntilStep("all panels visible", () => column.ChildrenOfType<ModPanel>().All(panel => !panel.Filtered.Value));
|
||||
AddUntilStep("checkbox visible", () => column.ChildrenOfType<OsuCheckbox>().Single().IsPresent);
|
||||
|
||||
void clickToggle() => AddStep("click toggle", () =>
|
||||
{
|
||||
var checkbox = this.ChildrenOfType<OsuCheckbox>().Single();
|
||||
InputManager.MoveMouseTo(checkbox);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestKeyboardSelection()
|
||||
{
|
||||
ModColumn column = null;
|
||||
AddStep("create content", () => Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(30),
|
||||
Child = column = new ModColumn(ModType.DifficultyReduction, true, new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P })
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
}
|
||||
});
|
||||
|
||||
AddUntilStep("wait for panel load", () => column.IsLoaded && column.ItemsLoaded);
|
||||
|
||||
AddStep("press W", () => InputManager.Key(Key.W));
|
||||
AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
|
||||
|
||||
AddStep("press W again", () => InputManager.Key(Key.W));
|
||||
AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
|
||||
|
||||
AddStep("set filter to NF", () => column.Filter = mod => mod.Acronym == "NF");
|
||||
|
||||
AddStep("press W", () => InputManager.Key(Key.W));
|
||||
AddAssert("NF panel selected", () => this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
|
||||
|
||||
AddStep("press W again", () => InputManager.Key(Key.W));
|
||||
AddAssert("NF panel deselected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
|
||||
|
||||
AddStep("filter out everything", () => column.Filter = _ => false);
|
||||
|
||||
AddStep("press W", () => InputManager.Key(Key.W));
|
||||
AddAssert("NF panel not selected", () => !this.ChildrenOfType<ModPanel>().Single(panel => panel.Mod.Acronym == "NF").Active.Value);
|
||||
|
||||
AddStep("clear filter", () => column.Filter = null);
|
||||
}
|
||||
|
||||
private class TestModColumn : ModColumn
|
||||
{
|
||||
public new bool SelectionAnimationRunning => base.SelectionAnimationRunning;
|
||||
|
||||
public TestModColumn(ModType modType, bool allowBulkSelection)
|
||||
: base(modType, allowBulkSelection)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Game.Tournament.Components;
|
||||
using osu.Game.Tournament.Models;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.Components
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneSongBar : OsuTestScene
|
||||
{
|
||||
[Cached]
|
||||
private readonly LadderInfo ladder = new LadderInfo();
|
||||
|
||||
[Test]
|
||||
public void TestSongBar()
|
||||
{
|
||||
SongBar songBar = null;
|
||||
|
||||
AddStep("create bar", () => Child = songBar = new SongBar
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre
|
||||
});
|
||||
AddUntilStep("wait for loaded", () => songBar.IsLoaded);
|
||||
|
||||
AddStep("set beatmap", () =>
|
||||
{
|
||||
var beatmap = CreateAPIBeatmap(Ruleset.Value);
|
||||
beatmap.CircleSize = 3.4f;
|
||||
beatmap.ApproachRate = 6.8f;
|
||||
beatmap.OverallDifficulty = 5.5f;
|
||||
beatmap.StarRating = 4.56f;
|
||||
beatmap.Length = 123456;
|
||||
beatmap.BPM = 133;
|
||||
|
||||
songBar.Beatmap = beatmap;
|
||||
});
|
||||
AddStep("set mods to HR", () => songBar.Mods = LegacyMods.HardRock);
|
||||
AddStep("set mods to DT", () => songBar.Mods = LegacyMods.DoubleTime);
|
||||
AddStep("unset mods", () => songBar.Mods = LegacyMods.None);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ namespace osu.Game.Tournament.Components
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new DiffPiece(stats),
|
||||
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.#}{srExtra}"))
|
||||
new DiffPiece(("Star Rating", $"{beatmap.StarRating:0.##}{srExtra}"))
|
||||
}
|
||||
},
|
||||
new FillFlowContainer
|
||||
|
||||
@@ -24,9 +24,6 @@ namespace osu.Game.Tournament.Components
|
||||
|
||||
private readonly string mod;
|
||||
|
||||
private const float horizontal_padding = 10;
|
||||
private const float vertical_padding = 10;
|
||||
|
||||
public const float HEIGHT = 50;
|
||||
|
||||
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
|
||||
|
||||
@@ -40,6 +40,8 @@ namespace osu.Game.Beatmaps
|
||||
[Backlink(nameof(ScoreInfo.BeatmapInfo))]
|
||||
public IQueryable<ScoreInfo> Scores { get; } = null!;
|
||||
|
||||
public BeatmapUserSettings UserSettings { get; set; } = null!;
|
||||
|
||||
public BeatmapInfo(RulesetInfo? ruleset = null, BeatmapDifficulty? difficulty = null, BeatmapMetadata? metadata = null)
|
||||
{
|
||||
ID = Guid.NewGuid();
|
||||
@@ -51,6 +53,7 @@ namespace osu.Game.Beatmaps
|
||||
};
|
||||
Difficulty = difficulty ?? new BeatmapDifficulty();
|
||||
Metadata = metadata ?? new BeatmapMetadata();
|
||||
UserSettings = new BeatmapUserSettings();
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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 Realms;
|
||||
|
||||
namespace osu.Game.Beatmaps
|
||||
{
|
||||
/// <summary>
|
||||
/// User settings overrides that are attached to a beatmap.
|
||||
/// </summary>
|
||||
public class BeatmapUserSettings : EmbeddedObject
|
||||
{
|
||||
/// <summary>
|
||||
/// An audio offset that can be used for timing adjustments.
|
||||
/// </summary>
|
||||
public double Offset { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -164,7 +164,7 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
int closestDivisor = 0;
|
||||
double closestTime = double.MaxValue;
|
||||
|
||||
foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS)
|
||||
foreach (int divisor in BindableBeatDivisor.PREDEFINED_DIVISORS)
|
||||
{
|
||||
double distanceFromSnap = Math.Abs(time - getClosestSnappedTime(timingPoint, time, divisor));
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,25 +3,28 @@
|
||||
|
||||
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;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Development;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Statistics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Stores;
|
||||
using Realms;
|
||||
using Realms.Exceptions;
|
||||
|
||||
@@ -53,8 +56,9 @@ namespace osu.Game.Database
|
||||
/// 11 2021-11-22 Use ShortName instead of RulesetID for ruleset key bindings.
|
||||
/// 12 2021-11-24 Add Status to RealmBeatmapSet.
|
||||
/// 13 2022-01-13 Final migration of beatmaps and scores to realm (multiple new storage fields).
|
||||
/// 14 2022-03-01 Added BeatmapUserSettings to BeatmapInfo.
|
||||
/// </summary>
|
||||
private const int schema_version = 13;
|
||||
private const int schema_version = 14;
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking realm retrieval during blocking periods.
|
||||
@@ -85,6 +89,14 @@ namespace osu.Game.Database
|
||||
|
||||
private static readonly GlobalStatistic<int> total_subscriptions = GlobalStatistics.Get<int>(@"Realm", @"Subscriptions");
|
||||
|
||||
private static readonly GlobalStatistic<int> total_reads_update = GlobalStatistics.Get<int>(@"Realm", @"Reads (Update)");
|
||||
|
||||
private static readonly GlobalStatistic<int> total_reads_async = GlobalStatistics.Get<int>(@"Realm", @"Reads (Async)");
|
||||
|
||||
private static readonly GlobalStatistic<int> total_writes_update = GlobalStatistics.Get<int>(@"Realm", @"Writes (Update)");
|
||||
|
||||
private static readonly GlobalStatistic<int> total_writes_async = GlobalStatistics.Get<int>(@"Realm", @"Writes (Async)");
|
||||
|
||||
private readonly object realmLock = new object();
|
||||
|
||||
private Realm? updateRealm;
|
||||
@@ -93,6 +105,8 @@ namespace osu.Game.Database
|
||||
|
||||
public Realm Realm => ensureUpdateRealm();
|
||||
|
||||
private const string realm_extension = @".realm";
|
||||
|
||||
private Realm ensureUpdateRealm()
|
||||
{
|
||||
if (isSendingNotificationResetEvents)
|
||||
@@ -137,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.
|
||||
@@ -149,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())
|
||||
@@ -213,8 +297,12 @@ namespace osu.Game.Database
|
||||
public T Run<T>(Func<Realm, T> action)
|
||||
{
|
||||
if (ThreadSafety.IsUpdateThread)
|
||||
{
|
||||
total_reads_update.Value++;
|
||||
return action(Realm);
|
||||
}
|
||||
|
||||
total_reads_async.Value++;
|
||||
using (var realm = getRealmInstance())
|
||||
return action(realm);
|
||||
}
|
||||
@@ -226,9 +314,13 @@ namespace osu.Game.Database
|
||||
public void Run(Action<Realm> action)
|
||||
{
|
||||
if (ThreadSafety.IsUpdateThread)
|
||||
{
|
||||
total_reads_update.Value++;
|
||||
action(Realm);
|
||||
}
|
||||
else
|
||||
{
|
||||
total_reads_async.Value++;
|
||||
using (var realm = getRealmInstance())
|
||||
action(realm);
|
||||
}
|
||||
@@ -241,14 +333,30 @@ namespace osu.Game.Database
|
||||
public void Write(Action<Realm> action)
|
||||
{
|
||||
if (ThreadSafety.IsUpdateThread)
|
||||
{
|
||||
total_writes_update.Value++;
|
||||
Realm.Write(action);
|
||||
}
|
||||
else
|
||||
{
|
||||
total_writes_async.Value++;
|
||||
|
||||
using (var realm = getRealmInstance())
|
||||
realm.Write(action);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Write changes to realm asynchronously, guaranteeing order of execution.
|
||||
/// </summary>
|
||||
/// <param name="action">The work to run.</param>
|
||||
public async Task WriteAsync(Action<Realm> action)
|
||||
{
|
||||
total_writes_async.Value++;
|
||||
using (var realm = getRealmInstance())
|
||||
await realm.WriteAsync(action);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Subscribe to a realm collection and begin watching for asynchronous changes.
|
||||
/// </summary>
|
||||
@@ -284,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>
|
||||
@@ -380,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).
|
||||
@@ -388,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,
|
||||
@@ -531,6 +699,11 @@ namespace osu.Game.Database
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 14:
|
||||
foreach (var beatmap in migration.NewRealm.All<BeatmapInfo>())
|
||||
beatmap.UserSettings = new BeatmapUserSettings();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ namespace osu.Game.Database
|
||||
c.CreateMap<BeatmapInfo, BeatmapInfo>()
|
||||
.ForMember(s => s.Ruleset, cc => cc.Ignore())
|
||||
.ForMember(s => s.Metadata, cc => cc.Ignore())
|
||||
.ForMember(s => s.UserSettings, cc => cc.Ignore())
|
||||
.ForMember(s => s.Difficulty, cc => cc.Ignore())
|
||||
.ForMember(s => s.BeatmapSet, cc => cc.Ignore())
|
||||
.AfterMap((s, d) =>
|
||||
@@ -154,6 +155,7 @@ namespace osu.Game.Database
|
||||
|
||||
c.CreateMap<RealmKeyBinding, RealmKeyBinding>();
|
||||
c.CreateMap<BeatmapMetadata, BeatmapMetadata>();
|
||||
c.CreateMap<BeatmapUserSettings, BeatmapUserSettings>();
|
||||
c.CreateMap<BeatmapDifficulty, BeatmapDifficulty>();
|
||||
c.CreateMap<RulesetInfo, RulesetInfo>();
|
||||
c.CreateMap<ScoreInfo, ScoreInfo>();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace osu.Game.Graphics.Containers
|
||||
/// </summary>
|
||||
public bool UserScrolling { get; private set; }
|
||||
|
||||
public void CancelUserScroll() => UserScrolling = false;
|
||||
|
||||
public UserTrackingScrollContainer()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
set => Component.Text = value;
|
||||
}
|
||||
|
||||
public Container TabbableContentContainer
|
||||
public CompositeDrawable TabbableContentContainer
|
||||
{
|
||||
set => Component.TabbableContentContainer = value;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class BeatmapOffsetControlStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.BeatmapOffsetControl";
|
||||
|
||||
/// <summary>
|
||||
/// "Beatmap offset"
|
||||
/// </summary>
|
||||
public static LocalisableString BeatmapOffset => new TranslatableString(getKey(@"beatmap_offset"), @"Beatmap offset");
|
||||
|
||||
/// <summary>
|
||||
/// "Previous play:"
|
||||
/// </summary>
|
||||
public static LocalisableString PreviousPlay => new TranslatableString(getKey(@"previous_play"), @"Previous play:");
|
||||
|
||||
/// <summary>
|
||||
/// "Previous play too short to use for calibration"
|
||||
/// </summary>
|
||||
public static LocalisableString PreviousPlayTooShortToUseForCalibration => new TranslatableString(getKey(@"previous_play_too_short_to_use_for_calibration"), @"Previous play too short to use for calibration");
|
||||
|
||||
/// <summary>
|
||||
/// "Calibrate using last play"
|
||||
/// </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}";
|
||||
}
|
||||
}
|
||||
@@ -54,6 +54,11 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString Resolution => new TranslatableString(getKey(@"resolution"), @"Resolution");
|
||||
|
||||
/// <summary>
|
||||
/// "Display"
|
||||
/// </summary>
|
||||
public static LocalisableString Display => new TranslatableString(getKey(@"display"), @"Display");
|
||||
|
||||
/// <summary>
|
||||
/// "UI scaling"
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class LeaderboardStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.Leaderboard";
|
||||
|
||||
/// <summary>
|
||||
/// "Couldn't fetch scores!"
|
||||
/// </summary>
|
||||
public static LocalisableString CouldntFetchScores => new TranslatableString(getKey(@"couldnt_fetch_scores"), @"Couldn't fetch scores!");
|
||||
|
||||
/// <summary>
|
||||
/// "Please select a beatmap!"
|
||||
/// </summary>
|
||||
public static LocalisableString PleaseSelectABeatmap => new TranslatableString(getKey(@"please_select_a_beatmap"), @"Please select a beatmap!");
|
||||
|
||||
/// <summary>
|
||||
/// "Leaderboards are not available for this ruleset!"
|
||||
/// </summary>
|
||||
public static LocalisableString LeaderboardsAreNotAvailableForThisRuleset => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_ruleset"), @"Leaderboards are not available for this ruleset!");
|
||||
|
||||
/// <summary>
|
||||
/// "Leaderboards are not available for this beatmap!"
|
||||
/// </summary>
|
||||
public static LocalisableString LeaderboardsAreNotAvailableForThisBeatmap => new TranslatableString(getKey(@"leaderboards_are_not_available_for_this_beatmap"), @"Leaderboards are not available for this beatmap!");
|
||||
|
||||
/// <summary>
|
||||
/// "No records yet!"
|
||||
/// </summary>
|
||||
public static LocalisableString NoRecordsYet => new TranslatableString(getKey(@"no_records_yet"), @"No records yet!");
|
||||
|
||||
/// <summary>
|
||||
/// "Please sign in to view online leaderboards!"
|
||||
/// </summary>
|
||||
public static LocalisableString PleaseSignInToViewOnlineLeaderboards => new TranslatableString(getKey(@"please_sign_in_to_view_online_leaderboards"), @"Please sign in to view online leaderboards!");
|
||||
|
||||
/// <summary>
|
||||
/// "Please invest in an osu!supporter tag to view this leaderboard!"
|
||||
/// </summary>
|
||||
public static LocalisableString PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard => new TranslatableString(getKey(@"please_invest_in_an_osu_supporter_tag_to_view_this_leaderboard"), @"Please invest in an osu!supporter tag to view this leaderboard!");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using Humanizer;
|
||||
using MessagePack;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
@@ -51,7 +52,10 @@ namespace osu.Game.Online.API
|
||||
Mod resultMod = ruleset.CreateModFromAcronym(Acronym);
|
||||
|
||||
if (resultMod == null)
|
||||
throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
|
||||
{
|
||||
Logger.Log($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}.");
|
||||
return new UnknownMod(Acronym);
|
||||
}
|
||||
|
||||
if (Settings.Count > 0)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
|
||||
namespace osu.Game.Online.API.Requests
|
||||
@@ -8,14 +10,14 @@ namespace osu.Game.Online.API.Requests
|
||||
public class GetWikiRequest : APIRequest<APIWikiPage>
|
||||
{
|
||||
private readonly string path;
|
||||
private readonly string locale;
|
||||
private readonly Language language;
|
||||
|
||||
public GetWikiRequest(string path, string locale = "en")
|
||||
public GetWikiRequest(string path, Language language = Language.en)
|
||||
{
|
||||
this.path = path;
|
||||
this.locale = locale;
|
||||
this.language = language;
|
||||
}
|
||||
|
||||
protected override string Target => $"wiki/{locale}/{path}";
|
||||
protected override string Target => $"wiki/{language.ToCultureCode()}/{path}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays.Chat;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
@@ -89,6 +90,12 @@ namespace osu.Game.Online.Chat
|
||||
/// </summary>
|
||||
public Bindable<bool> Joined = new Bindable<bool>();
|
||||
|
||||
/// <summary>
|
||||
/// Signals if there is a message to highlight.
|
||||
/// This is automatically cleared by the associated <see cref="DrawableChannel"/> after highlighting.
|
||||
/// </summary>
|
||||
public Bindable<Message> HighlightedMessage = new Bindable<Message>();
|
||||
|
||||
[JsonConstructor]
|
||||
public Channel()
|
||||
{
|
||||
|
||||
@@ -59,7 +59,13 @@ namespace osu.Game.Online.Chat
|
||||
return Id.Value.CompareTo(other.Id.Value);
|
||||
}
|
||||
|
||||
public virtual bool Equals(Message other) => Id.HasValue && Id == other?.Id;
|
||||
public virtual bool Equals(Message other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
|
||||
return Id.HasValue && Id == other.Id;
|
||||
}
|
||||
|
||||
// ReSharper disable once ImpureMethodCallOnReadonlyValueField
|
||||
public override int GetHashCode() => Id.GetHashCode();
|
||||
|
||||
@@ -114,7 +114,7 @@ namespace osu.Game.Online.Chat
|
||||
if (!notifyOnPrivateMessage.Value || channel.Type != ChannelType.PM)
|
||||
return false;
|
||||
|
||||
notifications.Post(new PrivateMessageNotification(message.Sender.Username, channel));
|
||||
notifications.Post(new PrivateMessageNotification(message, channel));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -122,7 +122,7 @@ namespace osu.Game.Online.Chat
|
||||
{
|
||||
if (!notifyOnUsername.Value || !CheckContainsUsername(message.Content, localUser.Value.Username)) return;
|
||||
|
||||
notifications.Post(new MentionNotification(message.Sender.Username, channel));
|
||||
notifications.Post(new MentionNotification(message, channel));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,47 +136,49 @@ namespace osu.Game.Online.Chat
|
||||
return Regex.IsMatch(message, $@"(^|\W)({fullName}|{underscoreName})($|\W)", RegexOptions.IgnoreCase);
|
||||
}
|
||||
|
||||
public class PrivateMessageNotification : OpenChannelNotification
|
||||
public class PrivateMessageNotification : HighlightMessageNotification
|
||||
{
|
||||
public PrivateMessageNotification(string username, Channel channel)
|
||||
: base(channel)
|
||||
public PrivateMessageNotification(Message message, Channel channel)
|
||||
: base(message, channel)
|
||||
{
|
||||
Icon = FontAwesome.Solid.Envelope;
|
||||
Text = $"You received a private message from '{username}'. Click to read it!";
|
||||
Text = $"You received a private message from '{message.Sender.Username}'. Click to read it!";
|
||||
}
|
||||
}
|
||||
|
||||
public class MentionNotification : OpenChannelNotification
|
||||
public class MentionNotification : HighlightMessageNotification
|
||||
{
|
||||
public MentionNotification(string username, Channel channel)
|
||||
: base(channel)
|
||||
public MentionNotification(Message message, Channel channel)
|
||||
: base(message, channel)
|
||||
{
|
||||
Icon = FontAwesome.Solid.At;
|
||||
Text = $"Your name was mentioned in chat by '{username}'. Click to find out why!";
|
||||
Text = $"Your name was mentioned in chat by '{message.Sender.Username}'. Click to find out why!";
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class OpenChannelNotification : SimpleNotification
|
||||
public abstract class HighlightMessageNotification : SimpleNotification
|
||||
{
|
||||
protected OpenChannelNotification(Channel channel)
|
||||
protected HighlightMessageNotification(Message message, Channel channel)
|
||||
{
|
||||
this.message = message;
|
||||
this.channel = channel;
|
||||
}
|
||||
|
||||
private readonly Message message;
|
||||
private readonly Channel channel;
|
||||
|
||||
public override bool IsImportant => false;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay, ChannelManager channelManager)
|
||||
private void load(OsuColour colours, ChatOverlay chatOverlay, NotificationOverlay notificationOverlay)
|
||||
{
|
||||
IconBackground.Colour = colours.PurpleDark;
|
||||
|
||||
Activated = delegate
|
||||
{
|
||||
notificationOverlay.Hide();
|
||||
chatOverlay.HighlightMessage(message, channel);
|
||||
chatOverlay.Show();
|
||||
channelManager.CurrentChannel.Value = channel;
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -22,6 +22,7 @@ using osu.Game.Online.API;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
@@ -311,25 +312,28 @@ namespace osu.Game.Online.Leaderboards
|
||||
switch (state)
|
||||
{
|
||||
case LeaderboardState.NetworkFailure:
|
||||
return new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync)
|
||||
return new ClickablePlaceholder(LeaderboardStrings.CouldntFetchScores, FontAwesome.Solid.Sync)
|
||||
{
|
||||
Action = RefetchScores
|
||||
};
|
||||
|
||||
case LeaderboardState.NoneSelected:
|
||||
return new MessagePlaceholder(@"Please select a beatmap!");
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseSelectABeatmap);
|
||||
|
||||
case LeaderboardState.Unavailable:
|
||||
return new MessagePlaceholder(@"Leaderboards are not available for this beatmap!");
|
||||
case LeaderboardState.RulesetUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisRuleset);
|
||||
|
||||
case LeaderboardState.BeatmapUnavailable:
|
||||
return new MessagePlaceholder(LeaderboardStrings.LeaderboardsAreNotAvailableForThisBeatmap);
|
||||
|
||||
case LeaderboardState.NoScores:
|
||||
return new MessagePlaceholder(@"No records yet!");
|
||||
return new MessagePlaceholder(LeaderboardStrings.NoRecordsYet);
|
||||
|
||||
case LeaderboardState.NotLoggedIn:
|
||||
return new LoginPlaceholder(@"Please sign in to view online leaderboards!");
|
||||
return new LoginPlaceholder(LeaderboardStrings.PleaseSignInToViewOnlineLeaderboards);
|
||||
|
||||
case LeaderboardState.NotSupporter:
|
||||
return new MessagePlaceholder(@"Please invest in an osu!supporter tag to view this leaderboard!");
|
||||
return new MessagePlaceholder(LeaderboardStrings.PleaseInvestInAnOsuSupporterTagToViewThisLeaderboard);
|
||||
|
||||
case LeaderboardState.Retrieving:
|
||||
return null;
|
||||
|
||||
@@ -8,7 +8,8 @@ namespace osu.Game.Online.Leaderboards
|
||||
Success,
|
||||
Retrieving,
|
||||
NetworkFailure,
|
||||
Unavailable,
|
||||
BeatmapUnavailable,
|
||||
RulesetUnavailable,
|
||||
NoneSelected,
|
||||
NoScores,
|
||||
NotLoggedIn,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
|
||||
namespace osu.Game.Online.Placeholders
|
||||
@@ -12,7 +13,7 @@ namespace osu.Game.Online.Placeholders
|
||||
[Resolved(CanBeNull = true)]
|
||||
private LoginOverlay login { get; set; }
|
||||
|
||||
public LoginPlaceholder(string actionMessage)
|
||||
public LoginPlaceholder(LocalisableString actionMessage)
|
||||
: base(actionMessage, FontAwesome.Solid.UserLock)
|
||||
{
|
||||
Action = () => login?.Show();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user