1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-15 21:03:13 +08:00

Compare commits

...

291 Commits

152 changed files with 3707 additions and 482 deletions
+2 -2
View File
@@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.903.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.903.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.910.0" />
</ItemGroup>
</Project>
@@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
[Timeout(10000)]
public class CatchBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
{
Name = @"Fruit Count",
Content = fruits.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Juice Stream Count",
Content = juiceStreams.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Banana Shower Count",
Content = bananaShowers.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
}
};
}
-2
View File
@@ -21,13 +21,11 @@ using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using System;
using osu.Framework.Testing;
using osu.Game.Rulesets.Catch.Skinning;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Catch
{
[ExcludeFromDynamicCompile]
public class CatchRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableCatchRuleset(this, beatmap, mods);
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty
public class CatchDifficultyAttributes : DifficultyAttributes
{
public double ApproachRate;
public int MaxCombo;
}
}
@@ -14,11 +14,13 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
[Timeout(10000)]
public class ManiaBeatmapConversionTest : BeatmapConversionTest<ManiaConvertMapping, ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase("basic")]
[TestCase("zero-length-slider")]
public void Test(string name) => base.Test(name);
protected override IEnumerable<ConvertValue> CreateConvertValue(HitObject hitObject)
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.UI;
@@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
new BeatmapStatistic
{
Name = @"Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = notes.ToString(),
Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Hold Note Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = holdnotes.ToString(),
Icon = FontAwesome.Regular.Circle
},
};
}
@@ -5,7 +5,6 @@ using osu.Game.Rulesets.Mania.Objects;
using System;
using System.Linq;
using System.Collections.Generic;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects;
@@ -167,8 +166,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
var positionData = original as IHasPosition;
for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration)
for (int i = 0; i <= generator.SpanCount; i++)
{
double time = original.StartTime + generator.SegmentDuration * i;
recordNote(time, positionData?.Position ?? Vector2.Zero);
computeDensity(time);
}
@@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
public readonly double EndTime;
public readonly double SegmentDuration;
private readonly int spanCount;
public readonly int SpanCount;
private PatternType convertType;
@@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
spanCount = repeatsData?.SpanCount() ?? 1;
SpanCount = repeatsData?.SpanCount() ?? 1;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * spanCount;
double distance = (distanceData?.Distance ?? 0) * SpanCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / spanCount;
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
}
public override IEnumerable<Pattern> Generate()
@@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return pattern;
}
if (spanCount > 1)
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
@@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, spanCount + 1);
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
@@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart)
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
@@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
bool increasing = Random.NextDouble() > 0.5;
for (int i = 0; i <= spanCount; i++)
for (int i = 0; i <= SpanCount; i++)
{
addToPattern(pattern, column, startTime, startTime);
startTime += SegmentDuration;
@@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i <= spanCount; i++)
for (int i = 0; i <= SpanCount; i++)
{
addToPattern(pattern, nextColumn, startTime, startTime);
@@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern();
int columnRepeat = Math.Min(spanCount, TotalColumns);
int columnRepeat = Math.Min(SpanCount, TotalColumns);
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
@@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var rowPattern = new Pattern();
for (int i = 0; i <= spanCount; i++)
for (int i = 0; i <= SpanCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
{
@@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint();
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mania.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint();
}
}
-2
View File
@@ -12,7 +12,6 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Testing;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Replays.Types;
@@ -35,7 +34,6 @@ using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania
{
[ExcludeFromDynamicCompile]
public class ManiaRuleset : Ruleset, ILegacyRuleset
{
/// <summary>
@@ -0,0 +1,14 @@
{
"Mappings": [{
"RandomW": 3083084786,
"RandomX": 273326509,
"RandomY": 273553282,
"RandomZ": 2659838971,
"StartTime": 4836,
"Objects": [{
"StartTime": 4836,
"EndTime": 4836,
"Column": 0
}]
}]
}
@@ -0,0 +1,20 @@
osu file format v14
[General]
StackLeniency: 0.7
Mode: 0
[Difficulty]
HPDrainRate:1
CircleSize:4
OverallDifficulty:1
ApproachRate:9
SliderMultiplier:2.5
SliderTickRate:0.5
[TimingPoints]
34,431.654676258993,4,1,0,50,1,0
4782,-66.6666666666667,4,1,0,20,0,0
[HitObjects]
15,199,4836,22,0,L,1,46.8750017881394
@@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
[Timeout(10000)]
public class OsuBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
+3 -4
View File
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
@@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
Name = @"Circle Count",
Content = circles.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
},
new BeatmapStatistic
{
Name = @"Slider Count",
Content = sliders.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
},
new BeatmapStatistic
{
Name = @"Spinner Count",
Content = spinners.ToString(),
Icon = FontAwesome.Regular.Circle
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
}
};
}
@@ -11,6 +11,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
public int MaxCombo;
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint();
}
}
@@ -5,6 +5,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -38,6 +39,13 @@ namespace osu.Game.Rulesets.Osu.Edit
new SpinnerCompositionTool()
};
private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" };
protected override IEnumerable<BindableBool> Toggles => new[]
{
distanceSnapToggle
};
[BackgroundDependencyLoader]
private void load()
{
@@ -45,6 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit
EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();
EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid();
distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid();
}
protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)
@@ -87,6 +96,10 @@ namespace osu.Game.Rulesets.Osu.Edit
{
distanceSnapGridContainer.Clear();
distanceSnapGridCache.Invalidate();
distanceSnapGrid = null;
if (!distanceSnapToggle.Value)
return;
switch (BlueprintContainer.CurrentTool)
{
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint();
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint();
}
}
-2
View File
@@ -30,14 +30,12 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using System;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu
{
[ExcludeFromDynamicCompile]
public class OsuRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableOsuRuleset(this, beatmap, mods);
@@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS);
protected new float CalculatedBorderPortion
// Roughly matches osu!stable's slider border portions.
=> base.CalculatedBorderPortion * 0.77f;
public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f);
protected override Color4 ColourAt(float position)
@@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Statistics
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
{
if (pointGrid.Content.Length == 0)
if (pointGrid.Content.Count == 0)
return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
@@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
[Timeout(10000)]
public class TaikoBeatmapConversionTest : BeatmapConversionTest<ConvertValue>
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
@@ -3,7 +3,6 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Taiko.Objects;
@@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
new BeatmapStatistic
{
Name = @"Hit Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles),
Content = hits.ToString(),
Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Drumroll Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders),
Content = drumrolls.ToString(),
Icon = FontAwesome.Regular.Circle
},
new BeatmapStatistic
{
Name = @"Swell Count",
CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners),
Content = swells.ToString(),
Icon = FontAwesome.Regular.Circle
}
};
}
@@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
public class TaikoDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public int MaxCombo;
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders);
public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint();
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles);
public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint();
}
}
@@ -1,6 +1,8 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Taiko.Edit.Blueprints;
@@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
}
public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners);
public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint();
}
}
-2
View File
@@ -22,7 +22,6 @@ using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring;
using System;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
@@ -32,7 +31,6 @@ using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko
{
[ExcludeFromDynamicCompile]
public class TaikoRuleset : Ruleset, ILegacyRuleset
{
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => new DrawableTaikoRuleset(this, beatmap, mods);
@@ -10,6 +10,7 @@ using System.Text;
using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
@@ -19,6 +20,7 @@ using osu.Game.Rulesets.Catch;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Skinning;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Beatmaps.Formats
@@ -26,18 +28,33 @@ namespace osu.Game.Tests.Beatmaps.Formats
[TestFixture]
public class LegacyBeatmapEncoderTest
{
private static IEnumerable<string> allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu"));
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name)
{
var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name));
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded));
var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name);
var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name);
sort(decoded);
sort(decodedAfterEncode);
sort(decoded.beatmap);
sort(decodedAfterEncode.beatmap);
Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize()));
Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize()));
Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration));
}
private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b)
{
// equal to null, no need to SequenceEqual
if (a.ComboColours == null && b.ComboColours == null)
return true;
if (a.ComboColours == null || b.ComboColours == null)
return false;
return a.ComboColours.SequenceEqual(b.ComboColours);
}
private void sort(IBeatmap beatmap)
@@ -50,18 +67,31 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
}
private IBeatmap decodeFromLegacy(Stream stream)
private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name)
{
using (var reader = new LineBufferedReader(stream))
return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader));
{
var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader);
var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name);
return (convert(beatmap), beatmapSkin);
}
}
private Stream encodeToLegacy(IBeatmap beatmap)
private class TestLegacySkin : LegacySkin
{
public TestLegacySkin(IResourceStore<byte[]> storage, string fileName)
: base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName)
{
}
}
private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap)
{
var (beatmap, beatmapSkin) = fullBeatmap;
var stream = new MemoryStream();
using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmap).Encode(writer);
new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer);
stream.Position = 0;
+76 -17
View File
@@ -15,8 +15,10 @@ using osu.Framework.Extensions;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.IO;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Resources;
using osu.Game.Users;
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
@@ -32,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWhenClosed()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -49,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDelete()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -70,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -96,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithReZip()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -154,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithChangedFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -205,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportThenImportWithDifferentFilename()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -257,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportCorruptThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -299,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestRollbackOnFailure()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -376,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImport()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -404,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(TestImportThenDeleteThenImportWithOnlineIDMismatch)}-{set}"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString()))
{
try
{
@@ -438,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO
public async Task TestImportWithDuplicateBeatmapIDs()
{
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -524,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWhenFileOpen()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -546,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithDuplicateHashes()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -588,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportNestedStructure()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -633,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestImportWithIgnoredDirectoryInArchive()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -687,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapInfo()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapInfo)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -717,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[Test]
public async Task TestUpdateBeatmapFile()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile)))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -756,6 +758,63 @@ namespace osu.Game.Tests.Beatmaps.IO
}
}
[Test]
public void TestCreateNewEmptyBeatmap()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
manager.Save(working.BeatmapInfo, working.Beatmap);
var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
// Check that the new file is referenced correctly by attempting a retrieval
Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0));
}
finally
{
host.Exit();
}
}
}
[Test]
public void TestCreateNewBeatmapWithObject()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
var manager = osu.Dependencies.Get<BeatmapManager>();
var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER);
((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 });
manager.Save(working.BeatmapInfo, working.Beatmap);
var retrievedSet = manager.GetAllUsableBeatmapSets()[0];
// Check that the new file is referenced correctly by attempting a retrieval
Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap;
Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1));
Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000));
}
finally
{
host.Exit();
}
}
}
public static async Task<BeatmapSetInfo> LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false)
{
var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack);
@@ -0,0 +1,221 @@
// 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.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Game.Collections;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Collections.IO
{
[TestFixture]
public class ImportCollectionsTest
{
[Test]
public async Task TestImportEmptyDatabase()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
await osu.CollectionManager.Import(new MemoryStream());
Assert.That(osu.CollectionManager.Collections.Count, Is.Zero);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportWithNoBeatmaps()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.Zero);
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.Zero);
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportWithBeatmaps()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12));
}
finally
{
host.Exit();
}
}
}
[Test]
public async Task TestImportMalformedDatabase()
{
bool exceptionThrown = false;
UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true;
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
AppDomain.CurrentDomain.UnhandledException += setException;
var osu = loadOsu(host, true);
using (var ms = new MemoryStream())
{
using (var bw = new BinaryWriter(ms, Encoding.UTF8, true))
{
for (int i = 0; i < 10000; i++)
bw.Write((byte)i);
}
ms.Seek(0, SeekOrigin.Begin);
await osu.CollectionManager.Import(ms);
}
Assert.That(host.UpdateThread.Running, Is.True);
Assert.That(exceptionThrown, Is.False);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0));
}
finally
{
host.Exit();
AppDomain.CurrentDomain.UnhandledException -= setException;
}
}
}
[Test]
public async Task TestSaveAndReload()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
var osu = loadOsu(host, true);
await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db"));
// Move first beatmap from second collection into the first.
osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]);
osu.CollectionManager.Collections[1].Beatmaps.RemoveAt(0);
// Rename the second collecction.
osu.CollectionManager.Collections[1].Name.Value = "Another";
}
finally
{
host.Exit();
}
}
using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload"))
{
try
{
var osu = loadOsu(host, true);
Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First"));
Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2));
Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another"));
Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11));
}
finally
{
host.Exit();
}
}
}
private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false)
{
var osu = new TestOsuGameBase(withBeatmap);
#pragma warning disable 4014
Task.Run(() => host.Run(osu));
#pragma warning restore 4014
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
return osu;
}
private void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 60000)
{
Task task = Task.Run(() =>
{
while (!result()) Thread.Sleep(200);
});
Assert.IsTrue(task.Wait(timeout), failureMessage);
}
private class TestOsuGameBase : OsuGameBase
{
public CollectionManager CollectionManager { get; private set; }
private readonly bool withBeatmap;
public TestOsuGameBase(bool withBeatmap)
{
this.withBeatmap = withBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
// Beatmap must be imported before the collection manager is loaded.
if (withBeatmap)
BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
AddInternal(CollectionManager = new CollectionManager(Storage));
}
}
}
}
@@ -351,7 +351,7 @@ namespace osu.Game.Tests.Editing
using (var encoded = new MemoryStream())
{
using (var sw = new StreamWriter(encoded))
new LegacyBeatmapEncoder(beatmap).Encode(sw);
new LegacyBeatmapEncoder(beatmap, null).Encode(sw);
return encoded.ToArray();
}
Binary file not shown.
+5 -5
View File
@@ -27,7 +27,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestBasicImport()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -66,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportMods()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -92,7 +92,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportStatistics()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -122,7 +122,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestImportWithDeletedBeatmapSet()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO
[Test]
public async Task TestOnlineScoreIsAvailableLocally()
{
using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally"))
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
{
try
{
@@ -0,0 +1,244 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Rulesets;
using osu.Game.Tests.Resources;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Collections
{
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly DialogOverlay dialogOverlay;
private readonly CollectionManager manager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private ManageCollectionsDialog dialog;
public TestSceneManageCollectionsDialog()
{
base.Content.AddRange(new Drawable[]
{
manager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both },
dialogOverlay = new DialogOverlay()
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(manager);
dependencies.Cache(dialogOverlay);
return dependencies;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
manager.Collections.Clear();
Child = dialog = new ManageCollectionsDialog();
});
[SetUpSteps]
public void SetUpSteps()
{
AddStep("show dialog", () => dialog.Show());
}
[Test]
public void TestHideDialog()
{
AddWaitStep("wait for animation", 3);
AddStep("hide dialog", () => dialog.Hide());
}
[Test]
public void TestLastItemIsPlaceholder()
{
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
}
[Test]
public void TestAddCollectionExternal()
{
AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } }));
assertCollectionCount(1);
assertCollectionName(0, "First collection");
AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } }));
assertCollectionCount(2);
assertCollectionName(1, "Second collection");
}
[Test]
public void TestFocusPlaceholderDoesNotCreateCollection()
{
AddStep("focus placeholder", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(0);
}
[Test]
public void TestAddCollectionViaPlaceholder()
{
DrawableCollectionListItem placeholderItem = null;
AddStep("focus placeholder", () =>
{
InputManager.MoveMouseTo(placeholderItem = dialog.ChildrenOfType<DrawableCollectionListItem>().Last());
InputManager.Click(MouseButton.Left);
});
// Done directly via the collection since InputManager methods cannot add text to textbox...
AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a");
assertCollectionCount(1);
AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model));
AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType<DrawableCollectionListItem>().Last().Model));
}
[Test]
public void TestRemoveCollectionExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
AddStep("remove first collection", () => manager.Collections.RemoveAt(0));
assertCollectionCount(1);
assertCollectionName(0, "2");
}
[Test]
public void TestRemoveCollectionViaButton()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
}));
assertCollectionCount(2);
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog not displayed", () => dialogOverlay.CurrentDialog == null);
assertCollectionCount(1);
assertCollectionName(0, "2");
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
AddStep("click confirmation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().First());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(0);
}
[Test]
public void TestCollectionNotRemovedWhenDialogCancelled()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } },
}));
assertCollectionCount(1);
AddStep("click first delete button", () =>
{
InputManager.MoveMouseTo(dialog.ChildrenOfType<DrawableCollectionListItem.DeleteButton>().First(), new Vector2(5, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog);
AddStep("click cancellation", () =>
{
InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType<PopupDialogButton>().Last());
InputManager.Click(MouseButton.Left);
});
assertCollectionCount(1);
}
[Test]
public void TestCollectionRenamedExternal()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First");
assertCollectionName(0, "First");
}
[Test]
public void TestCollectionRenamedOnTextChange()
{
AddStep("add two collections", () => manager.Collections.AddRange(new[]
{
new BeatmapCollection { Name = { Value = "1" } },
new BeatmapCollection { Name = { Value = "2" } },
}));
assertCollectionCount(2);
AddStep("change first collection name", () => dialog.ChildrenOfType<TextBox>().First().Text = "First");
AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First");
}
private void assertCollectionCount(int count)
=> AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType<DrawableCollectionListItem>().Count(i => i.IsCreated.Value) == count);
private void assertCollectionName(int index, string name)
=> AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType<DrawableCollectionListItem>().ElementAt(index).ChildrenOfType<TextBox>().First().Text == name);
}
}
@@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Editing
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
protected new TestEditor Editor => (TestEditor)base.Editor;
public override void SetUpSteps()
{
base.SetUpSteps();
@@ -35,6 +37,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps();
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@@ -64,9 +68,11 @@ namespace osu.Game.Tests.Visual.Editing
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
AddAssert("hitobject added", () => addedObject == expectedObject);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
addUndoSteps();
AddAssert("hitobject removed", () => removedObject == expectedObject);
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@@ -94,6 +100,17 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
}
[Test]
public void TestAddObjectThenSaveHasNoUnsavedChanges()
{
AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 }));
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
AddStep("save changes", () => Editor.Save());
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
}
[Test]
@@ -120,6 +137,7 @@ namespace osu.Game.Tests.Visual.Editing
addUndoSteps();
AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance)
AddAssert("no hitobject removed", () => removedObject == null);
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone
}
[Test]
@@ -148,19 +166,24 @@ namespace osu.Game.Tests.Visual.Editing
addRedoSteps();
AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo)
AddAssert("no hitobject added", () => addedObject == null);
AddAssert("no changes", () => !Editor.HasUnsavedChanges); // end result is empty beatmap, matching original state
}
private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo());
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo());
private void addRedoSteps() => AddStep("redo", () => Editor.Redo());
protected override Editor CreateEditor() => new TestEditor();
private class TestEditor : Editor
protected class TestEditor : Editor
{
public new void Undo() => base.Undo();
public new void Redo() => base.Redo();
public new void Save() => base.Save();
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
}
}
}
@@ -3,6 +3,7 @@
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Game.Screens.Edit.Components.RadioButtons;
namespace osu.Game.Tests.Visual.Editing
@@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing
{
new RadioButton("Item 1", () => { }),
new RadioButton("Item 2", () => { }),
new RadioButton("Item 3", () => { }),
new RadioButton("Item 3", () => { }, () => new SpriteIcon { Icon = FontAwesome.Regular.Angry }),
new RadioButton("Item 4", () => { }),
new RadioButton("Item 5", () => { })
}
@@ -0,0 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Setup;
namespace osu.Game.Tests.Visual.Editing
{
[TestFixture]
public class TestSceneSetupScreen : EditorClockTestScene
{
[Cached(typeof(EditorBeatmap))]
[Cached(typeof(IBeatSnapProvider))]
private readonly EditorBeatmap editorBeatmap;
public TestSceneSetupScreen()
{
editorBeatmap = new EditorBeatmap(new OsuBeatmap());
}
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap);
Child = new SetupScreen();
}
}
}
@@ -0,0 +1,54 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Rulesets;
namespace osu.Game.Tests.Visual.Gameplay
{
public class TestSceneOverlayActivation : OsuPlayerTestScene
{
protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer;
[Test]
public void TestGameplayOverlayActivation()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
}
[Test]
public void TestGameplayOverlayActivationPaused()
{
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("pause gameplay", () => Player.Pause());
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationReplayLoaded()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true);
AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
}
[Test]
public void TestGameplayOverlayActivationBreaks()
{
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime));
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime));
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
}
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer();
protected class OverlayTestPlayer : TestPlayer
{
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
}
}
}
@@ -48,7 +48,10 @@ namespace osu.Game.Tests.Visual.Gameplay
private class ExampleContainer : PlayerSettingsGroup
{
protected override string Title => @"example";
public ExampleContainer()
: base("example")
{
}
}
}
}
@@ -0,0 +1,83 @@
// 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.Testing;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Tests.Resources;
using osu.Game.Tests.Visual.Navigation;
namespace osu.Game.Tests.Visual.Menus
{
public class TestSceneMusicActionHandling : OsuGameTestScene
{
private GlobalActionContainer globalActionContainer => Game.ChildrenOfType<GlobalActionContainer>().First();
[Test]
public void TestMusicPlayAction()
{
AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething());
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused);
AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused);
}
[Test]
public void TestMusicNavigationActions()
{
int importId = 0;
Queue<(WorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null;
// ensure we have at least two beatmaps available to identify the direction the music controller navigated to.
AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
}
},
Metadata = new BeatmapMetadata
{
Artist = $"a test map {importId++}",
Title = "title",
}
}).Wait(), 5);
AddStep("import beatmap with track", () =>
{
var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result;
Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
});
AddStep("bind to track change", () =>
{
trackChangeQueue = new Queue<(WorkingBeatmap, TrackChangeDirection)>();
Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection));
});
AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000));
AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("no track change", () => trackChangeQueue.Count == 0);
AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000);
AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev));
AddAssert("track changed to previous", () =>
trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev);
AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext));
AddAssert("track changed to next", () =>
trackChangeQueue.Count == 1 &&
trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next);
}
}
}
@@ -162,6 +162,28 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value));
}
/// <summary>
/// Tests that the global mod instances are not retained by the rooms, as global mod instances are retained and re-used by the mod select overlay.
/// </summary>
[Test]
public void TestGlobalModInstancesNotRetained()
{
OsuModDoubleTime mod = null;
AddStep("set dt mod and store", () =>
{
SelectedMods.Value = new[] { new OsuModDoubleTime() };
// Mod select overlay replaces our mod.
mod = (OsuModDoubleTime)SelectedMods.Value[0];
});
AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem());
AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2);
AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value));
}
private class TestMatchSongSelect : MatchSongSelect
{
public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails;
@@ -12,6 +12,7 @@ using osu.Game.Rulesets;
using osu.Game.Rulesets.Mania;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens;
using osu.Game.Screens.Menu;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@@ -0,0 +1,237 @@
// 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.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Rulesets;
using osu.Game.Screens.Select;
using osu.Game.Tests.Resources;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect
{
public class TestSceneFilterControl : OsuManualInputManagerTestScene
{
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly CollectionManager collectionManager;
private RulesetStore rulesets;
private BeatmapManager beatmapManager;
private FilterControl control;
public TestSceneFilterControl()
{
base.Content.AddRange(new Drawable[]
{
collectionManager = new CollectionManager(LocalStorage),
content = new Container { RelativeSizeAxes = Axes.Both }
});
}
[BackgroundDependencyLoader]
private void load(GameHost host)
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
}
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
dependencies.Cache(collectionManager);
return dependencies;
}
[SetUp]
public void SetUp() => Schedule(() =>
{
collectionManager.Collections.Clear();
Child = control = new FilterControl
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = FilterControl.HEIGHT,
};
});
[Test]
public void TestEmptyCollectionFilterContainsAllBeatmaps()
{
assertCollectionDropdownContains("All beatmaps");
assertCollectionHeaderDisplays("All beatmaps");
}
[Test]
public void TestCollectionAddedToDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
assertCollectionDropdownContains("1");
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRemovedFromDropdown()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } }));
AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0));
assertCollectionDropdownContains("1", false);
assertCollectionDropdownContains("2");
}
[Test]
public void TestCollectionRenamed()
{
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select collection", () =>
{
var dropdown = control.ChildrenOfType<CollectionFilterDropdown>().Single();
dropdown.Current.Value = dropdown.ItemSource.ElementAt(1);
});
addExpandHeaderStep();
AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First");
assertCollectionDropdownContains("First");
assertCollectionHeaderDisplays("First");
}
[Test]
public void TestAllBeatmapFilterDoesNotHaveAddButton()
{
addExpandHeaderStep();
AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0)));
AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent);
}
[Test]
public void TestCollectionFilterHasAddButton()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1)));
AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent);
}
[Test]
public void TestButtonDisabledAndEnabledWithBeatmapChanges()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value);
AddStep("set dummy beatmap", () => Beatmap.SetDefault());
AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value);
}
[Test]
public void TestButtonChangesWhenAddedAndRemovedFromCollection()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear());
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
[Test]
public void TestButtonAddsAndRemovesBeatmap()
{
addExpandHeaderStep();
AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0]));
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare));
addClickAddOrRemoveButtonStep(1);
AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo));
AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare));
}
[Test]
public void TestManageCollectionsFilterIsNotSelected()
{
addExpandHeaderStep();
AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } }));
AddStep("select collection", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1));
InputManager.Click(MouseButton.Left);
});
addExpandHeaderStep();
AddStep("click manage collections filter", () =>
{
InputManager.MoveMouseTo(getCollectionDropdownItems().Last());
InputManager.Click(MouseButton.Left);
});
AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1");
}
private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true)
=> AddAssert($"collection dropdown header displays '{collectionName}'",
() => shouldDisplay == (control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single().ChildrenOfType<SpriteText>().First().Text == collectionName));
private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) =>
AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'",
// A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872
() => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType<FillFlowContainer>().OfType<IHasText>().First().Text == collectionName)));
private IconButton getAddOrRemoveButton(int index)
=> getCollectionDropdownItems().ElementAt(index).ChildrenOfType<IconButton>().Single();
private void addExpandHeaderStep() => AddStep("expand header", () =>
{
InputManager.MoveMouseTo(control.ChildrenOfType<CollectionFilterDropdown.CollectionDropdownHeader>().Single());
InputManager.Click(MouseButton.Left);
});
private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () =>
{
InputManager.MoveMouseTo(getAddOrRemoveButton(index));
InputManager.Click(MouseButton.Left);
});
private IEnumerable<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems()
=> control.ChildrenOfType<CollectionFilterDropdown>().Single().ChildrenOfType<Dropdown<CollectionMenuItem>.DropdownMenu.DrawableDropdownMenuItem>();
}
}
@@ -1,18 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Tests.Resources;
namespace osu.Game.Tests.Visual.UserInterface
{
@@ -24,14 +17,9 @@ namespace osu.Game.Tests.Visual.UserInterface
private NowPlayingOverlay nowPlayingOverlay;
private RulesetStore rulesets;
[BackgroundDependencyLoader]
private void load(AudioManager audio, GameHost host)
private void load()
{
Dependencies.Cache(rulesets = new RulesetStore(ContextFactory));
Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default));
Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo);
nowPlayingOverlay = new NowPlayingOverlay
@@ -51,49 +39,5 @@ namespace osu.Game.Tests.Visual.UserInterface
AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state);
AddStep(@"hide", () => nowPlayingOverlay.Hide());
}
private BeatmapManager manager { get; set; }
private int importId;
[Test]
public void TestPrevTrackBehavior()
{
// ensure we have at least two beatmaps available.
AddRepeatStep("import beatmap", () => manager.Import(new BeatmapSetInfo
{
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
}
},
Metadata = new BeatmapMetadata
{
Artist = $"a test map {importId++}",
Title = "title",
}
}).Wait(), 5);
WorkingBeatmap currentBeatmap = null;
AddStep("import beatmap with track", () =>
{
var setWithTrack = manager.Import(TestResources.GetTestBeatmapForImport()).Result;
Beatmap.Value = currentBeatmap = manager.GetWorkingBeatmap(setWithTrack.Beatmaps.First());
});
AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000));
AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value);
AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack.CurrentTime < 5000);
AddStep(@"Set previous", () => musicController.PreviousTrack());
AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value);
}
}
}
@@ -20,8 +20,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
{
private const int padding = 10;
protected override string Title => @"ladder";
private SettingsDropdown<TournamentRound> roundDropdown;
private PlayerCheckbox losersCheckbox;
private DateTextBox dateTimeBox;
@@ -34,6 +32,11 @@ namespace osu.Game.Tournament.Screens.Ladder.Components
[Resolved]
private LadderInfo ladderInfo { get; set; }
public LadderEditorSettings()
: base("ladder")
{
}
[BackgroundDependencyLoader]
private void load()
{
@@ -207,11 +207,11 @@ namespace osu.Game.Beatmaps
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
var attributes = calculator.Calculate(key.Mods);
return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo);
}
catch
{
return difficultyCache[key] = new StarDifficulty(0);
return difficultyCache[key] = new StarDifficulty();
}
}
@@ -233,7 +233,7 @@ namespace osu.Game.Beatmaps
if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
{
// If not, fall back to the existing star difficulty (e.g. from an online source).
existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty, beatmapInfo.MaxCombo ?? 0);
key = default;
return true;
@@ -298,10 +298,12 @@ namespace osu.Game.Beatmaps
public readonly struct StarDifficulty
{
public readonly double Stars;
public readonly int MaxCombo;
public StarDifficulty(double stars)
public StarDifficulty(double stars, int maxCombo)
{
Stars = stars;
MaxCombo = maxCombo;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
+2
View File
@@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.IO.Serialization;
using osu.Game.Rulesets;
@@ -14,6 +15,7 @@ using osu.Game.Scoring;
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
public class BeatmapInfo : IEquatable<BeatmapInfo>, IJsonSerializable, IHasPrimaryKey
{
+53 -8
View File
@@ -27,6 +27,8 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Objects;
using osu.Game.Users;
using osu.Game.Skinning;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Beatmaps
@@ -64,12 +66,14 @@ namespace osu.Game.Beatmaps
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;
private readonly AudioManager audioManager;
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
private readonly TextureStore textureStore;
private readonly ITrackStore trackStore;
[CanBeNull]
private readonly BeatmapOnlineLookupQueue onlineLookupQueue;
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null,
WorkingBeatmap defaultBeatmap = null)
WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false)
: base(storage, contextFactory, api, new BeatmapStore(contextFactory), host)
{
this.rulesets = rulesets;
@@ -83,7 +87,8 @@ namespace osu.Game.Beatmaps
beatmaps.ItemRemoved += removeWorkingCache;
beatmaps.ItemUpdated += removeWorkingCache;
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
if (performOnlineLookups)
onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage);
textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store));
trackStore = audioManager.GetTrackStore(Files.Store);
@@ -94,6 +99,34 @@ namespace osu.Game.Beatmaps
protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz";
public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user)
{
var metadata = new BeatmapMetadata
{
Artist = "artist",
Title = "title",
Author = user,
};
var set = new BeatmapSetInfo
{
Metadata = metadata,
Beatmaps = new List<BeatmapInfo>
{
new BeatmapInfo
{
BaseDifficulty = new BeatmapDifficulty(),
Ruleset = ruleset,
Metadata = metadata,
Version = "difficulty"
}
}
};
var working = Import(set).Result;
return GetWorkingBeatmap(working.Beatmaps.First());
}
protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default)
{
if (archive != null)
@@ -112,7 +145,8 @@ namespace osu.Game.Beatmaps
bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0);
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
if (onlineLookupQueue != null)
await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken);
// ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID.
if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0))
@@ -198,24 +232,35 @@ namespace osu.Game.Beatmaps
/// </summary>
/// <param name="info">The <see cref="BeatmapInfo"/> to save the content against. The file referenced by <see cref="BeatmapInfo.Path"/> will be replaced.</param>
/// <param name="beatmapContent">The <see cref="IBeatmap"/> content to write.</param>
public void Save(BeatmapInfo info, IBeatmap beatmapContent)
/// <param name="beatmapSkin">The beatmap <see cref="ISkin"/> content to write, null if to be omitted.</param>
public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null)
{
var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID));
var setInfo = info.BeatmapSet;
using (var stream = new MemoryStream())
{
using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true))
new LegacyBeatmapEncoder(beatmapContent).Encode(sw);
new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw);
stream.Seek(0, SeekOrigin.Begin);
using (ContextFactory.GetForWrite())
{
var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID);
var metadata = beatmapInfo.Metadata ?? setInfo.Metadata;
// grab the original file (or create a new one if not found).
var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo();
// metadata may have changed; update the path with the standard format.
beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu";
beatmapInfo.MD5Hash = stream.ComputeMD5Hash();
// update existing or populate new file's filename.
fileInfo.Filename = beatmapInfo.Path;
stream.Seek(0, SeekOrigin.Begin);
UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream);
UpdateFile(setInfo, fileInfo, stream);
}
}
@@ -33,6 +33,9 @@ namespace osu.Game.Beatmaps
protected override IBeatmap GetBeatmap()
{
if (BeatmapInfo.Path == null)
return new Beatmap { BeatmapInfo = BeatmapInfo };
try
{
using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path))))
@@ -67,6 +70,9 @@ namespace osu.Game.Beatmaps
protected override Track GetBeatmapTrack()
{
if (Metadata?.AudioFile == null)
return null;
try
{
return trackStore.Get(getPathForFile(Metadata.AudioFile));
@@ -80,6 +86,9 @@ namespace osu.Game.Beatmaps
protected override Waveform GetWaveform()
{
if (Metadata?.AudioFile == null)
return null;
try
{
var trackData = store.GetStream(getPathForFile(Metadata.AudioFile));
+2
View File
@@ -6,11 +6,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using Newtonsoft.Json;
using osu.Framework.Testing;
using osu.Game.Database;
using osu.Game.Users;
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
[Serializable]
public class BeatmapMetadata : IEquatable<BeatmapMetadata>, IHasPrimaryKey
{
+2
View File
@@ -5,10 +5,12 @@ using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using osu.Framework.Testing;
using osu.Game.Database;
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles<BeatmapSetFileInfo>, ISoftDelete, IEquatable<BeatmapSetInfo>
{
public int ID { get; set; }
+18 -1
View File
@@ -1,14 +1,31 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osuTK;
namespace osu.Game.Beatmaps
{
public class BeatmapStatistic
{
public IconUsage Icon;
[Obsolete("Use CreateIcon instead")] // can be removed 20210203
public IconUsage Icon = FontAwesome.Regular.QuestionCircle;
/// <summary>
/// A function to create the icon for display purposes. Use default icons available via <see cref="BeatmapStatisticIcon"/> whenever possible for conformity.
/// </summary>
public Func<Drawable> CreateIcon;
public string Content;
public string Name;
public BeatmapStatistic()
{
#pragma warning disable 618
CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) };
#pragma warning restore 618
}
}
}
+43
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 Humanizer;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Beatmaps
{
/// <summary>
/// A default implementation of an icon used to represent beatmap statistics.
/// </summary>
public class BeatmapStatisticIcon : Sprite
{
private readonly BeatmapStatisticsIconType iconType;
public BeatmapStatisticIcon(BeatmapStatisticsIconType iconType)
{
this.iconType = iconType;
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}");
}
}
public enum BeatmapStatisticsIconType
{
Accuracy,
ApproachRate,
Bpm,
Circles,
HpDrain,
Length,
OverallDifficulty,
Size,
Sliders,
Spinners,
}
}
@@ -8,6 +8,6 @@ namespace osu.Game.Beatmaps.Formats
{
public interface IHasCustomColours
{
Dictionary<string, Color4> CustomColours { get; set; }
Dictionary<string, Color4> CustomColours { get; }
}
}
@@ -7,13 +7,16 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using JetBrains.Annotations;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Legacy;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Beatmaps.Formats
{
@@ -23,9 +26,18 @@ namespace osu.Game.Beatmaps.Formats
private readonly IBeatmap beatmap;
public LegacyBeatmapEncoder(IBeatmap beatmap)
[CanBeNull]
private readonly ISkin skin;
/// <summary>
/// Creates a new <see cref="LegacyBeatmapEncoder"/>.
/// </summary>
/// <param name="beatmap">The beatmap to encode.</param>
/// <param name="skin">The beatmap's skin, used for encoding combo colours.</param>
public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin)
{
this.beatmap = beatmap;
this.skin = skin;
if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3)
throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap));
@@ -53,6 +65,9 @@ namespace osu.Game.Beatmaps.Formats
writer.WriteLine();
handleControlPoints(writer);
writer.WriteLine();
handleColours(writer);
writer.WriteLine();
handleHitObjects(writer);
}
@@ -196,6 +211,28 @@ namespace osu.Game.Beatmaps.Formats
}
}
private void handleColours(TextWriter writer)
{
var colours = skin?.GetConfig<GlobalSkinColours, IReadOnlyList<Color4>>(GlobalSkinColours.ComboColours)?.Value;
if (colours == null || colours.Count == 0)
return;
writer.WriteLine("[Colours]");
for (var i = 0; i < colours.Count; i++)
{
var comboColour = colours[i];
writer.Write(FormattableString.Invariant($"Combo{i}: "));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},"));
writer.Write(FormattableString.Invariant($"{(byte)(comboColour.A * byte.MaxValue)}"));
writer.WriteLine();
}
}
private void handleHitObjects(TextWriter writer)
{
if (beatmap.HitObjects.Count == 0)
+3 -1
View File
@@ -13,6 +13,7 @@ using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Statistics;
using osu.Framework.Testing;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
@@ -22,6 +23,7 @@ using osu.Game.Storyboards;
namespace osu.Game.Beatmaps
{
[ExcludeFromDynamicCompile]
public abstract class WorkingBeatmap : IWorkingBeatmap
{
public readonly BeatmapInfo BeatmapInfo;
@@ -53,7 +55,7 @@ namespace osu.Game.Beatmaps
{
const double excess_length = 1000;
var lastObject = Beatmap.HitObjects.LastOrDefault();
var lastObject = Beatmap?.HitObjects.LastOrDefault();
double length;
+47
View File
@@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Game.Beatmaps;
namespace osu.Game.Collections
{
/// <summary>
/// A collection of beatmaps grouped by a name.
/// </summary>
public class BeatmapCollection
{
/// <summary>
/// Invoked whenever any change occurs on this <see cref="BeatmapCollection"/>.
/// </summary>
public event Action Changed;
/// <summary>
/// The collection's name.
/// </summary>
public readonly Bindable<string> Name = new Bindable<string>();
/// <summary>
/// The beatmaps contained by the collection.
/// </summary>
public readonly BindableList<BeatmapInfo> Beatmaps = new BindableList<BeatmapInfo>();
/// <summary>
/// The date when this collection was last modified.
/// </summary>
public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow;
public BeatmapCollection()
{
Beatmaps.CollectionChanged += (_, __) => onChange();
Name.ValueChanged += _ => onChange();
}
private void onChange()
{
LastModifyDate = DateTimeOffset.Now;
Changed?.Invoke();
}
}
}
+303
View File
@@ -0,0 +1,303 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Logging;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.IO.Legacy;
using osu.Game.Overlays.Notifications;
namespace osu.Game.Collections
{
/// <summary>
/// Handles user-defined collections of beatmaps.
/// </summary>
/// <remarks>
/// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the
/// database backing the game. Going forward writing should be done in a similar way to other model stores.
/// </remarks>
public class CollectionManager : Component
{
/// <summary>
/// Database version in stable-compatible YYYYMMDD format.
/// </summary>
private const int database_version = 30000000;
private const string database_name = "collection.db";
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
public bool SupportsImportFromStable => RuntimeInfo.IsDesktop;
[Resolved]
private GameHost host { get; set; }
[Resolved]
private BeatmapManager beatmaps { get; set; }
private readonly Storage storage;
public CollectionManager(Storage storage)
{
this.storage = storage;
}
[BackgroundDependencyLoader]
private void load()
{
Collections.CollectionChanged += collectionsChanged;
if (storage.Exists(database_name))
{
using (var stream = storage.GetStream(database_name))
importCollections(readCollections(stream));
}
}
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
c.Changed += backgroundSave;
break;
case NotifyCollectionChangedAction.Remove:
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
c.Changed -= backgroundSave;
break;
case NotifyCollectionChangedAction.Replace:
foreach (var c in e.OldItems.Cast<BeatmapCollection>())
c.Changed -= backgroundSave;
foreach (var c in e.NewItems.Cast<BeatmapCollection>())
c.Changed += backgroundSave;
break;
}
backgroundSave();
}
/// <summary>
/// Set an endpoint for notifications to be posted to.
/// </summary>
public Action<Notification> PostNotification { protected get; set; }
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
/// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary>
public Task ImportFromStableAsync()
{
var stable = GetStableStorage?.Invoke();
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
if (!stable.Exists(database_name))
{
// This handles situations like when the user does not have a collections.db file
Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error);
return Task.CompletedTask;
}
return Task.Run(async () =>
{
using (var stream = stable.GetStream(database_name))
await Import(stream);
});
}
public async Task Import(Stream stream)
{
var notification = new ProgressNotification
{
State = ProgressNotificationState.Active,
Text = "Collections import is initialising..."
};
PostNotification?.Invoke(notification);
var collection = readCollections(stream, notification);
bool importCompleted = false;
Schedule(() =>
{
importCollections(collection);
importCompleted = true;
});
while (!IsDisposed && !importCompleted)
await Task.Delay(10);
notification.CompletionText = $"Imported {collection.Count} collections";
notification.State = ProgressNotificationState.Completed;
}
private void importCollections(List<BeatmapCollection> newCollections)
{
foreach (var newCol in newCollections)
{
var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name);
if (existing == null)
Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } });
foreach (var newBeatmap in newCol.Beatmaps)
{
if (!existing.Beatmaps.Contains(newBeatmap))
existing.Beatmaps.Add(newBeatmap);
}
}
}
private List<BeatmapCollection> readCollections(Stream stream, ProgressNotification notification = null)
{
if (notification != null)
{
notification.Text = "Reading collections...";
notification.Progress = 0;
}
var result = new List<BeatmapCollection>();
try
{
using (var sr = new SerializationReader(stream))
{
sr.ReadInt32(); // Version
int collectionCount = sr.ReadInt32();
result.Capacity = collectionCount;
for (int i = 0; i < collectionCount; i++)
{
if (notification?.CancellationToken.IsCancellationRequested == true)
return result;
var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } };
int mapCount = sr.ReadInt32();
for (int j = 0; j < mapCount; j++)
{
if (notification?.CancellationToken.IsCancellationRequested == true)
return result;
string checksum = sr.ReadString();
var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum);
if (beatmap != null)
collection.Beatmaps.Add(beatmap);
}
if (notification != null)
{
notification.Text = $"Imported {i + 1} of {collectionCount} collections";
notification.Progress = (float)(i + 1) / collectionCount;
}
result.Add(collection);
}
}
}
catch (Exception e)
{
Logger.Error(e, "Failed to read collection database.");
}
return result;
}
public void DeleteAll()
{
Collections.Clear();
PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" });
}
private readonly object saveLock = new object();
private int lastSave;
private int saveFailures;
/// <summary>
/// Perform a save with debounce.
/// </summary>
private void backgroundSave()
{
var current = Interlocked.Increment(ref lastSave);
Task.Delay(100).ContinueWith(task =>
{
if (current != lastSave)
return;
if (!save())
backgroundSave();
});
}
private bool save()
{
lock (saveLock)
{
Interlocked.Increment(ref lastSave);
try
{
// This is NOT thread-safe!!
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
{
sw.Write(database_version);
sw.Write(Collections.Count);
foreach (var c in Collections)
{
sw.Write(c.Name.Value);
sw.Write(c.Beatmaps.Count);
foreach (var b in c.Beatmaps)
sw.Write(b.MD5Hash);
}
}
if (saveFailures < 10)
saveFailures = 0;
return true;
}
catch (Exception e)
{
// Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing).
// Failures are thus only alerted if they exceed a threshold (once) to indicate "actual" errors having occurred.
if (++saveFailures == 10)
Logger.Error(e, "Failed to save collection database!");
}
return false;
}
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
save();
}
}
}
@@ -0,0 +1,34 @@
// 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 Humanizer;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Collections
{
public class DeleteCollectionDialog : PopupDialog
{
public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction)
{
HeaderText = "Confirm deletion of";
BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.Beatmaps.Count)})";
Icon = FontAwesome.Regular.TrashAlt;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Yes. Go for it.",
Action = deleteAction
},
new PopupDialogCancelButton
{
Text = @"No! Abort mission!",
},
};
}
}
}
@@ -0,0 +1,122 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.Containers;
using osuTK;
namespace osu.Game.Collections
{
/// <summary>
/// Visualises a list of <see cref="BeatmapCollection"/>s.
/// </summary>
public class DrawableCollectionList : OsuRearrangeableListContainer<BeatmapCollection>
{
private Scroll scroll;
protected override ScrollContainer<Drawable> CreateScrollContainer() => scroll = new Scroll();
protected override FillFlowContainer<RearrangeableListItem<BeatmapCollection>> CreateListFillFlowContainer() => new Flow
{
DragActive = { BindTarget = DragActive }
};
protected override OsuRearrangeableListItem<BeatmapCollection> CreateOsuDrawable(BeatmapCollection item)
{
if (item == scroll.PlaceholderItem.Model)
return scroll.ReplacePlaceholder();
return new DrawableCollectionListItem(item, true);
}
/// <summary>
/// The scroll container for this <see cref="DrawableCollectionList"/>.
/// Contains the main flow of <see cref="DrawableCollectionListItem"/> and attaches a placeholder item to the end of the list.
/// </summary>
/// <remarks>
/// Use <see cref="ReplacePlaceholder"/> to transfer the placeholder into the main list.
/// </remarks>
private class Scroll : OsuScrollContainer
{
/// <summary>
/// The currently-displayed placeholder item.
/// </summary>
public DrawableCollectionListItem PlaceholderItem { get; private set; }
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Container<DrawableCollectionListItem> placeholderContainer;
public Scroll()
{
ScrollbarVisible = false;
Padding = new MarginPadding(10);
base.Content.Add(new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
LayoutDuration = 200,
LayoutEasing = Easing.OutQuint,
Children = new Drawable[]
{
content = new Container { RelativeSizeAxes = Axes.X },
placeholderContainer = new Container<DrawableCollectionListItem>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y
}
}
});
ReplacePlaceholder();
}
protected override void Update()
{
base.Update();
// AutoSizeAxes cannot be used as the height should represent the post-layout-transform height at all times, so that the placeholder doesn't bounce around.
content.Height = ((Flow)Child).Children.Sum(c => c.DrawHeight + 5);
}
/// <summary>
/// Replaces the current <see cref="PlaceholderItem"/> with a new one, and returns the previous.
/// </summary>
/// <returns>The current <see cref="PlaceholderItem"/>.</returns>
public DrawableCollectionListItem ReplacePlaceholder()
{
var previous = PlaceholderItem;
placeholderContainer.Clear(false);
placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false));
return previous;
}
}
/// <summary>
/// The flow of <see cref="DrawableCollectionListItem"/>. Disables layout easing unless a drag is in progress.
/// </summary>
private class Flow : FillFlowContainer<RearrangeableListItem<BeatmapCollection>>
{
public readonly IBindable<bool> DragActive = new Bindable<bool>();
public Flow()
{
Spacing = new Vector2(0, 5);
LayoutEasing = Easing.OutQuint;
}
protected override void LoadComplete()
{
base.LoadComplete();
DragActive.BindValueChanged(active => LayoutDuration = active.NewValue ? 200 : 0);
}
}
}
}
@@ -0,0 +1,237 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Collections
{
/// <summary>
/// Visualises a <see cref="BeatmapCollection"/> inside a <see cref="DrawableCollectionList"/>.
/// </summary>
public class DrawableCollectionListItem : OsuRearrangeableListItem<BeatmapCollection>
{
private const float item_height = 35;
private const float button_width = item_height * 0.75f;
/// <summary>
/// Whether the <see cref="BeatmapCollection"/> currently exists inside the <see cref="CollectionManager"/>.
/// </summary>
public IBindable<bool> IsCreated => isCreated;
private readonly Bindable<bool> isCreated = new Bindable<bool>();
/// <summary>
/// Creates a new <see cref="DrawableCollectionListItem"/>.
/// </summary>
/// <param name="item">The <see cref="BeatmapCollection"/>.</param>
/// <param name="isCreated">Whether <paramref name="item"/> currently exists inside the <see cref="CollectionManager"/>.</param>
public DrawableCollectionListItem(BeatmapCollection item, bool isCreated)
: base(item)
{
this.isCreated.Value = isCreated;
ShowDragHandle.BindTo(this.isCreated);
}
protected override Drawable CreateContent() => new ItemContent(Model)
{
IsCreated = { BindTarget = isCreated }
};
/// <summary>
/// The main content of the <see cref="DrawableCollectionListItem"/>.
/// </summary>
private class ItemContent : CircularContainer
{
public readonly Bindable<bool> IsCreated = new Bindable<bool>();
private readonly IBindable<string> collectionName;
private readonly BeatmapCollection collection;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
private Container textBoxPaddingContainer;
private ItemTextBox textBox;
public ItemContent(BeatmapCollection collection)
{
this.collection = collection;
RelativeSizeAxes = Axes.X;
Height = item_height;
Masking = true;
collectionName = collection.Name.GetBoundCopy();
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Children = new Drawable[]
{
new DeleteButton(collection)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
IsCreated = { BindTarget = IsCreated },
IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v)
},
textBoxPaddingContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = button_width },
Children = new Drawable[]
{
textBox = new ItemTextBox
{
RelativeSizeAxes = Axes.Both,
Size = Vector2.One,
CornerRadius = item_height / 2,
Current = collection.Name,
PlaceholderText = IsCreated.Value ? string.Empty : "Create a new collection"
},
}
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
collectionName.BindValueChanged(_ => createNewCollection(), true);
IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true);
}
private void createNewCollection()
{
if (IsCreated.Value)
return;
if (string.IsNullOrEmpty(collectionName.Value))
return;
// Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again.
collectionManager?.Collections.Add(collection);
textBox.PlaceholderText = string.Empty;
// When this item changes from placeholder to non-placeholder (via changing containers), its textbox will lose focus, so it needs to be re-focused.
Schedule(() => GetContainingInputManager().ChangeFocus(textBox));
IsCreated.Value = true;
}
}
private class ItemTextBox : OsuTextBox
{
protected override float LeftRightPadding => item_height / 2;
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundUnfocused = colours.GreySeafoamDarker.Darken(0.5f);
BackgroundFocused = colours.GreySeafoam;
}
}
public class DeleteButton : CompositeDrawable
{
public readonly IBindable<bool> IsCreated = new Bindable<bool>();
public Func<Vector2, bool> IsTextBoxHovered;
[Resolved(CanBeNull = true)]
private DialogOverlay dialogOverlay { get; set; }
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
private readonly BeatmapCollection collection;
private Drawable fadeContainer;
private Drawable background;
public DeleteButton(BeatmapCollection collection)
{
this.collection = collection;
RelativeSizeAxes = Axes.Y;
Width = button_width + item_height / 2; // add corner radius to cover with fill
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = fadeContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
Children = new[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Red
},
new SpriteIcon
{
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre,
X = -button_width * 0.6f,
Size = new Vector2(10),
Icon = FontAwesome.Solid.Trash
}
}
};
}
protected override void LoadComplete()
{
base.LoadComplete();
IsCreated.BindValueChanged(created => Alpha = created.NewValue ? 1 : 0, true);
}
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos);
protected override bool OnHover(HoverEvent e)
{
fadeContainer.FadeTo(1f, 100, Easing.Out);
return false;
}
protected override void OnHoverLost(HoverLostEvent e)
{
fadeContainer.FadeTo(0.1f, 100);
}
protected override bool OnClick(ClickEvent e)
{
background.FlashColour(Color4.White, 150);
if (collection.Beatmaps.Count == 0)
deleteCollection();
else
dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection));
return true;
}
private void deleteCollection() => collectionManager?.Collections.Remove(collection);
}
}
}
@@ -0,0 +1,134 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osuTK;
namespace osu.Game.Collections
{
public class ManageCollectionsDialog : OsuFocusedOverlayContainer
{
private const double enter_duration = 500;
private const double exit_duration = 200;
[Resolved(CanBeNull = true)]
private CollectionManager collectionManager { get; set; }
public ManageCollectionsDialog()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
Size = new Vector2(0.5f, 0.8f);
Masking = true;
CornerRadius = 10;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Children = new Drawable[]
{
new Box
{
Colour = colours.GreySeafoamDark,
RelativeSizeAxes = Axes.Both,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = "Manage collections",
Font = OsuFont.GetFont(size: 30),
Padding = new MarginPadding { Vertical = 10 },
},
new IconButton
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Icon = FontAwesome.Solid.Times,
Colour = colours.GreySeafoamDarker,
Scale = new Vector2(0.8f),
X = -10,
Action = () => State.Value = Visibility.Hidden
}
}
}
},
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.GreySeafoamDarker
},
new DrawableCollectionList
{
RelativeSizeAxes = Axes.Both,
Items = { BindTarget = collectionManager?.Collections ?? new BindableList<BeatmapCollection>() }
}
}
}
},
}
}
}
};
}
protected override void PopIn()
{
base.PopIn();
this.FadeIn(enter_duration, Easing.OutQuint);
this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint);
}
protected override void PopOut()
{
base.PopOut();
this.FadeOut(exit_duration, Easing.OutQuint);
this.ScaleTo(0.9f, exit_duration);
// Ensure that textboxes commit
GetContainingInputManager()?.TriggerFocusContention(this);
}
}
}
+3 -1
View File
@@ -6,6 +6,7 @@ using osu.Framework.Configuration;
using osu.Framework.Configuration.Tracking;
using osu.Framework.Extensions;
using osu.Framework.Platform;
using osu.Framework.Testing;
using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Select;
@@ -13,6 +14,7 @@ using osu.Game.Screens.Select.Filter;
namespace osu.Game.Configuration
{
[ExcludeFromDynamicCompile]
public class OsuConfigManager : IniConfigManager<OsuSetting>
{
protected override void InitialiseDefaults()
@@ -231,6 +233,6 @@ namespace osu.Game.Configuration
UIHoldActivationDelay,
HitLighting,
MenuBackgroundSource,
GameplayDisableWinKey
GameplayDisableWinKey,
}
}
+12 -3
View File
@@ -397,15 +397,24 @@ namespace osu.Game.Database
}
}
/// <summary>
/// Update an existing file, or create a new entry if not already part of the <paramref name="model"/>'s files.
/// </summary>
/// <param name="model">The item to operate on.</param>
/// <param name="file">The file model to be updated or added.</param>
/// <param name="contents">The new file contents.</param>
public void UpdateFile(TModel model, TFileModel file, Stream contents)
{
using (var usage = ContextFactory.GetForWrite())
{
// Dereference the existing file info, since the file model will be removed.
Files.Dereference(file.FileInfo);
if (file.FileInfo != null)
{
Files.Dereference(file.FileInfo);
// Remove the file model.
usage.Context.Set<TFileModel>().Remove(file);
// Remove the file model.
usage.Context.Set<TFileModel>().Remove(file);
}
// Add the new file info and containing file model.
model.Files.Remove(file);
@@ -103,6 +103,8 @@ namespace osu.Game.Graphics.Containers
{
}
private bool playedPopInSound;
protected override void UpdateState(ValueChangedEvent<Visibility> state)
{
switch (state.NewValue)
@@ -110,16 +112,24 @@ namespace osu.Game.Graphics.Containers
case Visibility.Visible:
if (OverlayActivationMode.Value == OverlayActivation.Disabled)
{
// todo: visual/audible feedback that this operation could not complete.
State.Value = Visibility.Hidden;
return;
}
samplePopIn?.Play();
playedPopInSound = true;
if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this);
break;
case Visibility.Hidden:
samplePopOut?.Play();
if (playedPopInSound)
{
samplePopOut?.Play();
playedPopInSound = false;
}
if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this);
break;
}
@@ -12,13 +12,13 @@ namespace osu.Game.Graphics.Containers
/// <summary>
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
/// </summary>
private readonly BindableBool playlistDragActive = new BindableBool();
protected readonly BindableBool DragActive = new BindableBool();
protected override ScrollContainer<Drawable> CreateScrollContainer() => new OsuScrollContainer();
protected sealed override RearrangeableListItem<TModel> CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d =>
{
d.PlaylistDragActive.BindTo(playlistDragActive);
d.DragActive.BindTo(DragActive);
});
protected abstract OsuRearrangeableListItem<TModel> CreateOsuDrawable(TModel item);
@@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers
/// <summary>
/// Whether any item is currently being dragged. Used to hide other items' drag handles.
/// </summary>
public readonly BindableBool PlaylistDragActive = new BindableBool();
public readonly BindableBool DragActive = new BindableBool();
private Color4 handleColour = Color4.White;
@@ -44,8 +44,9 @@ namespace osu.Game.Graphics.Containers
/// <summary>
/// Whether the drag handle should be shown.
/// </summary>
protected virtual bool ShowDragHandle => true;
protected readonly Bindable<bool> ShowDragHandle = new Bindable<bool>();
private Container handleContainer;
private PlaylistItemHandle handle;
protected OsuRearrangeableListItem(TModel item)
@@ -58,8 +59,6 @@ namespace osu.Game.Graphics.Containers
[BackgroundDependencyLoader]
private void load()
{
Container handleContainer;
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.X,
@@ -88,9 +87,12 @@ namespace osu.Game.Graphics.Containers
ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) },
RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }
};
}
if (!ShowDragHandle)
handleContainer.Alpha = 0;
protected override void LoadComplete()
{
base.LoadComplete();
ShowDragHandle.BindValueChanged(show => handleContainer.Alpha = show.NewValue ? 1 : 0, true);
}
protected override bool OnDragStart(DragStartEvent e)
@@ -98,13 +100,13 @@ namespace osu.Game.Graphics.Containers
if (!base.OnDragStart(e))
return false;
PlaylistDragActive.Value = true;
DragActive.Value = true;
return true;
}
protected override void OnDragEnd(DragEndEvent e)
{
PlaylistDragActive.Value = false;
DragActive.Value = false;
base.OnDragEnd(e);
}
@@ -112,7 +114,7 @@ namespace osu.Game.Graphics.Containers
protected override bool OnHover(HoverEvent e)
{
handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value);
handle.UpdateHoverState(IsDragged || !DragActive.Value);
return base.OnHover(e);
}
@@ -5,6 +5,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Framework.Input.StateChanges;
namespace osu.Game.Graphics.Cursor
{
@@ -47,7 +48,10 @@ namespace osu.Game.Graphics.Cursor
{
base.Update();
if (!CanShowCursor)
var lastMouseSource = inputManager.CurrentState.Mouse.LastSource;
bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch);
if (!hasValidInput || !CanShowCursor)
{
currentTarget?.Cursor?.Hide();
currentTarget = null;
@@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
using osu.Framework.Graphics.Containers;
@@ -55,6 +56,12 @@ namespace osu.Game.Graphics.Sprites
set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value;
}
public Bindable<string> Current
{
get => spriteText.Current;
set => spriteText.Current = value;
}
public GlowingSpriteText()
{
AutoSizeAxes = Axes.Both;
@@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface
set
{
iconColour = value;
icon.Colour = value;
icon.FadeColour(value);
}
}
@@ -26,6 +26,8 @@ namespace osu.Game.Graphics.UserInterface
};
ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL };
MaxHeight = 250;
}
[BackgroundDependencyLoader]
@@ -17,6 +17,8 @@ namespace osu.Game.Graphics.UserInterface
{
public class OsuDropdown<T> : Dropdown<T>, IHasAccentColour
{
private const float corner_radius = 4;
private Color4 accentColour;
public Color4 AccentColour
@@ -57,9 +59,11 @@ namespace osu.Game.Graphics.UserInterface
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
public OsuDropdownMenu()
{
CornerRadius = 4;
CornerRadius = corner_radius;
BackgroundColour = Color4.Black.Opacity(0.5f);
MaskingContainer.CornerRadius = corner_radius;
// todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring
ItemsContainer.Padding = new MarginPadding(5);
}
@@ -138,7 +142,7 @@ namespace osu.Game.Graphics.UserInterface
Foreground.Padding = new MarginPadding(2);
Masking = true;
CornerRadius = 6;
CornerRadius = corner_radius;
}
[BackgroundDependencyLoader]
@@ -237,7 +241,7 @@ namespace osu.Game.Graphics.UserInterface
AutoSizeAxes = Axes.None;
Margin = new MarginPadding { Bottom = 4 };
CornerRadius = 4;
CornerRadius = corner_radius;
Height = 40;
Foreground.Children = new Drawable[]
@@ -9,16 +9,20 @@ using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.UserInterface;
namespace osu.Game.Graphics.UserInterface
{
public abstract class RollingCounter<T> : Container
public abstract class RollingCounter<T> : Container, IHasCurrentValue<T>
where T : struct, IEquatable<T>
{
/// <summary>
/// The current value.
/// </summary>
public Bindable<T> Current = new Bindable<T>();
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
}
private SpriteText displayedCountSpriteText;
@@ -3,6 +3,7 @@
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.UserInterface;
@@ -32,6 +33,11 @@ namespace osu.Game.Graphics.UserInterfaceV2
set => Component.Text = value;
}
public Container TabbableContentContainer
{
set => Component.TabbableContentContainer = value;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
+7 -1
View File
@@ -25,7 +25,13 @@ namespace osu.Game.IO
this.subPath = subPath;
}
protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
protected virtual string MutatePath(string path)
{
if (path == null)
return null;
return !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path;
}
protected virtual void ChangeTargetStorage(Storage newStorage)
{
+25
View File
@@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Input;
namespace osu.Game.Input
{
public class GameIdleTracker : IdleTracker
{
private InputManager inputManager;
public GameIdleTracker(int time)
: base(time)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override bool AllowIdle => inputManager.FocusedDrawable == null;
}
}
+4 -2
View File
@@ -56,13 +56,14 @@ namespace osu.Game.Online.Leaderboards
scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire();
scrollFlow = null;
loading.Hide();
showScoresDelegate?.Cancel();
showScoresCancellationSource?.Cancel();
if (scores == null || !scores.Any())
{
loading.Hide();
return;
}
// ensure placeholder is hidden when displaying scores
PlaceholderState = PlaceholderState.Successful;
@@ -84,6 +85,7 @@ namespace osu.Game.Online.Leaderboards
}
scrollContainer.ScrollTo(0f, false);
loading.Hide();
}, (showScoresCancellationSource = new CancellationTokenSource()).Token));
}
}
@@ -72,7 +72,7 @@ namespace osu.Game.Online.Leaderboards
}
[BackgroundDependencyLoader]
private void load(IAPIProvider api, OsuColour colour)
private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager)
{
var user = score.User;
@@ -194,7 +194,7 @@ namespace osu.Game.Online.Leaderboards
{
TextColour = Color4.White,
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
Text = score.TotalScore.ToString(@"N0"),
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.Numeric.With(size: 23),
},
RankContainer = new Container
+10 -24
View File
@@ -31,6 +31,7 @@ using osu.Framework.Input.Events;
using osu.Framework.Platform;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics;
using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
@@ -38,6 +39,7 @@ using osu.Game.Input;
using osu.Game.Overlays.Notifications;
using osu.Game.Input.Bindings;
using osu.Game.Online.Chat;
using osu.Game.Overlays.Music;
using osu.Game.Skinning;
using osuTK.Graphics;
using osu.Game.Overlays.Volume;
@@ -609,12 +611,19 @@ namespace osu.Game
d.Origin = Anchor.TopRight;
}), rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(new CollectionManager(Storage)
{
PostNotification = n => notifications.Post(n),
GetStableStorage = GetStorageForStableInstall
}, Add, true);
loadComponentSingleFile(screenshotManager, Add);
// dependency on notification overlay, dependent by settings overlay
loadComponentSingleFile(CreateUpdateManager(), Add, true);
// overlay elements
loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true);
loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true);
loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true);
loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true);
@@ -647,6 +656,7 @@ namespace osu.Game
chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible;
Add(externalLinkOpener = new ExternalLinkOpener());
Add(new MusicKeyBindingHandler());
// side overlays which cancel each other.
var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications };
@@ -718,24 +728,6 @@ namespace osu.Game
overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime);
}
public class GameIdleTracker : IdleTracker
{
private InputManager inputManager;
public GameIdleTracker(int time)
: base(time)
{
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
protected override bool AllowIdle => inputManager.FocusedDrawable == null;
}
private void forwardLoggedErrorsToNotifications()
{
int recentLogCount = 0;
@@ -991,10 +983,4 @@ namespace osu.Game
Exit();
}
}
public enum ScorePresentType
{
Results,
Gameplay
}
}
+12 -10
View File
@@ -58,6 +58,8 @@ namespace osu.Game
protected ScoreManager ScoreManager;
protected BeatmapDifficultyManager DifficultyManager;
protected SkinManager SkinManager;
protected RulesetStore RulesetStore;
@@ -197,8 +199,8 @@ namespace osu.Game
dependencies.Cache(FileStore = new FileStore(contextFactory, Storage));
// ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup()
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap));
dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => DifficultyManager, LocalConfig));
dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap, true));
// this should likely be moved to ArchiveModelManager when another case appers where it is necessary
// to have inter-dependent model managers. this could be obtained with an IHasForeign<T> interface to
@@ -221,9 +223,8 @@ namespace osu.Game
ScoreManager.Undelete(getBeatmapScores(item), true);
});
var difficultyManager = new BeatmapDifficultyManager();
dependencies.Cache(difficultyManager);
AddInternal(difficultyManager);
dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager());
AddInternal(DifficultyManager);
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
@@ -250,10 +251,11 @@ namespace osu.Game
AddInternal(apiAccess);
AddInternal(RulesetConfigCache);
GlobalActionContainer globalBinding;
MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both };
MenuCursorContainer.Child = globalBinding = new GlobalActionContainer(this)
GlobalActionContainer globalBindings;
MenuCursorContainer.Child = globalBindings = new GlobalActionContainer(this)
{
RelativeSizeAxes = Axes.Both,
Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both }
@@ -261,8 +263,8 @@ namespace osu.Game
base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer));
KeyBindingStore.Register(globalBinding);
dependencies.Cache(globalBinding);
KeyBindingStore.Register(globalBindings);
dependencies.Cache(globalBindings);
PreviewTrackManager previewTrackManager;
dependencies.Cache(previewTrackManager = new PreviewTrackManager());
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.BeatmapListing
public BeatmapListingTitle()
{
Title = "beatmap listing";
Description = "Browse for new beatmaps";
Description = "browse for new beatmaps";
IconTexture = "Icons/Hexacons/beatmap";
}
}
@@ -25,6 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private const float row_height = 22;
private const int text_size = 12;
[Resolved]
private ScoreManager scoreManager { get; set; }
private readonly FillFlowContainer backgroundFlow;
private Color4 highAccuracyColour;
@@ -121,7 +124,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
new OsuSpriteText
{
Margin = new MarginPadding { Right = horizontal_inset },
Text = $@"{score.TotalScore:N0}",
Current = scoreManager.GetBindableTotalScoreString(score),
Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium)
},
new OsuSpriteText
@@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
@@ -38,6 +39,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
private readonly FillFlowContainer<InfoColumn> statisticsColumns;
private readonly ModsInfoColumn modsColumn;
[Resolved]
private ScoreManager scoreManager { get; set; }
public TopScoreStatisticsSection()
{
RelativeSizeAxes = Axes.X;
@@ -87,6 +91,15 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
};
}
[BackgroundDependencyLoader]
private void load()
{
if (score != null)
totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(score);
}
private ScoreInfo score;
/// <summary>
/// Sets the score to be displayed.
/// </summary>
@@ -94,7 +107,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
set
{
totalScoreColumn.Text = $@"{value.TotalScore:N0}";
if (score == value)
return;
score = value;
accuracyColumn.Text = value.DisplayAccuracy;
maxComboColumn.Text = $@"{value.MaxCombo:N0}x";
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
@@ -102,6 +119,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value));
modsColumn.Mods = value.Mods;
if (scoreManager != null)
totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(value);
}
}
@@ -190,6 +210,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
{
set => text.Text = value;
}
public Bindable<string> Current
{
get => text.Current;
set => text.Current = value;
}
}
private class ModsInfoColumn : InfoColumn
@@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Changelog
public ChangelogHeaderTitle()
{
Title = "changelog";
Description = "Track recent dev updates in the osu! ecosystem";
Description = "track recent dev updates in the osu! ecosystem";
IconTexture = "Icons/Hexacons/devtools";
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ namespace osu.Game.Overlays
{
public string IconTexture => "Icons/Hexacons/messaging";
public string Title => "chat";
public string Description => "Join the real-time discussion";
public string Description => "join the real-time discussion";
private const float textbox_height = 60;
private const float channel_selection_min_height = 0.3f;
@@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Dashboard
public DashboardTitle()
{
Title = "dashboard";
Description = "View your friends and other information";
Description = "view your friends and other information";
IconTexture = "Icons/Hexacons/social";
}
}
@@ -0,0 +1,81 @@
// 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.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.OSD;
namespace osu.Game.Overlays.Music
{
/// <summary>
/// Handles <see cref="GlobalAction"/>s related to music playback, and displays <see cref="Toast"/>s via the global <see cref="OnScreenDisplay"/> accordingly.
/// </summary>
public class MusicKeyBindingHandler : Component, IKeyBindingHandler<GlobalAction>
{
[Resolved]
private IBindable<WorkingBeatmap> beatmap { get; set; }
[Resolved]
private MusicController musicController { get; set; }
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
public bool OnPressed(GlobalAction action)
{
if (beatmap.Disabled)
return false;
switch (action)
{
case GlobalAction.MusicPlay:
// use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842)
bool wasPlaying = musicController.IsPlaying;
if (musicController.TogglePause())
onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track"));
return true;
case GlobalAction.MusicNext:
musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track")));
return true;
case GlobalAction.MusicPrev:
musicController.PreviousTrack(res =>
{
switch (res)
{
case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicActionToast("Restart track"));
break;
case PreviousTrackResult.Previous:
onScreenDisplay?.Display(new MusicActionToast("Previous track"));
break;
}
});
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
private class MusicActionToast : Toast
{
public MusicActionToast(string action)
: base("Music Playback", action, string.Empty)
{
}
}
}
}
+21 -57
View File
@@ -12,12 +12,9 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Audio;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.Bindings;
using osu.Framework.Utils;
using osu.Framework.Threading;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Overlays.OSD;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Overlays
@@ -25,7 +22,7 @@ namespace osu.Game.Overlays
/// <summary>
/// Handles playback of the global music track.
/// </summary>
public class MusicController : CompositeDrawable, IKeyBindingHandler<GlobalAction>
public class MusicController : CompositeDrawable
{
[Resolved]
private BeatmapManager beatmaps { get; set; }
@@ -62,9 +59,6 @@ namespace osu.Game.Overlays
[Resolved]
private IBindable<IReadOnlyList<Mod>> mods { get; set; }
[Resolved(canBeNull: true)]
private OnScreenDisplay onScreenDisplay { get; set; }
[NotNull]
public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000));
@@ -207,7 +201,13 @@ namespace osu.Game.Overlays
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
/// </summary>
public void PreviousTrack() => Schedule(() => prev());
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
public void PreviousTrack(Action<PreviousTrackResult> onSuccess = null) => Schedule(() =>
{
PreviousTrackResult res = prev();
if (res != PreviousTrackResult.None)
onSuccess?.Invoke(res);
});
/// <summary>
/// Play the previous track or restart the current track if it's current time below <see cref="restart_cutoff_point"/>.
@@ -243,7 +243,14 @@ namespace osu.Game.Overlays
/// <summary>
/// Play the next random or playlist track.
/// </summary>
public void NextTrack() => Schedule(() => next());
/// <param name="onSuccess">Invoked when the operation has been performed successfully.</param>
/// <returns>A <see cref="ScheduledDelegate"/> of the operation.</returns>
public void NextTrack(Action onSuccess = null) => Schedule(() =>
{
bool res = next();
if (res)
onSuccess?.Invoke();
});
private bool next()
{
@@ -279,6 +286,11 @@ namespace osu.Game.Overlays
private void changeBeatmap(WorkingBeatmap newWorking)
{
// This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order
// (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when the local beatmap bindable is updated directly).
if (newWorking == current)
return;
var lastWorking = current;
TrackChangeDirection direction = TrackChangeDirection.None;
@@ -402,54 +414,6 @@ namespace osu.Game.Overlays
mod.ApplyToTrack(CurrentTrack);
}
}
public bool OnPressed(GlobalAction action)
{
if (beatmap.Disabled)
return false;
switch (action)
{
case GlobalAction.MusicPlay:
if (TogglePause())
onScreenDisplay?.Display(new MusicControllerToast(IsPlaying ? "Play track" : "Pause track"));
return true;
case GlobalAction.MusicNext:
if (next())
onScreenDisplay?.Display(new MusicControllerToast("Next track"));
return true;
case GlobalAction.MusicPrev:
switch (prev())
{
case PreviousTrackResult.Restart:
onScreenDisplay?.Display(new MusicControllerToast("Restart track"));
break;
case PreviousTrackResult.Previous:
onScreenDisplay?.Display(new MusicControllerToast("Previous track"));
break;
}
return true;
}
return false;
}
public void OnReleased(GlobalAction action)
{
}
public class MusicControllerToast : Toast
{
public MusicControllerToast(string action)
: base("Music Playback", action, string.Empty)
{
}
}
}
public enum TrackChangeDirection
+1 -1
View File
@@ -57,7 +57,7 @@ namespace osu.Game.Overlays.News
public NewsHeaderTitle()
{
Title = "news";
Description = "Get up-to-date on community happenings";
Description = "get up-to-date on community happenings";
IconTexture = "Icons/Hexacons/news";
}
}
+2 -2
View File
@@ -19,8 +19,8 @@ namespace osu.Game.Overlays
public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
{
public string IconTexture => "Icons/Hexacons/notification";
public string Title => "Notifications";
public string Description => "Waiting for 'ya";
public string Title => "notifications";
public string Description => "waiting for 'ya";
private const float width = 320;
+1 -1
View File
@@ -29,7 +29,7 @@ namespace osu.Game.Overlays
{
public string IconTexture => "Icons/Hexacons/music";
public string Title => "now playing";
public string Description => "Manage the currently playing track";
public string Description => "manage the currently playing track";
private const float player_height = 130;
private const float transition_length = 800;
@@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Rankings
public RankingsTitle()
{
Title = "ranking";
Description = "Find out who's the best right now";
Description = "find out who's the best right now";
IconTexture = "Icons/Hexacons/rankings";
}
}
@@ -1,12 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings.Sections.Gameplay;
using osu.Game.Rulesets;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Logging;
namespace osu.Game.Overlays.Settings.Sections
{
@@ -34,9 +36,17 @@ namespace osu.Game.Overlays.Settings.Sections
{
foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance()))
{
SettingsSubsection section = ruleset.CreateSettings();
if (section != null)
Add(section);
try
{
SettingsSubsection section = ruleset.CreateSettings();
if (section != null)
Add(section);
}
catch (Exception e)
{
Logger.Error(e, "Failed to load ruleset settings");
}
}
}
}
@@ -3,9 +3,11 @@
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Collections;
using osu.Game.Graphics.UserInterface;
using osu.Game.Scoring;
using osu.Game.Skinning;
@@ -19,14 +21,15 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
private TriangleButton importBeatmapsButton;
private TriangleButton importScoresButton;
private TriangleButton importSkinsButton;
private TriangleButton importCollectionsButton;
private TriangleButton deleteBeatmapsButton;
private TriangleButton deleteScoresButton;
private TriangleButton deleteSkinsButton;
private TriangleButton restoreButton;
private TriangleButton undeleteButton;
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, DialogOverlay dialogOverlay)
[BackgroundDependencyLoader(permitNulls: true)]
private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, DialogOverlay dialogOverlay)
{
if (beatmaps.SupportsImportFromStable)
{
@@ -93,20 +96,46 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
});
}
AddRange(new Drawable[]
Add(deleteSkinsButton = new DangerousSettingsButton
{
deleteSkinsButton = new DangerousSettingsButton
Text = "Delete ALL skins",
Action = () =>
{
Text = "Delete ALL skins",
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteSkinsButton.Enabled.Value = false;
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
}));
}
});
if (collectionManager != null)
{
if (collectionManager.SupportsImportFromStable)
{
Add(importCollectionsButton = new SettingsButton
{
Text = "Import collections from stable",
Action = () =>
{
importCollectionsButton.Enabled.Value = false;
collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true));
}
});
}
Add(new DangerousSettingsButton
{
Text = "Delete ALL collections",
Action = () =>
{
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() =>
{
deleteSkinsButton.Enabled.Value = false;
Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true));
}));
dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll));
}
},
});
}
AddRange(new Drawable[]
{
restoreButton = new SettingsButton
{
Text = "Restore all hidden difficulties",
+1 -1
View File
@@ -17,7 +17,7 @@ namespace osu.Game.Overlays
{
public string IconTexture => "Icons/Hexacons/settings";
public string Title => "settings";
public string Description => "Change the way osu! behaves";
public string Description => "change the way osu! behaves";
protected override IEnumerable<SettingsSection> CreateSections() => new SettingsSection[]
{

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