Merge remote-tracking branch 'origin/chat-mention-fix' into chat-mention-fix

This commit is contained in:
tbrose 2021-12-14 16:24:05 +01:00
commit a831744f05
147 changed files with 2567 additions and 1114 deletions

View File

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

View File

@ -52,7 +52,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.1203.0" /> <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>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- 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)) if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath))
return stableInstallPath; return stableInstallPath;
} }
catch { } catch
{
}
} }
stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!");
@ -113,7 +115,7 @@ namespace osu.Desktop
base.LoadComplete(); base.LoadComplete();
if (!noVersionOverlay) if (!noVersionOverlay)
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, ScreenContainer.Add);
LoadComponentAsync(new DiscordRichPresence(), Add); LoadComponentAsync(new DiscordRichPresence(), Add);

View File

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

View File

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

View File

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

View File

@ -11,49 +11,65 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{ {
public class OsuDifficultyHitObject : DifficultyHitObject public class OsuDifficultyHitObject : DifficultyHitObject
{ {
private const int normalized_radius = 50; // Change radius to 50 to make 100 the diameter. Easier for mental maths. private const int 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 int min_delta_time = 25;
private const float maximum_slider_radius = normalized_radius * 2.4f; private const float maximum_slider_radius = normalised_radius * 2.4f;
private const float assumed_slider_radius = normalized_radius * 1.8f; private const float assumed_slider_radius = normalised_radius * 1.8f;
protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject; protected new OsuHitObject BaseObject => (OsuHitObject)base.BaseObject;
/// <summary> /// <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> /// </summary>
public double JumpDistance { get; private set; } public readonly double StrainTime;
/// <summary> /// <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> /// </summary>
public double MovementDistance { get; private set; } public double LazyJumpDistance { get; private set; }
/// <summary> /// <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> /// </summary>
public double TravelDistance { get; private set; } 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> /// <summary>
/// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>. /// Angle the player has to take to hit this <see cref="OsuDifficultyHitObject"/>.
/// Calculated as the angle between the circles (current-2, current-1, current). /// Calculated as the angle between the circles (current-2, current-1, current).
/// </summary> /// </summary>
public double? Angle { get; private set; } 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 lastLastObject;
private readonly OsuHitObject lastObject; private readonly OsuHitObject lastObject;
@ -71,12 +87,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances(double clockRate) 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 // 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) if (BaseObject is Spinner || lastObject is Spinner)
return; return;
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps. // 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) if (BaseObject.Radius < 30)
{ {
@ -85,29 +108,40 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
} }
Vector2 lastCursorPosition = getEndCursorPosition(lastObject); 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) if (lastObject is Slider lastSlider)
{ {
computeSliderCursorPosition(lastSlider); double lastTravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
TravelDistance = lastSlider.LazyTravelDistance; MinimumJumpTime = Math.Max(StrainTime - lastTravelTime, min_delta_time);
TravelTime = Math.Max(lastSlider.LazyTravelTime / clockRate, min_delta_time);
MovementTime = Math.Max(StrainTime - TravelTime, 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; float tailJumpDistance = Vector2.Subtract(lastSlider.TailCircle.StackedPosition, BaseObject.StackedPosition).Length * scalingFactor;
MinimumJumpDistance = Math.Max(0, Math.Min(LazyJumpDistance - (maximum_slider_radius - assumed_slider_radius), tailJumpDistance - maximum_slider_radius));
// 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;
} }
if (lastLastObject != null && !(lastLastObject is Spinner)) 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. slider.LazyEndPosition = slider.StackedPosition + slider.Path.PositionAt(endTimeMin); // temporary lazy end position until a real result can be derived.
var currCursorPosition = slider.StackedPosition; 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++) 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) else if (currMovementObj is SliderRepeat)
{ {
// For a slider repeat, assume a tighter movement threshold to better assess repeat sliders. // For a slider repeat, assume a tighter movement threshold to better assess repeat sliders.
requiredMovement = normalized_radius; requiredMovement = normalised_radius;
} }
if (currMovementLength > requiredMovement) if (currMovementLength > requiredMovement)

View File

@ -44,24 +44,24 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1]; 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. // 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. // 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) if (osuLastObj.BaseObject is Slider && withSliders)
{ {
double movementVelocity = osuCurrObj.MovementDistance / osuCurrObj.MovementTime; // calculate the movement velocity from slider end to current object double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
double travelVelocity = osuCurrObj.TravelDistance / osuCurrObj.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. currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
} }
// As above, do the same for the previous hitobject. // 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) if (osuLastLastObj.BaseObject is Slider && withSliders)
{ {
double movementVelocity = osuLastObj.MovementDistance / osuLastObj.MovementTime; double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity); 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. 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.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.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. // 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) 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. // 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; prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
currVelocity = (osuCurrObj.JumpDistance + osuCurrObj.TravelDistance) / osuCurrObj.StrainTime; currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
// Scale with ratio of difference compared to 0.5 * max dist. // 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); 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 // Reward for % distance slowed down compared to previous, paying attention to not award overlap
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity) double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
// do not award overlap // 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. // Choose the largest bonus, multiplied by ratio.
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio; 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); 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. // 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. // 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); 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. // 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; 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) if (strainTime < min_speed_bonus)
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2); 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; return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -52,6 +52,45 @@ namespace osu.Game.Tests.Database
Assert.That(queryCount(GlobalAction.Select), Is.EqualTo(2)); 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) private int queryCount(GlobalAction? match = null)
{ {
using (var realm = realmContextFactory.CreateContext()) using (var realm = realmContextFactory.CreateContext())

View File

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

View File

@ -4,6 +4,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using NUnit.Framework; using NUnit.Framework;
@ -12,8 +13,12 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Tests.Resources 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, Combo = 250,
User = new APIUser { Username = "Test user" }, User = new APIUser { Username = "Test user" },
Date = DateTimeOffset.Now, Date = DateTimeOffset.Now,
OnlineScoreID = 12345, OnlineID = 12345,
}; };
var imported = await LoadScoreIntoOsu(osu, toImport); 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.Combo, imported.Combo);
Assert.AreEqual(toImport.User.Username, imported.User.Username); Assert.AreEqual(toImport.User.Username, imported.User.Username);
Assert.AreEqual(toImport.Date, imported.Date); Assert.AreEqual(toImport.Date, imported.Date);
Assert.AreEqual(toImport.OnlineScoreID, imported.OnlineScoreID); Assert.AreEqual(toImport.OnlineID, imported.OnlineID);
} }
finally finally
{ {
@ -163,12 +163,12 @@ namespace osu.Game.Tests.Scores.IO
{ {
var osu = LoadOsuIntoHost(host, true); 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>(); 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 // 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 finally
{ {

View File

@ -44,24 +44,6 @@ namespace osu.Game.Tests.Scores.IO
Assert.That(score1, Is.EqualTo(score2)); 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] [Test]
public void TestNonMatchingByNull() public void TestNonMatchingByNull()
{ {

View File

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

View File

@ -6,12 +6,14 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -227,7 +229,7 @@ namespace osu.Game.Tests.Visual.Beatmaps
new BasicScrollContainer new BasicScrollContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new FillFlowContainer Child = new ReverseChildIDFillFlowContainer<Drawable>
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
@ -248,6 +250,17 @@ namespace osu.Game.Tests.Visual.Beatmaps
} }
[Test] [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.Overlays.Dialog;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Tests.Beatmaps.IO; using osu.Game.Tests.Beatmaps.IO;
@ -89,6 +90,7 @@ namespace osu.Game.Tests.Visual.Editing
confirmEditingBeatmap(() => targetDifficulty); confirmEditingBeatmap(() => targetDifficulty);
AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any()); AddAssert("no objects selected", () => !EditorBeatmap.SelectedHitObjects.Any());
AddUntilStep("wait for drawable ruleset", () => Editor.ChildrenOfType<DrawableRuleset>().SingleOrDefault()?.IsLoaded == true);
AddStep("paste object", () => Editor.Paste()); AddStep("paste object", () => Editor.Paste());
if (sameRuleset) if (sameRuleset)

View File

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

View File

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

View File

@ -251,7 +251,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Test] [Test]
public void TestMutedNotificationMuteButton() 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> /// <remarks>

View File

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

View File

@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void addItem(Func<BeatmapInfo> beatmap) 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); InputManager.Click(MouseButton.Left);
}); });

View File

@ -11,12 +11,14 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Online.Rooms.RoomStatuses;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Screens.OnlinePlay.Lounge; using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Screens.OnlinePlay.Lounge.Components;
using osu.Game.Screens.OnlinePlay.Match;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osuTK; 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)); 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) private DrawableRoom createLoungeRoom(Room room)
{ {
room.Host.Value ??= new APIUser { Username = "peppy", Id = 2 }; 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestNonEditableNonSelectable() public void TestNonEditableNonSelectable()
{ {
createPlaylist(false, false); createPlaylist();
moveToItem(0); moveToItem(0);
assertHandleVisibility(0, false); assertHandleVisibility(0, false);
@ -61,7 +62,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestEditable() public void TestEditable()
{ {
createPlaylist(true, false); createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
});
moveToItem(0); moveToItem(0);
assertHandleVisibility(0, true); assertHandleVisibility(0, true);
@ -74,7 +79,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestMarkInvalid() 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()); AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid());
@ -87,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestSelectable() public void TestSelectable()
{ {
createPlaylist(false, true); createPlaylist(p => p.AllowSelection = true);
moveToItem(0); moveToItem(0);
assertHandleVisibility(0, false); assertHandleVisibility(0, false);
@ -101,7 +111,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestEditableSelectable() public void TestEditableSelectable()
{ {
createPlaylist(true, true); createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0); moveToItem(0);
assertHandleVisibility(0, true); assertHandleVisibility(0, true);
@ -115,7 +130,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test] [Test]
public void TestSelectionNotLostAfterRearrangement() public void TestSelectionNotLostAfterRearrangement()
{ {
createPlaylist(true, true); createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0); moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left)); 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]); 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] [Test]
public void TestDownloadButtonHiddenWhenBeatmapExists() public void TestDownloadButtonHiddenWhenBeatmapExists()
{ {
@ -224,7 +155,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait()); AddStep("import beatmap", () => manager.Import(beatmap.BeatmapSet).Wait());
createPlaylist(beatmap); createPlaylistWithBeatmaps(beatmap);
assertDownloadButtonVisible(false); assertDownloadButtonVisible(false);
@ -247,7 +178,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
var byChecksum = CreateAPIBeatmap(); var byChecksum = CreateAPIBeatmap();
byChecksum.Checksum = "1337"; // Some random checksum that does not exist locally. 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)); 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; beatmap.BeatmapSet.HasExplicitContent = true;
createPlaylist(beatmap); createPlaylistWithBeatmaps(beatmap);
} }
[Test] [Test]
@ -269,7 +200,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
AddStep("create playlist", () => AddStep("create playlist", () =>
{ {
Child = playlist = new TestPlaylist(false, false) Child = playlist = new TestPlaylist
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -312,11 +243,22 @@ namespace osu.Game.Tests.Visual.Multiplayer
[TestCase(true)] [TestCase(true)]
public void TestWithOwner(bool withOwner) 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)); 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) private void moveToItem(int index, Vector2? offset = null)
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DifficultyIcon>().ElementAt(index), offset)); => 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); 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) private void assertHandleVisibility(int index, bool visible)
=> AddAssert($"handle {index} {(visible ? "is" : "is not")} visible", => AddAssert($"handle {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<OsuRearrangeableListItem<PlaylistItem>.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == 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", => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible",
() => (playlist.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistRemoveButton>().ElementAt(2 + index * 2).Alpha > 0) == 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", () => AddStep("create playlist", () =>
{ {
Child = playlist = new TestPlaylist(allowEdit, allowSelection, showItemOwner) Child = playlist = new TestPlaylist
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
Size = new Vector2(500, 300) Size = new Vector2(500, 300)
}; };
setupPlaylist?.Invoke(playlist);
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
playlist.Items.Add(new PlaylistItem 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)); 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", () => AddStep("create playlist", () =>
{ {
Child = playlist = new TestPlaylist(false, false) Child = playlist = new TestPlaylist
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -423,11 +361,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
private class TestPlaylist : DrawableRoomPlaylist private class TestPlaylist : DrawableRoomPlaylist
{ {
public new IReadOnlyDictionary<PlaylistItem, RearrangeableListItem<PlaylistItem>> ItemMap => base.ItemMap; 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.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Screens.OnlinePlay;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osuTK.Input; using osuTK.Input;
@ -74,11 +75,19 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("api room updated", () => Client.APIRoom?.QueueMode.Value == QueueMode.AllPlayers); 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) private void selectNewItem(Func<BeatmapInfo> beatmap)
{ {
AddStep("click edit button", () => AddStep("click edit button", () =>
{ {
InputManager.MoveMouseTo(this.ChildrenOfType<MultiplayerMatchSubScreen>().Single().AddOrEditPlaylistButton); InputManager.MoveMouseTo(this.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().First());
InputManager.Click(MouseButton.Left); InputManager.Click(MouseButton.Left);
}); });
@ -90,5 +99,18 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen); AddUntilStep("wait for return to match", () => CurrentSubScreen is MultiplayerMatchSubScreen);
AddUntilStep("selected item is new beatmap", () => Client.CurrentMatchPlayingItem.Value?.Beatmap.Value?.OnlineID == otherBeatmap.OnlineID); 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", () => AddStep("Enter song select", () =>
{ {
var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen; var currentSubScreen = ((Screens.OnlinePlay.Multiplayer.Multiplayer)multiplayerScreenStack.CurrentScreen).CurrentSubScreen;
((MultiplayerMatchSubScreen)currentSubScreen).OpenSongSelection(client.CurrentMatchPlayingItem.Value);
((MultiplayerMatchSubScreen)currentSubScreen).SelectBeatmap();
}); });
AddUntilStep("wait for song select", () => this.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true); 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 new BeatmapCarousel Carousel => base.Carousel;
public TestMultiplayerMatchSongSelect(Room room, WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) 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. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -22,20 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var rulesetInfo = new OsuRuleset().RulesetInfo; var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = TestResources.CreateTestScoreInfo(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,
};
PlaylistItem playlistItem = new PlaylistItem PlaylistItem playlistItem = new PlaylistItem
{ {

View File

@ -1,15 +1,13 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic; using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Multiplayer namespace osu.Game.Tests.Visual.Multiplayer
{ {
@ -26,20 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
var rulesetInfo = new OsuRuleset().RulesetInfo; var rulesetInfo = new OsuRuleset().RulesetInfo;
var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo; var beatmapInfo = CreateBeatmap(rulesetInfo).BeatmapInfo;
var score = TestResources.CreateTestScoreInfo(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,
};
PlaylistItem playlistItem = new PlaylistItem 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); 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)); 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)); 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)); 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 imported = Game.ScoreManager.Import(new ScoreInfo
{ {
Hash = Guid.NewGuid().ToString(), Hash = Guid.NewGuid().ToString(),
OnlineScoreID = i, OnlineID = i,
BeatmapInfo = beatmap.Beatmaps.First(), BeatmapInfo = beatmap.Beatmaps.First(),
Ruleset = ruleset ?? new OsuRuleset().RulesetInfo Ruleset = ruleset ?? new OsuRuleset().RulesetInfo
}).Result.Value; }).Result.Value;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -13,6 +14,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Contracted;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
@ -22,13 +24,18 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestShowPanel() 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] [Test]
public void TestExcessMods() 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) 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;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
@ -34,21 +35,21 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
var author = new APIUser { Username = "mapper_name" }; var author = new APIUser { Username = "mapper_name" };
AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(author))));
{
BeatmapInfo = createTestBeatmap(author)
}));
} }
[Test] [Test]
public void TestExcessMods() public void TestExcessMods()
{ {
var author = new APIUser { Username = "mapper_name" }; AddStep("show excess mods score", () =>
AddStep("show excess mods score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo, true)
{ {
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")); 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] [Test]
public void TestMapWithUnknownMapper() public void TestMapWithUnknownMapper()
{ {
AddStep("show example score", () => showPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo) AddStep("show example score", () => showPanel(TestResources.CreateTestScoreInfo(createTestBeatmap(new APIUser()))));
{
BeatmapInfo = createTestBeatmap(new APIUser())
}));
AddAssert("mapped by text not present", () => AddAssert("mapped by text not present", () =>
this.ChildrenOfType<OsuSpriteText>().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); 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 mods = new Mod[] { ruleset.GetAutoplayMod() };
var beatmap = createTestBeatmap(new APIUser()); var beatmap = createTestBeatmap(new APIUser());
showPanel(new TestScoreInfo(ruleset.RulesetInfo) var score = TestResources.CreateTestScoreInfo(beatmap);
{
Mods = mods, score.Mods = mods;
BeatmapInfo = beatmap, score.Date = default;
Date = default,
}); showPanel(score);
}); });
AddAssert("play time not displayed", () => !this.ChildrenOfType<ExpandedPanelMiddleContent.PlayedOnText>().Any()); 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;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu;
using osu.Game.Screens.Ranking.Expanded; using osu.Game.Screens.Ranking.Expanded;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Ranking
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#444"), 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.Beatmaps;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Statistics; using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Tests.Resources;
using osuTK; using osuTK;
using osuTK.Input; using osuTK.Input;
@ -72,11 +72,10 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) var score = TestResources.CreateTestScoreInfo();
{
Accuracy = accuracy, score.Accuracy = accuracy;
Rank = rank score.Rank = rank;
};
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score))); AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen(score)));
AddUntilStep("wait for loaded", () => screen.IsLoaded); AddUntilStep("wait for loaded", () => screen.IsLoaded);
@ -204,7 +203,7 @@ namespace osu.Game.Tests.Visual.Ranking
{ {
DelayedFetchResultsScreen screen = null; 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); AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () => 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); 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 private class TestResultsContainer : Container
{ {
@ -282,7 +281,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i; score.TotalScore += 10 - i;
score.Hash = $"test{i}"; score.Hash = $"test{i}";
scores.Add(score); scores.Add(score);
@ -316,7 +315,7 @@ namespace osu.Game.Tests.Visual.Ranking
for (int i = 0; i < 20; i++) for (int i = 0; i < 20; i++)
{ {
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); var score = TestResources.CreateTestScoreInfo();
score.TotalScore += 10 - i; score.TotalScore += 10 - i;
scores.Add(score); scores.Add(score);
} }

View File

@ -3,10 +3,10 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
@ -17,7 +17,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestDRank() 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); addPanelStep(score);
} }
@ -25,7 +27,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestCRank() 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); addPanelStep(score);
} }
@ -33,7 +37,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestBRank() 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); addPanelStep(score);
} }
@ -41,7 +47,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestARank() 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); addPanelStep(score);
} }
@ -49,7 +57,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestSRank() 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); addPanelStep(score);
} }
@ -57,7 +67,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestAlmostSSRank() 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); addPanelStep(score);
} }
@ -65,7 +77,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestSSRank() 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); addPanelStep(score);
} }
@ -73,7 +87,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestAllHitResults() 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); addPanelStep(score);
} }
@ -81,7 +97,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestContractedPanel() 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); addPanelStep(score, PanelState.Contracted);
} }
@ -89,7 +107,9 @@ namespace osu.Game.Tests.Visual.Ranking
[Test] [Test]
public void TestExpandAndContract() 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); addPanelStep(score, PanelState.Contracted);
AddWaitStep("wait for transition", 10); AddWaitStep("wait for transition", 10);

View File

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

View File

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

View File

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

View File

@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
var score = new ScoreInfo var score = new ScoreInfo
{ {
OnlineScoreID = i, OnlineID = i,
BeatmapInfo = beatmapInfo, BeatmapInfo = beatmapInfo,
BeatmapInfoID = beatmapInfo.ID, BeatmapInfoID = beatmapInfo.ID,
Accuracy = RNG.NextDouble(), Accuracy = RNG.NextDouble(),
@ -163,7 +163,7 @@ namespace osu.Game.Tests.Visual.UserInterface
}); });
AddUntilStep("wait for fetch", () => leaderboard.Scores != null); 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] [Test]
@ -171,7 +171,7 @@ namespace osu.Game.Tests.Visual.UserInterface
{ {
AddStep("delete top score", () => scoreManager.Delete(importedScores[0])); AddStep("delete top score", () => scoreManager.Delete(importedScores[0]));
AddUntilStep("wait for fetch", () => leaderboard.Scores != null); 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 int ID { get; set; }
public bool IsManaged => ID > 0;
public float DrainRate { get; set; } = DEFAULT_DIFFICULTY; public float DrainRate { get; set; } = DEFAULT_DIFFICULTY;
public float CircleSize { get; set; } = DEFAULT_DIFFICULTY; public float CircleSize { get; set; } = DEFAULT_DIFFICULTY;
public float OverallDifficulty { 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; var localRulesetInfo = rulesetInfo as RulesetInfo;
// Difficulty can only be computed if the beatmap and ruleset are locally available. // 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). // 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)); 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 int ID { get; set; }
public bool IsManaged => ID > 0;
public int BeatmapVersion; public int BeatmapVersion;
private int? onlineID; private int? onlineID;

View File

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

View File

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

View File

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

View File

@ -31,10 +31,12 @@ namespace osu.Game.Beatmaps.Drawables.Cards
public class BeatmapCard : OsuClickableContainer public class BeatmapCard : OsuClickableContainer
{ {
public const float TRANSITION_DURATION = 400; 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 width = 408;
private const float height = 100; private const float height = 100;
private const float corner_radius = 10;
private const float icon_area_width = 30; private const float icon_area_width = 30;
private readonly APIBeatmapSet beatmapSet; private readonly APIBeatmapSet beatmapSet;
@ -42,6 +44,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private readonly BeatmapDownloadTracker downloadTracker; private readonly BeatmapDownloadTracker downloadTracker;
private BeatmapCardContent content = null!;
private BeatmapCardThumbnail thumbnail = null!; private BeatmapCardThumbnail thumbnail = null!;
private Container rightAreaBackground = null!; private Container rightAreaBackground = null!;
@ -73,242 +77,247 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
Width = width; Width = width;
Height = height; Height = height;
CornerRadius = corner_radius;
Masking = true;
FillFlowContainer leftIconArea; FillFlowContainer leftIconArea;
GridContainer titleContainer; GridContainer titleContainer;
GridContainer artistContainer; GridContainer artistContainer;
InternalChildren = new Drawable[] InternalChild = content = new BeatmapCardContent(height)
{ {
downloadTracker, MainContent = new Container
rightAreaBackground = new Container
{ {
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Both,
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,
Children = new Drawable[] Children = new Drawable[]
{ {
mainContentBackground = new BeatmapCardContentBackground(beatmapSet) downloadTracker,
rightAreaBackground = new Container
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Y,
}, Width = icon_area_width + 2 * CORNER_RADIUS,
new FillFlowContainer Anchor = Anchor.CentreRight,
{ Origin = Anchor.CentreRight,
RelativeSizeAxes = Axes.Both, // workaround for masking artifacts at the top & bottom of card,
Padding = new MarginPadding // 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, RelativeSizeAxes = Axes.Both,
Vertical = 4 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 Margin = new MarginPadding(5),
{ AutoSizeAxes = Axes.Both,
RelativeSizeAxes = Axes.X, Direction = FillDirection.Horizontal,
AutoSizeAxes = Axes.Y, Spacing = new Vector2(1)
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);
}),
} }
}, },
new Container new Container
{ {
Name = @"Bottom content", Name = @"Right (button) area",
RelativeSizeAxes = Axes.X, Width = 30,
AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Anchor = Anchor.BottomLeft, Origin = Anchor.TopRight,
Origin = Anchor.BottomLeft, Anchor = Anchor.TopRight,
Padding = new MarginPadding Padding = new MarginPadding { Vertical = 17.5f },
Child = rightAreaButtons = new Container<BeatmapCardIconButton>
{ {
Horizontal = 10, RelativeSizeAxes = Axes.Both,
Vertical = 4 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[] Children = new Drawable[]
{ {
idleBottomContent = new FillFlowContainer mainContentBackground = new BeatmapCardContentBackground(beatmapSet)
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.Both,
AutoSizeAxes = Axes.Y, },
new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding
{
Horizontal = 10,
Vertical = 4
},
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 3),
AlwaysPresent = true,
Children = new Drawable[] Children = new Drawable[]
{ {
statisticsContainer = new FillFlowContainer<BeatmapCardStatistic> titleContainer = new GridContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Horizontal, ColumnDimensions = new[]
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[]
{ {
new BeatmapSetOnlineStatusPill new Dimension(),
new Dimension(GridSizeMode.AutoSize)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new[]
{ {
AutoSizeAxes = Axes.Both, new OsuSpriteText
Status = beatmapSet.Status, {
Anchor = Anchor.CentreLeft, Text = new RomanisableString(beatmapSet.TitleUnicode, beatmapSet.Title),
Origin = Anchor.CentreLeft Font = OsuFont.Default.With(size: 22.5f, weight: FontWeight.SemiBold),
}, RelativeSizeAxes = Axes.X,
new DifficultySpectrumDisplay(beatmapSet) Truncate = true
{ },
Anchor = Anchor.CentreLeft, Empty()
Origin = Anchor.CentreLeft,
DotSize = new Vector2(6, 12)
} }
} }
} },
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, RelativeSizeAxes = Axes.X,
Height = 6, AutoSizeAxes = Axes.Y,
Anchor = Anchor.Centre, Anchor = Anchor.BottomLeft,
Origin = Anchor.Centre, Origin = Anchor.BottomLeft,
State = { BindTarget = downloadTracker.State }, Padding = new MarginPadding
Progress = { BindTarget = downloadTracker.Progress } {
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) if (beatmapSet.HasVideo)
@ -344,7 +353,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
{ {
base.LoadComplete(); base.LoadComplete();
downloadTracker.State.BindValueChanged(_ => updateState(), true); downloadTracker.State.BindValueChanged(_ => updateState());
Expanded.BindValueChanged(_ => updateState(), true);
FinishTransforms(true); FinishTransforms(true);
} }
@ -356,6 +366,8 @@ namespace osu.Game.Beatmaps.Drawables.Cards
protected override void OnHoverLost(HoverLostEvent e) protected override void OnHoverLost(HoverLostEvent e)
{ {
content.ScheduleHide();
updateState(); updateState();
base.OnHoverLost(e); base.OnHoverLost(e);
} }
@ -386,19 +398,25 @@ namespace osu.Game.Beatmaps.Drawables.Cards
private void updateState() private void updateState()
{ {
float targetWidth = width - height; bool showDetails = IsHovered || Expanded.Value;
if (IsHovered)
targetWidth = targetWidth - icon_area_width + corner_radius;
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); 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); 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) 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 int ID { get; set; }
public bool IsManaged => ID > 0;
public int? RulesetID { get; set; } public int? RulesetID { get; set; }
public int? Variant { get; set; } public int? Variant { get; set; }

View File

@ -476,7 +476,7 @@ namespace osu.Game.Database
{ {
Files.Dereference(file.FileInfo); 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 // 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. // 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); Update(model);
} }
@ -737,7 +737,7 @@ namespace osu.Game.Database
/// <param name="items">The usable items present in the store.</param> /// <param name="items">The usable items present in the store.</param>
/// <returns>Whether the <typeparamref name="TModel"/> exists.</returns> /// <returns>Whether the <typeparamref name="TModel"/> exists.</returns>
protected virtual bool CheckLocalAvailability(TModel model, IQueryable<TModel> items) 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> /// <summary>
/// Whether import can be skipped after finding an existing import early in the process. /// 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) 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); var userSkinChoice = config.GetBindable<string>(OsuSetting.Skin);
int.TryParse(userSkinChoice.Value, out int userSkinInt); int.TryParse(userSkinChoice.Value, out int userSkinInt);
@ -49,16 +59,6 @@ namespace osu.Game.Database
break; 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 realm = realmContextFactory.CreateContext())
using (var transaction = realm.BeginWrite()) using (var transaction = realm.BeginWrite())
{ {

View File

@ -11,5 +11,7 @@ namespace osu.Game.Database
[JsonIgnore] [JsonIgnore]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)] [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
int ID { get; set; } 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<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 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> /// <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); 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) private static bool matchesOnlineID(this IHasOnlineID<long>? instance, IHasOnlineID<long>? other)
{ {
if (instance == null || other == null) if (instance == null || other == null)

View File

@ -93,7 +93,7 @@ namespace osu.Game.Graphics.UserInterface
if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples)
capsTextAddedSample?.Play(); capsTextAddedSample?.Play();
else else
textAddedSamples[RNG.Next(0, 3)]?.Play(); playTextAddedSample();
} }
protected override void OnUserTextRemoved(string removed) protected override void OnUserTextRemoved(string removed)
@ -117,6 +117,70 @@ namespace osu.Game.Graphics.UserInterface
caretMovedSample?.Play(); 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) protected override void OnFocus(FocusEvent e)
{ {
BorderThickness = 3; BorderThickness = 3;
@ -142,6 +206,8 @@ namespace osu.Game.Graphics.UserInterface
SelectionColour = SelectionColour, SelectionColour = SelectionColour,
}; };
private void playTextAddedSample() => textAddedSamples[RNG.Next(0, textAddedSamples.Length)]?.Play();
private class OsuCaret : Caret private class OsuCaret : Caret
{ {
private const float caret_move_time = 60; private const float caret_move_time = 60;

View File

@ -9,6 +9,8 @@ namespace osu.Game.IO
{ {
public int ID { get; set; } public int ID { get; set; }
public bool IsManaged => ID > 0;
public string Hash { get; set; } public string Hash { get; set; }
public int ReferenceCount { 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. // compare counts in database vs defaults for each action type.
foreach (var defaultsForAction in defaults.GroupBy(k => k.Action)) foreach (var defaultsForAction in defaults.GroupBy(k => k.Action))
{ {
// avoid performing redundant queries when the database is empty and needs to be re-filled. IEnumerable<RealmKeyBinding> existing = existingBindings.Where(k =>
int existingCount = existingBindings.Count(k => k.RulesetName == rulesetName && k.Variant == variant && k.ActionInt == (int)defaultsForAction.Key); k.RulesetName == rulesetName
&& k.Variant == variant
&& k.ActionInt == (int)defaultsForAction.Key);
if (defaultsForAction.Count() <= existingCount) int defaultsCount = defaultsForAction.Count();
continue; int existingCount = existing.Count();
// insert any defaults which are missing. if (defaultsCount > existingCount)
realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
{ {
KeyCombinationString = k.KeyCombination.ToString(), // insert any defaults which are missing.
ActionInt = (int)k.Action, realm.Add(defaultsForAction.Skip(existingCount).Select(k => new RealmKeyBinding
RulesetName = rulesetName, {
Variant = variant 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) if (WebRequest != null)
{ {
Response = ((OsuJsonWebRequest<T>)WebRequest).ResponseObject; 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 namespace osu.Game.Online.API.Requests
{ {
public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScoreInfo>> public class GetUserScoresRequest : PaginatedAPIRequest<List<APIScore>>
{ {
private readonly long userId; private readonly long userId;
private readonly ScoreType type; private readonly ScoreType type;

View File

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

View File

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

View File

@ -111,7 +111,7 @@ namespace osu.Game.Online.Leaderboards
background = new Box background = new Box
{ {
RelativeSizeAxes = Axes.Both, 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, Alpha = background_alpha,
}, },
}, },

View File

@ -82,5 +82,17 @@ namespace osu.Game.Online.Multiplayer
/// </summary> /// </summary>
/// <param name="item">The item to add.</param> /// <param name="item">The item to add.</param>
Task AddPlaylistItem(MultiplayerPlaylistItem item); 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); Debug.Assert(joinedRoom != null);
// Populate playlist items. // 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. // Populate users.
Debug.Assert(joinedRoom.Users != null); Debug.Assert(joinedRoom.Users != null);
@ -335,6 +335,10 @@ namespace osu.Game.Online.Multiplayer
public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item); public abstract Task AddPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task EditPlaylistItem(MultiplayerPlaylistItem item);
public abstract Task RemovePlaylistItem(long playlistItemId);
Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state)
{ {
if (Room == null) if (Room == null)
@ -470,7 +474,32 @@ namespace osu.Game.Online.Multiplayer
Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) 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; return Task.CompletedTask;
} }
@ -629,7 +658,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null) if (Room == null)
return; return;
var playlistItem = await createPlaylistItem(item).ConfigureAwait(false); var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
Scheduler.Add(() => Scheduler.Add(() =>
{ {
@ -673,7 +702,7 @@ namespace osu.Game.Online.Multiplayer
if (Room == null) if (Room == null)
return; return;
var playlistItem = await createPlaylistItem(item).ConfigureAwait(false); var playlistItem = await createPlaylistItem(item, true).ConfigureAwait(false);
Scheduler.Add(() => Scheduler.Add(() =>
{ {
@ -728,10 +757,8 @@ namespace osu.Game.Online.Multiplayer
CurrentMatchPlayingItem.Value = APIRoom.Playlist.SingleOrDefault(p => p.ID == settings.PlaylistItemId); 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); var ruleset = Rulesets.GetRuleset(item.RulesetID);
Debug.Assert(ruleset != null); Debug.Assert(ruleset != null);
@ -741,8 +768,8 @@ namespace osu.Game.Online.Multiplayer
var playlistItem = new PlaylistItem var playlistItem = new PlaylistItem
{ {
ID = item.ID, ID = item.ID,
BeatmapID = item.BeatmapID,
OwnerID = item.OwnerID, OwnerID = item.OwnerID,
Beatmap = { Value = apiBeatmap },
Ruleset = { Value = ruleset }, Ruleset = { Value = ruleset },
Expired = item.Expired, Expired = item.Expired,
PlaylistOrder = item.PlaylistOrder, PlaylistOrder = item.PlaylistOrder,
@ -752,6 +779,9 @@ namespace osu.Game.Online.Multiplayer
playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance))); playlistItem.RequiredMods.AddRange(item.RequiredMods.Select(m => m.ToMod(rulesetInstance)));
playlistItem.AllowedMods.AddRange(item.AllowedMods.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; return playlistItem;
} }

View File

@ -162,6 +162,22 @@ namespace osu.Game.Online.Multiplayer
return connection.InvokeAsync(nameof(IMultiplayerServer.AddPlaylistItem), item); 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) protected override Task<APIBeatmap> GetAPIBeatmap(int beatmapId, CancellationToken cancellationToken = default)
{ {
return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken); return beatmapLookupCache.GetBeatmapAsync(beatmapId, cancellationToken);

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using System; using System;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Game.Extensions;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -35,7 +36,7 @@ namespace osu.Game.Online
var scoreInfo = new ScoreInfo var scoreInfo = new ScoreInfo
{ {
ID = TrackedItem.ID, ID = TrackedItem.ID,
OnlineScoreID = TrackedItem.OnlineScoreID OnlineID = TrackedItem.OnlineID
}; };
if (Manager.IsAvailableLocally(scoreInfo)) if (Manager.IsAvailableLocally(scoreInfo))
@ -113,7 +114,7 @@ namespace osu.Game.Online
UpdateState(DownloadState.NotDownloaded); 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 #region Disposal

View File

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

View File

@ -16,7 +16,7 @@ namespace osu.Game.Online.Solo
{ {
/// <summary> /// <summary>
/// A class specifically for sending scores to the API during score submission. /// 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> /// </summary>
[Serializable] [Serializable]
public class SubmittableScore public class SubmittableScore

View File

@ -103,7 +103,7 @@ namespace osu.Game
private Container topMostOverlayContent; private Container topMostOverlayContent;
private ScalingContainer screenContainer; protected ScalingContainer ScreenContainer { get; private set; }
protected Container ScreenOffsetContainer { get; private set; } protected Container ScreenOffsetContainer { get; private set; }
@ -179,7 +179,7 @@ namespace osu.Game
} }
private void updateBlockingOverlayFade() => 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) public void AddBlockingOverlay(OverlayContainer overlay)
{ {
@ -487,8 +487,8 @@ namespace osu.Game
// to ensure all the required data for presenting a replay are present. // to ensure all the required data for presenting a replay are present.
ScoreInfo databasedScoreInfo = null; ScoreInfo databasedScoreInfo = null;
if (score.OnlineScoreID != null) if (score.OnlineID > 0)
databasedScoreInfo = ScoreManager.Query(s => s.OnlineScoreID == score.OnlineScoreID); databasedScoreInfo = ScoreManager.Query(s => s.OnlineID == score.OnlineID);
databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash); databasedScoreInfo ??= ScoreManager.Query(s => s.Hash == score.Hash);
@ -698,7 +698,7 @@ namespace osu.Game
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new Drawable[] Children = new Drawable[]
{ {
screenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays) ScreenContainer = new ScalingContainer(ScalingMode.ExcludeOverlays)
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
@ -801,7 +801,7 @@ namespace osu.Game
loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true); loadComponentSingleFile(userProfile = new UserProfileOverlay(), overlayContent.Add, true);
loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true); loadComponentSingleFile(beatmapSetOverlay = new BeatmapSetOverlay(), overlayContent.Add, true);
loadComponentSingleFile(wikiOverlay = new WikiOverlay(), 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 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. // 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, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -94,7 +94,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
topScoresContainer.Add(new DrawableTopScore(topScore)); 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)); topScoresContainer.Add(new DrawableTopScore(userScoreInfo, userScore.Position));
}), TaskContinuationOptions.OnlyOnRanToCompletion); }), TaskContinuationOptions.OnlyOnRanToCompletion);
}); });

View File

@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -22,7 +23,7 @@ namespace osu.Game.Overlays.Profile
public abstract string Identifier { get; } public abstract string Identifier { get; }
private readonly FillFlowContainer content; private readonly FillFlowContainer<Drawable> content;
private readonly Box background; private readonly Box background;
private readonly Box underscore; 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, Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,

View File

@ -13,6 +13,7 @@ using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Graphics.Containers;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osuTK; using osuTK;
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.Profile.Sections
protected int VisiblePages; protected int VisiblePages;
protected int ItemsPerPage; protected int ItemsPerPage;
protected FillFlowContainer ItemsContainer { get; private set; } protected ReverseChildIDFillFlowContainer<Drawable> ItemsContainer { get; private set; }
private APIRequest<List<TModel>> retrievalRequest; private APIRequest<List<TModel>> retrievalRequest;
private CancellationTokenSource loadCancellation; private CancellationTokenSource loadCancellation;
@ -48,11 +49,15 @@ namespace osu.Game.Overlays.Profile.Sections
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Children = new Drawable[] 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, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
Spacing = new Vector2(0, 2), Spacing = new Vector2(0, 2),
// ensure the container and its contents are in front of the "more" button.
Depth = float.MinValue
}, },
moreButton = new ShowMoreButton moreButton = new ShowMoreButton
{ {

View File

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

View File

@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
{ {
private readonly double weight; private readonly double weight;
public DrawableProfileWeightedScore(APIScoreInfo score, double weight) public DrawableProfileWeightedScore(APIScore score, double weight)
: base(score) : base(score)
{ {
this.weight = weight; 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 namespace osu.Game.Overlays.Profile.Sections.Ranks
{ {
public class PaginatedScoreContainer : PaginatedProfileSubsection<APIScoreInfo> public class PaginatedScoreContainer : PaginatedProfileSubsection<APIScore>
{ {
private readonly ScoreType type; 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) if (VisiblePages == 0)
drawableItemIndex = 0; drawableItemIndex = 0;
@ -59,12 +59,12 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
base.OnItemsReceived(items); base.OnItemsReceived(items);
} }
protected override APIRequest<List<APIScoreInfo>> CreateRequest() => protected override APIRequest<List<APIScore>> CreateRequest() =>
new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage);
private int drawableItemIndex; private int drawableItemIndex;
protected override Drawable CreateDrawableItem(APIScoreInfo model) protected override Drawable CreateDrawableItem(APIScore model)
{ {
switch (type) switch (type)
{ {

View File

@ -135,7 +135,8 @@ namespace osu.Game.Overlays.Rankings
Children = new Drawable[] Children = new Drawable[]
{ {
new ScoresTable(1, response.Users), 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, AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X, 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. /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary> /// </summary>
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> /// <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()) foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{ {
if (combination is MultiMod multi) if (combination is MultiMod multi)
yield return Calculate(multi.Mods); yield return Calculate(multi.Mods, cancellationToken);
else 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(); 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); var track = new TrackVirtual(10000);
playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track)); playableMods.OfType<IApplicableToTrack>().ForEach(m => m.ApplyToTrack(track));

View File

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

View File

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

View File

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

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