1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-15 08:13:31 +08:00

Merge branch 'aim-refactor-base' into aim-refactor-slider

This commit is contained in:
Xexxar 2021-10-21 16:01:38 +00:00
commit bef6e100fa
108 changed files with 3153 additions and 710 deletions

View File

@ -28,7 +28,12 @@ namespace osu.Game.Rulesets.Mania.Configuration
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
{ {
new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime, new TrackedSetting<double>(ManiaRulesetSetting.ScrollTime,
v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)")) scrollTime => new SettingDescription(
rawValue: scrollTime,
name: "Scroll Speed",
value: $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / scrollTime)} ({scrollTime}ms)"
)
)
}; };
} }

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double OverallDifficulty { get; set; } public double OverallDifficulty { get; set; }
public double DrainRate { get; set; } public double DrainRate { get; set; }
public int HitCircleCount { get; set; } public int HitCircleCount { get; set; }
public int SliderCount { get; set; }
public int SpinnerCount { get; set; } public int SpinnerCount { get; set; }
} }
} }

View File

@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1); maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider);
int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner);
return new OsuDifficultyAttributes return new OsuDifficultyAttributes
@ -78,6 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
DrainRate = drainRate, DrainRate = drainRate,
MaxCombo = maxCombo, MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount, HitCircleCount = hitCirclesCount,
SliderCount = sliderCount,
SpinnerCount = spinnerCount, SpinnerCount = spinnerCount,
Skills = skills Skills = skills
}; };

View File

@ -40,21 +40,15 @@ namespace osu.Game.Rulesets.Osu.Difficulty
countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh); countMeh = Score.Statistics.GetValueOrDefault(HitResult.Meh);
countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss); countMiss = Score.Statistics.GetValueOrDefault(HitResult.Miss);
// Custom multipliers for NoFail and SpunOut.
double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things. double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things.
// Custom multipliers for NoFail and SpunOut.
if (mods.Any(m => m is OsuModNoFail)) if (mods.Any(m => m is OsuModNoFail))
multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss);
if (mods.Any(m => m is OsuModSpunOut)) if (mods.Any(m => m is OsuModSpunOut))
multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85);
if (mods.Any(h => h is OsuModRelax))
{
countMiss += countOk + countMeh;
multiplier *= 0.6;
}
double aimValue = computeAimValue(); double aimValue = computeAimValue();
double speedValue = computeSpeedValue(); double speedValue = computeSpeedValue();
double accuracyValue = computeAccuracyValue(); double accuracyValue = computeAccuracyValue();
@ -114,18 +108,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; double approachRateBonus = 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModBlinds))
aimValue *= 1.3 + (totalHits * (0.0016 / (1 + 2 * countMiss)) * Math.Pow(accuracy, 16)) * (1 - 0.003 * Attributes.DrainRate * Attributes.DrainRate);
else if (mods.Any(h => h is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
if (mods.Any(h => h is OsuModHidden))
aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); aimValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
aimValue *= approachRateBonus; aimValue *= approachRateBonus;
// Scale the aim value with accuracy // Scale the aim value with accuracy _slightly_.
aimValue *= accuracy; aimValue *= 0.5 + accuracy / 2.0;
// It is important to also consider accuracy difficulty when doing that. // It is important to also consider accuracy difficulty when doing that.
aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500;
@ -157,20 +147,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor; speedValue *= 1.0 + (0.03 + 0.37 * approachRateTotalHitsFactor) * approachRateFactor;
if (mods.Any(m => m is OsuModBlinds)) if (mods.Any(m => m is OsuModHidden))
{
// Increasing the speed value by object count for Blinds isn't ideal, so the minimum buff is given.
speedValue *= 1.12;
}
else if (mods.Any(m => m is OsuModHidden))
{
// We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR.
speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate);
}
// Scale the speed value with accuracy and OD. // Scale the speed value with accuracy and OD.
speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2);
// Scale the speed value with # of 50s to punish doubletapping. // Scale the speed value with # of 50s to punish doubletapping.
speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0);
@ -179,9 +160,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private double computeAccuracyValue() private double computeAccuracyValue()
{ {
if (mods.Any(h => h is OsuModRelax))
return 0.0;
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window. // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window.
double betterAccuracyPercentage; double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
@ -202,12 +180,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer. // Bonus for many hitcircles - it's harder to keep good accuracy up for longer.
accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3)); accuracyValue *= Math.Min(1.15, Math.Pow(amountHitObjectsWithAccuracy / 1000.0, 0.3));
// Increasing the accuracy value by object count for Blinds isn't ideal, so the minimum buff is given. if (mods.Any(m => m is OsuModHidden))
if (mods.Any(m => m is OsuModBlinds))
accuracyValue *= 1.14;
else if (mods.Any(m => m is OsuModHidden))
accuracyValue *= 1.08; accuracyValue *= 1.08;
if (mods.Any(m => m is OsuModFlashlight)) if (mods.Any(m => m is OsuModFlashlight))
accuracyValue *= 1.02; accuracyValue *= 1.02;

View File

@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
@ -26,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
/// </summary> /// </summary>
public double MovementDistance { get; private set; } public double MovementDistance { get; private set; }
/// <summary>JumpTravel /// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>. /// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary> /// </summary>
public double TravelDistance { get; private set; } public double TravelDistance { get; private set; }
@ -62,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
this.lastObject = (OsuHitObject)lastObject; this.lastObject = (OsuHitObject)lastObject;
// Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects. // Capped to 25ms to prevent difficulty calculation breaking from simulatenous objects.
StrainTime = Math.Max(DeltaTime, 25); StrainTime = Math.Max(DeltaTime, min_delta_time);
setDistances(clockRate); setDistances(clockRate);
} }
@ -82,22 +83,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
scalingFactor *= 1 + smallCircleBonus; scalingFactor *= 1 + smallCircleBonus;
} }
double sliderAbuseIndex = 1;
if (lastObject is Slider lastSlider) if (lastObject is Slider lastSlider)
{ {
computeSliderCursorPosition(lastSlider); computeSliderCursorPosition(lastSlider);
sliderAbuseIndex = Math.Clamp(Vector2.Subtract(lastSlider.StackedPosition * scalingFactor, BaseObject.StackedPosition * scalingFactor).Length - 100, 0, 25) / 25; TravelDistance = lastSlider.LazyTravelDistance * scalingFactor;
TravelDistance = lastSlider.LazyTravelDistance * scalingFactor * sliderAbuseIndex; TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, 25); MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, 25);
MovementDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor; MovementDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
} }
Vector2 lastCursorPosition = getEndCursorPosition(lastObject); Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length * sliderAbuseIndex; JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
MovementDistance = Math.Min(JumpDistance, MovementDistance) * sliderAbuseIndex; MovementDistance = Math.Min(JumpDistance, MovementDistance);
if (lastLastObject != null && !(lastLastObject is Spinner)) if (lastLastObject != null && !(lastLastObject is Spinner))
{ {
@ -120,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition; slider.LazyEndPosition = slider.StackedPosition;
float approxFollowCircleRadius = (float)(slider.Radius * 2.4); float followCircleRadius = (float)(slider.Radius * 2.4);
var computeVertex = new Action<double>(t => var computeVertex = new Action<double>(t =>
{ {
double progress = (t - slider.StartTime) / slider.SpanDuration; double progress = (t - slider.StartTime) / slider.SpanDuration;
@ -135,11 +133,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyTravelTime = t - slider.StartTime; slider.LazyTravelTime = t - slider.StartTime;
if (dist > approxFollowCircleRadius) if (dist > followCircleRadius)
{ {
// The cursor would be outside the follow circle, we need to move it // The cursor would be outside the follow circle, we need to move it
diff.Normalize(); // Obtain direction of diff diff.Normalize(); // Obtain direction of diff
dist -= approxFollowCircleRadius; dist -= followCircleRadius;
slider.LazyEndPosition += diff * dist; slider.LazyEndPosition += diff * dist;
slider.LazyTravelDistance += dist; slider.LazyTravelDistance += dist;
} }

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuPrevObj = (OsuDifficultyHitObject)Previous[0]; var osuPrevObj = (OsuDifficultyHitObject)Previous[0];
var osuLastObj = (OsuDifficultyHitObject)Previous[1]; var osuLastObj = (OsuDifficultyHitObject)Previous[1];
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start iwth the base distance / time double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime; // Start with the base distance / time
if (osuPrevObj.BaseObject is Slider) // If object is a slider if (osuPrevObj.BaseObject is Slider) // If object is a slider
{ {

View File

@ -0,0 +1,820 @@
// 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.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Extensions;
using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Archives;
using osu.Game.Models;
using osu.Game.Stores;
using osu.Game.Tests.Resources;
using Realms;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SharpCompress.Writers.Zip;
#nullable enable
namespace osu.Game.Tests.Database
{
[TestFixture]
public class BeatmapImporterTests : RealmTest
{
[Test]
public void TestImportBeatmapThenCleanup()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using (var importer = new BeatmapImporter(realmFactory, storage))
using (new RealmRulesetStore(realmFactory, storage))
{
ILive<RealmBeatmapSet>? imported;
using (var reader = new ZipArchiveReader(TestResources.GetTestBeatmapStream()))
imported = await importer.Import(reader);
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count());
Assert.NotNull(imported);
Debug.Assert(imported != null);
imported.PerformWrite(s => s.DeletePending = true);
Assert.AreEqual(1, realmFactory.Context.All<RealmBeatmapSet>().Count(s => s.DeletePending));
}
});
Logger.Log("Running with no work to purge pending deletions");
RunTestWithRealm((realmFactory, _) => { Assert.AreEqual(0, realmFactory.Context.All<RealmBeatmapSet>().Count()); });
}
[Test]
public void TestImportWhenClosed()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
await LoadOszIntoStore(importer, realmFactory.Context);
});
}
[Test]
public void TestImportThenDelete()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
deleteBeatmapSet(imported, realmFactory.Context);
});
}
[Test]
public void TestImportThenDeleteFromStream()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var tempPath = TestResources.GetTestBeatmapForImport();
ILive<RealmBeatmapSet>? importedSet;
using (var stream = File.OpenRead(tempPath))
{
importedSet = await importer.Import(new ImportTask(stream, Path.GetFileName(tempPath)));
ensureLoaded(realmFactory.Context);
}
Assert.NotNull(importedSet);
Debug.Assert(importedSet != null);
Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing");
File.Delete(tempPath);
var imported = realmFactory.Context.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
deleteBeatmapSet(imported, realmFactory.Context);
});
}
[Test]
public void TestImportThenImport()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
checkBeatmapSetCount(realmFactory.Context, 1);
checkSingleReferencedFileCount(realmFactory.Context, 18);
});
}
[Test]
public void TestImportThenImportWithReZip()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
string hashBefore = hashFile(temp);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
// zip files differ because different compression or encoder.
Assert.AreNotEqual(hashBefore, hashFile(temp));
var importedSecondTime = await importer.Import(new ImportTask(temp));
ensureLoaded(realmFactory.Context);
Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null);
// but contents doesn't, so existing should still be used.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportThenImportWithChangedHashedFile()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
await createScoreForBeatmap(realmFactory.Context, imported.Beatmaps.First());
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
// arbitrary write to hashed file
// this triggers the special BeatmapManager.PreImport deletion/replacement flow.
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.osu").First()).AppendText())
await sw.WriteLineAsync("// changed");
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await importer.Import(new ImportTask(temp));
ensureLoaded(realmFactory.Context);
// check the newly "imported" beatmap is not the original.
Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null);
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public void TestImportThenImportWithChangedFile()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
// arbitrary write to non-hashed file
using (var sw = new FileInfo(Directory.GetFiles(extractedFolder, "*.mp3").First()).AppendText())
await sw.WriteLineAsync("text");
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await importer.Import(new ImportTask(temp));
ensureLoaded(realmFactory.Context);
Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null);
// check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportThenImportWithDifferentFilename()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
// change filename
var firstFile = new FileInfo(Directory.GetFiles(extractedFolder).First());
firstFile.MoveTo(Path.Combine(firstFile.DirectoryName.AsNonNull(), $"{firstFile.Name}-changed{firstFile.Extension}"));
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var importedSecondTime = await importer.Import(new ImportTask(temp));
ensureLoaded(realmFactory.Context);
Assert.NotNull(importedSecondTime);
Debug.Assert(importedSecondTime != null);
// check the newly "imported" beatmap is not the original.
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.PerformRead(s => s.Beatmaps.First().ID));
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
[Ignore("intentionally broken by import optimisations")]
public void TestImportCorruptThenImport()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
var firstFile = imported.Files.First();
long originalLength;
using (var stream = storage.GetStream(firstFile.File.StoragePath))
originalLength = stream.Length;
using (var stream = storage.GetStream(firstFile.File.StoragePath, FileAccess.Write, FileMode.Create))
stream.WriteByte(0);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
using (var stream = storage.GetStream(firstFile.File.StoragePath))
Assert.AreEqual(stream.Length, originalLength, "Corruption was not fixed on second import");
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
checkBeatmapSetCount(realmFactory.Context, 1);
checkSingleReferencedFileCount(realmFactory.Context, 18);
});
}
[Test]
public void TestRollbackOnFailure()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
int loggedExceptionCount = 0;
Logger.NewEntry += l =>
{
if (l.Target == LoggingTarget.Database && l.Exception != null)
Interlocked.Increment(ref loggedExceptionCount);
};
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
realmFactory.Context.Write(() => imported.Hash += "-changed");
checkBeatmapSetCount(realmFactory.Context, 1);
checkBeatmapCount(realmFactory.Context, 12);
checkSingleReferencedFileCount(realmFactory.Context, 18);
var brokenTempFilename = TestResources.GetTestBeatmapForImport();
MemoryStream brokenOsu = new MemoryStream();
MemoryStream brokenOsz = new MemoryStream(await File.ReadAllBytesAsync(brokenTempFilename));
File.Delete(brokenTempFilename);
using (var outStream = File.Open(brokenTempFilename, FileMode.CreateNew))
using (var zip = ZipArchive.Open(brokenOsz))
{
zip.AddEntry("broken.osu", brokenOsu, false);
zip.SaveTo(outStream, CompressionType.Deflate);
}
// this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu.
try
{
await importer.Import(new ImportTask(brokenTempFilename));
}
catch
{
}
checkBeatmapSetCount(realmFactory.Context, 1);
checkBeatmapCount(realmFactory.Context, 12);
checkSingleReferencedFileCount(realmFactory.Context, 18);
Assert.AreEqual(1, loggedExceptionCount);
File.Delete(brokenTempFilename);
});
}
[Test]
public void TestImportThenDeleteThenImport()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
deleteBeatmapSet(imported, realmFactory.Context);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
// check the newly "imported" beatmap is actually just the restored previous import. since it matches hash.
Assert.IsTrue(imported.ID == importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID == importedSecondTime.Beatmaps.First().ID);
});
}
[Test]
public void TestImportThenDeleteThenImportWithOnlineIDsMissing()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var imported = await LoadOszIntoStore(importer, realmFactory.Context);
realmFactory.Context.Write(() =>
{
foreach (var b in imported.Beatmaps)
b.OnlineID = -1;
});
deleteBeatmapSet(imported, realmFactory.Context);
var importedSecondTime = await LoadOszIntoStore(importer, realmFactory.Context);
// check the newly "imported" beatmap has been reimported due to mismatch (even though hashes matched)
Assert.IsTrue(imported.ID != importedSecondTime.ID);
Assert.IsTrue(imported.Beatmaps.First().ID != importedSecondTime.Beatmaps.First().ID);
});
}
[Test]
public void TestImportWithDuplicateBeatmapIDs()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var metadata = new RealmBeatmapMetadata
{
Artist = "SomeArtist",
Author = "SomeAuthor"
};
var ruleset = realmFactory.Context.All<RealmRuleset>().First();
var toImport = new RealmBeatmapSet
{
OnlineID = 1,
Beatmaps =
{
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
{
OnlineID = 2,
},
new RealmBeatmap(ruleset, new RealmBeatmapDifficulty(), metadata)
{
OnlineID = 2,
Status = BeatmapSetOnlineStatus.Loved,
}
}
};
var imported = await importer.Import(toImport);
Assert.NotNull(imported);
Debug.Assert(imported != null);
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[0].OnlineID));
Assert.AreEqual(-1, imported.PerformRead(s => s.Beatmaps[1].OnlineID));
});
}
[Test]
public void TestImportWhenFileOpen()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
using (File.OpenRead(temp))
await importer.Import(temp);
ensureLoaded(realmFactory.Context);
File.Delete(temp);
Assert.IsFalse(File.Exists(temp), "We likely held a read lock on the file when we shouldn't");
});
}
[Test]
public void TestImportWithDuplicateHashes()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
Directory.CreateDirectory(extractedFolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(extractedFolder);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.AddEntry("duplicate.osu", Directory.GetFiles(extractedFolder, "*.osu").First());
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
await importer.Import(temp);
ensureLoaded(realmFactory.Context);
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportNestedStructure()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
string subfolder = Path.Combine(extractedFolder, "subfolder");
Directory.CreateDirectory(subfolder);
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(subfolder);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var imported = await importer.Import(new ImportTask(temp));
Assert.NotNull(imported);
Debug.Assert(imported != null);
ensureLoaded(realmFactory.Context);
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("subfolder"))), "Files contain common subfolder");
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestImportWithIgnoredDirectoryInArchive()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
string extractedFolder = $"{temp}_extracted";
string dataFolder = Path.Combine(extractedFolder, "actual_data");
string resourceForkFolder = Path.Combine(extractedFolder, "__MACOSX");
string resourceForkFilePath = Path.Combine(resourceForkFolder, ".extracted");
Directory.CreateDirectory(dataFolder);
Directory.CreateDirectory(resourceForkFolder);
using (var resourceForkFile = File.CreateText(resourceForkFilePath))
{
await resourceForkFile.WriteLineAsync("adding content so that it's not empty");
}
try
{
using (var zip = ZipArchive.Open(temp))
zip.WriteToDirectory(dataFolder);
using (var zip = ZipArchive.Create())
{
zip.AddAllFromDirectory(extractedFolder);
zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate));
}
var imported = await importer.Import(new ImportTask(temp));
Assert.NotNull(imported);
Debug.Assert(imported != null);
ensureLoaded(realmFactory.Context);
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("__MACOSX"))), "Files contain resource fork folder, which should be ignored");
Assert.IsFalse(imported.PerformRead(s => s.Files.Any(f => f.Filename.Contains("actual_data"))), "Files contain common subfolder");
}
finally
{
Directory.Delete(extractedFolder, true);
}
});
}
[Test]
public void TestUpdateBeatmapInfo()
{
RunTestWithRealmAsync(async (realmFactory, storage) =>
{
using var importer = new BeatmapImporter(realmFactory, storage);
using var store = new RealmRulesetStore(realmFactory, storage);
var temp = TestResources.GetTestBeatmapForImport();
await importer.Import(temp);
// Update via the beatmap, not the beatmap info, to ensure correct linking
RealmBeatmapSet setToUpdate = realmFactory.Context.All<RealmBeatmapSet>().First();
var beatmapToUpdate = setToUpdate.Beatmaps.First();
realmFactory.Context.Write(() => beatmapToUpdate.DifficultyName = "updated");
RealmBeatmap updatedInfo = realmFactory.Context.All<RealmBeatmap>().First(b => b.ID == beatmapToUpdate.ID);
Assert.That(updatedInfo.DifficultyName, Is.EqualTo("updated"));
});
}
public static async Task<RealmBeatmapSet?> LoadQuickOszIntoOsu(BeatmapImporter importer, Realm realm)
{
var temp = TestResources.GetQuickTestBeatmapForImport();
var importedSet = await importer.Import(new ImportTask(temp));
Assert.NotNull(importedSet);
ensureLoaded(realm);
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return realm.All<RealmBeatmapSet>().FirstOrDefault(beatmapSet => beatmapSet.ID == importedSet!.ID);
}
public static async Task<RealmBeatmapSet> LoadOszIntoStore(BeatmapImporter importer, Realm realm, string? path = null, bool virtualTrack = false)
{
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
var importedSet = await importer.Import(new ImportTask(temp));
Assert.NotNull(importedSet);
Debug.Assert(importedSet != null);
ensureLoaded(realm);
waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000);
return realm.All<RealmBeatmapSet>().First(beatmapSet => beatmapSet.ID == importedSet.ID);
}
private void deleteBeatmapSet(RealmBeatmapSet imported, Realm realm)
{
realm.Write(() => imported.DeletePending = true);
checkBeatmapSetCount(realm, 0);
checkBeatmapSetCount(realm, 1, true);
Assert.IsTrue(realm.All<RealmBeatmapSet>().First(_ => true).DeletePending);
}
private static Task createScoreForBeatmap(Realm realm, RealmBeatmap beatmap)
{
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
// OnlineScoreID = 2,
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());
return Task.CompletedTask;
}
private static void checkBeatmapSetCount(Realm realm, int expected, bool includeDeletePending = false)
{
Assert.AreEqual(expected, includeDeletePending
? realm.All<RealmBeatmapSet>().Count()
: realm.All<RealmBeatmapSet>().Count(s => !s.DeletePending));
}
private static string hashFile(string filename)
{
using (var s = File.OpenRead(filename))
return s.ComputeMD5Hash();
}
private static void checkBeatmapCount(Realm realm, int expected)
{
Assert.AreEqual(expected, realm.All<RealmBeatmap>().Where(_ => true).ToList().Count);
}
private static void checkSingleReferencedFileCount(Realm realm, int expected)
{
int singleReferencedCount = 0;
foreach (var f in realm.All<RealmFile>())
{
if (f.BacklinksCount == 1)
singleReferencedCount++;
}
Assert.AreEqual(expected, singleReferencedCount);
}
private static void ensureLoaded(Realm realm, int timeout = 60000)
{
IQueryable<RealmBeatmapSet>? resultSets = null;
waitForOrAssert(() => (resultSets = realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526)).Any(),
@"BeatmapSet did not import to the database in allocated time.", timeout);
// ensure we were stored to beatmap database backing...
Assert.IsTrue(resultSets?.Count() == 1, $@"Incorrect result count found ({resultSets?.Count()} but should be 1).");
IEnumerable<RealmBeatmapSet> queryBeatmapSets() => realm.All<RealmBeatmapSet>().Where(s => !s.DeletePending && s.OnlineID == 241526);
var set = queryBeatmapSets().First();
// ReSharper disable once PossibleUnintendedReferenceComparison
IEnumerable<RealmBeatmap> queryBeatmaps() => realm.All<RealmBeatmap>().Where(s => s.BeatmapSet != null && s.BeatmapSet == set);
waitForOrAssert(() => queryBeatmaps().Count() == 12, @"Beatmaps did not import to the database in allocated time", timeout);
waitForOrAssert(() => queryBeatmapSets().Count() == 1, @"BeatmapSet did not import to the database in allocated time", timeout);
int countBeatmapSetBeatmaps = 0;
int countBeatmaps = 0;
waitForOrAssert(() =>
(countBeatmapSetBeatmaps = queryBeatmapSets().First().Beatmaps.Count) ==
(countBeatmaps = queryBeatmaps().Count()),
$@"Incorrect database beatmap count post-import ({countBeatmaps} but should be {countBeatmapSetBeatmaps}).", timeout);
foreach (RealmBeatmap b in set.Beatmaps)
Assert.IsTrue(set.Beatmaps.Any(c => c.OnlineID == b.OnlineID));
Assert.IsTrue(set.Beatmaps.Count > 0);
}
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
const int sleep = 200;
while (timeout > 0)
{
Thread.Sleep(sleep);
timeout -= sleep;
if (result())
return;
}
Assert.Fail(failureMessage);
}
}
}

View File

@ -20,14 +20,15 @@ namespace osu.Game.Tests.Visual.Gameplay
/// </summary> /// </summary>
public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene public abstract class TestSceneAllRulesetPlayers : RateAdjustedBeatmapTestScene
{ {
protected Player Player; protected Player Player { get; private set; }
protected OsuConfigManager Config { get; private set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(RulesetStore rulesets) private void load(RulesetStore rulesets)
{ {
OsuConfigManager manager; Dependencies.Cache(Config = new OsuConfigManager(LocalStorage));
Dependencies.Cache(manager = new OsuConfigManager(LocalStorage)); Config.GetBindable<double>(OsuSetting.DimLevel).Value = 1.0;
manager.GetBindable<double>(OsuSetting.DimLevel).Value = 1.0;
} }
[Test] [Test]

View File

@ -2,7 +2,9 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using NUnit.Framework;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Configuration;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
@ -17,6 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay
return new FailPlayer(); return new FailPlayer();
} }
[Test]
public void TestOsuWithoutRedTint()
{
AddStep("Disable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
TestOsu();
AddStep("Enable red tint", () => Config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, true));
}
protected override void AddCheckSteps() protected override void AddCheckSteps()
{ {
AddUntilStep("wait for fail", () => Player.HasFailed); AddUntilStep("wait for fail", () => Player.HasFailed);

View File

@ -291,7 +291,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType<EpilepsyWarning>().Any() == warning); AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => (getWarning() != null) == warning);
if (warning) if (warning)
{ {
@ -335,12 +335,17 @@ namespace osu.Game.Tests.Visual.Gameplay
AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for current", () => loader.IsCurrentScreen());
AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType<EpilepsyWarning>().Single().Alpha > 0); AddUntilStep("wait for epilepsy warning", () => getWarning().Alpha > 0);
AddUntilStep("warning is shown", () => getWarning().State.Value == Visibility.Visible);
AddStep("exit early", () => loader.Exit()); AddStep("exit early", () => loader.Exit());
AddUntilStep("warning is hidden", () => getWarning().State.Value == Visibility.Hidden);
AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1);
} }
private EpilepsyWarning getWarning() => loader.ChildrenOfType<EpilepsyWarning>().SingleOrDefault();
private class TestPlayerLoader : PlayerLoader private class TestPlayerLoader : PlayerLoader
{ {
public new VisualSettings VisualSettings => base.VisualSettings; public new VisualSettings VisualSettings => base.VisualSettings;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -10,10 +11,13 @@ using osu.Game.Online.API;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Solo; using osu.Game.Online.Solo;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -32,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay
protected override bool HasCustomSteps => true; protected override bool HasCustomSteps => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new NonImportingPlayer(false);
protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset(); protected override Ruleset CreatePlayerRuleset() => createCustomRuleset?.Invoke() ?? new OsuRuleset();
@ -86,6 +90,46 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true); AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
} }
[Test]
public void TestSubmissionForDifferentRuleset()
{
prepareTokenResponse(true);
createPlayerTest(createRuleset: () => new TaikoRuleset());
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new TaikoRuleset().RulesetInfo.ID);
}
[Test]
public void TestSubmissionForConvertedBeatmap()
{
prepareTokenResponse(true);
createPlayerTest(createRuleset: () => new ManiaRuleset(), createBeatmap: _ => createTestBeatmap(new OsuRuleset().RulesetInfo));
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning);
addFakeHit();
AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime()));
AddUntilStep("results displayed", () => Player.GetChildScreen() is ResultsScreen);
AddAssert("ensure passing submission", () => Player.SubmittedScore?.ScoreInfo.Passed == true);
AddAssert("submitted score has correct ruleset ID", () => Player.SubmittedScore?.ScoreInfo.RulesetID == new ManiaRuleset().RulesetInfo.ID);
}
[Test] [Test]
public void TestNoSubmissionOnExitWithNoToken() public void TestNoSubmissionOnExitWithNoToken()
{ {
@ -183,12 +227,13 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("ensure no submission", () => Player.SubmittedScore == null); AddAssert("ensure no submission", () => Player.SubmittedScore == null);
} }
[Test] [TestCase(null)]
public void TestNoSubmissionOnCustomRuleset() [TestCase(10)]
public void TestNoSubmissionOnCustomRuleset(int? rulesetId)
{ {
prepareTokenResponse(true); prepareTokenResponse(true);
createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = 10 } }); createPlayerTest(false, createRuleset: () => new OsuRuleset { RulesetInfo = { ID = rulesetId } });
AddUntilStep("wait for token request", () => Player.TokenCreationRequested); AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
@ -242,5 +287,33 @@ namespace osu.Game.Tests.Visual.Gameplay
}); });
}); });
} }
private class NonImportingPlayer : TestPlayer
{
public NonImportingPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base(allowPause, showResults, pauseOnFocusLost)
{
}
protected override Task ImportScore(Score score)
{
// It was discovered that Score members could sometimes be half-populated.
// In particular, the RulesetID property could be set to 0 even on non-osu! maps.
// We want to test that the state of that property is consistent in this test.
// EF makes this impossible.
//
// First off, because of the EF navigational property-explicit foreign key field duality,
// it can happen that - for example - the Ruleset navigational property is correctly initialised to mania,
// but the RulesetID foreign key property is not initialised and remains 0.
// EF silently bypasses this by prioritising the Ruleset navigational property over the RulesetID foreign key one.
//
// Additionally, adding an entity to an EF DbSet CAUSES SIDE EFFECTS with regard to the foreign key property.
// In the above instance, if a ScoreInfo with Ruleset = {mania} and RulesetID = 0 is attached to an EF context,
// RulesetID WILL BE SILENTLY SET TO THE CORRECT VALUE of 3.
//
// For the above reasons, importing is disabled in this test.
return Task.CompletedTask;
}
}
} }
} }

View File

@ -0,0 +1,51 @@
// 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.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Tests.Visual.Spectator;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneSpectatorHost : PlayerTestScene
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
[Cached(typeof(SpectatorClient))]
private TestSpectatorClient spectatorClient { get; } = new TestSpectatorClient();
private DummyAPIAccess dummyAPIAccess => (DummyAPIAccess)API;
private const int dummy_user_id = 42;
public override void SetUpSteps()
{
AddStep("set dummy user", () => dummyAPIAccess.LocalUser.Value = new User
{
Id = dummy_user_id,
Username = "DummyUser"
});
AddStep("add test spectator client", () => Add(spectatorClient));
AddStep("add watching user", () => spectatorClient.WatchUser(dummy_user_id));
base.SetUpSteps();
}
[Test]
public void TestClientSendsCorrectRuleset()
{
AddUntilStep("spectator client sending frames", () => spectatorClient.PlayingUserStates.ContainsKey(dummy_user_id));
AddAssert("spectator client sent correct ruleset", () => spectatorClient.PlayingUserStates[dummy_user_id].RulesetID == Ruleset.Value.ID);
}
public override void TearDownSteps()
{
base.TearDownSteps();
AddStep("stop watching user", () => spectatorClient.StopWatchingUser(dummy_user_id));
AddStep("remove test spectator client", () => Remove(spectatorClient));
}
}
}

View File

@ -275,6 +275,68 @@ namespace osu.Game.Tests.Visual.Multiplayer
var state = i; var state = i;
AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); AddStep($"set state: {state}", () => Client.ChangeUserState(0, state));
} }
AddStep("set state: downloading", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.Downloading(0)));
AddStep("set state: locally available", () => Client.ChangeUserBeatmapAvailability(0, BeatmapAvailability.LocallyAvailable()));
}
[Test]
public void TestModOverlap()
{
AddStep("add dummy mods", () =>
{
Client.ChangeUserMods(new Mod[]
{
new OsuModNoFail(),
new OsuModDoubleTime()
});
});
AddStep("add user with mods", () =>
{
Client.AddUser(new User
{
Id = 0,
Username = "Baka",
RulesetsStatistics = new Dictionary<string, UserStatistics>
{
{
Ruleset.Value.ShortName,
new UserStatistics { GlobalRank = RNG.Next(1, 100000), }
}
},
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
});
Client.ChangeUserMods(0, new Mod[]
{
new OsuModHardRock(),
new OsuModDoubleTime()
});
});
AddStep("set 0 ready", () => Client.ChangeState(MultiplayerUserState.Ready));
AddStep("set 1 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
// Have to set back to idle due to status priority.
AddStep("set 0 no map, 1 ready", () =>
{
Client.ChangeState(MultiplayerUserState.Idle);
Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded());
Client.ChangeUserState(0, MultiplayerUserState.Ready);
});
AddStep("set 0 downloading", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0)));
AddStep("set 0 spectate", () => Client.ChangeUserState(0, MultiplayerUserState.Spectating));
AddStep("make both default", () =>
{
Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable());
Client.ChangeUserState(0, MultiplayerUserState.Idle);
Client.ChangeState(MultiplayerUserState.Idle);
});
} }
private void createNewParticipantsList() private void createNewParticipantsList()

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo AddStep("set undownloadable beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {
@ -40,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo AddStep("set undownloadable beatmapset without link", () => container.BeatmapSet = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo AddStep("set parts-removed beatmapset with link", () => container.BeatmapSet = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {
@ -75,7 +76,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo AddStep("set normal beatmapset", () => container.BeatmapSet = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {

View File

@ -11,6 +11,7 @@ using osu.Game.Users;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.Visual.Online namespace osu.Game.Tests.Visual.Online
{ {
@ -63,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
}, },
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Preview = @"https://b.ppy.sh/preview/12345.mp3", Preview = @"https://b.ppy.sh/preview/12345.mp3",
PlayCount = 123, PlayCount = 123,
@ -134,7 +135,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
}, },
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {
@ -224,7 +225,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers(), Covers = new BeatmapSetOnlineCovers(),
}, },
@ -309,7 +310,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
}, },
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Preview = @"https://b.ppy.sh/preview/123.mp3", Preview = @"https://b.ppy.sh/preview/123.mp3",
HasVideo = true, HasVideo = true,

View File

@ -8,6 +8,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapSet; using osu.Game.Overlays.BeatmapSet;
using osu.Game.Screens.Select.Details; using osu.Game.Screens.Select.Details;
@ -57,7 +58,7 @@ namespace osu.Game.Tests.Visual.Online
}, },
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Status = BeatmapSetOnlineStatus.Ranked Status = BeatmapSetOnlineStatus.Ranked
} }

View File

@ -7,6 +7,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online; using osu.Game.Online;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -74,7 +75,7 @@ namespace osu.Game.Tests.Visual.Online
{ {
ID = 1, ID = 1,
OnlineBeatmapSetID = 241526, OnlineBeatmapSetID = 241526,
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {

View File

@ -7,6 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapListing.Panels;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Users; using osu.Game.Users;
@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
}, },
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Availability = new BeatmapSetOnlineAvailability Availability = new BeatmapSetOnlineAvailability
{ {
@ -86,7 +87,7 @@ namespace osu.Game.Tests.Visual.Online
Id = 3, Id = 3,
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
HasVideo = true, HasVideo = true,
HasStoryboard = true, HasStoryboard = true,

View File

@ -3,11 +3,7 @@
using System; using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Overlays.Profile.Header.Components;
using osu.Game.Users; using osu.Game.Users;
@ -16,48 +12,50 @@ namespace osu.Game.Tests.Visual.Online
[TestFixture] [TestFixture]
public class TestSceneUserProfilePreviousUsernames : OsuTestScene public class TestSceneUserProfilePreviousUsernames : OsuTestScene
{ {
[Resolved] private PreviousUsernames container;
private IAPIProvider api { get; set; }
private readonly Bindable<User> user = new Bindable<User>(); [SetUp]
public void SetUp() => Schedule(() =>
public TestSceneUserProfilePreviousUsernames()
{ {
Child = new PreviousUsernames Child = container = new PreviousUsernames
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
User = { BindTarget = user },
}; };
});
User[] users = [Test]
public void TestVisibility()
{ {
new User { PreviousUsernames = new[] { "username1" } }, AddAssert("Is Hidden", () => container.Alpha == 0);
new User { PreviousUsernames = new[] { "longusername", "longerusername" } },
new User { PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } }, AddStep("1 username", () => container.User.Value = users[0]);
new User { PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } }, AddUntilStep("Is visible", () => container.Alpha == 1);
new User { PreviousUsernames = Array.Empty<string>() },
AddStep("2 usernames", () => container.User.Value = users[1]);
AddUntilStep("Is visible", () => container.Alpha == 1);
AddStep("3 usernames", () => container.User.Value = users[2]);
AddUntilStep("Is visible", () => container.Alpha == 1);
AddStep("4 usernames", () => container.User.Value = users[3]);
AddUntilStep("Is visible", () => container.Alpha == 1);
AddStep("No username", () => container.User.Value = users[4]);
AddUntilStep("Is hidden", () => container.Alpha == 0);
AddStep("Null user", () => container.User.Value = users[5]);
AddUntilStep("Is hidden", () => container.Alpha == 0);
}
private static readonly User[] users =
{
new User { Id = 1, PreviousUsernames = new[] { "username1" } },
new User { Id = 2, PreviousUsernames = new[] { "longusername", "longerusername" } },
new User { Id = 3, PreviousUsernames = new[] { "test", "angelsim", "verylongusername" } },
new User { Id = 4, PreviousUsernames = new[] { "ihavenoidea", "howcani", "makethistext", "anylonger" } },
new User { Id = 5, PreviousUsernames = Array.Empty<string>() },
null null
}; };
AddStep("single username", () => user.Value = users[0]);
AddStep("two usernames", () => user.Value = users[1]);
AddStep("three usernames", () => user.Value = users[2]);
AddStep("four usernames", () => user.Value = users[3]);
AddStep("no username", () => user.Value = users[4]);
AddStep("null user", () => user.Value = users[5]);
}
protected override void LoadComplete()
{
base.LoadComplete();
AddStep("online user (Angelsim)", () =>
{
var request = new GetUserRequest(1777162);
request.Success += user => this.user.Value = user;
api.Queue(request);
});
}
} }
} }

View File

@ -0,0 +1,63 @@
// 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.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Tests.Visual.Settings
{
public class TestSceneRestoreDefaultValueButton : OsuTestScene
{
[Resolved]
private OsuColour colours { get; set; }
private float scale = 1;
private readonly Bindable<float> current = new Bindable<float>
{
Default = default,
Value = 1,
};
[Test]
public void TestBasic()
{
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
AddStep("create button", () => Child = new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeafoam
},
restoreDefaultValueButton = new RestoreDefaultValueButton<float>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Scale = new Vector2(scale),
Current = current,
}
}
});
AddSliderStep("set scale", 1, 4, 1, scale =>
{
this.scale = scale;
if (restoreDefaultValueButton != null)
restoreDefaultValueButton.Scale = new Vector2(scale);
});
AddToggleStep("toggle default state", state => current.Value = state ? default : 1);
AddToggleStep("toggle disabled state", state => current.Disabled = state);
}
}
}

View File

@ -5,6 +5,9 @@ using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osu.Game.Overlays; using osu.Game.Overlays;
@ -29,9 +32,10 @@ namespace osu.Game.Tests.Visual.Settings
Value = "test" Value = "test"
} }
}; };
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
}); });
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default"); AddStep("change value from default", () => textBox.Current.Value = "non-default");
@ -41,6 +45,48 @@ namespace osu.Game.Tests.Visual.Settings
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
} }
[Test]
public void TestSetAndClearLabelText()
{
SettingsTextBox textBox = null;
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
OsuTextBox control = null;
AddStep("create settings item", () =>
{
Child = textBox = new SettingsTextBox
{
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
};
});
AddUntilStep("wait for loaded", () => textBox.IsLoaded);
AddStep("retrieve components", () =>
{
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
control = textBox.ChildrenOfType<OsuTextBox>().Single();
});
AddStep("set non-default value", () => restoreDefaultValueButton.Current.Value = "non-default");
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set label", () => textBox.LabelText = "label text");
AddAssert("default value button centre aligned to label size", () =>
{
var label = textBox.ChildrenOfType<OsuSpriteText>().Single(spriteText => spriteText.Text == "label text");
return Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, label.DrawHeight, 1);
});
AddStep("clear label", () => textBox.LabelText = default);
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
AddStep("set warning text", () => textBox.WarningText = "This is some very important warning text! Hopefully it doesn't break the alignment of the default value indicator...");
AddAssert("default value button centre aligned to control size", () => Precision.AlmostEquals(restoreDefaultValueButton.Parent.DrawHeight, control.DrawHeight, 1));
}
/// <summary> /// <summary>
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not. /// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision). /// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
@ -64,9 +110,9 @@ namespace osu.Game.Tests.Visual.Settings
Precision = 0.1f, Precision = 0.1f,
} }
}; };
restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single();
}); });
AddUntilStep("wait for loaded", () => sliderBar.IsLoaded);
AddStep("retrieve restore default button", () => restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single());
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0); AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);

View File

@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing;
using osuTK; using osuTK;
@ -111,7 +112,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {
@ -122,7 +123,7 @@ namespace osu.Game.Tests.Visual.UserInterface
private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo private static readonly BeatmapSetInfo no_cover_beatmap_set = new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {

View File

@ -11,6 +11,7 @@ using osu.Game.Users;
using System; using System;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
@ -69,7 +70,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100 Id = 100
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {
@ -90,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100 Id = 100
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {
@ -115,7 +116,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100 Id = 100
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {
@ -136,7 +137,7 @@ namespace osu.Game.Tests.Visual.UserInterface
Id = 100 Id = 100
} }
}, },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers Covers = new BeatmapSetOnlineCovers
{ {

View File

@ -0,0 +1,20 @@
// 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.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Tests.Visual.UserInterface
{
public class TestSceneOsuDropdown : ThemeComparisonTestScene
{
protected override Drawable CreateContent() =>
new OsuEnumDropdown<BeatmapSetOnlineStatus>
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Width = 150
};
}
}

View File

@ -1,39 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
public class TestSceneOsuTextBox : OsuTestScene public class TestSceneOsuTextBox : ThemeComparisonTestScene
{ {
private readonly OsuNumberBox numberBox; private IEnumerable<OsuNumberBox> numberBoxes => this.ChildrenOfType<OsuNumberBox>();
public TestSceneOsuTextBox() protected override Drawable CreateContent() => new FillFlowContainer
{ {
Child = new Container RelativeSizeAxes = Axes.X,
{ AutoSizeAxes = Axes.Y,
Masking = true,
CornerRadius = 10f,
AutoSizeAxes = Axes.Both,
Padding = new MarginPadding(15f),
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.DarkSlateGray,
Alpha = 0.75f,
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Padding = new MarginPadding(50f), Padding = new MarginPadding(50f),
Spacing = new Vector2(0f, 50f), Spacing = new Vector2(0f, 50f),
@ -41,40 +29,39 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
new OsuTextBox new OsuTextBox
{ {
Width = 500f, RelativeSizeAxes = Axes.X,
PlaceholderText = "Normal textbox", PlaceholderText = "Normal textbox",
}, },
new OsuPasswordTextBox new OsuPasswordTextBox
{ {
Width = 500f, RelativeSizeAxes = Axes.X,
PlaceholderText = "Password textbox", PlaceholderText = "Password textbox",
}, },
numberBox = new OsuNumberBox new OsuNumberBox
{ {
Width = 500f, RelativeSizeAxes = Axes.X,
PlaceholderText = "Number textbox" PlaceholderText = "Number textbox"
} }
} }
}
}
}; };
}
[Test] [Test]
public void TestNumberBox() public void TestNumberBox()
{ {
clearTextbox(numberBox); AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red));
AddStep("enter numbers", () => numberBox.Text = "987654321");
expectedValue(numberBox, "987654321");
clearTextbox(numberBox); clearTextboxes(numberBoxes);
AddStep("enter text + single number", () => numberBox.Text = "1 hello 2 world 3"); AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321"));
expectedValue(numberBox, "123"); expectedValue(numberBoxes, "987654321");
clearTextbox(numberBox); clearTextboxes(numberBoxes);
AddStep("enter text + single number", () => numberBoxes.ForEach(numberBox => numberBox.Text = "1 hello 2 world 3"));
expectedValue(numberBoxes, "123");
clearTextboxes(numberBoxes);
} }
private void clearTextbox(OsuTextBox textBox) => AddStep("clear textbox", () => textBox.Text = null); private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));
private void expectedValue(OsuTextBox textBox, string value) => AddAssert("expected textbox value", () => textBox.Text == value); private void expectedValue(IEnumerable<OsuTextBox> textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textbox => textbox.Text == value));
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
@ -22,21 +23,21 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestLocal([Values] BeatmapSetCoverType coverType) public void TestLocal([Values] BeatmapSetCoverType coverType)
{ {
AddStep("setup cover", () => Child = new UpdateableBeatmapSetCover(coverType) AddStep("setup cover", () => Child = new UpdateableOnlineBeatmapSetCover(coverType)
{ {
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
}); });
AddUntilStep("wait for load", () => this.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false); AddUntilStep("wait for load", () => this.ChildrenOfType<OnlineBeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
} }
[Test] [Test]
public void TestUnloadAndReload() public void TestUnloadAndReload()
{ {
OsuScrollContainer scroll = null; OsuScrollContainer scroll = null;
List<UpdateableBeatmapSetCover> covers = new List<UpdateableBeatmapSetCover>(); List<UpdateableOnlineBeatmapSetCover> covers = new List<UpdateableOnlineBeatmapSetCover>();
AddStep("setup covers", () => AddStep("setup covers", () =>
{ {
@ -65,7 +66,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
var coverType = coverTypes[i % coverTypes.Count]; var coverType = coverTypes[i % coverTypes.Count];
var cover = new UpdateableBeatmapSetCover(coverType) var cover = new UpdateableOnlineBeatmapSetCover(coverType)
{ {
BeatmapSet = setInfo, BeatmapSet = setInfo,
Height = 100, Height = 100,
@ -84,7 +85,7 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}); });
var loadedCovers = covers.Where(c => c.ChildrenOfType<BeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false); var loadedCovers = covers.Where(c => c.ChildrenOfType<OnlineBeatmapSetCover>().SingleOrDefault()?.IsLoaded ?? false);
AddUntilStep("some loaded", () => loadedCovers.Any()); AddUntilStep("some loaded", () => loadedCovers.Any());
AddStep("scroll to end", () => scroll.ScrollToEnd()); AddStep("scroll to end", () => scroll.ScrollToEnd());
@ -94,9 +95,9 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestSetNullBeatmapWhileLoading() public void TestSetNullBeatmapWhileLoading()
{ {
TestUpdateableBeatmapSetCover updateableCover = null; TestUpdateableOnlineBeatmapSetCover updateableCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover
{ {
BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet, BeatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -111,10 +112,10 @@ namespace osu.Game.Tests.Visual.UserInterface
[Test] [Test]
public void TestCoverChangeOnNewBeatmap() public void TestCoverChangeOnNewBeatmap()
{ {
TestUpdateableBeatmapSetCover updateableCover = null; TestUpdateableOnlineBeatmapSetCover updateableCover = null;
BeatmapSetCover initialCover = null; OnlineBeatmapSetCover initialCover = null;
AddStep("setup cover", () => Child = updateableCover = new TestUpdateableBeatmapSetCover(0) AddStep("setup cover", () => Child = updateableCover = new TestUpdateableOnlineBeatmapSetCover(0)
{ {
BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"), BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg"),
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
@ -122,38 +123,38 @@ namespace osu.Game.Tests.Visual.UserInterface
Alpha = 0.4f Alpha = 0.4f
}); });
AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Any()); AddUntilStep("cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Any());
AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<BeatmapSetCover>().Single()); AddStep("store initial cover", () => initialCover = updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Single());
AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1); AddUntilStep("wait for fade complete", () => initialCover.Alpha == 1);
AddStep("switch beatmap", AddStep("switch beatmap",
() => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg")); () => updateableCover.BeatmapSet = createBeatmapWithCover("https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg"));
AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<BeatmapSetCover>().Except(new[] { initialCover }).Any()); AddUntilStep("new cover loaded", () => updateableCover.ChildrenOfType<OnlineBeatmapSetCover>().Except(new[] { initialCover }).Any());
} }
private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo private static BeatmapSetInfo createBeatmapWithCover(string coverUrl) => new BeatmapSetInfo
{ {
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = new APIBeatmapSet
{ {
Covers = new BeatmapSetOnlineCovers { Cover = coverUrl } Covers = new BeatmapSetOnlineCovers { Cover = coverUrl }
} }
}; };
private class TestUpdateableBeatmapSetCover : UpdateableBeatmapSetCover private class TestUpdateableOnlineBeatmapSetCover : UpdateableOnlineBeatmapSetCover
{ {
private readonly int loadDelay; private readonly int loadDelay;
public TestUpdateableBeatmapSetCover(int loadDelay = 10000) public TestUpdateableOnlineBeatmapSetCover(int loadDelay = 10000)
{ {
this.loadDelay = loadDelay; this.loadDelay = loadDelay;
} }
protected override Drawable CreateDrawable(BeatmapSetInfo model) protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{ {
if (model == null) if (model == null)
return null; return null;
return new TestBeatmapSetCover(model, loadDelay) return new TestOnlineBeatmapSetCover(model, loadDelay)
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -163,11 +164,11 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
} }
private class TestBeatmapSetCover : BeatmapSetCover private class TestOnlineBeatmapSetCover : OnlineBeatmapSetCover
{ {
private readonly int loadDelay; private readonly int loadDelay;
public TestBeatmapSetCover(BeatmapSetInfo set, int loadDelay) public TestOnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, int loadDelay)
: base(set) : base(set)
{ {
this.loadDelay = loadDelay; this.loadDelay = loadDelay;

View File

@ -0,0 +1,69 @@
// 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.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual.UserInterface
{
public abstract class ThemeComparisonTestScene : OsuGridTestScene
{
protected ThemeComparisonTestScene()
: base(1, 2)
{
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Cell(0, 0).AddRange(new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeafoam
},
CreateContent()
});
}
protected void CreateThemedContent(OverlayColourScheme colourScheme)
{
var colourProvider = new OverlayColourProvider(colourScheme);
Cell(0, 1).Clear();
Cell(0, 1).Add(new DependencyProvidingContainer
{
RelativeSizeAxes = Axes.Both,
CachedDependencies = new (Type, object)[]
{
(typeof(OverlayColourProvider), colourProvider)
},
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background4
},
CreateContent()
}
});
}
protected abstract Drawable CreateContent();
[Test]
public void TestAllColourSchemes()
{
foreach (var scheme in Enum.GetValues(typeof(OverlayColourScheme)).Cast<OverlayColourScheme>())
AddStep($"set {scheme} scheme", () => CreateThemedContent(scheme));
}
}
}

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps.Drawables;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -21,7 +20,8 @@ namespace osu.Game.Tournament.Components
{ {
public class TournamentBeatmapPanel : CompositeDrawable public class TournamentBeatmapPanel : CompositeDrawable
{ {
public readonly BeatmapInfo BeatmapInfo; public readonly IBeatmapInfo BeatmapInfo;
private readonly string mod; private readonly string mod;
private const float horizontal_padding = 10; private const float horizontal_padding = 10;
@ -32,12 +32,13 @@ namespace osu.Game.Tournament.Components
private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>(); private readonly Bindable<TournamentMatch> currentMatch = new Bindable<TournamentMatch>();
private Box flash; private Box flash;
public TournamentBeatmapPanel(BeatmapInfo beatmapInfo, string mod = null) public TournamentBeatmapPanel(IBeatmapInfo beatmapInfo, string mod = null)
{ {
if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo)); if (beatmapInfo == null) throw new ArgumentNullException(nameof(beatmapInfo));
BeatmapInfo = beatmapInfo; BeatmapInfo = beatmapInfo;
this.mod = mod; this.mod = mod;
Width = 400; Width = 400;
Height = HEIGHT; Height = HEIGHT;
} }
@ -57,11 +58,11 @@ namespace osu.Game.Tournament.Components
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Black, Colour = Color4.Black,
}, },
new UpdateableBeatmapSetCover new UpdateableOnlineBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = OsuColour.Gray(0.5f), Colour = OsuColour.Gray(0.5f),
BeatmapSet = BeatmapInfo.BeatmapSet, BeatmapSet = BeatmapInfo.BeatmapSet as IBeatmapSetOnlineInfo,
}, },
new FillFlowContainer new FillFlowContainer
{ {
@ -74,9 +75,7 @@ namespace osu.Game.Tournament.Components
{ {
new TournamentSpriteText new TournamentSpriteText
{ {
Text = new RomanisableString( Text = BeatmapInfo.GetDisplayTitleRomanisable(false),
$"{BeatmapInfo.Metadata.ArtistUnicode ?? BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.TitleUnicode ?? BeatmapInfo.Metadata.Title}",
$"{BeatmapInfo.Metadata.Artist} - {BeatmapInfo.Metadata.Title}"),
Font = OsuFont.Torus.With(weight: FontWeight.Bold), Font = OsuFont.Torus.With(weight: FontWeight.Bold),
}, },
new FillFlowContainer new FillFlowContainer
@ -93,7 +92,7 @@ namespace osu.Game.Tournament.Components
}, },
new TournamentSpriteText new TournamentSpriteText
{ {
Text = BeatmapInfo.Metadata.AuthorString, Text = BeatmapInfo.Metadata?.Author,
Padding = new MarginPadding { Right = 20 }, Padding = new MarginPadding { Right = 20 },
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
}, },
@ -105,7 +104,7 @@ namespace osu.Game.Tournament.Components
}, },
new TournamentSpriteText new TournamentSpriteText
{ {
Text = BeatmapInfo.Version, Text = BeatmapInfo.DifficultyName,
Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14) Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 14)
}, },
} }
@ -149,7 +148,7 @@ namespace osu.Game.Tournament.Components
private void updateState() private void updateState()
{ {
var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineBeatmapID); var found = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == BeatmapInfo.OnlineID);
bool doFlash = found != choice; bool doFlash = found != choice;
choice = found; choice = found;

View File

@ -147,11 +147,11 @@ namespace osu.Game.Tournament.Screens.MapPool
if (map != null) if (map != null)
{ {
if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineBeatmapID != null) if (e.Button == MouseButton.Left && map.BeatmapInfo.OnlineID > 0)
addForBeatmap(map.BeatmapInfo.OnlineBeatmapID.Value); addForBeatmap(map.BeatmapInfo.OnlineID);
else else
{ {
var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineBeatmapID); var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.BeatmapInfo.OnlineID);
if (existing != null) if (existing != null)
{ {

View File

@ -178,7 +178,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID #region Implementation of IHasOnlineID
public int? OnlineID => OnlineBeatmapID; public int OnlineID => OnlineBeatmapID ?? -1;
#endregion #endregion

View File

@ -16,14 +16,19 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields. /// A user-presentable display title representing this beatmap, with localisation handling for potentially romanisable fields.
/// </summary> /// </summary>
public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo) public static RomanisableString GetDisplayTitleRomanisable(this IBeatmapInfo beatmapInfo, bool includeDifficultyName = true)
{ {
var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable(); var metadata = getClosestMetadata(beatmapInfo).GetDisplayTitleRomanisable();
var versionString = getVersionString(beatmapInfo);
if (includeDifficultyName)
{
var versionString = getVersionString(beatmapInfo);
return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim()); return new RomanisableString($"{metadata.GetPreferred(true)} {versionString}".Trim(), $"{metadata.GetPreferred(false)} {versionString}".Trim());
} }
return new RomanisableString($"{metadata.GetPreferred(true)}".Trim(), $"{metadata.GetPreferred(false)}".Trim());
}
public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[] public static string[] GetSearchableTerms(this IBeatmapInfo beatmapInfo) => new[]
{ {
beatmapInfo.DifficultyName beatmapInfo.DifficultyName

View File

@ -29,7 +29,7 @@ namespace osu.Game.Beatmaps
/// Handles general operations related to global beatmap management. /// Handles general operations related to global beatmap management.
/// </summary> /// </summary>
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, ICanAcceptFiles, IWorkingBeatmapCache, IDisposable public class BeatmapManager : IModelDownloader<BeatmapSetInfo>, IModelManager<BeatmapSetInfo>, IModelFileManager<BeatmapSetInfo, BeatmapSetFileInfo>, IWorkingBeatmapCache, IDisposable
{ {
private readonly BeatmapModelManager beatmapModelManager; private readonly BeatmapModelManager beatmapModelManager;
private readonly BeatmapModelDownloader beatmapModelDownloader; private readonly BeatmapModelDownloader beatmapModelDownloader;

View File

@ -123,15 +123,15 @@ namespace osu.Game.Beatmaps
// check if a set already exists with the same online id, delete if it does. // check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null) if (beatmapSet.OnlineBeatmapSetID != null)
{ {
var existingOnlineId = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID); var existingSetWithSameOnlineID = beatmaps.ConsumableItems.FirstOrDefault(b => b.OnlineBeatmapSetID == beatmapSet.OnlineBeatmapSetID);
if (existingOnlineId != null) if (existingSetWithSameOnlineID != null)
{ {
Delete(existingOnlineId); Delete(existingSetWithSameOnlineID);
// in order to avoid a unique key constraint, immediately remove the online ID from the previous set. // in order to avoid a unique key constraint, immediately remove the online ID from the previous set.
existingOnlineId.OnlineBeatmapSetID = null; existingSetWithSameOnlineID.OnlineBeatmapSetID = null;
foreach (var b in existingOnlineId.Beatmaps) foreach (var b in existingSetWithSameOnlineID.Beatmaps)
b.OnlineBeatmapID = null; b.OnlineBeatmapID = null;
LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted."); LogForModel(beatmapSet, $"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been deleted.");

View File

@ -83,9 +83,9 @@ namespace osu.Game.Beatmaps
if (res != null) if (res != null)
{ {
beatmapInfo.Status = res.Status; beatmapInfo.Status = res.Status;
beatmapInfo.BeatmapSet.Status = res.BeatmapSet.Status; beatmapInfo.BeatmapSet.Status = res.BeatmapSet?.Status ?? BeatmapSetOnlineStatus.None;
beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID; beatmapInfo.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmapInfo.OnlineBeatmapID = res.OnlineBeatmapID; beatmapInfo.OnlineBeatmapID = res.OnlineID;
if (beatmapInfo.Metadata != null) if (beatmapInfo.Metadata != null)
beatmapInfo.Metadata.AuthorID = res.AuthorID; beatmapInfo.Metadata.AuthorID = res.AuthorID;
@ -93,7 +93,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.BeatmapSet.Metadata != null) if (beatmapInfo.BeatmapSet.Metadata != null)
beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID; beatmapInfo.BeatmapSet.Metadata.AuthorID = res.AuthorID;
logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}."); logForModel(set, $"Online retrieval mapped {beatmapInfo} to {res.OnlineBeatmapSetID} / {res.OnlineID}.");
} }
} }
catch (Exception e) catch (Exception e)

View File

@ -6,13 +6,15 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using Newtonsoft.Json;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Database; using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
[ExcludeFromDynamicCompile] [ExcludeFromDynamicCompile]
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>, IBeatmapSetInfo, IBeatmapSetOnlineInfo
{ {
public int ID { get; set; } public int ID { get; set; }
@ -26,8 +28,6 @@ namespace osu.Game.Beatmaps
public DateTimeOffset DateAdded { get; set; } public DateTimeOffset DateAdded { get; set; }
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
public BeatmapMetadata Metadata { get; set; } public BeatmapMetadata Metadata { get; set; }
public List<BeatmapInfo> Beatmaps { get; set; } public List<BeatmapInfo> Beatmaps { get; set; }
@ -36,7 +36,7 @@ namespace osu.Game.Beatmaps
public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>(); public List<BeatmapSetFileInfo> Files { get; set; } = new List<BeatmapSetFileInfo>();
[NotMapped] [NotMapped]
public BeatmapSetOnlineInfo OnlineInfo { get; set; } public APIBeatmapSet OnlineInfo { get; set; }
[NotMapped] [NotMapped]
public BeatmapSetMetrics Metrics { get; set; } public BeatmapSetMetrics Metrics { get; set; }
@ -91,7 +91,7 @@ namespace osu.Game.Beatmaps
#region Implementation of IHasOnlineID #region Implementation of IHasOnlineID
public int? OnlineID => OnlineBeatmapSetID; public int OnlineID => OnlineBeatmapSetID ?? -1;
#endregion #endregion
@ -102,5 +102,141 @@ namespace osu.Game.Beatmaps
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files; IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => Files;
#endregion #endregion
#region Delegation for IBeatmapSetOnlineInfo
[NotMapped]
[JsonIgnore]
public DateTimeOffset Submitted
{
get => OnlineInfo.Submitted;
set => OnlineInfo.Submitted = value;
}
[NotMapped]
[JsonIgnore]
public DateTimeOffset? Ranked
{
get => OnlineInfo.Ranked;
set => OnlineInfo.Ranked = value;
}
[NotMapped]
[JsonIgnore]
public DateTimeOffset? LastUpdated
{
get => OnlineInfo.LastUpdated;
set => OnlineInfo.LastUpdated = value;
}
[NotMapped]
[JsonIgnore]
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
[NotMapped]
[JsonIgnore]
public bool HasExplicitContent
{
get => OnlineInfo.HasExplicitContent;
set => OnlineInfo.HasExplicitContent = value;
}
[NotMapped]
[JsonIgnore]
public bool HasVideo
{
get => OnlineInfo.HasVideo;
set => OnlineInfo.HasVideo = value;
}
[NotMapped]
[JsonIgnore]
public bool HasStoryboard
{
get => OnlineInfo.HasStoryboard;
set => OnlineInfo.HasStoryboard = value;
}
[NotMapped]
[JsonIgnore]
public BeatmapSetOnlineCovers Covers
{
get => OnlineInfo.Covers;
set => OnlineInfo.Covers = value;
}
[NotMapped]
[JsonIgnore]
public string Preview
{
get => OnlineInfo.Preview;
set => OnlineInfo.Preview = value;
}
[NotMapped]
[JsonIgnore]
public double BPM
{
get => OnlineInfo.BPM;
set => OnlineInfo.BPM = value;
}
[NotMapped]
[JsonIgnore]
public int PlayCount
{
get => OnlineInfo.PlayCount;
set => OnlineInfo.PlayCount = value;
}
[NotMapped]
[JsonIgnore]
public int FavouriteCount
{
get => OnlineInfo.FavouriteCount;
set => OnlineInfo.FavouriteCount = value;
}
[NotMapped]
[JsonIgnore]
public bool HasFavourited
{
get => OnlineInfo.HasFavourited;
set => OnlineInfo.HasFavourited = value;
}
[NotMapped]
[JsonIgnore]
public BeatmapSetOnlineAvailability Availability
{
get => OnlineInfo.Availability;
set => OnlineInfo.Availability = value;
}
[NotMapped]
[JsonIgnore]
public BeatmapSetOnlineGenre Genre
{
get => OnlineInfo.Genre;
set => OnlineInfo.Genre = value;
}
[NotMapped]
[JsonIgnore]
public BeatmapSetOnlineLanguage Language
{
get => OnlineInfo.Language;
set => OnlineInfo.Language = value;
}
[NotMapped]
[JsonIgnore]
public int? TrackId
{
get => OnlineInfo.TrackId;
set => OnlineInfo.TrackId = value;
}
#endregion
} }
} }

View File

@ -0,0 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
namespace osu.Game.Beatmaps
{
public struct BeatmapSetOnlineAvailability
{
[JsonProperty(@"download_disabled")]
public bool DownloadDisabled { get; set; }
[JsonProperty(@"more_information")]
public string ExternalLink { get; set; }
}
}

View File

@ -0,0 +1,25 @@
// 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;
namespace osu.Game.Beatmaps
{
public struct BeatmapSetOnlineCovers
{
public string CoverLowRes { get; set; }
[JsonProperty(@"cover@2x")]
public string Cover { get; set; }
public string CardLowRes { get; set; }
[JsonProperty(@"card@2x")]
public string Card { get; set; }
public string ListLowRes { get; set; }
[JsonProperty(@"list@2x")]
public string List { get; set; }
}
}

View File

@ -0,0 +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.
namespace osu.Game.Beatmaps
{
public struct BeatmapSetOnlineGenre
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -0,0 +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.
namespace osu.Game.Beatmaps
{
public struct BeatmapSetOnlineLanguage
{
public int Id { get; set; }
public string Name { get; set; }
}
}

View File

@ -9,12 +9,12 @@ using osu.Framework.Graphics.Textures;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
[LongRunningLoad] [LongRunningLoad]
public class BeatmapSetCover : Sprite public class OnlineBeatmapSetCover : Sprite
{ {
private readonly BeatmapSetInfo set; private readonly IBeatmapSetOnlineInfo set;
private readonly BeatmapSetCoverType type; private readonly BeatmapSetCoverType type;
public BeatmapSetCover(BeatmapSetInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover) public OnlineBeatmapSetCover(IBeatmapSetOnlineInfo set, BeatmapSetCoverType type = BeatmapSetCoverType.Cover)
{ {
if (set == null) if (set == null)
throw new ArgumentNullException(nameof(set)); throw new ArgumentNullException(nameof(set));
@ -31,15 +31,15 @@ namespace osu.Game.Beatmaps.Drawables
switch (type) switch (type)
{ {
case BeatmapSetCoverType.Cover: case BeatmapSetCoverType.Cover:
resource = set.OnlineInfo.Covers.Cover; resource = set.Covers.Cover;
break; break;
case BeatmapSetCoverType.Card: case BeatmapSetCoverType.Card:
resource = set.OnlineInfo.Covers.Card; resource = set.Covers.Card;
break; break;
case BeatmapSetCoverType.List: case BeatmapSetCoverType.List:
resource = set.OnlineInfo.Covers.List; resource = set.Covers.List;
break; break;
} }

View File

@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps.Drawables
{ {
// prefer online cover where available. // prefer online cover where available.
if (model?.BeatmapSet?.OnlineInfo != null) if (model?.BeatmapSet?.OnlineInfo != null)
return new BeatmapSetCover(model.BeatmapSet, beatmapSetCoverType); return new OnlineBeatmapSetCover(model.BeatmapSet, beatmapSetCoverType);
return model?.ID > 0 return model?.ID > 0
? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model)) ? new BeatmapBackgroundSprite(beatmaps.GetWorkingBeatmap(model))

View File

@ -9,11 +9,11 @@ using osu.Game.Graphics;
namespace osu.Game.Beatmaps.Drawables namespace osu.Game.Beatmaps.Drawables
{ {
public class UpdateableBeatmapSetCover : ModelBackedDrawable<BeatmapSetInfo> public class UpdateableOnlineBeatmapSetCover : ModelBackedDrawable<IBeatmapSetOnlineInfo>
{ {
private readonly BeatmapSetCoverType coverType; private readonly BeatmapSetCoverType coverType;
public BeatmapSetInfo BeatmapSet public IBeatmapSetOnlineInfo BeatmapSet
{ {
get => Model; get => Model;
set => Model = value; set => Model = value;
@ -25,7 +25,7 @@ namespace osu.Game.Beatmaps.Drawables
set => base.Masking = value; set => base.Masking = value;
} }
public UpdateableBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover) public UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType coverType = BeatmapSetCoverType.Cover)
{ {
this.coverType = coverType; this.coverType = coverType;
@ -43,12 +43,12 @@ namespace osu.Game.Beatmaps.Drawables
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad) protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
=> new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad); => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad);
protected override Drawable CreateDrawable(BeatmapSetInfo model) protected override Drawable CreateDrawable(IBeatmapSetOnlineInfo model)
{ {
if (model == null) if (model == null)
return null; return null;
return new BeatmapSetCover(model, coverType) return new OnlineBeatmapSetCover(model, coverType)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -1,139 +1,101 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using Newtonsoft.Json;
#nullable enable
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
/// <summary> /// <summary>
/// Beatmap set info retrieved for previewing locally without having the set downloaded. /// Beatmap set info retrieved for previewing locally without having the set downloaded.
/// </summary> /// </summary>
public class BeatmapSetOnlineInfo public interface IBeatmapSetOnlineInfo
{ {
/// <summary> /// <summary>
/// The date this beatmap set was submitted to the online listing. /// The date this beatmap set was submitted to the online listing.
/// </summary> /// </summary>
public DateTimeOffset Submitted { get; set; } DateTimeOffset Submitted { get; set; }
/// <summary> /// <summary>
/// The date this beatmap set was ranked. /// The date this beatmap set was ranked.
/// </summary> /// </summary>
public DateTimeOffset? Ranked { get; set; } DateTimeOffset? Ranked { get; set; }
/// <summary> /// <summary>
/// The date this beatmap set was last updated. /// The date this beatmap set was last updated.
/// </summary> /// </summary>
public DateTimeOffset? LastUpdated { get; set; } DateTimeOffset? LastUpdated { get; set; }
/// <summary> /// <summary>
/// The status of this beatmap set. /// The status of this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineStatus Status { get; set; } BeatmapSetOnlineStatus Status { get; set; }
/// <summary> /// <summary>
/// Whether or not this beatmap set has explicit content. /// Whether or not this beatmap set has explicit content.
/// </summary> /// </summary>
public bool HasExplicitContent { get; set; } bool HasExplicitContent { get; set; }
/// <summary> /// <summary>
/// Whether or not this beatmap set has a background video. /// Whether or not this beatmap set has a background video.
/// </summary> /// </summary>
public bool HasVideo { get; set; } bool HasVideo { get; set; }
/// <summary> /// <summary>
/// Whether or not this beatmap set has a storyboard. /// Whether or not this beatmap set has a storyboard.
/// </summary> /// </summary>
public bool HasStoryboard { get; set; } bool HasStoryboard { get; set; }
/// <summary> /// <summary>
/// The different sizes of cover art for this beatmap set. /// The different sizes of cover art for this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineCovers Covers { get; set; } BeatmapSetOnlineCovers Covers { get; set; }
/// <summary> /// <summary>
/// A small sample clip of this beatmap set's song. /// A small sample clip of this beatmap set's song.
/// </summary> /// </summary>
public string Preview { get; set; } string Preview { get; set; }
/// <summary> /// <summary>
/// The beats per minute of this beatmap set's song. /// The beats per minute of this beatmap set's song.
/// </summary> /// </summary>
public double BPM { get; set; } double BPM { get; set; }
/// <summary> /// <summary>
/// The amount of plays this beatmap set has. /// The amount of plays this beatmap set has.
/// </summary> /// </summary>
public int PlayCount { get; set; } int PlayCount { get; set; }
/// <summary> /// <summary>
/// The amount of people who have favourited this beatmap set. /// The amount of people who have favourited this beatmap set.
/// </summary> /// </summary>
public int FavouriteCount { get; set; } int FavouriteCount { get; set; }
/// <summary> /// <summary>
/// Whether this beatmap set has been favourited by the current user. /// Whether this beatmap set has been favourited by the current user.
/// </summary> /// </summary>
public bool HasFavourited { get; set; } bool HasFavourited { get; set; }
/// <summary> /// <summary>
/// The availability of this beatmap set. /// The availability of this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineAvailability Availability { get; set; } BeatmapSetOnlineAvailability Availability { get; set; }
/// <summary> /// <summary>
/// The song genre of this beatmap set. /// The song genre of this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineGenre Genre { get; set; } BeatmapSetOnlineGenre Genre { get; set; }
/// <summary> /// <summary>
/// The song language of this beatmap set. /// The song language of this beatmap set.
/// </summary> /// </summary>
public BeatmapSetOnlineLanguage Language { get; set; } BeatmapSetOnlineLanguage Language { get; set; }
/// <summary> /// <summary>
/// The track ID of this beatmap set. /// The track ID of this beatmap set.
/// Non-null only if the track is linked to a featured artist track entry. /// Non-null only if the track is linked to a featured artist track entry.
/// </summary> /// </summary>
public int? TrackId { get; set; } int? TrackId { get; set; }
}
public class BeatmapSetOnlineGenre
{
public int Id { get; set; }
public string Name { get; set; }
}
public class BeatmapSetOnlineLanguage
{
public int Id { get; set; }
public string Name { get; set; }
}
public class BeatmapSetOnlineCovers
{
public string CoverLowRes { get; set; }
[JsonProperty(@"cover@2x")]
public string Cover { get; set; }
public string CardLowRes { get; set; }
[JsonProperty(@"card@2x")]
public string Card { get; set; }
public string ListLowRes { get; set; }
[JsonProperty(@"list@2x")]
public string List { get; set; }
}
public class BeatmapSetOnlineAvailability
{
[JsonProperty(@"download_disabled")]
public bool DownloadDisabled { get; set; }
[JsonProperty(@"more_information")]
public string ExternalLink { get; set; }
} }
} }

View File

@ -181,7 +181,11 @@ namespace osu.Game.Collections
MaxHeight = 200; MaxHeight = 200;
} }
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
} }
protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem

View File

@ -6,10 +6,13 @@ using System.Diagnostics;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking; using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
@ -185,20 +188,52 @@ namespace osu.Game.Configuration
return new TrackedSettings return new TrackedSettings
{ {
new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), new TrackedSetting<bool>(OsuSetting.MouseDisableButtons, disabledState => new SettingDescription(
new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), rawValue: !disabledState,
new TrackedSetting<ScalingMode>(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), name: GlobalActionKeyBindingStrings.ToggleGameplayMouseButtons,
new TrackedSetting<int>(OsuSetting.Skin, m => value: disabledState ? CommonStrings.Disabled.ToLower() : CommonStrings.Enabled.ToLower(),
shortcut: LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))
),
new TrackedSetting<HUDVisibilityMode>(OsuSetting.HUDVisibilityMode, visibilityMode => new SettingDescription(
rawValue: visibilityMode,
name: GameplaySettingsStrings.HUDVisibilityMode,
value: visibilityMode.GetLocalisableDescription(),
shortcut: new TranslatableString(@"_", @"{0}: {1} {2}: {3}",
GlobalActionKeyBindingStrings.ToggleInGameInterface,
LookupKeyBindings(GlobalAction.ToggleInGameInterface),
GlobalActionKeyBindingStrings.HoldForHUD,
LookupKeyBindings(GlobalAction.HoldForHUD)))
),
new TrackedSetting<ScalingMode>(OsuSetting.Scaling, scalingMode => new SettingDescription(
rawValue: scalingMode,
name: GraphicsSettingsStrings.ScreenScaling,
value: scalingMode.GetLocalisableDescription()
)
),
new TrackedSetting<int>(OsuSetting.Skin, skin =>
{ {
string skinName = LookupSkinName(m) ?? string.Empty; string skinName = LookupSkinName(skin) ?? string.Empty;
return new SettingDescription(skinName, "skin", skinName, $"random: {LookupKeyBindings(GlobalAction.RandomSkin)}");
}) return new SettingDescription(
rawValue: skinName,
name: SkinSettingsStrings.SkinSectionHeader,
value: skinName,
shortcut: $"{GlobalActionKeyBindingStrings.RandomSkin}: {LookupKeyBindings(GlobalAction.RandomSkin)}"
);
}),
new TrackedSetting<float>(OsuSetting.UIScale, scale => new SettingDescription(
rawValue: scale,
name: GraphicsSettingsStrings.UIScaling,
value: $"{scale:N2}x"
// TODO: implement lookup for framework platform key bindings
)
),
}; };
} }
public Func<int, string> LookupSkinName { private get; set; } public Func<int, string> LookupSkinName { private get; set; }
public Func<GlobalAction, string> LookupKeyBindings { get; set; } public Func<GlobalAction, LocalisableString> LookupKeyBindings { get; set; }
} }
// IMPORTANT: These are used in user configuration files. // IMPORTANT: These are used in user configuration files.

View File

@ -30,7 +30,7 @@ namespace osu.Game.Database
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
/// <typeparam name="TFileModel">The associated file join type.</typeparam> /// <typeparam name="TFileModel">The associated file join type.</typeparam>
public abstract class ArchiveModelManager<TModel, TFileModel> : ICanAcceptFiles, IModelManager<TModel>, IModelFileManager<TModel, TFileModel> public abstract class ArchiveModelManager<TModel, TFileModel> : IModelManager<TModel>, IModelFileManager<TModel, TFileModel>
where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete where TModel : class, IHasFiles<TFileModel>, IHasPrimaryKey, ISoftDelete
where TFileModel : class, INamedFileInfo, new() where TFileModel : class, INamedFileInfo, new()
{ {

View File

@ -8,8 +8,12 @@ namespace osu.Game.Database
public interface IHasOnlineID public interface IHasOnlineID
{ {
/// <summary> /// <summary>
/// The server-side ID representing this instance, if one exists. /// The server-side ID representing this instance, if one exists. Any value 0 or less denotes a missing ID.
/// </summary> /// </summary>
int? OnlineID { get; } /// <remarks>
/// Generally we use -1 when specifying "missing" in code, but values of 0 are also considered missing as the online source
/// is generally a MySQL autoincrement value, which can never be 0.
/// </remarks>
int OnlineID { get; }
} }
} }

View File

@ -13,21 +13,9 @@ namespace osu.Game.Database
/// A class which handles importing of associated models to the game store. /// A class which handles importing of associated models to the game store.
/// </summary> /// </summary>
/// <typeparam name="TModel">The model type.</typeparam> /// <typeparam name="TModel">The model type.</typeparam>
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel> public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
where TModel : class where TModel : class
{ {
/// <summary>
/// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
/// </summary>
/// <remarks>
/// This will be treated as a low priority import if more than one path is specified; use <see cref="ArchiveModelManager{TModel,TFileModel}.Import(osu.Game.Database.ImportTask[])"/> to always import at standard priority.
/// This will post notifications tracking progress.
/// </remarks>
/// <param name="paths">One or more archive locations on disk.</param>
Task Import(params string[] paths);
Task Import(params ImportTask[] tasks);
Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks); Task<IEnumerable<ILive<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
/// <summary> /// <summary>

View File

@ -4,6 +4,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
#nullable enable
namespace osu.Game.Database namespace osu.Game.Database
{ {
public interface IPostImports<out TModel> public interface IPostImports<out TModel>
@ -12,6 +14,6 @@ namespace osu.Game.Database
/// <summary> /// <summary>
/// Fired when the user requests to view the resulting import. /// Fired when the user requests to view the resulting import.
/// </summary> /// </summary>
public Action<IEnumerable<ILive<TModel>>> PostImport { set; } public Action<IEnumerable<ILive<TModel>>>? PostImport { set; }
} }
} }

View File

@ -2,12 +2,14 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Linq;
using System.Threading; using System.Threading;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Development; using osu.Framework.Development;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Statistics; using osu.Framework.Statistics;
using osu.Game.Models;
using Realms; using Realms;
#nullable enable #nullable enable
@ -26,7 +28,12 @@ namespace osu.Game.Database
/// </summary> /// </summary>
public readonly string Filename; public readonly string Filename;
private const int schema_version = 6; /// <summary>
/// Version history:
/// 6 First tracked version (~20211018)
/// 7 Changed OnlineID fields to non-nullable to add indexing support (20211018)
/// </summary>
private const int schema_version = 7;
/// <summary> /// <summary>
/// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods. /// Lock object which is held during <see cref="BlockAllOperations"/> sections, blocking context creation during blocking periods.
@ -70,6 +77,27 @@ namespace osu.Game.Database
if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal)) if (!Filename.EndsWith(realm_extension, StringComparison.Ordinal))
Filename += realm_extension; Filename += realm_extension;
cleanupPendingDeletions();
}
private void cleanupPendingDeletions()
{
using (var realm = CreateContext())
using (var transaction = realm.BeginWrite())
{
var pendingDeleteSets = realm.All<RealmBeatmapSet>().Where(s => s.DeletePending);
foreach (var s in pendingDeleteSets)
{
foreach (var b in s.Beatmaps)
realm.Remove(b);
realm.Remove(s);
}
transaction.Commit();
}
} }
/// <summary> /// <summary>
@ -120,6 +148,36 @@ namespace osu.Game.Database
private void onMigration(Migration migration, ulong lastSchemaVersion) private void onMigration(Migration migration, ulong lastSchemaVersion)
{ {
if (lastSchemaVersion < 7)
{
convertOnlineIDs<RealmBeatmap>();
convertOnlineIDs<RealmBeatmapSet>();
convertOnlineIDs<RealmRuleset>();
void convertOnlineIDs<T>() where T : RealmObject
{
var className = typeof(T).Name.Replace(@"Realm", string.Empty);
// version was not bumped when the beatmap/ruleset models were added
// therefore we must manually check for their presence to avoid throwing on the `DynamicApi` calls.
if (!migration.OldRealm.Schema.TryFindObjectSchema(className, out _))
return;
var oldItems = migration.OldRealm.DynamicApi.All(className);
var newItems = migration.NewRealm.DynamicApi.All(className);
int itemCount = newItems.Count();
for (int i = 0; i < itemCount; i++)
{
var oldItem = oldItems.ElementAt(i);
var newItem = newItems.ElementAt(i);
long? nullableOnlineID = oldItem?.OnlineID;
newItem.OnlineID = (int)(nullableOnlineID ?? -1);
}
}
}
} }
/// <summary> /// <summary>

View File

@ -235,11 +235,18 @@ namespace osu.Game.Graphics
/// </summary> /// </summary>
public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc"); public readonly Color4 Blue3 = Color4Extensions.FromHex(@"3399cc");
public readonly Color4 Lime0 = Color4Extensions.FromHex(@"ccff99");
/// <summary> /// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>. /// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>.
/// </summary> /// </summary>
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66"); public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
/// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour3"/>.
/// </summary>
public readonly Color4 Lime3 = Color4Extensions.FromHex(@"7fcc33");
/// <summary> /// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Orange"/>'s <see cref="OverlayColourProvider.Colour1"/>. /// Equivalent to <see cref="OverlayColourProvider.Orange"/>'s <see cref="OverlayColourProvider.Colour1"/>.
/// </summary> /// </summary>

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
@ -8,6 +10,7 @@ using osu.Framework.Platform;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osuTK.Input; using osuTK.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Overlays;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -42,13 +45,13 @@ namespace osu.Game.Graphics.UserInterface
} }
[Resolved] [Resolved]
private GameHost host { get; set; } private GameHost? host { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load() private void load(OverlayColourProvider? colourProvider)
{ {
BackgroundUnfocused = new Color4(10, 10, 10, 255); BackgroundUnfocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
BackgroundFocused = new Color4(10, 10, 10, 255); BackgroundFocused = colourProvider?.Background5 ?? new Color4(10, 10, 10, 255);
} }
// We may not be focused yet, but we need to handle keyboard input to be able to request focus // We may not be focused yet, but we need to handle keyboard input to be able to request focus

View File

@ -21,44 +21,17 @@ using osuTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class OsuDropdown<T> : Dropdown<T>, IHasAccentColour public class OsuDropdown<T> : Dropdown<T>
{ {
private const float corner_radius = 5; private const float corner_radius = 5;
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
updateAccentColour();
}
}
[BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, OsuColour colours)
{
if (accentColour == default)
accentColour = colourProvider?.Light4 ?? colours.PinkDarker;
updateAccentColour();
}
private void updateAccentColour()
{
if (Header is IHasAccentColour header) header.AccentColour = accentColour;
if (Menu is IHasAccentColour menu) menu.AccentColour = accentColour;
}
protected override DropdownHeader CreateHeader() => new OsuDropdownHeader(); protected override DropdownHeader CreateHeader() => new OsuDropdownHeader();
protected override DropdownMenu CreateMenu() => new OsuDropdownMenu(); protected override DropdownMenu CreateMenu() => new OsuDropdownMenu();
#region OsuDropdownMenu #region OsuDropdownMenu
protected class OsuDropdownMenu : DropdownMenu, IHasAccentColour protected class OsuDropdownMenu : DropdownMenu
{ {
public override bool HandleNonPositionalInput => State == MenuState.Open; public override bool HandleNonPositionalInput => State == MenuState.Open;
@ -78,9 +51,11 @@ namespace osu.Game.Graphics.UserInterface
} }
[BackgroundDependencyLoader(true)] [BackgroundDependencyLoader(true)]
private void load(OverlayColourProvider? colourProvider, AudioManager audio) private void load(OverlayColourProvider? colourProvider, OsuColour colours, AudioManager audio)
{ {
BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f); BackgroundColour = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
HoverColour = colourProvider?.Light4 ?? colours.PinkDarker;
SelectionColour = colourProvider?.Background3 ?? colours.PinkDarker.Opacity(0.5f);
sampleOpen = audio.Samples.Get(@"UI/dropdown-open"); sampleOpen = audio.Samples.Get(@"UI/dropdown-open");
sampleClose = audio.Samples.Get(@"UI/dropdown-close"); sampleClose = audio.Samples.Get(@"UI/dropdown-close");
@ -121,57 +96,77 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
private Color4 accentColour; private Color4 hoverColour;
public Color4 AccentColour public Color4 HoverColour
{ {
get => accentColour; get => hoverColour;
set set
{ {
accentColour = value; hoverColour = value;
foreach (var c in Children.OfType<IHasAccentColour>()) foreach (var c in Children.OfType<DrawableOsuDropdownMenuItem>())
c.AccentColour = value; c.BackgroundColourHover = value;
}
}
private Color4 selectionColour;
public Color4 SelectionColour
{
get => selectionColour;
set
{
selectionColour = value;
foreach (var c in Children.OfType<DrawableOsuDropdownMenuItem>())
c.BackgroundColourSelected = value;
} }
} }
protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical); protected override Menu CreateSubMenu() => new OsuMenu(Direction.Vertical);
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item) { AccentColour = accentColour }; protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuDropdownMenuItem(item)
{
BackgroundColourHover = HoverColour,
BackgroundColourSelected = SelectionColour
};
protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction); protected override ScrollContainer<Drawable> CreateScrollContainer(Direction direction) => new OsuScrollContainer(direction);
#region DrawableOsuDropdownMenuItem #region DrawableOsuDropdownMenuItem
public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem, IHasAccentColour public class DrawableOsuDropdownMenuItem : DrawableDropdownMenuItem
{ {
// IsHovered is used // IsHovered is used
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
private Color4? accentColour; public new Color4 BackgroundColourHover
public Color4 AccentColour
{ {
get => accentColour ?? nonAccentSelectedColour; get => base.BackgroundColourHover;
set set
{ {
accentColour = value; base.BackgroundColourHover = value;
updateColours();
}
}
public new Color4 BackgroundColourSelected
{
get => base.BackgroundColourSelected;
set
{
base.BackgroundColourSelected = value;
updateColours(); updateColours();
} }
} }
private void updateColours() private void updateColours()
{ {
BackgroundColourHover = accentColour ?? nonAccentHoverColour;
BackgroundColourSelected = accentColour ?? nonAccentSelectedColour;
BackgroundColour = BackgroundColourHover.Opacity(0); BackgroundColour = BackgroundColourHover.Opacity(0);
UpdateBackgroundColour(); UpdateBackgroundColour();
UpdateForegroundColour(); UpdateForegroundColour();
} }
private Color4 nonAccentHoverColour;
private Color4 nonAccentSelectedColour;
public DrawableOsuDropdownMenuItem(MenuItem item) public DrawableOsuDropdownMenuItem(MenuItem item)
: base(item) : base(item)
{ {
@ -182,12 +177,8 @@ namespace osu.Game.Graphics.UserInterface
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load()
{ {
nonAccentHoverColour = colours.PinkDarker;
nonAccentSelectedColour = Color4.Black.Opacity(0.5f);
updateColours();
AddInternal(new HoverSounds()); AddInternal(new HoverSounds());
} }
@ -290,7 +281,7 @@ namespace osu.Game.Graphics.UserInterface
#endregion #endregion
public class OsuDropdownHeader : DropdownHeader, IHasAccentColour public class OsuDropdownHeader : DropdownHeader
{ {
protected readonly SpriteText Text; protected readonly SpriteText Text;
@ -302,18 +293,6 @@ namespace osu.Game.Graphics.UserInterface
protected readonly SpriteIcon Icon; protected readonly SpriteIcon Icon;
private Color4 accentColour;
public virtual Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
BackgroundColourHover = accentColour;
}
}
public OsuDropdownHeader() public OsuDropdownHeader()
{ {
Foreground.Padding = new MarginPadding(10); Foreground.Padding = new MarginPadding(10);

View File

@ -11,13 +11,33 @@ using osu.Framework.Input.Events;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class OsuTabDropdown<T> : OsuDropdown<T> public class OsuTabDropdown<T> : OsuDropdown<T>, IHasAccentColour
{ {
private Color4 accentColour;
public Color4 AccentColour
{
get => accentColour;
set
{
accentColour = value;
if (IsLoaded)
propagateAccentColour();
}
}
public OsuTabDropdown() public OsuTabDropdown()
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
} }
protected override void LoadComplete()
{
base.LoadComplete();
propagateAccentColour();
}
protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu(); protected override DropdownMenu CreateMenu() => new OsuTabDropdownMenu();
protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader protected override DropdownHeader CreateHeader() => new OsuTabDropdownHeader
@ -26,6 +46,18 @@ namespace osu.Game.Graphics.UserInterface
Origin = Anchor.TopRight Origin = Anchor.TopRight
}; };
private void propagateAccentColour()
{
if (Menu is OsuDropdownMenu dropdownMenu)
{
dropdownMenu.HoverColour = accentColour;
dropdownMenu.SelectionColour = accentColour.Opacity(0.5f);
}
if (Header is OsuTabDropdownHeader tabDropdownHeader)
tabDropdownHeader.AccentColour = accentColour;
}
private class OsuTabDropdownMenu : OsuDropdownMenu private class OsuTabDropdownMenu : OsuDropdownMenu
{ {
public OsuTabDropdownMenu() public OsuTabDropdownMenu()
@ -37,7 +69,7 @@ namespace osu.Game.Graphics.UserInterface
MaxHeight = 400; MaxHeight = 400;
} }
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item) { AccentColour = AccentColour }; protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableOsuTabDropdownMenuItem(item);
private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem private class DrawableOsuTabDropdownMenuItem : DrawableOsuDropdownMenuItem
{ {
@ -49,15 +81,18 @@ namespace osu.Game.Graphics.UserInterface
} }
} }
protected class OsuTabDropdownHeader : OsuDropdownHeader protected class OsuTabDropdownHeader : OsuDropdownHeader, IHasAccentColour
{ {
public override Color4 AccentColour private Color4 accentColour;
public Color4 AccentColour
{ {
get => base.AccentColour; get => accentColour;
set set
{ {
base.AccentColour = value; accentColour = value;
Foreground.Colour = value; BackgroundColourHover = value;
updateColour();
} }
} }
@ -93,15 +128,20 @@ namespace osu.Game.Graphics.UserInterface
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
Foreground.Colour = BackgroundColour; updateColour();
return base.OnHover(e); return base.OnHover(e);
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
Foreground.Colour = BackgroundColourHover; updateColour();
base.OnHoverLost(e); base.OnHoverLost(e);
} }
private void updateColour()
{
Foreground.Colour = IsHovered ? BackgroundColour : BackgroundColourHover;
}
} }
} }
} }

View File

@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
#nullable enable
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
@ -17,18 +19,13 @@ using osu.Framework.Input.Events;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK; using osuTK;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
public class OsuTextBox : BasicTextBox public class OsuTextBox : BasicTextBox
{ {
private readonly Sample[] textAddedSamples = new Sample[4];
private Sample capsTextAddedSample;
private Sample textRemovedSample;
private Sample textCommittedSample;
private Sample caretMovedSample;
/// <summary> /// <summary>
/// Whether to allow playing a different samples based on the type of character. /// Whether to allow playing a different samples based on the type of character.
/// If set to false, the same sample will be used for all characters. /// If set to false, the same sample will be used for all characters.
@ -42,10 +39,17 @@ namespace osu.Game.Graphics.UserInterface
protected override SpriteText CreatePlaceholder() => new OsuSpriteText protected override SpriteText CreatePlaceholder() => new OsuSpriteText
{ {
Font = OsuFont.GetFont(italics: true), Font = OsuFont.GetFont(italics: true),
Colour = new Color4(180, 180, 180, 255),
Margin = new MarginPadding { Left = 2 }, Margin = new MarginPadding { Left = 2 },
}; };
private readonly Sample?[] textAddedSamples = new Sample[4];
private Sample? capsTextAddedSample;
private Sample? textRemovedSample;
private Sample? textCommittedSample;
private Sample? caretMovedSample;
private OsuCaret? caret;
public OsuTextBox() public OsuTextBox()
{ {
Height = 40; Height = 40;
@ -56,12 +60,18 @@ namespace osu.Game.Graphics.UserInterface
Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; }; Current.DisabledChanged += disabled => { Alpha = disabled ? 0.3f : 1; };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader(true)]
private void load(OsuColour colour, AudioManager audio) private void load(OverlayColourProvider? colourProvider, OsuColour colour, AudioManager audio)
{ {
BackgroundUnfocused = Color4.Black.Opacity(0.5f); BackgroundUnfocused = colourProvider?.Background5 ?? Color4.Black.Opacity(0.5f);
BackgroundFocused = OsuColour.Gray(0.3f).Opacity(0.8f); BackgroundFocused = colourProvider?.Background4 ?? OsuColour.Gray(0.3f).Opacity(0.8f);
BackgroundCommit = BorderColour = colour.Yellow; BackgroundCommit = BorderColour = colourProvider?.Highlight1 ?? colour.Yellow;
selectionColour = colourProvider?.Background1 ?? new Color4(249, 90, 255, 255);
if (caret != null)
caret.SelectionColour = selectionColour;
Placeholder.Colour = colourProvider?.Foreground1 ?? new Color4(180, 180, 180, 255);
for (int i = 0; i < textAddedSamples.Length; i++) for (int i = 0; i < textAddedSamples.Length; i++)
textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}"); textAddedSamples[i] = audio.Samples.Get($@"Keyboard/key-press-{1 + i}");
@ -72,7 +82,9 @@ namespace osu.Game.Graphics.UserInterface
caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement"); caretMovedSample = audio.Samples.Get(@"Keyboard/key-movement");
} }
protected override Color4 SelectionColour => new Color4(249, 90, 255, 255); private Color4 selectionColour;
protected override Color4 SelectionColour => selectionColour;
protected override void OnUserTextAdded(string added) protected override void OnUserTextAdded(string added)
{ {
@ -124,7 +136,7 @@ namespace osu.Game.Graphics.UserInterface
Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) }, Child = new OsuSpriteText { Text = c.ToString(), Font = OsuFont.GetFont(size: CalculatedTextSize) },
}; };
protected override Caret CreateCaret() => new OsuCaret protected override Caret CreateCaret() => caret = new OsuCaret
{ {
CaretWidth = CaretWidth, CaretWidth = CaretWidth,
SelectionColour = SelectionColour, SelectionColour = SelectionColour,

View File

@ -24,6 +24,11 @@ namespace osu.Game.Localisation
/// </summary> /// </summary>
public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled"); public static LocalisableString Enabled => new TranslatableString(getKey(@"enabled"), @"Enabled");
/// <summary>
/// "Disabled"
/// </summary>
public static LocalisableString Disabled => new TranslatableString(getKey(@"disabled"), @"Disabled");
/// <summary> /// <summary>
/// "Default" /// "Default"
/// </summary> /// </summary>

View File

@ -29,6 +29,9 @@ namespace osu.Game.Localisation
{ {
var split = lookup.Split(':'); var split = lookup.Split(':');
if (split.Length < 2)
return null;
string ns = split[0]; string ns = split[0];
string key = split[1]; string key = split[1];

View File

@ -0,0 +1,39 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Localisation;
namespace osu.Game.Localisation
{
public static class ToastStrings
{
private const string prefix = @"osu.Game.Resources.Localisation.Toast";
/// <summary>
/// "no key bound"
/// </summary>
public static LocalisableString NoKeyBound => new TranslatableString(getKey(@"no_key_bound"), @"no key bound");
/// <summary>
/// "Music Playback"
/// </summary>
public static LocalisableString MusicPlayback => new TranslatableString(getKey(@"music_playback"), @"Music Playback");
/// <summary>
/// "Pause track"
/// </summary>
public static LocalisableString PauseTrack => new TranslatableString(getKey(@"pause_track"), @"Pause track");
/// <summary>
/// "Play track"
/// </summary>
public static LocalisableString PlayTrack => new TranslatableString(getKey(@"play_track"), @"Play track");
/// <summary>
/// "Restart track"
/// </summary>
public static LocalisableString RestartTrack => new TranslatableString(getKey(@"restart_track"), @"Restart track");
private static string getKey(string key) => $@"{prefix}:{key}";
}
}

View File

@ -44,7 +44,8 @@ namespace osu.Game.Models
[MapTo(nameof(Status))] [MapTo(nameof(Status))]
public int StatusInt { get; set; } public int StatusInt { get; set; }
public int? OnlineID { get; set; } [Indexed]
public int OnlineID { get; set; } = -1;
public double Length { get; set; } public double Length { get; set; }

View File

@ -20,7 +20,8 @@ namespace osu.Game.Models
[PrimaryKey] [PrimaryKey]
public Guid ID { get; set; } = Guid.NewGuid(); public Guid ID { get; set; } = Guid.NewGuid();
public int? OnlineID { get; set; } [Indexed]
public int OnlineID { get; set; } = -1;
public DateTimeOffset DateAdded { get; set; } public DateTimeOffset DateAdded { get; set; }
@ -62,7 +63,7 @@ namespace osu.Game.Models
if (IsManaged && other.IsManaged) if (IsManaged && other.IsManaged)
return ID == other.ID; return ID == other.ID;
if (OnlineID.HasValue && other.OnlineID.HasValue) if (OnlineID > 0 && other.OnlineID > 0)
return OnlineID == other.OnlineID; return OnlineID == other.OnlineID;
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash)) if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))

View File

@ -18,7 +18,8 @@ namespace osu.Game.Models
[PrimaryKey] [PrimaryKey]
public string ShortName { get; set; } = string.Empty; public string ShortName { get; set; } = string.Empty;
public int? OnlineID { get; set; } [Indexed]
public int OnlineID { get; set; } = -1;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -29,7 +30,7 @@ namespace osu.Game.Models
ShortName = shortName; ShortName = shortName;
Name = name; Name = name;
InstantiationInfo = instantiationInfo; InstantiationInfo = instantiationInfo;
OnlineID = onlineID; OnlineID = onlineID ?? -1;
} }
[UsedImplicitly] [UsedImplicitly]
@ -39,7 +40,7 @@ namespace osu.Game.Models
public RealmRuleset(int? onlineID, string name, string shortName, bool available) public RealmRuleset(int? onlineID, string name, string shortName, bool available)
{ {
OnlineID = onlineID; OnlineID = onlineID ?? -1;
Name = name; Name = name;
ShortName = shortName; ShortName = shortName;
Available = available; Available = available;

View File

@ -39,17 +39,19 @@ namespace osu.Game.Online.API
if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username."); if (string.IsNullOrEmpty(username)) throw new ArgumentException("Missing username.");
if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password."); if (string.IsNullOrEmpty(password)) throw new ArgumentException("Missing password.");
using (var req = new AccessTokenRequestPassword(username, password) var accessTokenRequest = new AccessTokenRequestPassword(username, password)
{ {
Url = $@"{endpoint}/oauth/token", Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post, Method = HttpMethod.Post,
ClientId = clientId, ClientId = clientId,
ClientSecret = clientSecret ClientSecret = clientSecret
}) };
using (accessTokenRequest)
{ {
try try
{ {
req.Perform(); accessTokenRequest.Perform();
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -60,7 +62,7 @@ namespace osu.Game.Online.API
try try
{ {
// attempt to decode a displayable error string. // attempt to decode a displayable error string.
var error = JsonConvert.DeserializeObject<OAuthError>(req.GetResponseString() ?? string.Empty); var error = JsonConvert.DeserializeObject<OAuthError>(accessTokenRequest.GetResponseString() ?? string.Empty);
if (error != null) if (error != null)
throwableException = new APIException(error.UserDisplayableError, ex); throwableException = new APIException(error.UserDisplayableError, ex);
} }
@ -71,7 +73,7 @@ namespace osu.Game.Online.API
throw throwableException; throw throwableException;
} }
Token.Value = req.ResponseObject; Token.Value = accessTokenRequest.ResponseObject;
} }
} }
@ -79,17 +81,19 @@ namespace osu.Game.Online.API
{ {
try try
{ {
using (var req = new AccessTokenRequestRefresh(refresh) var refreshRequest = new AccessTokenRequestRefresh(refresh)
{ {
Url = $@"{endpoint}/oauth/token", Url = $@"{endpoint}/oauth/token",
Method = HttpMethod.Post, Method = HttpMethod.Post,
ClientId = clientId, ClientId = clientId,
ClientSecret = clientSecret ClientSecret = clientSecret
}) };
{
req.Perform();
Token.Value = req.ResponseObject; using (refreshRequest)
{
refreshRequest.Perform();
Token.Value = refreshRequest.ResponseObject;
return true; return true;
} }
} }

View File

@ -6,12 +6,14 @@ using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets; using osu.Game.Rulesets;
#nullable enable
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
{ {
public class APIBeatmap : BeatmapMetadata public class APIBeatmap : BeatmapMetadata, IBeatmapInfo
{ {
[JsonProperty(@"id")] [JsonProperty(@"id")]
public int OnlineBeatmapID { get; set; } public int OnlineID { get; set; }
[JsonProperty(@"beatmapset_id")] [JsonProperty(@"beatmapset_id")]
public int OnlineBeatmapSetID { get; set; } public int OnlineBeatmapSetID { get; set; }
@ -19,8 +21,11 @@ namespace osu.Game.Online.API.Requests.Responses
[JsonProperty(@"status")] [JsonProperty(@"status")]
public BeatmapSetOnlineStatus Status { get; set; } public BeatmapSetOnlineStatus Status { get; set; }
[JsonProperty("checksum")]
public string Checksum { get; set; } = string.Empty;
[JsonProperty(@"beatmapset")] [JsonProperty(@"beatmapset")]
public APIBeatmapSet BeatmapSet { get; set; } public APIBeatmapSet? BeatmapSet { get; set; }
[JsonProperty(@"playcount")] [JsonProperty(@"playcount")]
private int playCount { get; set; } private int playCount { get; set; }
@ -29,10 +34,10 @@ namespace osu.Game.Online.API.Requests.Responses
private int passCount { get; set; } private int passCount { get; set; }
[JsonProperty(@"mode_int")] [JsonProperty(@"mode_int")]
private int ruleset { get; set; } public int RulesetID { get; set; }
[JsonProperty(@"difficulty_rating")] [JsonProperty(@"difficulty_rating")]
private double starDifficulty { get; set; } public double StarRating { get; set; }
[JsonProperty(@"drain")] [JsonProperty(@"drain")]
private float drainRate { get; set; } private float drainRate { get; set; }
@ -47,7 +52,7 @@ namespace osu.Game.Online.API.Requests.Responses
private float overallDifficulty { get; set; } private float overallDifficulty { get; set; }
[JsonProperty(@"total_length")] [JsonProperty(@"total_length")]
private double length { get; set; } public double Length { get; set; }
[JsonProperty(@"count_circles")] [JsonProperty(@"count_circles")]
private int circleCount { get; set; } private int circleCount { get; set; }
@ -56,10 +61,10 @@ namespace osu.Game.Online.API.Requests.Responses
private int sliderCount { get; set; } private int sliderCount { get; set; }
[JsonProperty(@"version")] [JsonProperty(@"version")]
private string version { get; set; } public string DifficultyName { get; set; } = string.Empty;
[JsonProperty(@"failtimes")] [JsonProperty(@"failtimes")]
private BeatmapMetrics metrics { get; set; } private BeatmapMetrics? metrics { get; set; }
[JsonProperty(@"max_combo")] [JsonProperty(@"max_combo")]
private int? maxCombo { get; set; } private int? maxCombo { get; set; }
@ -71,13 +76,14 @@ namespace osu.Game.Online.API.Requests.Responses
return new BeatmapInfo return new BeatmapInfo
{ {
Metadata = set?.Metadata ?? this, Metadata = set?.Metadata ?? this,
Ruleset = rulesets.GetRuleset(ruleset), Ruleset = rulesets.GetRuleset(RulesetID),
StarDifficulty = starDifficulty, StarDifficulty = StarRating,
OnlineBeatmapID = OnlineBeatmapID, OnlineBeatmapID = OnlineID,
Version = version, Version = DifficultyName,
// this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength).
Length = TimeSpan.FromSeconds(length).TotalMilliseconds, Length = TimeSpan.FromSeconds(Length).TotalMilliseconds,
Status = Status, Status = Status,
MD5Hash = Checksum,
BeatmapSet = set, BeatmapSet = set,
Metrics = metrics, Metrics = metrics,
MaxCombo = maxCombo, MaxCombo = maxCombo,
@ -97,5 +103,28 @@ namespace osu.Game.Online.API.Requests.Responses
}, },
}; };
} }
#region Implementation of IBeatmapInfo
public IBeatmapMetadataInfo Metadata => this;
public IBeatmapDifficultyInfo Difficulty => new BeatmapDifficulty
{
DrainRate = drainRate,
CircleSize = circleSize,
ApproachRate = approachRate,
OverallDifficulty = overallDifficulty,
};
IBeatmapSetInfo? IBeatmapInfo.BeatmapSet => BeatmapSet;
public string MD5Hash => Checksum;
public IRulesetInfo Ruleset => new RulesetInfo { ID = RulesetID };
public double BPM => throw new NotImplementedException();
public string Hash => throw new NotImplementedException();
#endregion
} }
} }

View File

@ -6,65 +6,62 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Rulesets; using osu.Game.Rulesets;
#nullable enable
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.API.Requests.Responses
{ {
public class APIBeatmapSet : BeatmapMetadata // todo: this is a bit wrong... public class APIBeatmapSet : BeatmapMetadata, IBeatmapSetOnlineInfo, IBeatmapSetInfo
{ {
[JsonProperty(@"covers")] [JsonProperty(@"covers")]
private BeatmapSetOnlineCovers covers { get; set; } public BeatmapSetOnlineCovers Covers { get; set; }
private int? onlineBeatmapSetID;
[JsonProperty(@"id")] [JsonProperty(@"id")]
public int? OnlineBeatmapSetID public int OnlineID { get; set; }
{
get => onlineBeatmapSetID;
set => onlineBeatmapSetID = value > 0 ? value : null;
}
[JsonProperty(@"status")] [JsonProperty(@"status")]
public BeatmapSetOnlineStatus Status { get; set; } public BeatmapSetOnlineStatus Status { get; set; }
[JsonProperty(@"preview_url")] [JsonProperty(@"preview_url")]
private string preview { get; set; } public string Preview { get; set; } = string.Empty;
[JsonProperty(@"has_favourited")] [JsonProperty(@"has_favourited")]
private bool hasFavourited { get; set; } public bool HasFavourited { get; set; }
[JsonProperty(@"play_count")] [JsonProperty(@"play_count")]
private int playCount { get; set; } public int PlayCount { get; set; }
[JsonProperty(@"favourite_count")] [JsonProperty(@"favourite_count")]
private int favouriteCount { get; set; } public int FavouriteCount { get; set; }
[JsonProperty(@"bpm")] [JsonProperty(@"bpm")]
private double bpm { get; set; } public double BPM { get; set; }
[JsonProperty(@"nsfw")] [JsonProperty(@"nsfw")]
private bool hasExplicitContent { get; set; } public bool HasExplicitContent { get; set; }
[JsonProperty(@"video")] [JsonProperty(@"video")]
private bool hasVideo { get; set; } public bool HasVideo { get; set; }
[JsonProperty(@"storyboard")] [JsonProperty(@"storyboard")]
private bool hasStoryboard { get; set; } public bool HasStoryboard { get; set; }
[JsonProperty(@"submitted_date")] [JsonProperty(@"submitted_date")]
private DateTimeOffset submitted { get; set; } public DateTimeOffset Submitted { get; set; }
[JsonProperty(@"ranked_date")] [JsonProperty(@"ranked_date")]
private DateTimeOffset? ranked { get; set; } public DateTimeOffset? Ranked { get; set; }
[JsonProperty(@"last_updated")] [JsonProperty(@"last_updated")]
private DateTimeOffset lastUpdated { get; set; } public DateTimeOffset? LastUpdated { get; set; }
[JsonProperty(@"ratings")] [JsonProperty(@"ratings")]
private int[] ratings { get; set; } private int[] ratings { get; set; } = Array.Empty<int>();
[JsonProperty(@"track_id")] [JsonProperty(@"track_id")]
private int? trackId { get; set; } public int? TrackId { get; set; }
[JsonProperty(@"user_id")] [JsonProperty(@"user_id")]
private int creatorId private int creatorId
@ -73,48 +70,29 @@ namespace osu.Game.Online.API.Requests.Responses
} }
[JsonProperty(@"availability")] [JsonProperty(@"availability")]
private BeatmapSetOnlineAvailability availability { get; set; } public BeatmapSetOnlineAvailability Availability { get; set; }
[JsonProperty(@"genre")] [JsonProperty(@"genre")]
private BeatmapSetOnlineGenre genre { get; set; } public BeatmapSetOnlineGenre Genre { get; set; }
[JsonProperty(@"language")] [JsonProperty(@"language")]
private BeatmapSetOnlineLanguage language { get; set; } public BeatmapSetOnlineLanguage Language { get; set; }
[JsonProperty(@"beatmaps")] [JsonProperty(@"beatmaps")]
private IEnumerable<APIBeatmap> beatmaps { get; set; } private IEnumerable<APIBeatmap> beatmaps { get; set; } = Array.Empty<APIBeatmap>();
public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets)
{ {
var beatmapSet = new BeatmapSetInfo var beatmapSet = new BeatmapSetInfo
{ {
OnlineBeatmapSetID = OnlineBeatmapSetID, OnlineBeatmapSetID = OnlineID,
Metadata = this, Metadata = this,
Status = Status, Status = Status,
Metrics = ratings == null ? null : new BeatmapSetMetrics { Ratings = ratings }, Metrics = new BeatmapSetMetrics { Ratings = ratings },
OnlineInfo = new BeatmapSetOnlineInfo OnlineInfo = this
{
Covers = covers,
Preview = preview,
PlayCount = playCount,
FavouriteCount = favouriteCount,
BPM = bpm,
Status = Status,
HasExplicitContent = hasExplicitContent,
HasVideo = hasVideo,
HasStoryboard = hasStoryboard,
Submitted = submitted,
Ranked = ranked,
LastUpdated = lastUpdated,
Availability = availability,
HasFavourited = hasFavourited,
Genre = genre,
Language = language,
TrackId = trackId
},
}; };
beatmapSet.Beatmaps = beatmaps?.Select(b => beatmapSet.Beatmaps = beatmaps.Select(b =>
{ {
var beatmap = b.ToBeatmapInfo(rulesets); var beatmap = b.ToBeatmapInfo(rulesets);
beatmap.BeatmapSet = beatmapSet; beatmap.BeatmapSet = beatmapSet;
@ -124,5 +102,19 @@ namespace osu.Game.Online.API.Requests.Responses
return beatmapSet; return beatmapSet;
} }
#region Implementation of IBeatmapSetInfo
IEnumerable<IBeatmapInfo> IBeatmapSetInfo.Beatmaps => beatmaps;
IBeatmapMetadataInfo IBeatmapSetInfo.Metadata => this;
DateTimeOffset IBeatmapSetInfo.DateAdded => throw new NotImplementedException();
IEnumerable<INamedFileUsage> IBeatmapSetInfo.Files => throw new NotImplementedException();
double IBeatmapSetInfo.MaxStarDifficulty => throw new NotImplementedException();
double IBeatmapSetInfo.MaxLength => throw new NotImplementedException();
double IBeatmapSetInfo.MaxBPM => throw new NotImplementedException();
#endregion
} }
} }

View File

@ -1,23 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Newtonsoft.Json;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
namespace osu.Game.Online.Rooms
{
public class APIPlaylistBeatmap : APIBeatmap
{
[JsonProperty("checksum")]
public string Checksum { get; set; }
public override BeatmapInfo ToBeatmapInfo(RulesetStore rulesets)
{
var b = base.ToBeatmapInfo(rulesets);
b.MD5Hash = Checksum;
return b;
}
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Online.Rooms
protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet)
{ {
int? beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineBeatmapID; int beatmapId = SelectedItem.Value?.Beatmap.Value.OnlineID ?? -1;
string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash; string checksum = SelectedItem.Value?.Beatmap.Value.MD5Hash;
var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum);
@ -75,10 +75,10 @@ namespace osu.Game.Online.Rooms
protected override bool IsModelAvailableLocally() protected override bool IsModelAvailableLocally()
{ {
int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; int onlineId = SelectedItem.Value.Beatmap.Value.OnlineID;
string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash;
var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == onlineId && b.MD5Hash == checksum);
return beatmap?.BeatmapSet.DeletePending == false; return beatmap?.BeatmapSet.DeletePending == false;
} }

View File

@ -7,6 +7,7 @@ using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
@ -42,7 +43,7 @@ namespace osu.Game.Online.Rooms
public readonly BindableList<Mod> RequiredMods = new BindableList<Mod>(); public readonly BindableList<Mod> RequiredMods = new BindableList<Mod>();
[JsonProperty("beatmap")] [JsonProperty("beatmap")]
private APIPlaylistBeatmap apiBeatmap { get; set; } private APIBeatmap apiBeatmap { get; set; }
private APIMod[] allowedModsBacking; private APIMod[] allowedModsBacking;

View File

@ -657,9 +657,9 @@ namespace osu.Game
var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l); var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l);
if (combinations.Count == 0) if (combinations.Count == 0)
return "none"; return ToastStrings.NoKeyBound;
return string.Join(" or ", combinations); return string.Join(" / ", combinations);
}; };
Container logoContainer; Container logoContainer;

View File

@ -76,7 +76,7 @@ namespace osu.Game.Overlays.BeatmapListing
private readonly BeatmapSearchFilterRow<SearchExplicit> explicitContentFilter; private readonly BeatmapSearchFilterRow<SearchExplicit> explicitContentFilter;
private readonly Box background; private readonly Box background;
private readonly UpdateableBeatmapSetCover beatmapCover; private readonly UpdateableOnlineBeatmapSetCover beatmapCover;
public BeatmapListingSearchControl() public BeatmapListingSearchControl()
{ {
@ -196,7 +196,7 @@ namespace osu.Game.Overlays.BeatmapListing
} }
} }
private class TopSearchBeatmapSetCover : UpdateableBeatmapSetCover private class TopSearchBeatmapSetCover : UpdateableOnlineBeatmapSetCover
{ {
protected override bool TransformImmediately => true; protected override bool TransformImmediately => true;
} }

View File

@ -160,7 +160,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
return icons; return icons;
} }
protected Drawable CreateBackground() => new UpdateableBeatmapSetCover protected Drawable CreateBackground() => new UpdateableOnlineBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
BeatmapSet = SetInfo, BeatmapSet = SetInfo,

View File

@ -92,7 +92,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels
break; break;
default: default:
if (BeatmapSet.Value?.OnlineInfo?.Availability?.DownloadDisabled ?? false) if (BeatmapSet.Value?.OnlineInfo?.Availability.DownloadDisabled ?? false)
{ {
button.Enabled.Value = false; button.Enabled.Value = false;
button.TooltipText = "this beatmap is currently not available for download."; button.TooltipText = "this beatmap is currently not available for download.";

View File

@ -16,8 +16,8 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
private BeatmapSetInfo beatmapSet; private BeatmapSetInfo beatmapSet;
private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability?.DownloadDisabled ?? false; private bool downloadDisabled => BeatmapSet?.OnlineInfo.Availability.DownloadDisabled ?? false;
private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability?.ExternalLink); private bool hasExternalLink => !string.IsNullOrEmpty(BeatmapSet?.OnlineInfo.Availability.ExternalLink);
private readonly LinkFlowContainer textContainer; private readonly LinkFlowContainer textContainer;

View File

@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapSet
public readonly Details Details; public readonly Details Details;
public readonly BeatmapPicker Picker; public readonly BeatmapPicker Picker;
private readonly UpdateableBeatmapSetCover cover; private readonly UpdateableOnlineBeatmapSetCover cover;
private readonly Box coverGradient; private readonly Box coverGradient;
private readonly OsuSpriteText title, artist; private readonly OsuSpriteText title, artist;
private readonly AuthorInfo author; private readonly AuthorInfo author;
@ -68,7 +68,7 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
cover = new UpdateableBeatmapSetCover cover = new UpdateableOnlineBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
@ -266,7 +266,7 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
if (BeatmapSet.Value == null) return; if (BeatmapSet.Value == null) return;
if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) if (BeatmapSet.Value.OnlineInfo.Availability.DownloadDisabled && State.Value != DownloadState.LocallyAvailable)
{ {
downloadButtonsContainer.Clear(); downloadButtonsContainer.Clear();
return; return;

View File

@ -117,8 +117,8 @@ namespace osu.Game.Overlays.BeatmapSet
{ {
source.Text = b.NewValue?.Metadata.Source ?? string.Empty; source.Text = b.NewValue?.Metadata.Source ?? string.Empty;
tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty; tags.Text = b.NewValue?.Metadata.Tags ?? string.Empty;
genre.Text = b.NewValue?.OnlineInfo?.Genre?.Name ?? string.Empty; genre.Text = b.NewValue?.OnlineInfo?.Genre.Name ?? string.Empty;
language.Text = b.NewValue?.OnlineInfo?.Language?.Name ?? string.Empty; language.Text = b.NewValue?.OnlineInfo?.Language.Name ?? string.Empty;
var setHasLeaderboard = b.NewValue?.OnlineInfo?.Status > 0; var setHasLeaderboard = b.NewValue?.OnlineInfo?.Status > 0;
successRate.Alpha = setHasLeaderboard ? 1 : 0; successRate.Alpha = setHasLeaderboard ? 1 : 0;
notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1; notRankedPlaceholder.Alpha = setHasLeaderboard ? 0 : 1;

View File

@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Dashboard.Home
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 6, CornerRadius = 6,
Child = new UpdateableBeatmapSetCover Child = new UpdateableOnlineBeatmapSetCover
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

View File

@ -29,12 +29,6 @@ namespace osu.Game.Overlays.Login
} }
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Gray5;
}
protected class UserDropdownMenu : OsuDropdownMenu protected class UserDropdownMenu : OsuDropdownMenu
{ {
public UserDropdownMenu() public UserDropdownMenu()
@ -56,6 +50,8 @@ namespace osu.Game.Overlays.Login
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.Gray3; BackgroundColour = colours.Gray3;
SelectionColour = colours.Gray4;
HoverColour = colours.Gray5;
} }
protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item); protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new DrawableUserDropdownMenuItem(item);
@ -118,6 +114,7 @@ namespace osu.Game.Overlays.Login
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.Gray3; BackgroundColour = colours.Gray3;
BackgroundColourHover = colours.Gray5;
} }
} }
} }

View File

@ -19,12 +19,6 @@ namespace osu.Game.Overlays.Music
{ {
protected override bool ShowManageCollectionsItem => false; protected override bool ShowManageCollectionsItem => false;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
AccentColour = colours.Gray6;
}
protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader(); protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader();
protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu(); protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu();
@ -41,6 +35,8 @@ namespace osu.Game.Overlays.Music
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.Gray4; BackgroundColour = colours.Gray4;
SelectionColour = colours.Gray5;
HoverColour = colours.Gray6;
} }
} }
@ -50,6 +46,7 @@ namespace osu.Game.Overlays.Music
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.Gray4; BackgroundColour = colours.Gray4;
BackgroundColourHover = colours.Gray6;
} }
public CollectionsHeader() public CollectionsHeader()

View File

@ -3,12 +3,15 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events; using osu.Framework.Input.Events;
using osu.Framework.Localisation;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation;
using osu.Game.Overlays.OSD; using osu.Game.Overlays.OSD;
namespace osu.Game.Overlays.Music namespace osu.Game.Overlays.Music
@ -39,11 +42,11 @@ namespace osu.Game.Overlays.Music
bool wasPlaying = musicController.IsPlaying; bool wasPlaying = musicController.IsPlaying;
if (musicController.TogglePause()) if (musicController.TogglePause())
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", e.Action)); onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? ToastStrings.PauseTrack : ToastStrings.PlayTrack, e.Action));
return true; return true;
case GlobalAction.MusicNext: case GlobalAction.MusicNext:
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", e.Action))); musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicNext, e.Action)));
return true; return true;
@ -53,11 +56,11 @@ namespace osu.Game.Overlays.Music
switch (res) switch (res)
{ {
case PreviousTrackResult.Restart: case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicActionToast("Restart track", e.Action)); onScreenDisplay?.Display(new MusicActionToast(ToastStrings.RestartTrack, e.Action));
break; break;
case PreviousTrackResult.Previous: case PreviousTrackResult.Previous:
onScreenDisplay?.Display(new MusicActionToast("Previous track", e.Action)); onScreenDisplay?.Display(new MusicActionToast(GlobalActionKeyBindingStrings.MusicPrev, e.Action));
break; break;
} }
}); });
@ -76,8 +79,8 @@ namespace osu.Game.Overlays.Music
{ {
private readonly GlobalAction action; private readonly GlobalAction action;
public MusicActionToast(string value, GlobalAction action) public MusicActionToast(LocalisableString value, GlobalAction action)
: base("Music Playback", value, string.Empty) : base(ToastStrings.MusicPlayback, value, string.Empty)
{ {
this.action = action; this.action = action;
} }
@ -85,7 +88,7 @@ namespace osu.Game.Overlays.Music
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant(); ShortcutText.Text = config.LookupKeyBindings(action).ToUpper();
} }
} }
} }

View File

@ -1,13 +1,16 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Localisation;
namespace osu.Game.Overlays.OSD namespace osu.Game.Overlays.OSD
{ {
@ -23,7 +26,7 @@ namespace osu.Game.Overlays.OSD
protected readonly OsuSpriteText ShortcutText; protected readonly OsuSpriteText ShortcutText;
protected Toast(string description, string value, string shortcut) protected Toast(LocalisableString description, LocalisableString value, LocalisableString shortcut)
{ {
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
Origin = Anchor.Centre; Origin = Anchor.Centre;
@ -60,12 +63,12 @@ namespace osu.Game.Overlays.OSD
Spacing = new Vector2(1, 0), Spacing = new Vector2(1, 0),
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Text = description.ToUpperInvariant() Text = description.ToUpper()
}, },
ValueText = new OsuSpriteText ValueText = new OsuSpriteText
{ {
Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light), Font = OsuFont.GetFont(size: 24, weight: FontWeight.Light),
Padding = new MarginPadding { Left = 10, Right = 10 }, Padding = new MarginPadding { Horizontal = 10 },
Name = "Value", Name = "Value",
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -77,9 +80,9 @@ namespace osu.Game.Overlays.OSD
Origin = Anchor.BottomCentre, Origin = Anchor.BottomCentre,
Name = "Shortcut", Name = "Shortcut",
Alpha = 0.3f, Alpha = 0.3f,
Margin = new MarginPadding { Bottom = 15 }, Margin = new MarginPadding { Bottom = 15, Horizontal = 10 },
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold),
Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant() Text = string.IsNullOrEmpty(shortcut.ToString()) ? ToastStrings.NoKeyBound.ToUpper() : shortcut.ToUpper()
}, },
}; };
} }

View File

@ -29,7 +29,7 @@ namespace osu.Game.Overlays.OSD
private Sample sampleChange; private Sample sampleChange;
public TrackedSettingToast(SettingDescription description) public TrackedSettingToast(SettingDescription description)
: base(description.Name.ToString(), description.Value.ToString(), description.Shortcut.ToString()) : base(description.Name, description.Value, description.Shortcut)
{ {
FillFlowContainer<OptionLight> optionLights; FillFlowContainer<OptionLight> optionLights;

View File

@ -60,12 +60,12 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
protected override APIRequest<List<APIBeatmapSet>> CreateRequest() => protected override APIRequest<List<APIBeatmapSet>> CreateRequest() =>
new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
protected override Drawable CreateDrawableItem(APIBeatmapSet model) => !model.OnlineBeatmapSetID.HasValue protected override Drawable CreateDrawableItem(APIBeatmapSet model) => model.OnlineID > 0
? null ? new GridBeatmapPanel(model.ToBeatmapSet(Rulesets))
: new GridBeatmapPanel(model.ToBeatmapSet(Rulesets))
{ {
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
}; }
: null;
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
{ {
AddRangeInternal(new Drawable[] AddRangeInternal(new Drawable[]
{ {
new UpdateableBeatmapSetCover(BeatmapSetCoverType.List) new UpdateableOnlineBeatmapSetCover(BeatmapSetCoverType.List)
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = cover_width, Width = cover_width,

View File

@ -175,18 +175,18 @@ namespace osu.Game.Overlays.Rankings
private class SpotlightsDropdown : OsuDropdown<APISpotlight> private class SpotlightsDropdown : OsuDropdown<APISpotlight>
{ {
private DropdownMenu menu; private OsuDropdownMenu menu;
protected override DropdownMenu CreateMenu() => menu = base.CreateMenu().With(m => m.MaxHeight = 400); protected override DropdownMenu CreateMenu() => menu = (OsuDropdownMenu)base.CreateMenu().With(m => m.MaxHeight = 400);
protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader(); protected override DropdownHeader CreateHeader() => new SpotlightsDropdownHeader();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
// osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour
AccentColour = colourProvider.Background6.Opacity(0.8f);
menu.BackgroundColour = colourProvider.Background5; menu.BackgroundColour = colourProvider.Background5;
menu.HoverColour = colourProvider.Background4;
menu.SelectionColour = colourProvider.Background3;
Padding = new MarginPadding { Vertical = 20 }; Padding = new MarginPadding { Vertical = 20 };
} }
@ -205,7 +205,8 @@ namespace osu.Game.Overlays.Rankings
private void load(OverlayColourProvider colourProvider) private void load(OverlayColourProvider colourProvider)
{ {
BackgroundColour = colourProvider.Background6.Opacity(0.5f); BackgroundColour = colourProvider.Background6.Opacity(0.5f);
BackgroundColourHover = colourProvider.Background5; // osu-web adds a 0.6 opacity container on top of the 0.5 base one when hovering, 0.8 on a single container here matches the resulting colour
BackgroundColourHover = colourProvider.Background6.Opacity(0.8f);
} }
} }
} }

View File

@ -3,10 +3,8 @@
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osuTK.Graphics;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
@ -14,6 +12,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
@ -45,30 +44,21 @@ namespace osu.Game.Overlays
} }
} }
private bool hovering; [Resolved]
private OsuColour colours { get; set; }
public RestoreDefaultValueButton() private const float size = 4;
{
Height = 1;
RelativeSizeAxes = Axes.Y;
Width = SettingsPanel.CONTENT_MARGINS;
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colour) private void load(OsuColour colour)
{ {
BackgroundColour = colour.Yellow; BackgroundColour = colour.Lime1;
Content.Width = 0.33f; Size = new Vector2(3 * size);
Content.CornerRadius = 3;
Content.EdgeEffect = new EdgeEffectParameters Content.RelativeSizeAxes = Axes.None;
{ Content.Size = new Vector2(size);
Colour = BackgroundColour.Opacity(0.1f), Content.CornerRadius = size / 2;
Type = EdgeEffectType.Glow,
Radius = 2,
};
Padding = new MarginPadding { Vertical = 1.5f };
Alpha = 0f; Alpha = 0f;
Action += () => Action += () =>
@ -81,39 +71,55 @@ namespace osu.Game.Overlays
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
updateState();
// avoid unnecessary transforms on first display. FinishTransforms(true);
Alpha = currentAlpha;
Background.Colour = currentColour;
} }
public LocalisableString TooltipText => "revert to default"; public LocalisableString TooltipText => "revert to default";
protected override bool OnHover(HoverEvent e) protected override bool OnHover(HoverEvent e)
{ {
hovering = true;
UpdateState(); UpdateState();
return false; return false;
} }
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
hovering = false;
UpdateState(); UpdateState();
} }
public void UpdateState() => Scheduler.AddOnce(updateState); public void UpdateState() => Scheduler.AddOnce(updateState);
private float currentAlpha => current.IsDefault ? 0f : hovering && !current.Disabled ? 1f : 0.65f; private const double fade_duration = 200;
private ColourInfo currentColour => current.Disabled ? Color4.Gray : BackgroundColour;
private void updateState() private void updateState()
{ {
if (current == null) if (current == null)
return; return;
this.FadeTo(currentAlpha, 200, Easing.OutQuint); Enabled.Value = !Current.Disabled;
Background.FadeColour(currentColour, 200, Easing.OutQuint);
if (!Current.Disabled)
{
this.FadeTo(Current.IsDefault ? 0 : 1, fade_duration, Easing.OutQuint);
Background.FadeColour(IsHovered ? colours.Lime0 : colours.Lime1, fade_duration, Easing.OutQuint);
Content.TweenEdgeEffectTo(new EdgeEffectParameters
{
Colour = (IsHovered ? colours.Lime1 : colours.Lime3).Opacity(0.4f),
Radius = IsHovered ? 8 : 4,
Type = EdgeEffectType.Glow
}, fade_duration, Easing.OutQuint);
}
else
{
Background.FadeColour(colours.Lime3, fade_duration, Easing.OutQuint);
Content.TweenEdgeEffectTo(new EdgeEffectParameters
{
Colour = colours.Lime3.Opacity(0.1f),
Radius = 2,
Type = EdgeEffectType.Glow
}, fade_duration, Easing.OutQuint);
}
} }
} }
} }

View File

@ -82,16 +82,29 @@ namespace osu.Game.Overlays.Settings.Sections.Input
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Padding = new MarginPadding { Horizontal = SettingsPanel.CONTENT_MARGINS }; Padding = new MarginPadding { Right = SettingsPanel.CONTENT_MARGINS };
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
new RestoreDefaultValueButton<bool> new Container
{
RelativeSizeAxes = Axes.Y,
Width = SettingsPanel.CONTENT_MARGINS,
Child = new RestoreDefaultValueButton<bool>
{ {
Current = isDefault, Current = isDefault,
Action = RestoreDefaults, Action = RestoreDefaults,
Origin = Anchor.TopRight, Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}, },
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
Children = new Drawable[]
{
content = new Container content = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
@ -138,6 +151,8 @@ namespace osu.Game.Overlays.Settings.Sections.Input
}, },
} }
} }
}
}
}, },
new HoverClickSounds() new HoverClickSounds()
}; };

View File

@ -6,7 +6,6 @@ using System.Linq;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -28,11 +27,6 @@ namespace osu.Game.Overlays.Settings
public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString())); public override IEnumerable<string> FilterTerms => base.FilterTerms.Concat(Control.Items.Select(i => i.ToString()));
public SettingsDropdown()
{
FlowContent.Spacing = new Vector2(0, 10);
}
protected sealed override Drawable CreateControl() => CreateDropdown(); protected sealed override Drawable CreateControl() => CreateDropdown();
protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl(); protected virtual OsuDropdown<T> CreateDropdown() => new DropdownControl();

View File

@ -14,6 +14,7 @@ using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
@ -34,6 +35,7 @@ namespace osu.Game.Overlays.Settings
private OsuTextFlowContainer warningText; private OsuTextFlowContainer warningText;
public bool ShowsDefaultIndicator = true; public bool ShowsDefaultIndicator = true;
private readonly Container defaultValueIndicatorContainer;
public LocalisableString TooltipText { get; set; } public LocalisableString TooltipText { get; set; }
@ -54,6 +56,7 @@ namespace osu.Game.Overlays.Settings
} }
labelText.Text = value; labelText.Text = value;
updateLayout();
} }
} }
@ -108,16 +111,23 @@ namespace osu.Game.Overlays.Settings
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
FlowContent = new FillFlowContainer defaultValueIndicatorContainer = new Container
{
Width = SettingsPanel.CONTENT_MARGINS,
},
new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS }, Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS },
Children = new[] Child = FlowContent = new FillFlowContainer
{ {
Control = CreateControl(), RelativeSizeAxes = Axes.X,
}, AutoSizeAxes = Axes.Y,
}, Spacing = new Vector2(0, 10),
Child = Control = CreateControl(),
}
}
}; };
// IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is // IMPORTANT: all bindable logic is in constructor intentionally to support "CreateSettingsControls" being used in a context it is
@ -135,13 +145,25 @@ namespace osu.Game.Overlays.Settings
// intentionally done before LoadComplete to avoid overhead. // intentionally done before LoadComplete to avoid overhead.
if (ShowsDefaultIndicator) if (ShowsDefaultIndicator)
{ {
AddInternal(new RestoreDefaultValueButton<T> defaultValueIndicatorContainer.Add(new RestoreDefaultValueButton<T>
{ {
Current = controlWithCurrent.Current, Current = controlWithCurrent.Current,
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}); });
updateLayout();
} }
} }
private void updateLayout()
{
bool hasLabel = labelText != null && !string.IsNullOrEmpty(labelText.Text.ToString());
// if the settings item is providing a label, the default value indicator should be centred vertically to the left of the label.
// otherwise, it should be centred vertically to the left of the main control of the settings item.
defaultValueIndicatorContainer.Height = hasLabel ? labelText.DrawHeight : Control.DrawHeight;
}
private void updateDisabled() private void updateDisabled()
{ {
if (labelText != null) if (labelText != null)

View File

@ -13,7 +13,6 @@ namespace osu.Game.Overlays.Settings
protected override Drawable CreateControl() => new NumberControl protected override Drawable CreateControl() => new NumberControl
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Top = 5 }
}; };
private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?> private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?>

View File

@ -19,7 +19,6 @@ namespace osu.Game.Overlays.Settings
{ {
protected override Drawable CreateControl() => new TSlider protected override Drawable CreateControl() => new TSlider
{ {
Margin = new MarginPadding { Vertical = 10 },
RelativeSizeAxes = Axes.X RelativeSizeAxes = Axes.X
}; };

View File

@ -11,7 +11,6 @@ namespace osu.Game.Overlays.Settings
{ {
protected override Drawable CreateControl() => new OutlinedTextBox protected override Drawable CreateControl() => new OutlinedTextBox
{ {
Margin = new MarginPadding { Top = 5 },
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true CommitOnFocusLost = true
}; };

View File

@ -57,7 +57,7 @@ namespace osu.Game.Rulesets
#region Implementation of IHasOnlineID #region Implementation of IHasOnlineID
public int? OnlineID => ID; public int OnlineID => ID ?? -1;
#endregion #endregion
} }

View File

@ -25,7 +25,7 @@ using osu.Game.Rulesets.Scoring;
namespace osu.Game.Scoring namespace osu.Game.Scoring
{ {
public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>, ICanAcceptFiles public class ScoreManager : IModelManager<ScoreInfo>, IModelFileManager<ScoreInfo, ScoreFileInfo>, IModelDownloader<ScoreInfo>
{ {
private readonly Scheduler scheduler; private readonly Scheduler scheduler;
private readonly Func<BeatmapDifficultyCache> difficulties; private readonly Func<BeatmapDifficultyCache> difficulties;

View File

@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title), Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title),
Font = OsuFont.GetFont(size: TextSize), Font = OsuFont.GetFont(size: TextSize),
} }
}, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); }, LinkAction.OpenBeatmap, beatmap.Value.OnlineID.ToString(), "Open beatmap");
} }
} }
} }

View File

@ -61,7 +61,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
{ {
var beatmap = playlistItem?.Beatmap.Value; var beatmap = playlistItem?.Beatmap.Value;
if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers?.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers?.Cover) if (background?.BeatmapInfo?.BeatmapSet?.OnlineInfo?.Covers.Cover == beatmap?.BeatmapSet?.OnlineInfo?.Covers.Cover)
return; return;
cancellationSource?.Cancel(); cancellationSource?.Cancel();

View File

@ -17,7 +17,6 @@ using osu.Framework.Input.Events;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Input; using osu.Game.Input;
@ -126,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Height = Header.HEIGHT, Height = Header.HEIGHT,
Child = searchTextBox = new LoungeSearchTextBox Child = searchTextBox = new SearchTextBox
{ {
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
@ -362,15 +361,5 @@ namespace osu.Game.Screens.OnlinePlay.Lounge
protected abstract RoomSubScreen CreateRoomSubScreen(Room room); protected abstract RoomSubScreen CreateRoomSubScreen(Room room);
protected abstract ListingPollingComponent CreatePollingComponent(); protected abstract ListingPollingComponent CreatePollingComponent();
private class LoungeSearchTextBox : SearchTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = OsuColour.Gray(0.06f);
BackgroundFocused = OsuColour.Gray(0.12f);
}
}
} }
} }

View File

@ -12,7 +12,6 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osuTK; using osuTK;
using osuTK.Graphics;
namespace osu.Game.Screens.OnlinePlay.Match.Components namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
@ -91,31 +90,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{ {
} }
protected class SettingsTextBox : OsuTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = Color4.Black;
BackgroundFocused = Color4.Black;
}
}
protected class SettingsNumberTextBox : SettingsTextBox
{
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
}
protected class SettingsPasswordTextBox : OsuPasswordTextBox
{
[BackgroundDependencyLoader]
private void load()
{
BackgroundUnfocused = Color4.Black;
BackgroundFocused = Color4.Black;
}
}
protected class SectionContainer : FillFlowContainer<Section> protected class SectionContainer : FillFlowContainer<Section>
{ {
public SectionContainer() public SectionContainer()

View File

@ -153,7 +153,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{ {
new Section("Room name") new Section("Room name")
{ {
Child = NameField = new SettingsTextBox Child = NameField = new OsuTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
@ -202,7 +202,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
new Section("Max participants") new Section("Max participants")
{ {
Alpha = disabled_alpha, Alpha = disabled_alpha,
Child = MaxParticipantsField = new SettingsNumberTextBox Child = MaxParticipantsField = new OsuNumberBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
@ -211,7 +211,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
}, },
new Section("Password (optional)") new Section("Password (optional)")
{ {
Child = PasswordTextBox = new SettingsPasswordTextBox Child = PasswordTextBox = new OsuPasswordTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,

View File

@ -15,6 +15,7 @@ using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -190,6 +191,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability);
if ((User.BeatmapAvailability.State == DownloadState.LocallyAvailable) && (User.State != MultiplayerUserState.Spectating))
userModsDisplay.FadeIn(fade_time);
else
userModsDisplay.FadeOut(fade_time);
if (Client.IsHost && !User.Equals(Client.LocalUser)) if (Client.IsHost && !User.Equals(Client.LocalUser))
kickButton.FadeIn(fade_time); kickButton.FadeIn(fade_time);
else else

View File

@ -121,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
{ {
new Section("Room name") new Section("Room name")
{ {
Child = NameField = new SettingsTextBox Child = NameField = new OsuTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
@ -150,7 +150,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
}, },
new Section("Allowed attempts (across all playlist items)") new Section("Allowed attempts (across all playlist items)")
{ {
Child = MaxAttemptsField = new SettingsNumberTextBox Child = MaxAttemptsField = new OsuNumberBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
@ -168,7 +168,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Section("Max participants") new Section("Max participants")
{ {
Alpha = disabled_alpha, Alpha = disabled_alpha,
Child = MaxParticipantsField = new SettingsNumberTextBox Child = MaxParticipantsField = new OsuNumberBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,
@ -178,7 +178,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
new Section("Password (optional)") new Section("Password (optional)")
{ {
Alpha = disabled_alpha, Alpha = disabled_alpha,
Child = new SettingsPasswordTextBox Child = new OsuPasswordTextBox
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
TabbableContentContainer = this, TabbableContentContainer = this,

View File

@ -13,7 +13,9 @@ namespace osu.Game.Screens.Play.Break
public class BreakInfo : Container public class BreakInfo : Container
{ {
public PercentageBreakInfoLine AccuracyDisplay; public PercentageBreakInfoLine AccuracyDisplay;
public BreakInfoLine<int> RankDisplay;
// Currently unused but may be revisited in a future design update (see https://github.com/ppy/osu/discussions/15185)
// public BreakInfoLine<int> RankDisplay;
public BreakInfoLine<ScoreRank> GradeDisplay; public BreakInfoLine<ScoreRank> GradeDisplay;
public BreakInfo() public BreakInfo()
@ -41,7 +43,9 @@ namespace osu.Game.Screens.Play.Break
Children = new Drawable[] Children = new Drawable[]
{ {
AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"), AccuracyDisplay = new PercentageBreakInfoLine("Accuracy"),
RankDisplay = new BreakInfoLine<int>("Rank"),
// See https://github.com/ppy/osu/discussions/15185
// RankDisplay = new BreakInfoLine<int>("Rank"),
GradeDisplay = new BreakInfoLine<ScoreRank>("Grade"), GradeDisplay = new BreakInfoLine<ScoreRank>("Grade"),
}, },
} }

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