1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-13 13:32:54 +08:00

Merge branch 'master' into chat-mention-fix

This commit is contained in:
Dan Balasescu 2021-12-14 11:49:57 +09:00 committed by GitHub
commit 51b6b9d857
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
147 changed files with 2567 additions and 1114 deletions

View File

@ -27,10 +27,10 @@
]
},
"ppy.localisationanalyser.tools": {
"version": "2021.725.0",
"version": "2021.1210.0",
"commands": [
"localisation"
]
}
}
}
}

View File

@ -52,7 +52,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1206.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.1210.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -70,7 +70,9 @@ namespace osu.Desktop
if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath;
}
catch { }
catch
{
}
}
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
@ -113,7 +115,7 @@ namespace osu.Desktop
base.LoadComplete();
if (!noVersionOverlay)
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
LoadComponentAsync(new DiscordRichPresence(), Add);

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
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 normalised_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths.
private const int min_delta_time = 25;
private const float maximum_slider_radius = normalized_radius * 2.4f;
private const float assumed_slider_radius = normalized_radius * 1.8f;
private const float maximum_slider_radius = normalised_radius * 2.4f;
private const float assumed_slider_radius = normalised_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary>
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double JumpDistance { get; private set; }
public readonly double StrainTime;
/// <summary>
/// Minimum distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// Normalised distance from the "lazy" end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// <para>
/// The "lazy" end position is the position at which the cursor ends up if the previous hitobject is followed with as minimal movement as possible (i.e. on the edge of slider follow circles).
/// </para>
/// </summary>
public double MovementDistance { get; private set; }
public double LazyJumpDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// Normalised shortest distance to consider for a jump between the previous <see cref="OsuDifficultyHitObject"/> and this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
/// <remarks>
/// This is bounded from above by <see cref="LazyJumpDistance"/>, and is smaller than the former if a more natural path is able to be taken through the previous <see cref="OsuDifficultyHitObject"/>.
/// </remarks>
/// <example>
/// Suppose a linear slider - circle pattern.
/// <br />
/// Following the slider lazily (see: <see cref="LazyJumpDistance"/>) will result in underestimating the true end position of the slider as being closer towards the start position.
/// As a result, <see cref="LazyJumpDistance"/> overestimates the jump distance because the player is able to take a more natural path by following through the slider to its end,
/// such that the jump is felt as only starting from the slider's true end position.
/// <br />
/// Now consider a slider - circle pattern where the circle is stacked along the path inside the slider.
/// In this case, the lazy end position correctly estimates the true end position of the slider and provides the more natural movement path.
/// </example>
public double MinimumJumpDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="MinimumJumpDistance"/>, with a minimum value of 25ms.
/// </summary>
public double MinimumJumpTime { get; private set; }
/// <summary>
/// Normalised distance between the start and end position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double TravelDistance { get; private set; }
/// <summary>
/// The time taken to travel through <see cref="TravelDistance"/>, with a minimum value of 25ms for a non-zero distance.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary>
public double? Angle { get; private set; }
/// <summary>
/// Milliseconds elapsed since the end time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double MovementTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/> to the end time of the same previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public double TravelTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 25ms.
/// </summary>
public readonly double StrainTime;
private readonly OsuHitObject lastLastObject;
private readonly OsuHitObject lastObject;
@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances(double clockRate)
{
if (BaseObject is Slider currentSlider)
{
computeSliderCursorPosition(currentSlider);
TravelDistance = currentSlider.LazyTravelDistance;
TravelTime = Math.Max(currentSlider.LazyTravelTime / clockRate, min_delta_time);
}
// We don't need to calculate either angle or distance when one of the last->curr objects is a spinner
if (BaseObject is Spinner || lastObject is Spinner)
return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
float scalingFactor = normalised_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
}
Vector2 lastCursorPosition = getEndCursorPosition(lastObject);
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
LazyJumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
MinimumJumpTime = StrainTime;
MinimumJumpDistance = LazyJumpDistance;
if (lastObject is Slider lastSlider)
{
computeSliderCursorPosition(lastSlider);
TravelDistance = lastSlider.LazyTravelDistance;
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, min_delta_time);
double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
//
// There are two types of slider-to-object patterns to consider in order to better approximate the real movement a player will take to jump between the hitobjects.
//
// 1. The anti-flow pattern, where players cut the slider short in order to move to the next hitobject.
//
// <======o==> ← slider
// | ← most natural jump path
// o ← a follow-up hitcircle
//
// In this case the most natural jump path is approximated by LazyJumpDistance.
//
// 2. The flow pattern, where players follow through the slider to its visual extent into the next hitobject.
//
// <======o==>---o
// ↑
// most natural jump path
//
// In this case the most natural jump path is better approximated by a new distance called "tailJumpDistance" - the distance between the slider's tail and the next hitobject.
//
// Thus, the player is assumed to jump the minimum of these two distances in all cases.
//
// Jump distance from the slider tail to the next object, as opposed to the lazy position of JumpDistance.
float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
// For hitobjects which continue in the direction of the slider, the player will normally follow through the slider,
// such that they're not jumping from the lazy position but rather from very close to (or the end of) the slider.
// In such cases, a leniency is applied by also considering the jump distance from the tail of the slider, and taking the minimum jump distance.
// Additional distance is removed based on position of jump relative to slider follow circle radius.
// JumpDistance is the leniency distance beyond the assumed_slider_radius. tailJumpDistance is maximum_slider_radius since the full distance of radial leniency is still possible.
MovementDistance = Math.Max(0, Math.Min(JumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
else
{
MovementTime = StrainTime;
MovementDistance = JumpDistance;
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
}
if (lastLastObject != null && !(lastLastObject is Spinner))
@ -139,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition;
double scalingFactor = normalized_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
double scalingFactor = normalised_radius / slider.Radius; // lazySliderDistance is coded to be sensitive to scaling, this makes the maths easier with the thresholds being used.
for (int i = 1; i < slider.NestedHitObjects.Count; i++)
{
@ -167,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
else if (currMovementObj is SliderRepeat)
{
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalized_radius;
requiredMovement = normalised_radius;
}
if (currMovementLength > requiredMovement)

View File

@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
double currVelocity = osuCurrObj.JumpDistance / osuCurrObj.StrainTime;
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
if (osuLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
}
// As above, do the same for the previous hitobject.
double prevVelocity = osuLastObj.JumpDistance / osuLastObj.StrainTime;
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
if (osuLastLastObj.BaseObject is Slider && withSliders)
{
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime;
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
}
@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.JumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
}
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
@ -107,8 +107,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (Math.Max(prevVelocity, currVelocity) != 0)
{
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
prevVelocity = (osuLastObj.JumpDistance + osuLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime;
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
// Scale with ratio of difference compared to 0.5 * max dist.
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
@ -119,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.JumpDistance, osuLastObj.JumpDistance) / 100)), 2);
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
// Choose the largest bonus, multiplied by ratio.
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
@ -128,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
}
if (osuCurrObj.TravelTime != 0)
if (osuLastObj.TravelTime != 0)
{
// Reward sliders based on velocity.
sliderBonus = osuCurrObj.TravelDistance / osuCurrObj.TravelTime;
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
}
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.

View File

@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
// We also want to nerf stacks so that only the first object of the stack is accounted for.
double stackNerf = Math.Min(1.0, (osuPrevious.JumpDistance / scalingFactor) / 25.0);
double stackNerf = Math.Min(1.0, (osuPrevious.LazyJumpDistance / scalingFactor) / 25.0);
result += Math.Pow(0.8, i) * stackNerf * scalingFactor * jumpDistance / cumulativeStrainTime;
}

View File

@ -154,7 +154,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
double distance = Math.Min(single_spacing_threshold, osuCurrObj.TravelDistance + osuCurrObj.JumpDistance);
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
}

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -32,5 +32,7 @@
</array>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/AppIcon.appiconset</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>

View File

@ -62,7 +62,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
public void TestCultureInvariance()
{
var ruleset = new OsuRuleset().RulesetInfo;
var scoreInfo = new TestScoreInfo(ruleset);
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
var beatmap = new TestBeatmap(ruleset);
var score = new Score
{

View File

@ -1022,7 +1022,7 @@ namespace osu.Game.Tests.Beatmaps.IO
{
return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
{
OnlineScoreID = 2,
OnlineID = 2,
BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmapInfo.ID
}, new ImportScoreTest.TestArchiveReader());

View File

@ -809,7 +809,7 @@ namespace osu.Game.Tests.Database
// TODO: reimplement when we have score support in realm.
// return ImportScoreTest.LoadScoreIntoOsu(osu, new ScoreInfo
// {
// OnlineScoreID = 2,
// OnlineID = 2,
// Beatmap = beatmap,
// BeatmapInfoID = beatmap.ID
// }, new ImportScoreTest.TestArchiveReader());

View File

@ -52,6 +52,45 @@ namespace osu.Game.Tests.Database
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2));
}
[Test]
public void TestDefaultsPopulationRemovesExcess()
{
Assert.That(queryCount(), Is.EqualTo(0));
KeyBindingContainer testContainer = new TestKeyBindingContainer();
// Add some excess bindings for an action which only supports 1.
using (var realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
{
realm.Add(new RealmKeyBinding
{
Action = GlobalAction.Back,
KeyCombination = new KeyCombination(InputKey.A)
});
realm.Add(new RealmKeyBinding
{
Action = GlobalAction.Back,
KeyCombination = new KeyCombination(InputKey.S)
});
realm.Add(new RealmKeyBinding
{
Action = GlobalAction.Back,
KeyCombination = new KeyCombination(InputKey.D)
});
transaction.Commit();
}
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(3));
keyBindingStore.Register(testContainer, Enumerable.Empty<RulesetInfo>());
Assert.That(queryCount(GlobalAction.Back), Is.EqualTo(1));
}
private int queryCount(GlobalAction? match = null)
{
using (var realm = realmContextFactory.CreateContext())

View File

@ -13,7 +13,6 @@ using osu.Game.Online.Solo;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
@ -94,7 +93,7 @@ namespace osu.Game.Tests.Online
[Test]
public void TestDeserialiseSubmittableScoreWithEmptyMods()
{
var score = new SubmittableScore(new ScoreInfo { Ruleset = new OsuRuleset().RulesetInfo });
var score = new SubmittableScore(new ScoreInfo());
var deserialised = JsonConvert.DeserializeObject<SubmittableScore>(JsonConvert.SerializeObject(score));
@ -106,7 +105,6 @@ namespace osu.Game.Tests.Online
{
var score = new SubmittableScore(new ScoreInfo
{
Ruleset = new OsuRuleset().RulesetInfo,
Mods = new Mod[] { new OsuModDoubleTime { SpeedChange = { Value = 2 } } }
});

View File

@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using NUnit.Framework;
@ -12,8 +13,12 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Tests.Resources
{
@ -137,5 +142,63 @@ namespace osu.Game.Tests.Resources
}
}
}
/// <summary>
/// Create a test score model.
/// </summary>
/// <param name="ruleset">The ruleset for which the score was set against.</param>
/// <returns></returns>
public static ScoreInfo CreateTestScoreInfo(RulesetInfo ruleset = null) =>
CreateTestScoreInfo(CreateTestBeatmapSetInfo(1, new[] { ruleset ?? new OsuRuleset().RulesetInfo }).Beatmaps.First());
/// <summary>
/// Create a test score model.
/// </summary>
/// <param name="beatmap">The beatmap for which the score was set against.</param>
/// <returns></returns>
public static ScoreInfo CreateTestScoreInfo(BeatmapInfo beatmap) => new ScoreInfo
{
User = new APIUser
{
Id = 2,
Username = "peppy",
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
BeatmapInfo = beatmap,
Ruleset = beatmap.Ruleset,
RulesetID = beatmap.Ruleset.ID ?? 0,
Mods = new Mod[] { new TestModHardRock(), new TestModDoubleTime() },
TotalScore = 2845370,
Accuracy = 0.95,
MaxCombo = 999,
Position = 1,
Rank = ScoreRank.S,
Date = DateTimeOffset.Now,
Statistics = new Dictionary<HitResult, int>
{
[HitResult.Miss] = 1,
[HitResult.Meh] = 50,
[HitResult.Ok] = 100,
[HitResult.Good] = 200,
[HitResult.Great] = 300,
[HitResult.Perfect] = 320,
[HitResult.SmallTickHit] = 50,
[HitResult.SmallTickMiss] = 25,
[HitResult.LargeTickHit] = 100,
[HitResult.LargeTickMiss] = 50,
[HitResult.SmallBonus] = 10,
[HitResult.SmallBonus] = 50
},
};
private class TestModHardRock : ModHardRock
{
public override double ScoreMultiplier => 1;
}
private class TestModDoubleTime : ModDoubleTime
{
public override double ScoreMultiplier => 1;
}
}
}

View File

@ -40,7 +40,7 @@ namespace osu.Game.Tests.Scores.IO
Combo = 250,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineScoreID = 12345,
OnlineID = 12345,
};
var imported = await LoadScoreIntoOsu(osu, toImport);
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Scores.IO
Assert.AreEqual(toImport.Combo, imported.Combo);
Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineScoreID, imported.OnlineScoreID);
Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
}
finally
{
@ -163,12 +163,12 @@ namespace osu.Game.Tests.Scores.IO
{
var osu = LoadOsuIntoHost(host, true);
await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader());
await LoadScoreIntoOsu(osu, new ScoreInfo { OnlineID = 2 }, new TestArchiveReader());
var scoreManager = osu.Dependencies.Get<ScoreManager>();
// Note: A new score reference is used here since the import process mutates the original object to set an ID
Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineScoreID = 2 }));
Assert.That(scoreManager.IsAvailableLocally(new ScoreInfo { OnlineID = 2 }));
}
finally
{

View File

@ -44,24 +44,6 @@ namespace osu.Game.Tests.Scores.IO
Assert.That(score1, Is.EqualTo(score2));
}
[Test]
public void TestNonMatchingByHash()
{
ScoreInfo score1 = new ScoreInfo { Hash = "a" };
ScoreInfo score2 = new ScoreInfo { Hash = "b" };
Assert.That(score1, Is.Not.EqualTo(score2));
}
[Test]
public void TestMatchingByHash()
{
ScoreInfo score1 = new ScoreInfo { Hash = "a" };
ScoreInfo score2 = new ScoreInfo { Hash = "a" };
Assert.That(score1, Is.EqualTo(score2));
}
[Test]
public void TestNonMatchingByNull()
{

View File

@ -18,7 +18,6 @@ using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Scoring;
@ -28,7 +27,6 @@ using osu.Game.Screens.Play;
using osu.Game.Screens.Play.PlayerSettings;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Select;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Graphics;
@ -229,12 +227,7 @@ namespace osu.Game.Tests.Visual.Background
FadeAccessibleResults results = null;
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
{
User = new APIUser { Username = "osu!" },
BeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo,
Ruleset = Ruleset.Value,
})));
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(TestResources.CreateTestScoreInfo())));
AddUntilStep("Wait for results is current", () => results.IsCurrentScreen());

View File

@ -6,12 +6,14 @@ using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses;
@ -227,7 +229,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
new BasicScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer
Child = new ReverseChildIDFillFlowContainer<Drawable>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
@ -248,6 +250,17 @@ namespace osu.Game.Tests.Visual.Beatmaps
}
[Test]
public void TestNormal() => createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
public void TestNormal()
{
createTestCase(beatmapSetInfo => new BeatmapCard(beatmapSetInfo));
AddToggleStep("toggle expanded state", expanded =>
{
var card = this.ChildrenOfType<BeatmapCard>().Last();
if (!card.Expanded.Disabled)
card.Expanded.Value = expanded;
});
AddToggleStep("disable/enable expansion", disabled => this.ChildrenOfType<BeatmapCard>().ForEach(card => card.Expanded.Disabled = disabled));
}
}
}

View File

@ -10,6 +10,7 @@ using osu.Game.Beatmaps;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps.IO;
@ -89,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
confirmEditingBeatmap(() => targetDifficulty);
AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
AddUntilStep("wait for drawable ruleset", () => Editor.ChildrenOfType<DrawableRuleset>().SingleOrDefault()?.IsLoaded == true);
AddStep("paste object", () => Editor.Paste());
if (sameRuleset)

View File

@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Editing
public void TestCreateNewBeatmap()
{
AddStep("save beatmap", () => Editor.Save());
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0);
AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.IsManaged);
AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false);
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit;
@ -44,6 +45,7 @@ namespace osu.Game.Tests.Visual.Editing
protected override void LoadEditor()
{
Beatmap.Value = beatmaps.GetWorkingBeatmap(importedBeatmapSet.Beatmaps.First(b => b.RulesetID == 0));
SelectedMods.Value = new[] { new ModCinema() };
base.LoadEditor();
}
@ -67,6 +69,7 @@ namespace osu.Game.Tests.Visual.Editing
var background = this.ChildrenOfType<BackgroundScreenBeatmap>().Single();
return background.Colour == Color4.DarkGray && background.BlurAmount.Value == 0;
});
AddAssert("no mods selected", () => SelectedMods.Value.Count == 0);
}
[Test]

View File

@ -26,6 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
AddAssert("total number of results == 1", () =>
{
var score = new ScoreInfo();
((FailPlayer)Player).ScoreProcessor.PopulateScore(score);
return score.Statistics.Values.Sum() == 1;

View File

@ -251,7 +251,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test]
public void TestMutedNotificationMuteButton()
{
addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value);
addVolumeSteps("mute button", () =>
{
// Importantly, in the case the volume is muted but the user has a volume level set, it should be retained.
audioManager.VolumeTrack.Value = 0.5f;
volumeOverlay.IsMuted.Value = true;
}, () => !volumeOverlay.IsMuted.Value && audioManager.VolumeTrack.Value == 0.5f);
}
/// <remarks>

View File

@ -164,7 +164,7 @@ namespace osu.Game.Tests.Visual.Gameplay
private ScoreInfo getScoreInfo(bool replayAvailable)
{
return new APIScoreInfo
return new APIScore
{
OnlineID = 2553163309,
RulesetID = 0,

View File

@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addItem(Func<BeatmapInfo> beatmap)
{
AddStep("click edit button", () =>
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});

View File

@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Tests.Beatmaps;
using osuTK;
@ -172,6 +174,39 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("password icon hidden", () => Precision.AlmostEquals(0, drawableRoom.ChildrenOfType<DrawableRoom.PasswordProtectedIcon>().Single().Alpha));
}
[Test]
public void TestMultiplayerRooms()
{
AddStep("create rooms", () => Child = new FillFlowContainer
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5),
Children = new[]
{
new DrawableMatchRoom(new Room
{
Name = { Value = "A host-only room" },
QueueMode = { Value = QueueMode.HostOnly },
Type = { Value = MatchType.HeadToHead }
}),
new DrawableMatchRoom(new Room
{
Name = { Value = "An all-players, team-versus room" },
QueueMode = { Value = QueueMode.AllPlayers },
Type = { Value = MatchType.TeamVersus }
}),
new DrawableMatchRoom(new Room
{
Name = { Value = "A round-robin room" },
QueueMode = { Value = QueueMode.AllPlayersRoundRobin },
Type = { Value = MatchType.HeadToHead }
}),
}
});
}
private DrawableRoom createLoungeRoom(Room room)
{
room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 };

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestNonEditableNonSelectable()
{
createPlaylist(false, false);
createPlaylist();
moveToItem(0);
assertHandleVisibility(0, false);
@ -61,7 +62,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditable()
{
createPlaylist(true, false);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
});
moveToItem(0);
assertHandleVisibility(0, true);
@ -74,7 +79,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestMarkInvalid()
{
createPlaylist(true, true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid());
@ -87,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectable()
{
createPlaylist(false, true);
createPlaylist(p => p.AllowSelection = true);
moveToItem(0);
assertHandleVisibility(0, false);
@ -101,7 +111,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditableSelectable()
{
createPlaylist(true, true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0);
assertHandleVisibility(0, true);
@ -115,7 +130,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectionNotLostAfterRearrangement()
{
createPlaylist(true, true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
@ -128,95 +148,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 1 is selected", () => playlist.SelectedItem.Value == playlist.Items[1]);
}
[Test]
public void TestItemRemovedOnDeletion()
{
PlaylistItem selectedItem = null;
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
[Test]
public void TestNextItemSelectedAfterDeletion()
{
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestLastItemSelectedAfterLastItemDeleted()
{
createPlaylist(true, true);
AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
AddStep("scroll to bottom", () => playlist.ChildrenOfType<ScrollContainer<Drawable>>().First().ScrollToEnd(false));
moveToItem(19);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(19);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
}
[Test]
public void TestSelectionResetWhenAllItemsDeleted()
{
createPlaylist(true, true);
AddStep("remove all but one item", () =>
{
playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
// Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
// [Test]
public void TestNextItemSelectedAfterExternalDeletion()
{
createPlaylist(true, true);
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestChangeBeatmapAndRemove()
{
createPlaylist(true, true);
AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30);
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
}
[Test]
public void TestDownloadButtonHiddenWhenBeatmapExists()
{
@ -224,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait());
createPlaylist(beatmap);
createPlaylistWithBeatmaps(beatmap);
assertDownloadButtonVisible(false);
@ -247,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var byChecksum = CreateAPIBeatmap();
byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally.
createPlaylist(byOnlineId, byChecksum);
createPlaylistWithBeatmaps(byOnlineId, byChecksum);
AddAssert("download buttons shown", () => playlist.ChildrenOfType<BeatmapDownloadButton>().All(d => d.IsPresent));
}
@ -261,7 +192,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
beatmap.BeatmapSet.HasExplicitContent = true;
createPlaylist(beatmap);
createPlaylistWithBeatmaps(beatmap);
}
[Test]
@ -269,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist(false, false)
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -312,11 +243,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(true)]
public void TestWithOwner(bool withOwner)
{
createPlaylist(false, false, withOwner);
createPlaylist(p => p.ShowItemOwners = withOwner);
AddAssert("owner visible", () => playlist.ChildrenOfType<UpdateableAvatar>().All(a => a.IsPresent == withOwner));
}
[Test]
public void TestWithAllButtonsEnabled()
{
createPlaylist(p =>
{
p.AllowDeletion = true;
p.AllowShowingResults = true;
p.AllowEditing = true;
});
}
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DifficultyIcon>().ElementAt(index), offset));
@ -326,12 +268,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
InputManager.MoveMouseTo(item.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().Single(), offset);
});
private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
});
private void assertHandleVisibility(int index, bool visible)
=> AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible);
@ -340,17 +276,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
=> AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == visible);
private void createPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
private void createPlaylist(Action<TestPlaylist> setupPlaylist = null)
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner)
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
setupPlaylist?.Invoke(playlist);
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem
@ -386,11 +324,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
private void createPlaylist(params IBeatmapInfo[] beatmaps)
private void createPlaylistWithBeatmaps(params IBeatmapInfo[] beatmaps)
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist(false, false)
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -423,11 +361,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestPlaylist : DrawableRoomPlaylist
{
public new IReadOnlyDictionary<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap;
public TestPlaylist(bool allowEdit, bool allowSelection, bool showItemOwner = false)
: base(allowEdit, allowSelection, showItemOwner: showItemOwner)
{
}
}
}
}

View File

@ -7,6 +7,7 @@ using NUnit.Framework;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osuTK.Input;
@ -74,11 +75,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
}
[Test]
public void TestAddItemsAsHost()
{
addItem(() => OtherBeatmap);
AddAssert("playlist contains two items", () => Client.APIRoom?.Playlist.Count == 2);
}
private void selectNewItem(Func<BeatmapInfo> beatmap)
{
AddStep("click edit button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton);
InputManager.MoveMouseTo(this.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().First());
InputManager.Click(MouseButton.Left);
});
@ -90,5 +99,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID);
}
private void addItem(Func<BeatmapInfo> beatmap)
{
AddStep("click add button", () =>
{
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen.AddItemButton>().Single());
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for song select", () => CurrentSubScreen is Screens.Select.SongSelect select && select.BeatmapSetsLoaded);
AddStep("select other beatmap", () => ((Screens.Select.SongSelect)CurrentSubScreen).FinaliseSelection(beatmap()));
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
}
}
}

View File

@ -416,8 +416,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("Enter song select", () =>
{
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap();
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value);
});
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);

View File

@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
public new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null)
: base(room, beatmap, ruleset)
: base(room, null, beatmap, ruleset)
{
}
}

View File

@ -0,0 +1,168 @@
// 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.Audio;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestSceneMultiplayerQueueList : MultiplayerTestScene
{
private MultiplayerQueueList playlist;
[Cached(typeof(UserLookupCache))]
private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache();
private BeatmapManager beatmaps;
private RulesetStore rulesets;
private BeatmapSetInfo importedSet;
private BeatmapInfo importedBeatmap;
[BackgroundDependencyLoader]
private void load(GameHost host, AudioManager audio)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, Resources, host, Beatmap.Default));
}
public override void SetUpSteps()
{
base.SetUpSteps();
AddStep("create playlist", () =>
{
Child = playlist = new MultiplayerQueueList
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300),
SelectedItem = { BindTarget = Client.CurrentMatchPlayingItem },
Items = { BindTarget = Client.APIRoom!.Playlist }
};
});
AddStep("import beatmap", () =>
{
beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait();
importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First();
importedBeatmap = importedSet.Beatmaps.First(b => b.RulesetID == 0);
});
AddStep("change to all players mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
}
[Test]
public void TestDeleteButtonAlwaysVisibleForHost()
{
AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(1, true);
addPlaylistItem(() => 1234);
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestDeleteButtonOnlyVisibleForItemOwnerIfNotHost()
{
AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
AddStep("join other user", () => Client.AddUser(new APIUser { Id = 1234 }));
AddStep("set other user as host", () => Client.TransferHost(1234));
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(1, true);
addPlaylistItem(() => 1234);
assertDeleteButtonVisibility(2, false);
AddStep("set local user as host", () => Client.TransferHost(API.LocalUser.Value.OnlineID));
assertDeleteButtonVisibility(1, true);
assertDeleteButtonVisibility(2, true);
}
[Test]
public void TestCurrentItemDoesNotHaveDeleteButton()
{
AddStep("set all players queue mode", () => Client.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayers }));
AddUntilStep("wait for queue mode change", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers);
assertDeleteButtonVisibility(0, false);
addPlaylistItem(() => API.LocalUser.Value.OnlineID);
assertDeleteButtonVisibility(0, false);
assertDeleteButtonVisibility(1, true);
// Run through gameplay.
AddStep("set state to ready", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Ready));
AddUntilStep("local state is ready", () => Client.LocalUser?.State == MultiplayerUserState.Ready);
AddStep("start match", () => Client.StartMatch());
AddUntilStep("match started", () => Client.LocalUser?.State == MultiplayerUserState.WaitingForLoad);
AddStep("set state to loaded", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.Loaded));
AddUntilStep("local state is playing", () => Client.LocalUser?.State == MultiplayerUserState.Playing);
AddStep("set state to finished play", () => Client.ChangeUserState(API.LocalUser.Value.Id, MultiplayerUserState.FinishedPlay));
AddUntilStep("local state is results", () => Client.LocalUser?.State == MultiplayerUserState.Results);
assertDeleteButtonVisibility(1, false);
}
private void addPlaylistItem(Func<int> userId)
{
long itemId = -1;
AddStep("add playlist item", () =>
{
MultiplayerPlaylistItem item = new MultiplayerPlaylistItem(new PlaylistItem
{
Beatmap = { Value = importedBeatmap },
BeatmapID = importedBeatmap.OnlineID ?? -1,
});
Client.AddUserPlaylistItem(userId(), item);
itemId = item.ID;
});
AddUntilStep("item arrived in playlist", () => playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Any(i => i.Model.ID == itemId));
}
private void deleteItem(int index)
{
OsuRearrangeableListItem<PlaylistItem> item = null;
AddStep($"move mouse to delete button {index}", () =>
{
item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0));
});
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddUntilStep("item removed from playlist", () => !playlist.ChildrenOfType<RearrangeableListItem<PlaylistItem>>().Contains(item));
}
private void assertDeleteButtonVisibility(int index, bool visible)
=> AddUntilStep($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(index).Alpha > 0) == visible);
}
}

View File

@ -1,13 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -22,20 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
BeatmapInfo = beatmapInfo,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineScoreID = 12345,
Ruleset = rulesetInfo,
};
var score = TestResources.CreateTestScoreInfo(beatmapInfo);
PlaylistItem playlistItem = new PlaylistItem
{

View File

@ -1,15 +1,13 @@
// 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 NUnit.Framework;
using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer
{
@ -26,20 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{
var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = new ScoreInfo
{
Rank = ScoreRank.B,
TotalScore = 987654,
Accuracy = 0.8,
MaxCombo = 500,
Combo = 250,
BeatmapInfo = beatmapInfo,
User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now,
OnlineScoreID = 12345,
Ruleset = rulesetInfo,
};
var score = TestResources.CreateTestScoreInfo(beatmapInfo);
PlaylistItem playlistItem = new PlaylistItem
{

View File

@ -0,0 +1,188 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables;
using osu.Game.Database;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Tests.Beatmaps;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Multiplayer
{
public class TestScenePlaylistsRoomSettingsPlaylist : OsuManualInputManagerTestScene
{
private TestPlaylist playlist;
[Cached(typeof(UserLookupCache))]
private readonly TestUserLookupCache userLookupCache = new TestUserLookupCache();
[Test]
public void TestItemRemovedOnDeletion()
{
PlaylistItem selectedItem = null;
createPlaylist();
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("retrieve selection", () => selectedItem = playlist.SelectedItem.Value);
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item removed", () => !playlist.Items.Contains(selectedItem));
}
[Test]
public void TestNextItemSelectedAfterDeletion()
{
createPlaylist();
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestLastItemSelectedAfterLastItemDeleted()
{
createPlaylist();
AddWaitStep("wait for flow", 5); // Items may take 1 update frame to flow. A wait count of 5 is guaranteed to result in the flow being updated as desired.
AddStep("scroll to bottom", () => playlist.ChildrenOfType<ScrollContainer<Drawable>>().First().ScrollToEnd(false));
moveToItem(19);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(19);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("item 18 is selected", () => playlist.SelectedItem.Value == playlist.Items[18]);
}
[Test]
public void TestSelectionResetWhenAllItemsDeleted()
{
createPlaylist();
AddStep("remove all but one item", () =>
{
playlist.Items.RemoveRange(1, playlist.Items.Count - 1);
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
AddAssert("no item selected", () => playlist.SelectedItem.Value == null);
}
// Todo: currently not possible due to bindable list shortcomings (https://github.com/ppy/osu-framework/issues/3081)
// [Test]
public void TestNextItemSelectedAfterExternalDeletion()
{
createPlaylist();
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));
AddStep("remove item 0", () => playlist.Items.RemoveAt(0));
AddAssert("item 0 is selected", () => playlist.SelectedItem.Value == playlist.Items[0]);
}
[Test]
public void TestChangeBeatmapAndRemove()
{
createPlaylist();
AddStep("change beatmap of first item", () => playlist.Items[0].BeatmapID = 30);
moveToDeleteButton(0);
AddStep("click delete button", () => InputManager.Click(MouseButton.Left));
}
private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DifficultyIcon>().ElementAt(index), offset));
private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () =>
{
var item = playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>>().ElementAt(index);
InputManager.MoveMouseTo(item.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(0), offset);
});
private void createPlaylist()
{
AddStep("create playlist", () =>
{
Child = playlist = new TestPlaylist
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(500, 300)
};
for (int i = 0; i < 20; i++)
{
playlist.Items.Add(new PlaylistItem
{
ID = i,
OwnerID = 2,
Beatmap =
{
Value = i % 2 == 1
? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo
: new BeatmapInfo
{
Metadata = new BeatmapMetadata
{
Artist = "Artist",
Author = new APIUser { Username = "Creator name here" },
Title = "Long title used to check background colour",
},
BeatmapSet = new BeatmapSetInfo()
}
},
Ruleset = { Value = new OsuRuleset().RulesetInfo },
RequiredMods =
{
new OsuModHardRock(),
new OsuModDoubleTime(),
new OsuModAutoplay()
}
});
}
});
AddUntilStep("wait for items to load", () => playlist.ItemMap.Values.All(i => i.IsLoaded));
}
private class TestPlaylist : PlaylistsRoomSettingsPlaylist
{
public new IReadOnlyDictionary<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap;
public TestPlaylist()
{
AllowSelection = true;
}
}
}
}

View File

@ -77,7 +77,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddAssert("user state arrived", () => client.Room?.Users.FirstOrDefault()?.MatchState is TeamVersusUserState);
}
@ -162,13 +162,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
}
});
AddAssert("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("room type is head to head", () => client.Room?.Settings.MatchType == MatchType.HeadToHead);
AddUntilStep("team displays are not displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam == null));
AddStep("change to team vs", () => client.ChangeSettings(matchType: MatchType.TeamVersus));
AddAssert("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("room type is team vs", () => client.Room?.Settings.MatchType == MatchType.TeamVersus);
AddUntilStep("team displays are displaying teams", () => multiplayerScreenStack.ChildrenOfType<TeamDisplay>().All(d => d.DisplayedTeam != null));
}

View File

@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation
imported = Game.ScoreManager.Import(new ScoreInfo
{
Hash = Guid.NewGuid().ToString(),
OnlineScoreID = i,
OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
}).Result.Value;

View File

@ -47,9 +47,9 @@ namespace osu.Game.Tests.Visual.Online
var allScores = new APIScoresCollection
{
Scores = new List<APIScoreInfo>
Scores = new List<APIScore>
{
new APIScoreInfo
new APIScore
{
User = new APIUser
{
@ -74,7 +74,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567890,
Accuracy = 1,
},
new APIScoreInfo
new APIScore
{
User = new APIUser
{
@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234789,
Accuracy = 0.9997,
},
new APIScoreInfo
new APIScore
{
User = new APIUser
{
@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 12345678,
Accuracy = 0.9854,
},
new APIScoreInfo
new APIScore
{
User = new APIUser
{
@ -143,7 +143,7 @@ namespace osu.Game.Tests.Visual.Online
TotalScore = 1234567,
Accuracy = 0.8765,
},
new APIScoreInfo
new APIScore
{
User = new APIUser
{
@ -166,7 +166,7 @@ namespace osu.Game.Tests.Visual.Online
var myBestScore = new APIScoreWithPosition
{
Score = new APIScoreInfo
Score = new APIScore
{
User = new APIUser
{
@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual.Online
var myBestScoreWithNullPosition = new APIScoreWithPosition
{
Score = new APIScoreInfo
Score = new APIScore
{
User = new APIUser
{
@ -212,9 +212,9 @@ namespace osu.Game.Tests.Visual.Online
var oneScore = new APIScoresCollection
{
Scores = new List<APIScoreInfo>
Scores = new List<APIScore>
{
new APIScoreInfo
new APIScore
{
User = new APIUser
{

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.Online
{
public TestSceneUserProfileScores()
{
var firstScore = new APIScoreInfo
var firstScore = new APIScore
{
PP = 1047.21,
Rank = ScoreRank.SH,
@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9813
};
var secondScore = new APIScoreInfo
var secondScore = new APIScore
{
PP = 134.32,
Rank = ScoreRank.A,
@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.998546
};
var thirdScore = new APIScoreInfo
var thirdScore = new APIScore
{
PP = 96.83,
Rank = ScoreRank.S,
@ -81,7 +81,7 @@ namespace osu.Game.Tests.Visual.Online
Accuracy = 0.9726
};
var noPPScore = new APIScoreInfo
var noPPScore = new APIScore
{
Rank = ScoreRank.B,
Beatmap = new APIBeatmap

View File

@ -22,14 +22,17 @@ using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Playlists;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Playlists
{
public class TestScenePlaylistsResultsScreen : ScreenTestScene
{
private const int scores_per_result = 10;
private const int real_user_position = 200;
private TestResultsScreen resultsScreen;
private int currentScoreId;
private bool requestComplete;
private int totalCount;
@ -37,7 +40,7 @@ namespace osu.Game.Tests.Visual.Playlists
[SetUp]
public void Setup() => Schedule(() =>
{
currentScoreId = 0;
currentScoreId = 1;
requestComplete = false;
totalCount = 0;
bindHandler();
@ -50,13 +53,17 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(userScore: userScore);
});
createResults(() => userScore);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
AddAssert($"score panel position is {real_user_position}",
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).ScorePosition.Value == real_user_position);
}
[Test]
@ -74,14 +81,16 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(true, userScore);
});
createResults(() => userScore);
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded);
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
}
[Test]
@ -123,7 +132,9 @@ namespace osu.Game.Tests.Visual.Playlists
AddStep("bind user score info handler", () =>
{
userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ };
userScore = TestResources.CreateTestScoreInfo();
userScore.OnlineID = currentScoreId++;
bindHandler(userScore: userScore);
});
@ -230,12 +241,12 @@ namespace osu.Game.Tests.Visual.Playlists
{
var multiplayerUserScore = new MultiplayerScore
{
ID = (int)(userScore.OnlineScoreID ?? currentScoreId++),
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
Accuracy = userScore.Accuracy,
EndedAt = userScore.Date,
Passed = userScore.Passed,
Rank = userScore.Rank,
Position = 200,
Position = real_user_position,
MaxCombo = userScore.MaxCombo,
TotalScore = userScore.TotalScore,
User = userScore.User,

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
@ -13,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -22,13 +24,18 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestShowPanel()
{
AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo)));
AddStep("show example score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), TestResources.CreateTestScoreInfo()));
}
[Test]
public void TestExcessMods()
{
AddStep("show excess mods score", () => showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), new TestScoreInfo(new OsuRuleset().RulesetInfo, true)));
AddStep("show excess mods score", () =>
{
var score = TestResources.CreateTestScoreInfo();
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
showPanel(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), score);
});
}
private void showPanel(WorkingBeatmap workingBeatmap, ScoreInfo score)

View File

@ -20,6 +20,7 @@ using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -34,21 +35,21 @@ namespace osu.Game.Tests.Visual.Ranking
{
var author = new APIUser { Username = "mapper_name" };
AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
BeatmapInfo = createTestBeatmap(author)
}));
AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author))));
}
[Test]
public void TestExcessMods()
{
var author = new APIUser { Username = "mapper_name" };
AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true)
AddStep("show excess mods score", () =>
{
BeatmapInfo = createTestBeatmap(author)
}));
var author = new APIUser { Username = "mapper_name" };
var score = TestResources.CreateTestScoreInfo(createTestBeatmap(author));
score.Mods = score.BeatmapInfo.Ruleset.CreateInstance().CreateAllMods().ToArray();
showPanel(score);
});
AddAssert("mapper name present", () => this.ChildrenOfType<OsuSpriteText>().Any(spriteText => spriteText.Current.Value == "mapper_name"));
}
@ -56,10 +57,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestMapWithUnknownMapper()
{
AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
BeatmapInfo = createTestBeatmap(new APIUser())
}));
AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser()))));
AddAssert("mapped by text not present", () =>
this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by")));
@ -77,12 +75,12 @@ namespace osu.Game.Tests.Visual.Ranking
var mods = new Mod[] { ruleset.GetAutoplayMod() };
var beatmap = createTestBeatmap(new APIUser());
showPanel(new TestScoreInfo(ruleset.RulesetInfo)
{
Mods = mods,
BeatmapInfo = beatmap,
Date = default,
});
var score = TestResources.CreateTestScoreInfo(beatmap);
score.Mods = mods;
score.Date = default;
showPanel(score);
});
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any());

View File

@ -5,8 +5,8 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"),
},
new ExpandedPanelTopContent(new TestScoreInfo(new OsuRuleset().RulesetInfo).User),
new ExpandedPanelTopContent(TestResources.CreateTestScoreInfo().User),
}
};
}

View File

@ -15,12 +15,12 @@ using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
@ -72,11 +72,10 @@ namespace osu.Game.Tests.Visual.Ranking
{
TestResultsScreen screen = null;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
Accuracy = accuracy,
Rank = rank
};
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = accuracy;
score.Rank = rank;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
@ -204,7 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking
{
DelayedFetchResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000)));
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(TestResources.CreateTestScoreInfo(), 3000)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () =>
{
@ -237,9 +236,9 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("download button is enabled", () => screen.ChildrenOfType<DownloadButton>().Last().Enabled.Value);
}
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? new TestScoreInfo(new OsuRuleset().RulesetInfo));
private TestResultsScreen createResultsScreen(ScoreInfo score = null) => new TestResultsScreen(score ?? TestResources.CreateTestScoreInfo());
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(TestResources.CreateTestScoreInfo());
private class TestResultsContainer : Container
{
@ -282,7 +281,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++)
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
score.Hash = $"test{i}";
scores.Add(score);
@ -316,7 +315,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++)
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i;
scores.Add(score);
}

View File

@ -3,10 +3,10 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Ranking
{
@ -17,7 +17,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestDRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.5, Rank = ScoreRank.D };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.5;
score.Rank = ScoreRank.D;
addPanelStep(score);
}
@ -25,7 +27,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestCRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.75, Rank = ScoreRank.C };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.75;
score.Rank = ScoreRank.C;
addPanelStep(score);
}
@ -33,7 +37,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestBRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.85, Rank = ScoreRank.B };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.85;
score.Rank = ScoreRank.B;
addPanelStep(score);
}
@ -41,7 +47,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestARank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
addPanelStep(score);
}
@ -49,7 +57,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.975, Rank = ScoreRank.S };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.975;
score.Rank = ScoreRank.S;
addPanelStep(score);
}
@ -57,7 +67,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAlmostSSRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.9999, Rank = ScoreRank.S };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.9999;
score.Rank = ScoreRank.S;
addPanelStep(score);
}
@ -65,7 +77,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSSRank()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 1, Rank = ScoreRank.X };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 1;
score.Rank = ScoreRank.X;
addPanelStep(score);
}
@ -73,7 +87,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAllHitResults()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Statistics = { [HitResult.Perfect] = 350, [HitResult.Ok] = 200 } };
var score = TestResources.CreateTestScoreInfo();
score.Statistics[HitResult.Perfect] = 350;
score.Statistics[HitResult.Ok] = 200;
addPanelStep(score);
}
@ -81,7 +97,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestContractedPanel()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
addPanelStep(score, PanelState.Contracted);
}
@ -89,7 +107,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestExpandAndContract()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { Accuracy = 0.925, Rank = ScoreRank.A };
var score = TestResources.CreateTestScoreInfo();
score.Accuracy = 0.925;
score.Rank = ScoreRank.A;
addPanelStep(score, PanelState.Contracted);
AddWaitStep("wait for transition", 10);

View File

@ -7,9 +7,9 @@ using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking
@ -29,14 +29,14 @@ namespace osu.Game.Tests.Visual.Ranking
{
createListStep(() => new ScorePanelList
{
SelectedScore = { Value = new TestScoreInfo(new OsuRuleset().RulesetInfo) }
SelectedScore = { Value = TestResources.CreateTestScoreInfo() }
});
}
[Test]
public void TestAddPanelAfterSelectingScore()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var score = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList
{
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddPanelBeforeSelectingScore()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var score = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo));
list.AddScore(TestResources.CreateTestScoreInfo());
});
assertFirstPanelCentred();
@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyScoresAfterExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add many scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1));
});
assertScoreState(initialScore, true);
@ -107,7 +107,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyScoresBeforeExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add scores", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1));
});
assertScoreState(initialScore, true);
@ -130,7 +130,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddManyPanelsOnBothSidesOfExpandedPanel()
{
var initialScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var initialScore = TestResources.CreateTestScoreInfo();
createListStep(() => new ScorePanelList());
@ -143,10 +143,10 @@ namespace osu.Game.Tests.Visual.Ranking
AddStep("add scores after", () =>
{
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore - i - 1 });
list.AddScore(createScoreForTotalScore(initialScore.TotalScore - i - 1));
for (int i = 0; i < 20; i++)
list.AddScore(new TestScoreInfo(new OsuRuleset().RulesetInfo) { TotalScore = initialScore.TotalScore + i + 1 });
list.AddScore(createScoreForTotalScore(initialScore.TotalScore + i + 1));
});
assertScoreState(initialScore, true);
@ -156,11 +156,11 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestSelectMultipleScores()
{
var firstScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var secondScore = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var firstScore = TestResources.CreateTestScoreInfo();
var secondScore = TestResources.CreateTestScoreInfo();
firstScore.User.Username = "A";
secondScore.User.Username = "B";
firstScore.UserString = "A";
secondScore.UserString = "B";
createListStep(() => new ScorePanelList());
@ -190,7 +190,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestAddScoreImmediately()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
var score = TestResources.CreateTestScoreInfo();
createListStep(() =>
{
@ -206,9 +206,14 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestKeyboardNavigation()
{
var lowestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 100 };
var middleScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 200 };
var highestScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { MaxCombo = 300 };
var lowestScore = TestResources.CreateTestScoreInfo();
lowestScore.MaxCombo = 100;
var middleScore = TestResources.CreateTestScoreInfo();
middleScore.MaxCombo = 200;
var highestScore = TestResources.CreateTestScoreInfo();
highestScore.MaxCombo = 300;
createListStep(() => new ScorePanelList());
@ -270,6 +275,13 @@ namespace osu.Game.Tests.Visual.Ranking
assertExpandedPanelCentred();
}
private ScoreInfo createScoreForTotalScore(long totalScore)
{
var score = TestResources.CreateTestScoreInfo();
score.TotalScore = totalScore;
return score;
}
private void createListStep(Func<ScorePanelList> creationFunc)
{
AddStep("create list", () => Child = list = creationFunc().With(d =>

View File

@ -6,11 +6,11 @@ using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Tests.Resources;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
@ -20,10 +20,8 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithTimeStatistics()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents()
};
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents();
loadPanel(score);
}
@ -31,10 +29,8 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithPositionStatistics()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
HitEvents = createPositionDistributedHitEvents()
};
var score = TestResources.CreateTestScoreInfo();
score.HitEvents = createPositionDistributedHitEvents();
loadPanel(score);
}
@ -42,7 +38,7 @@ namespace osu.Game.Tests.Visual.Ranking
[Test]
public void TestScoreWithoutStatistics()
{
loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo));
loadPanel(TestResources.CreateTestScoreInfo());
}
[Test]

View File

@ -835,12 +835,7 @@ namespace osu.Game.Tests.Visual.SongSelect
// this beatmap change should be overridden by the present.
Beatmap.Value = manager.GetWorkingBeatmap(getSwitchBeatmap());
songSelect.PresentScore(new ScoreInfo
{
User = new APIUser { Username = "woo" },
BeatmapInfo = getPresentBeatmap(),
Ruleset = getPresentBeatmap().Ruleset
});
songSelect.PresentScore(TestResources.CreateTestScoreInfo(getPresentBeatmap()));
});
AddUntilStep("wait for results screen presented", () => !songSelect.IsCurrentScreen());

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
var score = new ScoreInfo
{
OnlineScoreID = i,
OnlineID = i,
BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmapInfo.ID,
Accuracy = RNG.NextDouble(),
@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
});
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != scoreBeingDeleted.OnlineScoreID));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
}
[Test]
@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{
AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineScoreID != importedScores[0].OnlineScoreID));
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != importedScores[0].OnlineID));
}
}
}

View File

@ -15,6 +15,8 @@ namespace osu.Game.Beatmaps
public int ID { get; set; }
public bool IsManaged => ID > 0;
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
public float OverallDifficulty { get; set; } = DEFAULT_DIFFICULTY;

View File

@ -135,7 +135,7 @@ namespace osu.Game.Beatmaps
var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available.
if (localBeatmapInfo == null || localBeatmapInfo.ID == 0 || localRulesetInfo == null)
if (localBeatmapInfo?.IsManaged != true || localRulesetInfo == null)
{
// If not, fall back to the existing star difficulty (e.g. from an online source).
return Task.FromResult<StarDifficulty?>(new StarDifficulty(beatmapInfo.StarRating, (beatmapInfo as IBeatmapOnlineInfo)?.MaxCombo ?? 0));

View File

@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int BeatmapVersion;
private int? onlineID;

View File

@ -20,6 +20,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public string Title { get; set; } = string.Empty;
[JsonProperty("title_unicode")]

View File

@ -11,6 +11,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int BeatmapSetInfoID { get; set; }
public int FileInfoID { get; set; }

View File

@ -18,6 +18,8 @@ namespace osu.Game.Beatmaps
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
private int? onlineID;
[Column("OnlineBeatmapSetID")]

View File

@ -31,10 +31,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public class BeatmapCard : OsuClickableContainer
{
public const float TRANSITION_DURATION = 400;
public const float CORNER_RADIUS = 10;
public Bindable<bool> Expanded { get; } = new BindableBool();
private const float width = 408;
private const float height = 100;
private const float corner_radius = 10;
private const float icon_area_width = 30;
private readonly APIBeatmapSet beatmapSet;
@ -42,6 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly BeatmapDownloadTracker downloadTracker;
private BeatmapCardContent content = null!;
private BeatmapCardThumbnail thumbnail = null!;
private Container rightAreaBackground = null!;
@ -73,242 +77,247 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
Width = width;
Height = height;
CornerRadius = corner_radius;
Masking = true;
FillFlowContainer leftIconArea;
GridContainer titleContainer;
GridContainer artistContainer;
InternalChildren = new Drawable[]
InternalChild = content = new BeatmapCardContent(height)
{
downloadTracker,
rightAreaBackground = new Container
MainContent = new Container
{
RelativeSizeAxes = Axes.Y,
Width = icon_area_width + 2 * corner_radius,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// workaround for masking artifacts at the top & bottom of card,
// which become especially visible on downloaded beatmaps (when the icon area has a lime background).
Padding = new MarginPadding { Vertical = 1 },
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White
},
},
thumbnail = new BeatmapCardThumbnail(beatmapSet)
{
Name = @"Left (icon) area",
Size = new Vector2(height),
Padding = new MarginPadding { Right = corner_radius },
Child = leftIconArea = new FillFlowContainer
{
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
new Container
{
Name = @"Right (button) area",
Width = 30,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Padding = new MarginPadding { Vertical = 17.5f },
Child = rightAreaButtons = new Container<BeatmapCardIconButton>
{
RelativeSizeAxes = Axes.Both,
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet)
{
Current = favouriteState,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
new DownloadButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State }
},
new GoToBeatmapButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State }
}
}
}
},
mainContent = new Container
{
Name = @"Main content",
X = height - corner_radius,
Height = height,
CornerRadius = corner_radius,
Masking = true,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
downloadTracker,
rightAreaBackground = new Container
{
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
RelativeSizeAxes = Axes.Y,
Width = icon_area_width + 2 * CORNER_RADIUS,
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
// workaround for masking artifacts at the top & bottom of card,
// which become especially visible on downloaded beatmaps (when the icon area has a lime background).
Padding = new MarginPadding { Vertical = 1 },
Child = new Box
{
Horizontal = 10,
Vertical = 4
RelativeSizeAxes = Axes.Both,
Colour = Colour4.White
},
Direction = FillDirection.Vertical,
Children = new Drawable[]
},
thumbnail = new BeatmapCardThumbnail(beatmapSet)
{
Name = @"Left (icon) area",
Size = new Vector2(height),
Padding = new MarginPadding { Right = CORNER_RADIUS },
Child = leftIconArea = new FillFlowContainer
{
titleContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new OsuSpriteText
{
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
}
}
},
artistContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new OsuSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
Margin = new MarginPadding(5),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1)
}
},
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding
Name = @"Right (button) area",
Width = 30,
RelativeSizeAxes = Axes.Y,
Origin = Anchor.TopRight,
Anchor = Anchor.TopRight,
Padding = new MarginPadding { Vertical = 17.5f },
Child = rightAreaButtons = new Container<BeatmapCardIconButton>
{
Horizontal = 10,
Vertical = 4
},
RelativeSizeAxes = Axes.Both,
Children = new BeatmapCardIconButton[]
{
new FavouriteButton(beatmapSet)
{
Current = favouriteState,
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre
},
new DownloadButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State }
},
new GoToBeatmapButton(beatmapSet)
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
State = { BindTarget = downloadTracker.State }
}
}
}
},
mainContent = new Container
{
Name = @"Main content",
X = height - CORNER_RADIUS,
Height = height,
CornerRadius = CORNER_RADIUS,
Masking = true,
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.Both,
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 10,
Vertical = 4
},
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
AlwaysPresent = true,
Children = new Drawable[]
{
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
titleContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Children = new Drawable[]
ColumnDimensions = new[]
{
new BeatmapSetOnlineStatusPill
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new DifficultySpectrumDisplay(beatmapSet)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DotSize = new Vector2(6, 12)
new OsuSpriteText
{
Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
}
}
}
},
artistContainer = new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{
new OsuSpriteText
{
Text = createArtistText(),
Font = OsuFont.Default.With(size: 17.5f, weight: FontWeight.SemiBold),
RelativeSizeAxes = Axes.X,
Truncate = true
},
Empty()
},
}
},
new LinkFlowContainer(s =>
{
s.Shadow = false;
s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold);
}).With(d =>
{
d.AutoSizeAxes = Axes.Both;
d.Margin = new MarginPadding { Top = 2 };
d.AddText("mapped by ", t => t.Colour = colourProvider.Content2);
d.AddUserLink(beatmapSet.Author);
}),
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
new Container
{
Name = @"Bottom content",
RelativeSizeAxes = Axes.X,
Height = 6,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = downloadTracker.State },
Progress = { BindTarget = downloadTracker.Progress }
AutoSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomLeft,
Padding = new MarginPadding
{
Horizontal = 10,
Vertical = 4
},
Children = new Drawable[]
{
idleBottomContent = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
AlwaysPresent = true,
Children = new Drawable[]
{
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Alpha = 0,
AlwaysPresent = true,
ChildrenEnumerable = createStatistics()
},
new BeatmapCardExtraInfoRow(beatmapSet)
{
Hovered = _ =>
{
content.ScheduleShow();
return false;
},
Unhovered = _ =>
{
// This hide should only trigger if the expanded content has not shown yet.
// ie. if the user has not shown intent to want to see it (quickly moved over the info row area).
if (!Expanded.Value)
content.ScheduleHide();
}
}
}
},
downloadProgressBar = new BeatmapCardDownloadProgressBar
{
RelativeSizeAxes = Axes.X,
Height = 6,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
State = { BindTarget = downloadTracker.State },
Progress = { BindTarget = downloadTracker.Progress }
}
}
}
}
}
}
}
},
ExpandedContent = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = 10, Vertical = 13 },
Child = new BeatmapCardDifficultyList(beatmapSet)
},
Expanded = { BindTarget = Expanded }
};
if (beatmapSet.HasVideo)
@ -344,7 +353,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{
base.LoadComplete();
downloadTracker.State.BindValueChanged(_ => updateState(), true);
downloadTracker.State.BindValueChanged(_ => updateState());
Expanded.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
@ -356,6 +366,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override void OnHoverLost(HoverLostEvent e)
{
content.ScheduleHide();
updateState();
base.OnHoverLost(e);
}
@ -386,19 +398,25 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState()
{
float targetWidth = width - height;
if (IsHovered)
targetWidth = targetWidth - icon_area_width + corner_radius;
bool showDetails = IsHovered || Expanded.Value;
thumbnail.Dimmed.Value = IsHovered;
float targetWidth = width - height;
if (showDetails)
targetWidth = targetWidth - icon_area_width + CORNER_RADIUS;
thumbnail.Dimmed.Value = showDetails;
// Scale value is intentionally chosen to fit in the spacing of listing displays, as to not overlap horizontally with adjacent cards.
// This avoids depth issues where a hovered (scaled) card to the right of another card would be beneath the card to the left.
content.ScaleTo(Expanded.Value ? 1.03f : 1, 500, Easing.OutQuint);
mainContent.ResizeWidthTo(targetWidth, TRANSITION_DURATION, Easing.OutQuint);
mainContentBackground.Dimmed.Value = IsHovered;
mainContentBackground.Dimmed.Value = showDetails;
statisticsContainer.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
statisticsContainer.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
rightAreaBackground.FadeColour(downloadTracker.State.Value == DownloadState.LocallyAvailable ? colours.Lime0 : colourProvider.Background3, TRANSITION_DURATION, Easing.OutQuint);
rightAreaButtons.FadeTo(IsHovered ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
rightAreaButtons.FadeTo(showDetails ? 1 : 0, TRANSITION_DURATION, Easing.OutQuint);
foreach (var button in rightAreaButtons)
{

View File

@ -0,0 +1,234 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Graphics.Containers;
using osu.Game.Overlays;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class BeatmapCardContent : CompositeDrawable
{
public Drawable MainContent
{
set => bodyContent.Child = value;
}
public Drawable ExpandedContent
{
set => dropdownScroll.Child = value;
}
public Bindable<bool> Expanded { get; } = new BindableBool();
private readonly Box background;
private readonly Container content;
private readonly Container bodyContent;
private readonly Container dropdownContent;
private readonly OsuScrollContainer dropdownScroll;
private readonly Container borderContainer;
public BeatmapCardContent(float height)
{
RelativeSizeAxes = Axes.X;
Height = height;
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
InternalChild = content = new HoverHandlingContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
Unhovered = _ => checkForHide(),
Children = new Drawable[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both
},
bodyContent = new Container
{
RelativeSizeAxes = Axes.X,
Height = height,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
},
dropdownContent = new HoverHandlingContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Margin = new MarginPadding { Top = height },
Alpha = 0,
Hovered = _ =>
{
keep();
return true;
},
Unhovered = _ => checkForHide(),
Child = dropdownScroll = new ExpandedContentScrollContainer
{
RelativeSizeAxes = Axes.X,
ScrollbarVisible = false
}
},
borderContainer = new Container
{
RelativeSizeAxes = Axes.Both,
CornerRadius = BeatmapCard.CORNER_RADIUS,
Masking = true,
BorderThickness = 3,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
background.Colour = colourProvider.Background2;
borderContainer.BorderColour = colourProvider.Highlight1;
}
protected override void LoadComplete()
{
base.LoadComplete();
Expanded.BindValueChanged(_ => updateState(), true);
FinishTransforms(true);
}
private ScheduledDelegate? scheduledExpandedChange;
public void ScheduleShow()
{
scheduledExpandedChange?.Cancel();
if (Expanded.Disabled || Expanded.Value)
return;
scheduledExpandedChange = Scheduler.AddDelayed(() =>
{
if (!Expanded.Disabled)
Expanded.Value = true;
}, 100);
}
public void ScheduleHide()
{
scheduledExpandedChange?.Cancel();
if (Expanded.Disabled || !Expanded.Value)
return;
scheduledExpandedChange = Scheduler.AddDelayed(() =>
{
if (!Expanded.Disabled)
Expanded.Value = false;
}, 500);
}
private void checkForHide()
{
if (Expanded.Disabled)
return;
if (content.IsHovered || dropdownContent.IsHovered)
return;
scheduledExpandedChange?.Cancel();
Expanded.Value = false;
}
private void keep()
{
if (Expanded.Disabled)
return;
scheduledExpandedChange?.Cancel();
Expanded.Value = true;
}
private void updateState()
{
background.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
dropdownContent.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
borderContainer.FadeTo(Expanded.Value ? 1 : 0, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
content.TweenEdgeEffectTo(new EdgeEffectParameters
{
Type = EdgeEffectType.Shadow,
Offset = new Vector2(0, 2),
Radius = 10,
Colour = Colour4.Black.Opacity(Expanded.Value ? 0.3f : 0f),
Hollow = true,
}, BeatmapCard.TRANSITION_DURATION, Easing.OutQuint);
}
private class ExpandedContentScrollContainer : OsuScrollContainer
{
public ExpandedContentScrollContainer()
{
ScrollbarVisible = false;
}
protected override void Update()
{
base.Update();
Height = Math.Min(Content.DrawHeight, 400);
}
private bool allowScroll => !Precision.AlmostEquals(DrawSize, Content.DrawSize);
protected override bool OnDragStart(DragStartEvent e)
{
if (!allowScroll)
return false;
return base.OnDragStart(e);
}
protected override void OnDrag(DragEvent e)
{
if (!allowScroll)
return;
base.OnDrag(e);
}
protected override void OnDragEnd(DragEndEvent e)
{
if (!allowScroll)
return;
base.OnDragEnd(e);
}
protected override bool OnScroll(ScrollEvent e)
{
if (!allowScroll)
return false;
return base.OnScroll(e);
}
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class BeatmapCardExtraInfoRow : HoverHandlingContainer
{
public BeatmapCardExtraInfoRow(APIBeatmapSet beatmapSet)
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
Child = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(4, 0),
Children = new Drawable[]
{
new BeatmapSetOnlineStatusPill
{
AutoSizeAxes = Axes.Both,
Status = beatmapSet.Status,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
new DifficultySpectrumDisplay(beatmapSet)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
DotSize = new Vector2(6, 12)
}
}
};
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Events;
namespace osu.Game.Beatmaps.Drawables.Cards
{
public class HoverHandlingContainer : Container
{
public Func<HoverEvent, bool>? Hovered { get; set; }
public Action<HoverLostEvent>? Unhovered { get; set; }
protected override bool OnHover(HoverEvent e)
{
bool handledByBase = base.OnHover(e);
return Hovered?.Invoke(e) ?? handledByBase;
}
protected override void OnHoverLost(HoverLostEvent e)
{
base.OnHoverLost(e);
Unhovered?.Invoke(e);
}
}
}

View File

@ -11,6 +11,8 @@ namespace osu.Game.Configuration
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int? RulesetID { get; set; }
public int? Variant { get; set; }

View File

@ -476,7 +476,7 @@ namespace osu.Game.Database
{
Files.Dereference(file.FileInfo);
if (file.ID > 0)
if (file.IsManaged)
{
// This shouldn't be required, but here for safety in case the provided TModel is not being change tracked
// Definitely can be removed once we rework the database backend.
@ -505,7 +505,7 @@ namespace osu.Game.Database
});
}
if (model.ID > 0)
if (model.IsManaged)
Update(model);
}
@ -737,7 +737,7 @@ namespace osu.Game.Database
/// <param name="items">The usable items present in the store.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
protected virtual bool CheckLocalAvailability(TModel model, IQueryable<TModel> items)
=> model.ID > 0 && items.Any(i => i.ID == model.ID && i.Files.Any());
=> model.IsManaged && items.Any(i => i.ID == model.ID && i.Files.Any());
/// <summary>
/// Whether import can be skipped after finding an existing import early in the process.

View File

@ -35,6 +35,16 @@ namespace osu.Game.Database
private void migrateSkins(DatabaseWriteUsage db)
{
// can be removed 20220530.
var existingSkins = db.Context.SkinInfo
.Include(s => s.Files)
.ThenInclude(f => f.FileInfo)
.ToList();
// previous entries in EF are removed post migration.
if (!existingSkins.Any())
return;
var userSkinChoice = config.GetBindable<string>(OsuSetting.Skin);
int.TryParse(userSkinChoice.Value, out int userSkinInt);
@ -49,16 +59,6 @@ namespace osu.Game.Database
break;
}
// migrate ruleset settings. can be removed 20220530.
var existingSkins = db.Context.SkinInfo
.Include(s => s.Files)
.ThenInclude(f => f.FileInfo)
.ToList();
// previous entries in EF are removed post migration.
if (!existingSkins.Any())
return;
using (var realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite())
{

View File

@ -11,5 +11,7 @@ namespace osu.Game.Database
[JsonIgnore]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
int ID { get; set; }
bool IsManaged { get; }
}
}

View File

@ -147,7 +147,7 @@ namespace osu.Game.Database
modelBuilder.Entity<BeatmapInfo>().HasOne(b => b.BaseDifficulty);
modelBuilder.Entity<ScoreInfo>().HasIndex(b => b.OnlineScoreID).IsUnique();
modelBuilder.Entity<ScoreInfo>().HasIndex(b => b.OnlineID).IsUnique();
}
private class OsuDbLoggerFactory : ILoggerFactory

View File

@ -104,6 +104,14 @@ namespace osu.Game.Extensions
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this APIUser? instance, APIUser? other) => matchesOnlineID(instance, other);
/// <summary>
/// Check whether the online ID of two <see cref="IScoreInfo"/>s match.
/// </summary>
/// <param name="instance">The instance to compare.</param>
/// <param name="other">The other instance to compare against.</param>
/// <returns>Whether online IDs match. If either instance is missing an online ID, this will return false.</returns>
public static bool MatchesOnlineID(this IScoreInfo? instance, IScoreInfo? other) => matchesOnlineID(instance, other);
private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
{
if (instance == null || other == null)

View File

@ -93,7 +93,7 @@ namespace osu.Game.Graphics.UserInterface
if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
capsTextAddedSample?.Play();
else
textAddedSamples[RNG.Next(0, 3)]?.Play();
playTextAddedSample();
}
protected override void OnUserTextRemoved(string removed)
@ -117,6 +117,70 @@ namespace osu.Game.Graphics.UserInterface
caretMovedSample?.Play();
}
protected override void OnImeComposition(string newComposition, int removedTextLength, int addedTextLength, bool caretMoved)
{
base.OnImeComposition(newComposition, removedTextLength, addedTextLength, caretMoved);
if (string.IsNullOrEmpty(newComposition))
{
switch (removedTextLength)
{
case 0:
// empty composition event, composition wasn't changed, don't play anything.
return;
case 1:
// composition probably ended by pressing backspace, or was cancelled.
textRemovedSample?.Play();
return;
default:
// longer text removed, composition ended because it was cancelled.
// could be a different sample if desired.
textRemovedSample?.Play();
return;
}
}
if (addedTextLength > 0)
{
// some text was added, probably due to typing new text or by changing the candidate.
playTextAddedSample();
return;
}
if (removedTextLength > 0)
{
// text was probably removed by backspacing.
// it's also possible that a candidate that only removed text was changed to.
textRemovedSample?.Play();
return;
}
if (caretMoved)
{
// only the caret/selection was moved.
caretMovedSample?.Play();
}
}
protected override void OnImeResult(string result, bool successful)
{
base.OnImeResult(result, successful);
if (successful)
{
// composition was successfully completed, usually by pressing the enter key.
textCommittedSample?.Play();
}
else
{
// composition was prematurely ended, eg. by clicking inside the textbox.
// could be a different sample if desired.
textCommittedSample?.Play();
}
}
protected override void OnFocus(FocusEvent e)
{
BorderThickness = 3;
@ -142,6 +206,8 @@ namespace osu.Game.Graphics.UserInterface
SelectionColour = SelectionColour,
};
private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play();
private class OsuCaret : Caret
{
private const float caret_move_time = 60;

View File

@ -9,6 +9,8 @@ namespace osu.Game.IO
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public string Hash { get; set; }
public int ReferenceCount { get; set; }

View File

@ -81,20 +81,37 @@ namespace osu.Game.Input
// compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
{
// avoid performing redundant queries when the database is empty and needs to be re-filled.
int existingCount = existingBindings.Count(k => k.RulesetName == rulesetName && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key);
IEnumerable<RealmKeyBinding> existing = existingBindings.Where(k =>
k.RulesetName == rulesetName
&& k.Variant == variant
&& k.ActionInt == (int)defaultsForAction.Key);
if (defaultsForAction.Count() <= existingCount)
continue;
int defaultsCount = defaultsForAction.Count();
int existingCount = existing.Count();
// insert any defaults which are missing.
realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
if (defaultsCount > existingCount)
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetName = rulesetName,
Variant = variant
}));
// insert any defaults which are missing.
realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
{
KeyCombinationString = k.KeyCombination.ToString(),
ActionInt = (int)k.Action,
RulesetName = rulesetName,
Variant = variant
}));
}
else if (defaultsCount < existingCount)
{
// generally this shouldn't happen, but if the user has more key bindings for an action than we expect,
// remove the last entries until the count matches for sanity.
foreach (var k in existing.TakeLast(existingCount - defaultsCount).ToArray())
{
realm.Remove(k);
// Remove from the local flattened/cached list so future lookups don't query now deleted rows.
existingBindings.Remove(k);
}
}
}
}

View File

@ -42,7 +42,7 @@ namespace osu.Game.Online.API
if (WebRequest != null)
{
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject;
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes");
Logger.Log($"{GetType()} finished with response size of {WebRequest.ResponseStream.Length:#,0} bytes", LoggingTarget.Network);
}
}

View File

@ -8,7 +8,7 @@ using osu.Game.Rulesets;
namespace osu.Game.Online.API.Requests
{
public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScoreInfo>>
public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScore>>
{
private readonly long userId;
private readonly ScoreType type;

View File

@ -13,10 +13,11 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Scoring.Legacy;
using osu.Game.Users;
namespace osu.Game.Online.API.Requests.Responses
{
public class APIScoreInfo : IScoreInfo
public class APIScore : IScoreInfo
{
[JsonProperty(@"score")]
public long TotalScore { get; set; }
@ -101,7 +102,7 @@ namespace osu.Game.Online.API.Requests.Responses
BeatmapInfo = beatmap,
User = User,
Accuracy = Accuracy,
OnlineScoreID = OnlineID,
OnlineID = OnlineID,
Date = Date,
PP = PP,
RulesetID = RulesetID,
@ -150,6 +151,11 @@ namespace osu.Game.Online.API.Requests.Responses
public IRulesetInfo Ruleset => new RulesetInfo { OnlineID = RulesetID };
IEnumerable<INamedFileUsage> IHasNamedFiles.Files => throw new NotImplementedException();
#region Implementation of IScoreInfo
IBeatmapInfo IScoreInfo.Beatmap => Beatmap;
IUser IScoreInfo.User => User;
#endregion
}
}

View File

@ -14,7 +14,7 @@ namespace osu.Game.Online.API.Requests.Responses
public int? Position;
[JsonProperty(@"score")]
public APIScoreInfo Score;
public APIScore Score;
public ScoreInfo CreateScoreInfo(RulesetStore rulesets, BeatmapInfo beatmap = null)
{

View File

@ -9,7 +9,7 @@ namespace osu.Game.Online.API.Requests.Responses
public class APIScoresCollection
{
[JsonProperty(@"scores")]
public List<APIScoreInfo> Scores;
public List<APIScore> Scores;
[JsonProperty(@"userScore")]
public APIScoreWithPosition UserScore;

View File

@ -111,7 +111,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = user.Id == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Colour = user.OnlineID == api.LocalUser.Value.Id && allowHighlight ? colour.Green : Color4.Black,
Alpha = background_alpha,
},
},

View File

@ -82,5 +82,17 @@ namespace osu.Game.Online.Multiplayer
/// </summary>
/// <param name="item">The item to add.</param>
Task AddPlaylistItem(MultiplayerPlaylistItem item);
/// <summary>
/// Edits an existing playlist item with new values.
/// </summary>
/// <param name="item">The item to edit, containing new properties. Must have an ID.</param>
Task EditPlaylistItem(MultiplayerPlaylistItem item);
/// <summary>
/// Removes an item from the playlist.
/// </summary>
/// <param name="playlistItemId">The item to remove.</param>
Task RemovePlaylistItem(long playlistItemId);
}
}

View File

@ -163,7 +163,7 @@ namespace osu.Game.Online.Multiplayer
Debug.Assert(joinedRoom != null);
// Populate playlist items.
var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(createPlaylistItem)).ConfigureAwait(false);
var playlistItems = await Task.WhenAll(joinedRoom.Playlist.Select(item => createPlaylistItem(item, item.ID == joinedRoom.Settings.PlaylistItemId))).ConfigureAwait(false);
// Populate users.
Debug.Assert(joinedRoom.Users != null);
@ -335,6 +335,10 @@ namespace osu.Game.Online.Multiplayer
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task RemovePlaylistItem(long playlistItemId);
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{
if (Room == null)
@ -470,7 +474,32 @@ namespace osu.Game.Online.Multiplayer
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings)
{
Scheduler.Add(() => updateLocalRoomSettings(newSettings));
Debug.Assert(APIRoom != null);
Debug.Assert(Room != null);
Scheduler.Add(() =>
{
// ensure the new selected item is populated immediately.
var playlistItem = APIRoom.Playlist.Single(p => p.ID == newSettings.PlaylistItemId);
if (playlistItem != null)
{
GetAPIBeatmap(playlistItem.BeatmapID).ContinueWith(b =>
{
// Should be called outside of the `Scheduler` logic (and specifically accessing `Exception`) to suppress an exception from firing outwards.
bool success = b.Exception == null;
Scheduler.Add(() =>
{
if (success)
playlistItem.Beatmap.Value = b.Result;
updateLocalRoomSettings(newSettings);
});
});
}
});
return Task.CompletedTask;
}
@ -629,7 +658,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
var playlistItem = await createPlaylistItem(item).ConfigureAwait(false);
var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
Scheduler.Add(() =>
{
@ -673,7 +702,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null)
return;
var playlistItem = await createPlaylistItem(item).ConfigureAwait(false);
var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
Scheduler.Add(() =>
{
@ -728,10 +757,8 @@ namespace osu.Game.Online.Multiplayer
CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId);
}
private async Task<PlaylistItem> createPlaylistItem(MultiplayerPlaylistItem item)
private async Task<PlaylistItem> createPlaylistItem(MultiplayerPlaylistItem item, bool populateBeatmapImmediately)
{
var apiBeatmap = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false);
var ruleset = Rulesets.GetRuleset(item.RulesetID);
Debug.Assert(ruleset != null);
@ -741,8 +768,8 @@ namespace osu.Game.Online.Multiplayer
var playlistItem = new PlaylistItem
{
ID = item.ID,
BeatmapID = item.BeatmapID,
OwnerID = item.OwnerID,
Beatmap = { Value = apiBeatmap },
Ruleset = { Value = ruleset },
Expired = item.Expired,
PlaylistOrder = item.PlaylistOrder,
@ -752,6 +779,9 @@ namespace osu.Game.Online.Multiplayer
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
playlistItem.AllowedMods.AddRange(item.AllowedMods.Select(m => m.ToMod(rulesetInstance)));
if (populateBeatmapImmediately)
playlistItem.Beatmap.Value = await GetAPIBeatmap(item.BeatmapID).ConfigureAwait(false);
return playlistItem;
}

View File

@ -162,6 +162,22 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item);
}
public override Task EditPlaylistItem(MultiplayerPlaylistItem item)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.EditPlaylistItem), item);
}
public override Task RemovePlaylistItem(long playlistItemId)
{
if (!IsConnected.Value)
return Task.CompletedTask;
return connection.InvokeAsync(nameof(IMultiplayerServer.RemovePlaylistItem), playlistItemId);
}
protected override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{
return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken);

View File

@ -62,6 +62,7 @@ namespace osu.Game.Online.Rooms
public MultiplayerPlaylistItem(PlaylistItem item)
{
ID = item.ID;
OwnerID = item.OwnerID;
BeatmapID = item.BeatmapID;
BeatmapChecksum = item.Beatmap.Value?.MD5Hash ?? string.Empty;
RulesetID = item.RulesetID;

View File

@ -69,7 +69,7 @@ namespace osu.Game.Online.Rooms
var scoreInfo = new ScoreInfo
{
OnlineScoreID = ID,
OnlineID = ID,
TotalScore = TotalScore,
MaxCombo = MaxCombo,
BeatmapInfo = beatmap,

View File

@ -31,6 +31,7 @@ namespace osu.Game.Online.Rooms
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
req.Timeout = 30000;
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{

View File

@ -3,6 +3,7 @@
using System;
using osu.Framework.Allocation;
using osu.Game.Extensions;
using osu.Game.Online.API;
using osu.Game.Scoring;
@ -35,7 +36,7 @@ namespace osu.Game.Online
var scoreInfo = new ScoreInfo
{
ID = TrackedItem.ID,
OnlineScoreID = TrackedItem.OnlineScoreID
OnlineID = TrackedItem.OnlineID
};
if (Manager.IsAvailableLocally(scoreInfo))
@ -113,7 +114,7 @@ namespace osu.Game.Online
UpdateState(DownloadState.NotDownloaded);
});
private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.OnlineID == y.OnlineID;
private bool checkEquality(IScoreInfo x, IScoreInfo y) => x.MatchesOnlineID(y);
#region Disposal

View File

@ -31,6 +31,7 @@ namespace osu.Game.Online.Solo
req.ContentType = "application/json";
req.Method = HttpMethod.Put;
req.Timeout = 30000;
req.AddRaw(JsonConvert.SerializeObject(score, new JsonSerializerSettings
{

View File

@ -16,7 +16,7 @@ namespace osu.Game.Online.Solo
{
/// <summary>
/// A class specifically for sending scores to the API during score submission.
/// This is used instead of <see cref="APIScoreInfo"/> due to marginally different serialisation naming requirements.
/// This is used instead of <see cref="APIScore"/> due to marginally different serialisation naming requirements.
/// </summary>
[Serializable]
public class SubmittableScore

View File

@ -103,7 +103,7 @@ namespace osu.Game
private Container topMostOverlayContent;
private ScalingContainer screenContainer;
protected ScalingContainer ScreenContainer { get; private set; }
protected Container ScreenOffsetContainer { get; private set; }
@ -179,7 +179,7 @@ namespace osu.Game
}
private void updateBlockingOverlayFade() =>
screenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
ScreenContainer.FadeColour(visibleBlockingOverlays.Any() ? OsuColour.Gray(0.5f) : Color4.White, 500, Easing.OutQuint);
public void AddBlockingOverlay(OverlayContainer overlay)
{
@ -487,8 +487,8 @@ namespace osu.Game
// to ensure all the required data for presenting a replay are present.
ScoreInfo databasedScoreInfo = null;
if (score.OnlineScoreID != null)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID);
if (score.OnlineID > 0)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
@ -698,7 +698,7 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
ScreenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
@ -801,7 +801,7 @@ namespace osu.Game
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), overlayContent.Add, true);
loadComponentSingleFile(skinEditor = new SkinEditorOverlay(screenContainer), overlayContent.Add, true);
loadComponentSingleFile(skinEditor = new SkinEditorOverlay(ScreenContainer), overlayContent.Add, true);
loadComponentSingleFile(new LoginOverlay
{

View File

@ -151,7 +151,8 @@ namespace osu.Game.Overlays
}
// spawn new children with the contained so we only clear old content at the last moment.
var content = new FillFlowContainer<BeatmapCard>
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
var content = new ReverseChildIDFillFlowContainer<BeatmapCard>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,

View File

@ -94,7 +94,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
topScoresContainer.Add(new DrawableTopScore(topScore));
if (userScoreInfo != null && userScoreInfo.OnlineScoreID != topScore.OnlineScoreID)
if (userScoreInfo != null && userScoreInfo.OnlineID != topScore.OnlineID)
topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion);
});

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation;
using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Profile
public abstract string Identifier { get; }
private readonly FillFlowContainer content;
private readonly FillFlowContainer<Drawable> content;
private readonly Box background;
private readonly Box underscore;
@ -79,7 +80,9 @@ namespace osu.Game.Overlays.Profile
}
}
},
content = new FillFlowContainer
// reverse ID flow is required for correct Z-ordering of the content (last item should be front-most).
// particularly important in BeatmapsSection, as it uses beatmap cards, which have expandable overhanging content.
content = new ReverseChildIDFillFlowContainer<Drawable>
{
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y,

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses;
using osuTK;
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections
protected int VisiblePages;
protected int ItemsPerPage;
protected FillFlowContainer ItemsContainer { get; private set; }
protected ReverseChildIDFillFlowContainer<Drawable> ItemsContainer { get; private set; }
private APIRequest<List<TModel>> retrievalRequest;
private CancellationTokenSource loadCancellation;
@ -48,11 +49,15 @@ namespace osu.Game.Overlays.Profile.Sections
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
ItemsContainer = new FillFlowContainer
// reverse ID flow is required for correct Z-ordering of the items (last item should be front-most).
// particularly important in PaginatedBeatmapContainer, as it uses beatmap cards, which have expandable overhanging content.
ItemsContainer = new ReverseChildIDFillFlowContainer<Drawable>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2),
// ensure the container and its contents are in front of the "more" button.
Depth = float.MinValue
},
moreButton = new ShowMoreButton
{

View File

@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
private const float performance_background_shear = 0.45f;
protected readonly APIScoreInfo Score;
protected readonly APIScore Score;
[Resolved]
private OsuColour colours { get; set; }
@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
[Resolved]
private OverlayColourProvider colourProvider { get; set; }
public DrawableProfileScore(APIScoreInfo score)
public DrawableProfileScore(APIScore score)
{
Score = score;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{
private readonly double weight;
public DrawableProfileWeightedScore(APIScoreInfo score, double weight)
public DrawableProfileWeightedScore(APIScore score, double weight)
: base(score)
{
this.weight = weight;

View File

@ -15,7 +15,7 @@ using APIUser = osu.Game.Online.API.Requests.Responses.APIUser;
namespace osu.Game.Overlays.Profile.Sections.Ranks
{
public class PaginatedScoreContainer : PaginatedProfileSubsection<APIScoreInfo>
public class PaginatedScoreContainer : PaginatedProfileSubsection<APIScore>
{
private readonly ScoreType type;
@ -51,7 +51,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
}
}
protected override void OnItemsReceived(List<APIScoreInfo> items)
protected override void OnItemsReceived(List<APIScore> items)
{
if (VisiblePages == 0)
drawableItemIndex = 0;
@ -59,12 +59,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items);
}
protected override APIRequest<List<APIScoreInfo>> CreateRequest() =>
protected override APIRequest<List<APIScore>> CreateRequest() =>
new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
private int drawableItemIndex;
protected override Drawable CreateDrawableItem(APIScoreInfo model)
protected override Drawable CreateDrawableItem(APIScore model)
{
switch (type)
{

View File

@ -135,7 +135,8 @@ namespace osu.Game.Overlays.Rankings
Children = new Drawable[]
{
new ScoresTable(1, response.Users),
new FillFlowContainer
// reverse ID flow is required for correct Z-ordering of the cards' expandable content (last card should be front-most).
new ReverseChildIDFillFlowContainer<BeatmapCard>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,

View File

@ -120,14 +120,14 @@ namespace osu.Game.Rulesets.Difficulty
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary>
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
public IEnumerable<DifficultyAttributes> CalculateAll()
public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default)
{
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
if (combination is MultiMod multi)
yield return Calculate(multi.Mods);
yield return Calculate(multi.Mods, cancellationToken);
else
yield return Calculate(combination.Yield());
yield return Calculate(combination.Yield(), cancellationToken);
}
}
@ -145,7 +145,11 @@ namespace osu.Game.Rulesets.Difficulty
{
playableMods = mods.Select(m => m.DeepClone()).ToArray();
Beatmap = beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
// Only pass through the cancellation token if it's non-default.
// This allows for the default timeout to be applied for playable beatmap construction.
Beatmap = cancellationToken == default
? beatmap.GetPlayableBeatmap(ruleset, playableMods)
: beatmap.GetPlayableBeatmap(ruleset, playableMods, cancellationToken);
var track = new TrackVirtual(10000);
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));

View File

@ -4,14 +4,14 @@
using System;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Users;
namespace osu.Game.Scoring
{
public interface IScoreInfo : IHasOnlineID<long>, IHasNamedFiles
{
APIUser User { get; }
IUser User { get; }
long TotalScore { get; }

View File

@ -80,12 +80,9 @@ namespace osu.Game.Scoring.Legacy
byte[] compressedReplay = sr.ReadByteArray();
if (version >= 20140721)
scoreInfo.OnlineScoreID = sr.ReadInt64();
scoreInfo.OnlineID = sr.ReadInt64();
else if (version >= 20121008)
scoreInfo.OnlineScoreID = sr.ReadInt32();
if (scoreInfo.OnlineScoreID <= 0)
scoreInfo.OnlineScoreID = null;
scoreInfo.OnlineID = sr.ReadInt32();
if (compressedReplay?.Length > 0)
{

View File

@ -11,6 +11,8 @@ namespace osu.Game.Scoring
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int FileInfoID { get; set; }
public FileInfo FileInfo { get; set; }

View File

@ -14,6 +14,7 @@ using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Users;
using osu.Game.Utils;
namespace osu.Game.Scoring
@ -22,6 +23,8 @@ namespace osu.Game.Scoring
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public ScoreRank Rank { get; set; }
public long TotalScore { get; set; }
@ -134,7 +137,14 @@ namespace osu.Game.Scoring
[Column("Beatmap")]
public BeatmapInfo BeatmapInfo { get; set; }
public long? OnlineScoreID { get; set; }
private long? onlineID;
[Column("OnlineScoreID")]
public long? OnlineID
{
get => onlineID;
set => onlineID = value > 0 ? value : null;
}
public DateTimeOffset Date { get; set; }
@ -229,24 +239,18 @@ namespace osu.Game.Scoring
public bool Equals(ScoreInfo other)
{
if (other == null)
return false;
if (ReferenceEquals(this, other)) return true;
if (other == null) return false;
if (ID != 0 && other.ID != 0)
return ID == other.ID;
if (OnlineScoreID.HasValue && other.OnlineScoreID.HasValue)
return OnlineScoreID == other.OnlineScoreID;
if (!string.IsNullOrEmpty(Hash) && !string.IsNullOrEmpty(other.Hash))
return Hash == other.Hash;
return ReferenceEquals(this, other);
return false;
}
#region Implementation of IHasOnlineID
public long OnlineID => OnlineScoreID ?? -1;
long IHasOnlineID<long>.OnlineID => OnlineID ?? -1;
#endregion
@ -254,6 +258,7 @@ namespace osu.Game.Scoring
IBeatmapInfo IScoreInfo.Beatmap => BeatmapInfo;
IRulesetInfo IScoreInfo.Ruleset => Ruleset;
IUser IScoreInfo.User => User;
bool IScoreInfo.HasReplay => Files.Any();
#endregion

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