1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 17:32:54 +08:00

Merge branch 'master' into multiplayer-playlist-deletion

This commit is contained in:
Dean Herbert 2021-12-09 19:04:06 +09:00 committed by GitHub
commit 538a822760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 170 additions and 93 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -62,7 +62,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditable()
{
createPlaylist(p => p.AllowReordering = p.AllowDeletion = true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
});
moveToItem(0);
assertHandleVisibility(0, true);
@ -75,7 +79,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestMarkInvalid()
{
createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
AddStep("mark item 0 as invalid", () => playlist.Items[0].MarkInvalid());
@ -102,7 +111,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestEditableSelectable()
{
createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0);
assertHandleVisibility(0, true);
@ -116,7 +130,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
[Test]
public void TestSelectionNotLostAfterRearrangement()
{
createPlaylist(p => p.AllowReordering = p.AllowDeletion = p.AllowSelection = true);
createPlaylist(p =>
{
p.AllowReordering = true;
p.AllowDeletion = true;
p.AllowSelection = true;
});
moveToItem(0);
AddStep("click", () => InputManager.Click(MouseButton.Left));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -22,6 +22,8 @@ namespace osu.Game.Scoring
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public ScoreRank Rank { get; set; }
public long TotalScore { get; set; }

View File

@ -46,8 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
Padding = new MarginPadding { Bottom = 10 },
Child = playlist = new PlaylistsRoomSettingsPlaylist
{
RelativeSizeAxes = Axes.Both,
AllowSelection = true,
RelativeSizeAxes = Axes.Both
}
}
},

View File

@ -13,13 +13,13 @@ using osuTK;
namespace osu.Game.Screens.OnlinePlay
{
/// <summary>
/// A list scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>.
/// A scrollable list which displays the <see cref="PlaylistItem"/>s in a <see cref="Room"/>.
/// </summary>
public class DrawableRoomPlaylist : OsuRearrangeableListContainer<PlaylistItem>
{
/// <summary>
/// The currently-selected item, used to show a border around items.
/// May be updated by playlist items if <see cref="AllowSelection"/> is <c>true</c>.
/// The currently-selected item. Selection is visually represented with a border.
/// May be updated by clicking playlist items if <see cref="AllowSelection"/> is <c>true</c>.
/// </summary>
public readonly Bindable<PlaylistItem> SelectedItem = new Bindable<PlaylistItem>();

View File

@ -38,7 +38,8 @@ namespace osu.Game.Screens.OnlinePlay
public class DrawableRoomPlaylistItem : OsuRearrangeableListItem<PlaylistItem>
{
public const float HEIGHT = 50;
public const float ICON_HEIGHT = 34;
private const float icon_height = 34;
/// <summary>
/// Invoked when this item requests to be deleted.
@ -238,7 +239,7 @@ namespace osu.Game.Screens.OnlinePlay
}
if (Item.Beatmap.Value != null)
difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(ICON_HEIGHT) };
difficultyIconContainer.Child = new DifficultyIcon(Item.Beatmap.Value, ruleset.Value, requiredMods, performBackgroundDifficultyLookup: false) { Size = new Vector2(icon_height) };
else
difficultyIconContainer.Clear();
@ -392,7 +393,7 @@ namespace osu.Game.Screens.OnlinePlay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(ICON_HEIGHT),
Size = new Vector2(icon_height),
Margin = new MarginPadding { Right = 8 },
Masking = true,
CornerRadius = 4,
@ -405,22 +406,21 @@ namespace osu.Game.Screens.OnlinePlay
};
}
private IEnumerable<Drawable> createButtons() =>
new[]
private IEnumerable<Drawable> createButtons() => new[]
{
showResultsButton = new ShowResultsButton
{
showResultsButton = new ShowResultsButton
{
Action = () => RequestResults?.Invoke(Item),
Alpha = AllowShowingResults ? 1 : 0,
},
Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item),
removeButton = new PlaylistRemoveButton
{
Size = new Vector2(30, 30),
Alpha = AllowDeletion ? 1 : 0,
Action = () => RequestDeletion?.Invoke(Item),
},
};
Action = () => RequestResults?.Invoke(Item),
Alpha = AllowShowingResults ? 1 : 0,
},
Item.Beatmap.Value == null ? Empty() : new PlaylistDownloadButton(Item),
removeButton = new PlaylistRemoveButton
{
Size = new Vector2(30, 30),
Alpha = AllowDeletion ? 1 : 0,
Action = () => RequestDeletion?.Invoke(Item),
},
};
public class PlaylistRemoveButton : GrayButton
{

View File

@ -807,14 +807,14 @@ namespace osu.Game.Screens.Select
private void delete(BeatmapSetInfo beatmap)
{
if (beatmap == null || beatmap.ID <= 0) return;
if (beatmap == null || !beatmap.IsManaged) return;
dialogOverlay?.Push(new BeatmapDeleteDialog(beatmap));
}
private void clearScores(BeatmapInfo beatmapInfo)
{
if (beatmapInfo == null || beatmapInfo.ID <= 0) return;
if (beatmapInfo == null || !beatmapInfo.IsManaged) return;
dialogOverlay?.Push(new BeatmapClearScoresDialog(beatmapInfo, () =>
// schedule done here rather than inside the dialog as the dialog may fade out and never callback.

View File

@ -57,5 +57,7 @@ namespace osu.Game.Skinning
string author = Creator == null ? string.Empty : $"({Creator})";
return $"{Name} {author}".Trim();
}
public bool IsManaged => ID > 0;
}
}

View File

@ -11,6 +11,8 @@ namespace osu.Game.Skinning
{
public int ID { get; set; }
public bool IsManaged => ID > 0;
public int SkinInfoID { get; set; }
public EFSkinInfo SkinInfo { get; set; }

View File

@ -1,4 +1,4 @@
// 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.
using System;