mirror of
https://github.com/ppy/osu.git
synced 2025-01-19 02:52:54 +08:00
Merge branch 'master' into timeline-redesign
This commit is contained in:
commit
95464ebf36
@ -28,6 +28,7 @@ using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Ranking.Statistics;
|
||||
using osu.Game.Skinning;
|
||||
|
||||
@ -222,6 +223,12 @@ namespace osu.Game.Rulesets.Catch
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new CatchHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new CatchBeatmapVerifier();
|
||||
|
||||
public override StatisticItem[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
@ -18,6 +19,8 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public BindableBool ShowSpeedChanges { get; } = new BindableBool();
|
||||
|
||||
public double? TimelineTimeRange { get; set; }
|
||||
|
||||
public new IScrollingInfo ScrollingInfo => base.ScrollingInfo;
|
||||
|
||||
public DrawableManiaEditorRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods)
|
||||
@ -38,5 +41,11 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
Origin = Anchor.Centre,
|
||||
Size = Vector2.One
|
||||
};
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
TargetTimeRange = TimelineTimeRange == null || ShowSpeedChanges.Value ? ComputeScrollTime(Config.Get<int>(ManiaRulesetSetting.ScrollSpeed)) : TimelineTimeRange.Value;
|
||||
base.Update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,10 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@ -14,6 +13,7 @@ using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osuTK;
|
||||
|
||||
@ -21,7 +21,10 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
{
|
||||
public partial class ManiaHitObjectComposer : ScrollingHitObjectComposer<ManiaHitObject>
|
||||
{
|
||||
private DrawableManiaEditorRuleset drawableRuleset;
|
||||
private DrawableManiaEditorRuleset drawableRuleset = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorScreenWithTimeline? screenWithTimeline { get; set; }
|
||||
|
||||
public ManiaHitObjectComposer(Ruleset ruleset)
|
||||
: base(ruleset)
|
||||
@ -72,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
||||
continue;
|
||||
|
||||
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
ManiaHitObject? current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
@ -83,5 +86,13 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (screenWithTimeline?.TimelineArea.Timeline != null)
|
||||
drawableRuleset.TimelineTimeRange = EditorClock.TrackLength / screenWithTimeline.TimelineArea.Timeline.CurrentZoom / 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
private LabelledSliderBar<float> keyCountSlider { get; set; } = null!;
|
||||
private LabelledSwitchButton specialStyle { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
@ -49,6 +50,13 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
specialStyle = new LabelledSwitchButton
|
||||
{
|
||||
Label = "Use special (N+1) style",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
@ -145,6 +153,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = keyCountSlider.Current.Value;
|
||||
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
|
@ -1,49 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Edit.Setup
|
||||
{
|
||||
public partial class ManiaSetupSection : RulesetSetupSection
|
||||
{
|
||||
private LabelledSwitchButton specialStyle;
|
||||
|
||||
public ManiaSetupSection()
|
||||
: base(new ManiaRuleset().RulesetInfo)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
specialStyle = new LabelledSwitchButton
|
||||
{
|
||||
Label = "Use special (N+1) style",
|
||||
Description = "Changes one column to act as a classic \"scratch\" or \"special\" column, which can be moved around by the user's skin (to the left/right/centre). Generally used in 6K (5+1) or 8K (7+1) configurations.",
|
||||
Current = { Value = Beatmap.BeatmapInfo.SpecialStyle }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
specialStyle.Current.BindValueChanged(_ => updateBeatmap());
|
||||
}
|
||||
|
||||
private void updateBeatmap()
|
||||
{
|
||||
Beatmap.BeatmapInfo.SpecialStyle = specialStyle.Current.Value;
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
@ -419,9 +419,10 @@ namespace osu.Game.Rulesets.Mania
|
||||
return new ManiaFilterCriteria();
|
||||
}
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new ManiaSetupSection();
|
||||
|
||||
public override SetupSection CreateEditorDifficultySection() => new ManiaDifficultySection();
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new ManiaDifficultySection(),
|
||||
];
|
||||
|
||||
public int GetKeyCount(IBeatmapInfo beatmapInfo, IReadOnlyList<Mod>? mods = null)
|
||||
=> ManiaBeatmapConverter.GetColumnCount(LegacyBeatmapConversionDifficultyInfo.FromBeatmapInfo(beatmapInfo), mods);
|
||||
|
@ -8,9 +8,10 @@ using osu.Framework.Audio.Track;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Input.Handlers;
|
||||
@ -56,13 +57,18 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly Bindable<ManiaScrollingDirection> configDirection = new Bindable<ManiaScrollingDirection>();
|
||||
private readonly BindableInt configScrollSpeed = new BindableInt();
|
||||
private double smoothTimeRange;
|
||||
|
||||
private double currentTimeRange;
|
||||
protected double TargetTimeRange;
|
||||
|
||||
// Stores the current speed adjustment active in gameplay.
|
||||
private readonly Track speedAdjustmentTrack = new TrackVirtual(0);
|
||||
|
||||
private ISkinSource currentSkin = null!;
|
||||
|
||||
[Resolved]
|
||||
private GameHost gameHost { get; set; } = null!;
|
||||
|
||||
public DrawableManiaRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null)
|
||||
: base(ruleset, beatmap, mods)
|
||||
{
|
||||
@ -101,9 +107,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
configDirection.BindValueChanged(direction => Direction.Value = (ScrollingDirection)direction.NewValue, true);
|
||||
|
||||
Config.BindWith(ManiaRulesetSetting.ScrollSpeed, configScrollSpeed);
|
||||
configScrollSpeed.BindValueChanged(speed => this.TransformTo(nameof(smoothTimeRange), ComputeScrollTime(speed.NewValue), 200, Easing.OutQuint));
|
||||
configScrollSpeed.BindValueChanged(speed => TargetTimeRange = ComputeScrollTime(speed.NewValue));
|
||||
|
||||
TimeRange.Value = smoothTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
TimeRange.Value = TargetTimeRange = currentTimeRange = ComputeScrollTime(configScrollSpeed.Value);
|
||||
|
||||
KeyBindingInputManager.Add(new ManiaTouchInputArea());
|
||||
}
|
||||
@ -144,7 +150,9 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
// This scaling factor preserves the scroll speed as the scroll length varies from changes to the hit position.
|
||||
float scale = lengthToHitPosition / length_to_default_hit_position;
|
||||
|
||||
TimeRange.Value = smoothTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
|
||||
// we're intentionally using the game host's update clock here to decouple the time range tween from the gameplay clock (which can be arbitrarily paused, or even rewinding)
|
||||
currentTimeRange = Interpolation.DampContinuously(currentTimeRange, TargetTimeRange, 50, gameHost.UpdateThread.Clock.ElapsedFrameTime);
|
||||
TimeRange.Value = currentTimeRange * speedAdjustmentTrack.AggregateTempo.Value * speedAdjustmentTrack.AggregateFrequency.Value * scale;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
150
osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs
Normal file
150
osu.Game.Rulesets.Osu/Edit/Setup/OsuDifficultySection.cs
Normal file
@ -0,0 +1,150 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
public partial class OsuDifficultySection : SetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> stackLeniency { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
circleSizeSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.CircleSizeDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.CircleSize)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
approachRateSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.ApproachRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.ApproachRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
stackLeniency = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = "Stack Leniency",
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Precision = 0.1f
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
private void updateValues()
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Setup
|
||||
{
|
||||
public partial class OsuSetupSection : RulesetSetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> stackLeniency;
|
||||
|
||||
public OsuSetupSection()
|
||||
: base(new OsuRuleset().RulesetInfo)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
stackLeniency = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = "Stack Leniency",
|
||||
Description = "In play mode, osu! automatically stacks notes which occur at the same location. Increasing this value means it is more likely to snap notes of further time-distance.",
|
||||
Current = new BindableFloat(Beatmap.BeatmapInfo.StackLeniency)
|
||||
{
|
||||
Default = 0.7f,
|
||||
MinValue = 0,
|
||||
MaxValue = 1,
|
||||
Precision = 0.1f
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
stackLeniency.Current.BindValueChanged(_ => updateBeatmap());
|
||||
}
|
||||
|
||||
private void updateBeatmap()
|
||||
{
|
||||
Beatmap.BeatmapInfo.StackLeniency = stackLeniency.Current.Value;
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
@ -336,7 +336,11 @@ namespace osu.Game.Rulesets.Osu
|
||||
};
|
||||
}
|
||||
|
||||
public override RulesetSetupSection CreateEditorSetupSection() => new OsuSetupSection();
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new OsuDifficultySection(),
|
||||
new ColoursSection(),
|
||||
];
|
||||
|
||||
/// <seealso cref="OsuHitObject.ApplyDefaultsToSelf"/>
|
||||
/// <seealso cref="OsuHitWindows"/>
|
||||
|
105
osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs
Normal file
105
osu.Game.Rulesets.Taiko/Edit/Setup/TaikoDifficultySection.cs
Normal file
@ -0,0 +1,105 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Edit.Setup
|
||||
{
|
||||
public partial class TaikoDifficultySection : SetupSection
|
||||
{
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.DrainRateDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.DrainRate)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.OverallDifficultyDescription,
|
||||
Current = new BindableFloat(Beatmap.Difficulty.OverallDifficulty)
|
||||
{
|
||||
Default = BeatmapDifficulty.DEFAULT_DIFFICULTY,
|
||||
MinValue = 0,
|
||||
MaxValue = 10,
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.BaseVelocityDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderMultiplier)
|
||||
{
|
||||
Default = 1.4,
|
||||
MinValue = 0.4,
|
||||
MaxValue = 3.6,
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
Description = EditorSetupStrings.TickRateDescription,
|
||||
Current = new BindableDouble(Beatmap.Difficulty.SliderTickRate)
|
||||
{
|
||||
Default = 1,
|
||||
MinValue = 1,
|
||||
MaxValue = 4,
|
||||
Precision = 1,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<float>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
|
||||
foreach (var item in Children.OfType<LabelledSliderBar<double>>())
|
||||
item.Current.ValueChanged += _ => updateValues();
|
||||
}
|
||||
|
||||
private void updateValues()
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,8 @@ using osu.Game.Rulesets.Configuration;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
using osu.Game.Rulesets.Taiko.Configuration;
|
||||
using osu.Game.Rulesets.Taiko.Edit.Setup;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko
|
||||
{
|
||||
@ -188,6 +190,11 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new TaikoHitObjectComposer(this);
|
||||
|
||||
public override IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new TaikoDifficultySection(),
|
||||
];
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new TaikoBeatmapVerifier();
|
||||
|
||||
public override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(RulesetInfo, beatmap);
|
||||
|
@ -168,12 +168,12 @@ namespace osu.Game.Tests.Database
|
||||
Assert.That(importAfterUpdate, Is.Not.Null);
|
||||
Debug.Assert(importAfterUpdate != null);
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
// should only contain the modified beatmap (others purged).
|
||||
Assert.That(importBeforeUpdate.Value.Beatmaps, Has.Count.EqualTo(1));
|
||||
Assert.That(importAfterUpdate.Value.Beatmaps, Has.Count.EqualTo(count_beatmaps));
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
checkCount<BeatmapInfo>(realm, count_beatmaps + 1);
|
||||
checkCount<BeatmapMetadata>(realm, count_beatmaps + 1);
|
||||
|
||||
|
@ -71,6 +71,35 @@ namespace osu.Game.Tests.Database
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubscriptionInitialChangeSetNull()
|
||||
{
|
||||
ChangeSet? firstChanges = null;
|
||||
int receivedChangesCount = 0;
|
||||
|
||||
RunTestWithRealm((realm, _) =>
|
||||
{
|
||||
var registration = realm.RegisterForNotifications(r => r.All<BeatmapSetInfo>(), onChanged);
|
||||
|
||||
realm.WriteAsync(r => r.Add(TestResources.CreateTestBeatmapSetInfo())).WaitSafely();
|
||||
|
||||
realm.Run(r => r.Refresh());
|
||||
|
||||
Assert.That(receivedChangesCount, Is.EqualTo(1));
|
||||
Assert.That(firstChanges, Is.Null);
|
||||
|
||||
registration.Dispose();
|
||||
});
|
||||
|
||||
void onChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||
{
|
||||
if (receivedChangesCount == 0)
|
||||
firstChanges = changes;
|
||||
|
||||
receivedChangesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSubscriptionWithAsyncWrite()
|
||||
{
|
||||
|
@ -0,0 +1,142 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.DailyChallenge;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.DailyChallenge
|
||||
{
|
||||
public partial class TestSceneDailyChallengeLeaderboard : OsuTestScene
|
||||
{
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Plum);
|
||||
|
||||
[Test]
|
||||
public void TestBasicBehaviour()
|
||||
{
|
||||
DailyChallengeLeaderboard leaderboard = null!;
|
||||
|
||||
AddStep("set up response without user best", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
if (req is IndexPlaylistScoresRequest indexRequest)
|
||||
{
|
||||
indexRequest.TriggerSuccess(createResponse(50, false));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.8f),
|
||||
});
|
||||
|
||||
AddStep("set up response with user best", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
if (req is IndexPlaylistScoresRequest indexRequest)
|
||||
{
|
||||
indexRequest.TriggerSuccess(createResponse(50, true));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("force refetch", () => leaderboard.RefetchScores());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLoadingBehaviour()
|
||||
{
|
||||
IndexPlaylistScoresRequest pendingRequest = null!;
|
||||
DailyChallengeLeaderboard leaderboard = null!;
|
||||
|
||||
AddStep("set up requests handler", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = req =>
|
||||
{
|
||||
if (req is IndexPlaylistScoresRequest indexRequest)
|
||||
{
|
||||
pendingRequest = indexRequest;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
});
|
||||
AddStep("create leaderboard", () => Child = leaderboard = new DailyChallengeLeaderboard(new Room { RoomID = { Value = 1 } }, new PlaylistItem(Beatmap.Value.BeatmapInfo))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(0.8f),
|
||||
});
|
||||
AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(3, true)));
|
||||
AddStep("force refetch", () => leaderboard.RefetchScores());
|
||||
AddStep("complete load", () => pendingRequest.TriggerSuccess(createResponse(4, true)));
|
||||
}
|
||||
|
||||
private IndexedMultiplayerScores createResponse(int scoreCount, bool returnUserBest)
|
||||
{
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
for (int i = 0; i < scoreCount; ++i)
|
||||
{
|
||||
result.Scores.Add(new MultiplayerScore
|
||||
{
|
||||
ID = i,
|
||||
Accuracy = 1 - (float)i / (2 * scoreCount),
|
||||
Position = i + 1,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = (ScoreRank)RNG.Next((int)ScoreRank.D, (int)ScoreRank.XH),
|
||||
MaxCombo = 1000 - i,
|
||||
TotalScore = (long)(1_000_000 * (1 - (float)i / (2 * scoreCount))),
|
||||
User = new APIUser { Username = $"user {i}" },
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
});
|
||||
}
|
||||
|
||||
if (returnUserBest)
|
||||
{
|
||||
result.UserScore = new MultiplayerScore
|
||||
{
|
||||
ID = 99999,
|
||||
Accuracy = 0.91,
|
||||
Position = 4,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.A,
|
||||
MaxCombo = 100,
|
||||
TotalScore = 800000,
|
||||
User = dummyAPI.LocalUser.Value,
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
@ -13,6 +14,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@ -29,24 +31,102 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
public partial class TestSceneBeatmapEditorNavigation : OsuGameTestScene
|
||||
{
|
||||
private BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
[Test]
|
||||
public void TestExternalEditingNoChange()
|
||||
{
|
||||
string difficultyName = null!;
|
||||
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName);
|
||||
|
||||
AddStep("open file menu", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick());
|
||||
AddStep("click external edit", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick());
|
||||
|
||||
AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded);
|
||||
|
||||
AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().FirstOrDefault()?.Enabled.Value == true);
|
||||
|
||||
AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddAssert("beatmapset didn't change", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.EqualTo(beatmapSet));
|
||||
AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName));
|
||||
AddAssert("old beatmapset not deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Not.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalEditingWithChange()
|
||||
{
|
||||
string difficultyName = null!;
|
||||
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("store difficulty name", () => difficultyName = getEditor().Beatmap.Value.BeatmapInfo.DifficultyName);
|
||||
|
||||
AddStep("open file menu", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "File").TriggerClick());
|
||||
AddStep("click external edit", () => getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(m => m.Item.Text.Value.ToString() == "Edit externally").TriggerClick());
|
||||
|
||||
AddUntilStep("wait for external edit screen", () => Game.ScreenStack.CurrentScreen is ExternalEditScreen externalEditScreen && externalEditScreen.IsLoaded);
|
||||
|
||||
AddUntilStep("wait for button ready", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().FirstOrDefault()?.Enabled.Value == true);
|
||||
|
||||
AddStep("add file externally", () =>
|
||||
{
|
||||
var op = ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).EditOperation!;
|
||||
File.WriteAllText(Path.Combine(op.MountedPath, "test.txt"), "test");
|
||||
});
|
||||
|
||||
AddStep("finish external edit", () => ((ExternalEditScreen)Game.ScreenStack.CurrentScreen).ChildrenOfType<DangerousRoundedButton>().First().TriggerClick());
|
||||
|
||||
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
|
||||
AddAssert("beatmapset changed", () => getEditor().Beatmap.Value.BeatmapSetInfo, () => Is.Not.EqualTo(beatmapSet));
|
||||
AddAssert("beatmapset is locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Status, () => Is.EqualTo(BeatmapOnlineStatus.LocallyModified));
|
||||
AddAssert("all difficulties are locally modified", () => getEditor().Beatmap.Value.BeatmapSetInfo.Beatmaps.All(b => b.Status == BeatmapOnlineStatus.LocallyModified));
|
||||
AddAssert("difficulty didn't change", () => getEditor().Beatmap.Value.BeatmapInfo.DifficultyName, () => Is.EqualTo(difficultyName));
|
||||
AddAssert("old beatmapset deleted", () => Game.BeatmapManager.QueryBeatmapSet(s => s.ID == beatmapSet.ID), () => Is.Null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSaveThenDeleteActuallyDeletesAtSongSelect()
|
||||
{
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
makeMetadataChange();
|
||||
|
||||
AddAssert("save", () => getEditor().Save());
|
||||
|
||||
AddStep("delete beatmap", () => Game.BeatmapManager.Delete(beatmapSet));
|
||||
|
||||
AddStep("exit", () => getEditor().Exit());
|
||||
|
||||
AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.Beatmap.Value is DummyWorkingBeatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangeMetadataExitWhileTextboxFocusedPromptsSave()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
makeMetadataChange(commit: false);
|
||||
|
||||
AddStep("exit", () => getEditor().Exit());
|
||||
|
||||
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||
}
|
||||
|
||||
private void makeMetadataChange(bool commit = true)
|
||||
{
|
||||
AddStep("change to song setup", () => InputManager.Key(Key.F4));
|
||||
|
||||
TextBox textbox = null!;
|
||||
@ -77,24 +157,14 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
InputManager.Keys(PlatformAction.Paste);
|
||||
});
|
||||
|
||||
AddStep("exit", () => Game.ChildrenOfType<Editor>().Single().Exit());
|
||||
|
||||
AddUntilStep("save dialog displayed", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault()?.CurrentDialog is PromptForSaveDialog);
|
||||
if (commit) AddStep("commit", () => InputManager.Key(Key.Enter));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditorGameplayTestAlwaysUsesOriginalRuleset()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
prepareBeatmap();
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
AddStep("switch ruleset", () => Game.Ruleset.Value = new ManiaRuleset().RulesetInfo);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
@ -147,19 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestExitEditorWithoutSelection()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("escape once", () => InputManager.Key(Key.Escape));
|
||||
|
||||
@ -169,19 +228,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestExitEditorWithSelection()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("make selection", () =>
|
||||
{
|
||||
@ -203,19 +251,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[Test]
|
||||
public void TestLastTimestampRememberedOnExit()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("seek to arbitrary time", () => getEditor().ChildrenOfType<EditorClock>().First().Seek(1234));
|
||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||
@ -223,32 +260,21 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit());
|
||||
openEditor();
|
||||
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
AddUntilStep("time is correct", () => getEditor().ChildrenOfType<EditorClock>().First().CurrentTime, () => Is.EqualTo(1234));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAttemptGlobalMusicOperationFromEditor()
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
prepareBeatmap();
|
||||
|
||||
AddUntilStep("wait for music playing", () => Game.MusicController.IsPlaying);
|
||||
AddStep("user request stop", () => Game.MusicController.Stop(requestedByUser: true));
|
||||
AddUntilStep("wait for music stopped", () => !Game.MusicController.IsPlaying);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
openEditor();
|
||||
|
||||
AddUntilStep("music still stopped", () => !Game.MusicController.IsPlaying);
|
||||
AddStep("user request play", () => Game.MusicController.Play(requestedByUser: true));
|
||||
@ -266,20 +292,10 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
[TestCase(SortMode.Difficulty)]
|
||||
public void TestSelectionRetainedOnExit(SortMode sortMode)
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep($"set sort mode to {sortMode}", () => Game.LocalConfig.SetValue(OsuSetting.SongSelectSortingMode, sortMode));
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
prepareBeatmap();
|
||||
openEditor();
|
||||
|
||||
AddStep("exit editor", () => InputManager.Key(Key.Escape));
|
||||
AddUntilStep("wait for editor exit", () => Game.ScreenStack.CurrentScreen is not Editor);
|
||||
@ -296,6 +312,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("open editor", () => Game.ChildrenOfType<ButtonSystem>().Single().OnEditBeatmap?.Invoke());
|
||||
AddUntilStep("wait for editor", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.IsLoaded);
|
||||
|
||||
AddStep("click on file", () =>
|
||||
{
|
||||
var item = getEditor().ChildrenOfType<Menu.DrawableMenuItem>().Single(i => i.Item.Text.Value.ToString() == "File");
|
||||
@ -318,6 +335,24 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("editor beatmap uses catch ruleset", () => getEditorBeatmap().BeatmapInfo.Ruleset.ShortName == "fruits");
|
||||
}
|
||||
|
||||
private void prepareBeatmap()
|
||||
{
|
||||
AddStep("import test beatmap", () => Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely());
|
||||
AddStep("retrieve beatmap", () => beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach());
|
||||
|
||||
AddStep("present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("wait for song select",
|
||||
() => Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded);
|
||||
}
|
||||
|
||||
private void openEditor()
|
||||
{
|
||||
AddStep("open editor", () => ((PlaySongSelect)Game.ScreenStack.CurrentScreen).Edit(beatmapSet.Beatmaps.First(beatmap => beatmap.Ruleset.OnlineID == 0)));
|
||||
AddUntilStep("wait for editor open", () => Game.ScreenStack.CurrentScreen is Editor editor && editor.ReadyForUse);
|
||||
}
|
||||
|
||||
private EditorBeatmap getEditorBeatmap() => getEditor().ChildrenOfType<EditorBeatmap>().Single();
|
||||
|
||||
private Editor getEditor() => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
|
@ -49,6 +49,7 @@ using osu.Game.Screens.Select.Options;
|
||||
using osu.Game.Tests.Beatmaps.IO;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
using SharpCompress;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
@ -839,18 +840,15 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
{
|
||||
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
|
||||
|
||||
ProgressNotification progressNotification = null!;
|
||||
|
||||
AddStep("start ongoing operation", () =>
|
||||
AddRepeatStep("start ongoing operation", () =>
|
||||
{
|
||||
progressNotification = new ProgressNotification
|
||||
Game.Notifications.Post(new ProgressNotification
|
||||
{
|
||||
Text = "Something is still running",
|
||||
Progress = 0.5f,
|
||||
State = ProgressNotificationState.Active,
|
||||
};
|
||||
Game.Notifications.Post(progressNotification);
|
||||
});
|
||||
});
|
||||
}, 15);
|
||||
|
||||
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
|
||||
AddUntilStep("confirmation dialog shown", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
|
||||
@ -861,8 +859,11 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
|
||||
AddStep("complete operation", () =>
|
||||
{
|
||||
progressNotification.Progress = 100;
|
||||
progressNotification.State = ProgressNotificationState.Completed;
|
||||
this.ChildrenOfType<ProgressNotification>().ForEach(n =>
|
||||
{
|
||||
n.Progress = 100;
|
||||
n.State = ProgressNotificationState.Completed;
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Hold escape", () => InputManager.PressKey(Key.Escape));
|
||||
@ -878,7 +879,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddStep("set hold delay to 0", () => Game.LocalConfig.SetValue(OsuSetting.UIHoldActivationDelay, 0.0));
|
||||
AddUntilStep("wait for dialog overlay", () => Game.ChildrenOfType<DialogOverlay>().SingleOrDefault() != null);
|
||||
|
||||
AddStep("start ongoing operation", () =>
|
||||
AddRepeatStep("start ongoing operation", () =>
|
||||
{
|
||||
Game.Notifications.Post(new ProgressNotification
|
||||
{
|
||||
@ -886,7 +887,7 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
Progress = 0.5f,
|
||||
State = ProgressNotificationState.Active,
|
||||
});
|
||||
});
|
||||
}, 15);
|
||||
|
||||
AddRepeatStep("attempt force exit", () => Game.ScreenStack.CurrentScreen.Exit(), 2);
|
||||
AddUntilStep("stopped at exit confirm", () => Game.ChildrenOfType<DialogOverlay>().Single().CurrentDialog is ConfirmExitDialog);
|
||||
|
@ -413,7 +413,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
};
|
||||
}
|
||||
|
||||
private partial class TestResultsScreen : PlaylistsResultsScreen
|
||||
private partial class TestResultsScreen : PlaylistItemUserResultsScreen
|
||||
{
|
||||
public new LoadingSpinner LeftSpinner => base.LeftSpinner;
|
||||
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
|
||||
|
@ -50,35 +50,73 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
});
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void Setup() => Schedule(() =>
|
||||
[Test]
|
||||
public void TestSheared()
|
||||
{
|
||||
Children = new Drawable[]
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
fillFlow = new FillFlowContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Width = relativeWidth,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = new Vector2(OsuGame.SHEAR, 0)
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
};
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Width = relativeWidth,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
Shear = new Vector2(OsuGame.SHEAR, 0)
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
};
|
||||
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
{
|
||||
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
|
||||
{
|
||||
Rank = scoreInfo.Position,
|
||||
IsPersonalBest = scoreInfo.User.Id == 2,
|
||||
Shear = Vector2.Zero,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var score in fillFlow.Children)
|
||||
score.Show();
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonSheared()
|
||||
{
|
||||
AddStep("create content", () =>
|
||||
{
|
||||
fillFlow.Add(new LeaderboardScoreV2(scoreInfo, scoreInfo.Position, scoreInfo.User.Id == 2)
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Shear = Vector2.Zero,
|
||||
});
|
||||
}
|
||||
fillFlow = new FillFlowContainer
|
||||
{
|
||||
Width = relativeWidth,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0f, 2f),
|
||||
},
|
||||
drawWidthText = new OsuSpriteText(),
|
||||
};
|
||||
|
||||
foreach (var score in fillFlow.Children)
|
||||
score.Show();
|
||||
});
|
||||
foreach (var scoreInfo in getTestScores())
|
||||
{
|
||||
fillFlow.Add(new LeaderboardScoreV2(scoreInfo)
|
||||
{
|
||||
Rank = scoreInfo.Position,
|
||||
IsPersonalBest = scoreInfo.User.Id == 2,
|
||||
});
|
||||
}
|
||||
|
||||
foreach (var score in fillFlow.Children)
|
||||
score.Show();
|
||||
});
|
||||
}
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
|
@ -11,6 +11,7 @@ using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
@ -61,6 +62,22 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
clearTextboxes(numberBoxes);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectAllOnFocus()
|
||||
{
|
||||
AddStep("create themed content", () => CreateThemedContent(OverlayColourScheme.Red));
|
||||
|
||||
AddStep("enter numbers", () => numberBoxes.ForEach(numberBox => numberBox.Text = "987654321"));
|
||||
|
||||
AddAssert("nothing selected", () => string.IsNullOrEmpty(numberBoxes.First().SelectedText));
|
||||
AddStep("click on a number box", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(numberBoxes.First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddAssert("text selected", () => numberBoxes.First().SelectedText == "987654321");
|
||||
}
|
||||
|
||||
private void clearTextboxes(IEnumerable<OsuTextBox> textBoxes) => AddStep("clear textbox", () => textBoxes.ForEach(textBox => textBox.Text = null));
|
||||
private void expectedValue(IEnumerable<OsuTextBox> textBoxes, string value) => AddAssert("expected textbox value", () => textBoxes.All(textBox => textBox.Text == value));
|
||||
}
|
||||
|
@ -2,10 +2,16 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Screens.Footer;
|
||||
@ -15,25 +21,31 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public partial class TestSceneScreenFooter : OsuManualInputManagerTestScene
|
||||
{
|
||||
private DependencyProvidingContainer contentContainer = null!;
|
||||
private ScreenFooter screenFooter = null!;
|
||||
private TestModSelectOverlay overlay = null!;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
Children = new Drawable[]
|
||||
screenFooter = new ScreenFooter();
|
||||
|
||||
Child = contentContainer = new DependencyProvidingContainer
|
||||
{
|
||||
overlay = new TestModSelectOverlay
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
CachedDependencies = new (Type, object)[]
|
||||
{
|
||||
Padding = new MarginPadding
|
||||
{
|
||||
Bottom = ScreenFooter.HEIGHT
|
||||
}
|
||||
(typeof(ScreenFooter), screenFooter)
|
||||
},
|
||||
new PopoverContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = screenFooter = new ScreenFooter(),
|
||||
overlay = new TestModSelectOverlay(),
|
||||
new PopoverContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = float.MinValue,
|
||||
Child = screenFooter,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@ -82,14 +94,156 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExternalOverlayContent()
|
||||
{
|
||||
TestShearedOverlayContainer externalOverlay = null!;
|
||||
|
||||
AddStep("add overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
|
||||
AddStep("set buttons", () => screenFooter.SetButtons(new[]
|
||||
{
|
||||
new ScreenFooterButton(externalOverlay)
|
||||
{
|
||||
AccentColour = Dependencies.Get<OsuColour>().Orange1,
|
||||
Icon = FontAwesome.Solid.Toolbox,
|
||||
Text = "One",
|
||||
},
|
||||
new ScreenFooterButton { Text = "Two", Action = () => { } },
|
||||
new ScreenFooterButton { Text = "Three", Action = () => { } },
|
||||
}));
|
||||
AddWaitStep("wait for transition", 3);
|
||||
|
||||
AddStep("show overlay", () => externalOverlay.Show());
|
||||
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
|
||||
AddUntilStep("other buttons hidden", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.Child.Parent!.Y > 0));
|
||||
|
||||
AddStep("hide overlay", () => externalOverlay.Hide());
|
||||
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
|
||||
AddUntilStep("other buttons returned", () => screenFooter.ChildrenOfType<ScreenFooterButton>().Skip(1).All(b => b.ChildrenOfType<Container>().First().Y == 0));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTemporarilyShowFooter()
|
||||
{
|
||||
TestShearedOverlayContainer externalOverlay = null!;
|
||||
|
||||
AddStep("hide footer", () => screenFooter.Hide());
|
||||
AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
|
||||
|
||||
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
|
||||
AddStep("show external overlay", () => externalOverlay.Show());
|
||||
AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible);
|
||||
AddAssert("content displayed in footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().Single().IsPresent);
|
||||
|
||||
AddStep("hide external overlay", () => externalOverlay.Hide());
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
|
||||
AddUntilStep("content hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
|
||||
|
||||
AddStep("show footer", () => screenFooter.Show());
|
||||
AddAssert("content still hidden from footer", () => screenFooter.ChildrenOfType<TestShearedOverlayContainer.TestFooterContent>().SingleOrDefault()?.IsPresent != true);
|
||||
|
||||
AddStep("show external overlay", () => externalOverlay.Show());
|
||||
AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("hide external overlay", () => externalOverlay.Hide());
|
||||
AddAssert("footer still visible", () => screenFooter.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("hide footer", () => screenFooter.Hide());
|
||||
AddStep("show external overlay", () => externalOverlay.Show());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackButton()
|
||||
{
|
||||
TestShearedOverlayContainer externalOverlay = null!;
|
||||
|
||||
AddStep("hide footer", () => screenFooter.Hide());
|
||||
AddStep("remove buttons", () => screenFooter.SetButtons(Array.Empty<ScreenFooterButton>()));
|
||||
|
||||
AddStep("add external overlay", () => contentContainer.Add(externalOverlay = new TestShearedOverlayContainer()));
|
||||
AddStep("show external overlay", () => externalOverlay.Show());
|
||||
AddAssert("footer shown", () => screenFooter.State.Value == Visibility.Visible);
|
||||
|
||||
AddStep("press back", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
|
||||
AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden);
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("show external overlay", () => externalOverlay.Show());
|
||||
AddStep("set block count", () => externalOverlay.BackButtonCount = 1);
|
||||
AddStep("press back", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
|
||||
AddAssert("overlay still visible", () => externalOverlay.State.Value == Visibility.Visible);
|
||||
AddAssert("footer still shown", () => screenFooter.State.Value == Visibility.Visible);
|
||||
AddStep("press back again", () => this.ChildrenOfType<ScreenBackButton>().Single().TriggerClick());
|
||||
AddAssert("overlay hidden", () => externalOverlay.State.Value == Visibility.Hidden);
|
||||
AddAssert("footer hidden", () => screenFooter.State.Value == Visibility.Hidden);
|
||||
}
|
||||
|
||||
private partial class TestModSelectOverlay : UserModSelectOverlay
|
||||
{
|
||||
protected override bool ShowPresets => true;
|
||||
}
|
||||
|
||||
public TestModSelectOverlay()
|
||||
: base(OverlayColourScheme.Aquamarine)
|
||||
private partial class TestShearedOverlayContainer : ShearedOverlayContainer
|
||||
{
|
||||
public override bool UseNewFooter => true;
|
||||
|
||||
public TestShearedOverlayContainer()
|
||||
: base(OverlayColourScheme.Orange)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Header.Title = "Test overlay";
|
||||
Header.Description = "An overlay that is made purely for testing purposes.";
|
||||
}
|
||||
|
||||
public int BackButtonCount;
|
||||
|
||||
public override bool OnBackButton()
|
||||
{
|
||||
if (BackButtonCount > 0)
|
||||
{
|
||||
BackButtonCount--;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public override Drawable CreateFooterContent() => new TestFooterContent();
|
||||
|
||||
public partial class TestFooterContent : VisibilityContainer
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
new ShearedButton(200) { Text = "Action #1", Action = () => { } },
|
||||
new ShearedButton(140) { Text = "Action #2", Action = () => { } },
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
this.MoveToY(0, 400, Easing.OutQuint)
|
||||
.FadeIn(400, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
{
|
||||
this.MoveToY(-20f, 200, Easing.OutQuint)
|
||||
.FadeOut(200, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public override async Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original)
|
||||
{
|
||||
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(true);
|
||||
Guid originalId = original.ID;
|
||||
|
||||
var imported = await Import(notification, new[] { importTask }).ConfigureAwait(false);
|
||||
|
||||
if (!imported.Any())
|
||||
return null;
|
||||
@ -53,7 +55,7 @@ namespace osu.Game.Beatmaps
|
||||
var first = imported.First();
|
||||
|
||||
// If there were no changes, ensure we don't accidentally nuke ourselves.
|
||||
if (first.ID == original.ID)
|
||||
if (first.ID == originalId)
|
||||
{
|
||||
first.PerformRead(s =>
|
||||
{
|
||||
@ -69,7 +71,8 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
Logger.Log($"Beatmap \"{updated}\" update completed successfully", LoggingTarget.Database);
|
||||
|
||||
original = realm!.Find<BeatmapSetInfo>(original.ID)!;
|
||||
// Re-fetch as we are likely on a different thread.
|
||||
original = realm!.Find<BeatmapSetInfo>(originalId)!;
|
||||
|
||||
// Generally the import process will do this for us if the OnlineIDs match,
|
||||
// but that isn't a guarantee (ie. if the .osu file doesn't have OnlineIDs populated).
|
||||
|
@ -415,6 +415,9 @@ namespace osu.Game.Beatmaps
|
||||
public Task<Live<BeatmapSetInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask importTask, BeatmapSetInfo original) =>
|
||||
beatmapImporter.ImportAsUpdate(notification, importTask, original);
|
||||
|
||||
public Task<ExternalEditOperation<BeatmapSetInfo>> BeginExternalEditing(BeatmapSetInfo model) =>
|
||||
beatmapImporter.BeginExternalEditing(model);
|
||||
|
||||
public Task Export(BeatmapSetInfo beatmap) => beatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||
|
||||
public Task ExportLegacy(BeatmapSetInfo beatmap) => legacyBeatmapExporter.ExportAsync(beatmap.ToLive(Realm));
|
||||
|
@ -22,8 +22,6 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
public override bool IsPresent => true;
|
||||
|
||||
private readonly CircularContainer foreground;
|
||||
|
||||
private readonly Box backgroundFill;
|
||||
private readonly Box foregroundFill;
|
||||
|
||||
@ -35,22 +33,17 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
public BeatmapCardDownloadProgressBar()
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
InternalChild = new CircularContainer
|
||||
{
|
||||
new CircularContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = backgroundFill = new Box
|
||||
backgroundFill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
},
|
||||
foreground = new CircularContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
Child = foregroundFill = new Box
|
||||
},
|
||||
foregroundFill = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
}
|
||||
@ -89,7 +82,7 @@ namespace osu.Game.Beatmaps.Drawables.Cards
|
||||
|
||||
private void progressChanged()
|
||||
{
|
||||
foreground.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint);
|
||||
foregroundFill.ResizeWidthTo((float)progress.Value, progress.Value > 0 ? BeatmapCard.TRANSITION_DURATION : 0, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
65
osu.Game/Database/ExternalEditOperation.cs
Normal file
65
osu.Game/Database/ExternalEditOperation.cs
Normal file
@ -0,0 +1,65 @@
|
||||
// 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.IO;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
|
||||
namespace osu.Game.Database
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains information related to an active external edit operation.
|
||||
/// </summary>
|
||||
public class ExternalEditOperation<TModel> where TModel : class, IHasGuidPrimaryKey
|
||||
{
|
||||
/// <summary>
|
||||
/// The temporary path at which the model has been exported to for editing.
|
||||
/// </summary>
|
||||
public readonly string MountedPath;
|
||||
|
||||
/// <summary>
|
||||
/// Whether the model is still mounted at <see cref="MountedPath"/>.
|
||||
/// </summary>
|
||||
public bool IsMounted { get; private set; }
|
||||
|
||||
private readonly IModelImporter<TModel> importer;
|
||||
private readonly TModel original;
|
||||
|
||||
public ExternalEditOperation(IModelImporter<TModel> importer, TModel original, string path)
|
||||
{
|
||||
this.importer = importer;
|
||||
this.original = original;
|
||||
|
||||
MountedPath = path;
|
||||
|
||||
IsMounted = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finish the external edit operation.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will trigger an asynchronous reimport of the model.
|
||||
/// Subsequent calls will be a no-op.
|
||||
/// </remarks>
|
||||
/// <returns>A task which will eventuate in the newly imported model with changes applied.</returns>
|
||||
public async Task<Live<TModel>?> Finish()
|
||||
{
|
||||
if (!Directory.Exists(MountedPath) || !IsMounted)
|
||||
return null;
|
||||
|
||||
IsMounted = false;
|
||||
|
||||
Live<TModel>? imported = await importer.ImportAsUpdate(new ProgressNotification(), new ImportTask(MountedPath), original)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
Directory.Delete(MountedPath, true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
return imported;
|
||||
}
|
||||
}
|
||||
}
|
@ -34,6 +34,15 @@ namespace osu.Game.Database
|
||||
/// <returns>The imported model.</returns>
|
||||
Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original);
|
||||
|
||||
/// <summary>
|
||||
/// Mount all files for a model to a temporary directory to allow for external editing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// When editing is completed, call Finish() on the returned operation class to begin the import-and-update process.
|
||||
/// </remarks>
|
||||
/// <param name="model">The model to mount.</param>
|
||||
public Task<ExternalEditOperation<TModel>> BeginExternalEditing(TModel model);
|
||||
|
||||
/// <summary>
|
||||
/// A user displayable name for the model type associated with this manager.
|
||||
/// </summary>
|
||||
|
@ -179,6 +179,30 @@ namespace osu.Game.Database
|
||||
|
||||
public virtual Task<Live<TModel>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, TModel original) => throw new NotImplementedException();
|
||||
|
||||
public async Task<ExternalEditOperation<TModel>> BeginExternalEditing(TModel model)
|
||||
{
|
||||
string mountedPath = Path.Join(Path.GetTempPath(), model.Hash);
|
||||
|
||||
if (Directory.Exists(mountedPath))
|
||||
Directory.Delete(mountedPath, true);
|
||||
|
||||
Directory.CreateDirectory(mountedPath);
|
||||
|
||||
foreach (var realmFile in model.Files)
|
||||
{
|
||||
string sourcePath = Files.Storage.GetFullPath(realmFile.File.GetStoragePath());
|
||||
string destinationPath = Path.Join(mountedPath, realmFile.Filename);
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
|
||||
|
||||
using (var inStream = Files.Storage.GetStream(sourcePath))
|
||||
using (var outStream = File.Create(destinationPath))
|
||||
await inStream.CopyToAsync(outStream).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return new ExternalEditOperation<TModel>(this, model, mountedPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
||||
|
@ -65,7 +65,8 @@ namespace osu.Game.Database
|
||||
if (!d.Beatmaps.Contains(existingBeatmap))
|
||||
{
|
||||
Debug.Fail("Beatmaps should never become detached under normal circumstances. If this ever triggers, it should be investigated further.");
|
||||
Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database, LogLevel.Important);
|
||||
Logger.Log("WARNING: One of the difficulties in a beatmap was detached from its set. Please save a copy of logs and report this to devs.", LoggingTarget.Database,
|
||||
LogLevel.Important);
|
||||
d.Beatmaps.Add(existingBeatmap);
|
||||
}
|
||||
|
||||
@ -291,7 +292,24 @@ namespace osu.Game.Database
|
||||
if (!RealmAccess.CurrentThreadSubscriptionsAllowed)
|
||||
throw new InvalidOperationException($"Make sure to call {nameof(RealmAccess)}.{nameof(RealmAccess.RegisterForNotifications)}");
|
||||
|
||||
return collection.SubscribeForNotifications(callback);
|
||||
bool initial = true;
|
||||
return collection.SubscribeForNotifications(((sender, changes) =>
|
||||
{
|
||||
if (initial)
|
||||
{
|
||||
initial = false;
|
||||
|
||||
// Realm might coalesce the initial callback, meaning we never receive a `ChangeSet` of `null` marking the first callback.
|
||||
// Let's decouple it for simplicity in handling.
|
||||
if (changes != null)
|
||||
{
|
||||
callback(sender, null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback(sender, changes);
|
||||
}));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -7,6 +7,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
protected override bool AllowIme => false;
|
||||
|
||||
public OsuNumberBox()
|
||||
{
|
||||
SelectAllOnFocus = true;
|
||||
}
|
||||
|
||||
protected override bool CanAddCharacter(char character) => char.IsAsciiDigit(character);
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +63,11 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private Dictionary<FeedbackSampleType, Sample?[]> sampleMap = new Dictionary<FeedbackSampleType, Sample?[]>();
|
||||
|
||||
/// <summary>
|
||||
/// Whether all text should be selected when the <see cref="OsuTextBox"/> gains focus.
|
||||
/// </summary>
|
||||
public bool SelectAllOnFocus { get; set; }
|
||||
|
||||
public OsuTextBox()
|
||||
{
|
||||
Height = 40;
|
||||
@ -255,6 +260,9 @@ namespace osu.Game.Graphics.UserInterface
|
||||
BorderThickness = 3;
|
||||
|
||||
base.OnFocus(e);
|
||||
|
||||
if (SelectAllOnFocus)
|
||||
SelectAll();
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
|
@ -28,6 +28,12 @@ namespace osu.Game.Graphics.UserInterfaceV2
|
||||
set => Component.ReadOnly = value;
|
||||
}
|
||||
|
||||
public bool SelectAllOnFocus
|
||||
{
|
||||
get => Component.SelectAllOnFocus;
|
||||
set => Component.SelectAllOnFocus = value;
|
||||
}
|
||||
|
||||
public LocalisableString PlaceholderText
|
||||
{
|
||||
set => Component.PlaceholderText = value;
|
||||
|
23
osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs
Normal file
23
osu.Game/Online/Rooms/ShowPlaylistScoreRequest.cs
Normal file
@ -0,0 +1,23 @@
|
||||
// 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.Game.Online.API;
|
||||
|
||||
namespace osu.Game.Online.Rooms
|
||||
{
|
||||
public class ShowPlaylistScoreRequest : APIRequest<MultiplayerScore>
|
||||
{
|
||||
private readonly long roomId;
|
||||
private readonly long playlistItemId;
|
||||
private readonly long scoreId;
|
||||
|
||||
public ShowPlaylistScoreRequest(long roomId, long playlistItemId, long scoreId)
|
||||
{
|
||||
this.roomId = roomId;
|
||||
this.playlistItemId = playlistItemId;
|
||||
this.scoreId = scoreId;
|
||||
}
|
||||
|
||||
protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores/{scoreId}";
|
||||
}
|
||||
}
|
@ -126,7 +126,8 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
Origin = Anchor.Centre,
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0,
|
||||
Size = new Vector2(6)
|
||||
Size = new Vector2(6),
|
||||
Icon = FontAwesome.Solid.CaretDown,
|
||||
});
|
||||
}
|
||||
|
||||
@ -136,7 +137,7 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
SortDirection.BindValueChanged(direction =>
|
||||
{
|
||||
icon.Icon = direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? FontAwesome.Solid.CaretUp : FontAwesome.Solid.CaretDown;
|
||||
icon.ScaleTo(direction.NewValue == Overlays.SortDirection.Ascending && Active.Value ? new Vector2(1f, -1f) : Vector2.One, 300, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
@ -11,45 +9,52 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Footer;
|
||||
|
||||
namespace osu.Game.Overlays.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// A sheared overlay which provides a header and footer and basic animations.
|
||||
/// Exposes <see cref="TopLevelContent"/>, <see cref="MainAreaContent"/> and <see cref="Footer"/> as valid targets for content.
|
||||
/// A sheared overlay which provides a header and basic animations.
|
||||
/// Exposes <see cref="TopLevelContent"/> and <see cref="MainAreaContent"/> as valid targets for content.
|
||||
/// </summary>
|
||||
public abstract partial class ShearedOverlayContainer : OsuFocusedOverlayContainer
|
||||
{
|
||||
protected const float PADDING = 14;
|
||||
public const float PADDING = 14;
|
||||
|
||||
[Cached]
|
||||
protected readonly OverlayColourProvider ColourProvider;
|
||||
public readonly OverlayColourProvider ColourProvider;
|
||||
|
||||
/// <summary>
|
||||
/// The overlay's header.
|
||||
/// </summary>
|
||||
protected ShearedOverlayHeader Header { get; private set; }
|
||||
protected ShearedOverlayHeader Header { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// The overlay's footer.
|
||||
/// </summary>
|
||||
protected Container Footer { get; private set; }
|
||||
protected Container Footer { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScreenFooter? footer { get; set; }
|
||||
|
||||
// todo: very temporary property that will be removed once ModSelectOverlay and FirstRunSetupOverlay are updated to use new footer.
|
||||
public virtual bool UseNewFooter => false;
|
||||
|
||||
/// <summary>
|
||||
/// A container containing all content, including the header and footer.
|
||||
/// May be used for overlay-wide animations.
|
||||
/// </summary>
|
||||
protected Container TopLevelContent { get; private set; }
|
||||
protected Container TopLevelContent { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A container for content that is to be displayed between the header and footer.
|
||||
/// </summary>
|
||||
protected Container MainAreaContent { get; private set; }
|
||||
protected Container MainAreaContent { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// A container for content that is to be displayed inside the footer.
|
||||
/// </summary>
|
||||
protected Container FooterContent { get; private set; }
|
||||
protected Container FooterContent { get; private set; } = null!;
|
||||
|
||||
protected override bool StartHidden => true;
|
||||
|
||||
@ -65,7 +70,7 @@ namespace osu.Game.Overlays.Mods
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
const float footer_height = 50;
|
||||
const float footer_height = ScreenFooter.HEIGHT;
|
||||
|
||||
Child = TopLevelContent = new Container
|
||||
{
|
||||
@ -113,6 +118,17 @@ namespace osu.Game.Overlays.Mods
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates content to be displayed on the game-wide footer.
|
||||
/// </summary>
|
||||
public virtual Drawable CreateFooterContent() => Empty();
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the back button in the footer is pressed.
|
||||
/// </summary>
|
||||
/// <returns>Whether the back button should not close the overlay.</returns>
|
||||
public virtual bool OnBackButton() => false;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
{
|
||||
if (State.Value == Visibility.Visible)
|
||||
@ -124,6 +140,8 @@ namespace osu.Game.Overlays.Mods
|
||||
return base.OnClick(e);
|
||||
}
|
||||
|
||||
private bool hideFooterOnPopOut;
|
||||
|
||||
protected override void PopIn()
|
||||
{
|
||||
const double fade_in_duration = 400;
|
||||
@ -131,7 +149,19 @@ namespace osu.Game.Overlays.Mods
|
||||
this.FadeIn(fade_in_duration, Easing.OutQuint);
|
||||
|
||||
Header.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
Footer.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
|
||||
if (UseNewFooter && footer != null)
|
||||
{
|
||||
footer.SetActiveOverlayContainer(this);
|
||||
|
||||
if (footer.State.Value == Visibility.Hidden)
|
||||
{
|
||||
footer.Show();
|
||||
hideFooterOnPopOut = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
Footer.MoveToY(0, fade_in_duration, Easing.OutQuint);
|
||||
}
|
||||
|
||||
protected override void PopOut()
|
||||
@ -142,7 +172,19 @@ namespace osu.Game.Overlays.Mods
|
||||
this.FadeOut(fade_out_duration, Easing.OutQuint);
|
||||
|
||||
Header.MoveToY(-Header.DrawHeight, fade_out_duration, Easing.OutQuint);
|
||||
Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint);
|
||||
|
||||
if (UseNewFooter && footer != null)
|
||||
{
|
||||
footer.ClearActiveOverlayContainer();
|
||||
|
||||
if (hideFooterOnPopOut)
|
||||
{
|
||||
footer.Hide();
|
||||
hideFooterOnPopOut = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
Footer.MoveToY(Footer.DrawHeight, fade_out_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,11 +9,11 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
public class OverlayColourProvider
|
||||
{
|
||||
private readonly OverlayColourScheme colourScheme;
|
||||
public OverlayColourScheme ColourScheme { get; private set; }
|
||||
|
||||
public OverlayColourProvider(OverlayColourScheme colourScheme)
|
||||
{
|
||||
this.colourScheme = colourScheme;
|
||||
ColourScheme = colourScheme;
|
||||
}
|
||||
|
||||
// Note that the following five colours are also defined in `OsuColour` as `{colourScheme}{0,1,2,3,4}`.
|
||||
@ -47,7 +47,17 @@ namespace osu.Game.Overlays
|
||||
public Color4 Background5 => getColour(0.1f, 0.15f);
|
||||
public Color4 Background6 => getColour(0.1f, 0.1f);
|
||||
|
||||
private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(colourScheme), saturation, lightness, 1));
|
||||
/// <summary>
|
||||
/// Changes the value of <see cref="ColourScheme"/> to a different colour scheme.
|
||||
/// Note that this does not trigger any kind of signal to any drawable that received colours from here, all drawables need to be updated manually.
|
||||
/// </summary>
|
||||
/// <param name="colourScheme">The proposed colour scheme.</param>
|
||||
public void ChangeColourScheme(OverlayColourScheme colourScheme)
|
||||
{
|
||||
ColourScheme = colourScheme;
|
||||
}
|
||||
|
||||
private Color4 getColour(float saturation, float lightness) => Color4.FromHsl(new Vector4(getBaseHue(ColourScheme), saturation, lightness, 1));
|
||||
|
||||
// See https://github.com/ppy/osu-web/blob/5a536d217a21582aad999db50a981003d3ad5659/app/helpers.php#L1620-L1628
|
||||
private static float getBaseHue(OverlayColourScheme colourScheme)
|
||||
|
@ -269,9 +269,16 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
composerFocusMode.BindValueChanged(_ =>
|
||||
{
|
||||
float targetAlpha = composerFocusMode.Value ? 0.5f : 1;
|
||||
leftToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint);
|
||||
rightToolboxBackground.FadeTo(targetAlpha, 400, Easing.OutQuint);
|
||||
if (!composerFocusMode.Value)
|
||||
{
|
||||
leftToolboxBackground.FadeIn(750, Easing.OutQuint);
|
||||
rightToolboxBackground.FadeIn(750, Easing.OutQuint);
|
||||
}
|
||||
else
|
||||
{
|
||||
leftToolboxBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint);
|
||||
rightToolboxBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
|
@ -394,13 +394,12 @@ namespace osu.Game.Rulesets
|
||||
public virtual IRulesetFilterCriteria? CreateRulesetFilterCriteria() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Can be overridden to add a ruleset-specific section to the editor beatmap setup screen.
|
||||
/// Can be overridden to add ruleset-specific sections to the editor beatmap setup screen.
|
||||
/// </summary>
|
||||
public virtual RulesetSetupSection? CreateEditorSetupSection() => null;
|
||||
|
||||
/// <summary>
|
||||
/// Can be overridden to alter the difficulty section to the editor beatmap setup screen.
|
||||
/// </summary>
|
||||
public virtual SetupSection? CreateEditorDifficultySection() => null;
|
||||
public virtual IEnumerable<SetupSection> CreateEditorSetupSections() =>
|
||||
[
|
||||
new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -15,10 +15,10 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.IO.Archives;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
|
||||
namespace osu.Game.Scoring
|
||||
@ -214,6 +214,7 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
|
||||
public Task<Live<ScoreInfo>?> ImportAsUpdate(ProgressNotification notification, ImportTask task, ScoreInfo original) => scoreImporter.ImportAsUpdate(notification, task, original);
|
||||
public Task<ExternalEditOperation<ScoreInfo>> BeginExternalEditing(ScoreInfo model) => scoreImporter.BeginExternalEditing(model);
|
||||
|
||||
public Live<ScoreInfo>? Import(ScoreInfo item, ArchiveReader? archive = null, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
scoreImporter.ImportModel(item, archive, parameters, cancellationToken);
|
||||
|
@ -82,10 +82,13 @@ namespace osu.Game.Screens.Edit
|
||||
saveInProgress.BindValueChanged(_ => TestGameplayButton.Enabled.Value = !saveInProgress.Value, true);
|
||||
composerFocusMode.BindValueChanged(_ =>
|
||||
{
|
||||
float targetAlpha = composerFocusMode.Value ? 0.5f : 1;
|
||||
|
||||
foreach (var c in this.ChildrenOfType<BottomBarContainer>())
|
||||
c.Background.FadeTo(targetAlpha, 400, Easing.OutQuint);
|
||||
{
|
||||
if (!composerFocusMode.Value)
|
||||
c.Background.FadeIn(750, Easing.OutQuint);
|
||||
else
|
||||
c.Background.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
using osu.Game.Graphics;
|
||||
@ -17,32 +18,54 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts
|
||||
{
|
||||
private readonly BindableList<BreakPeriod> breaks = new BindableList<BreakPeriod>();
|
||||
|
||||
private DrawablePool<BreakVisualisation> pool = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
AddInternal(pool = new DrawablePool<BreakVisualisation>(10));
|
||||
}
|
||||
|
||||
protected override void LoadBeatmap(EditorBeatmap beatmap)
|
||||
{
|
||||
base.LoadBeatmap(beatmap);
|
||||
|
||||
breaks.UnbindAll();
|
||||
breaks.BindTo(beatmap.Breaks);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
breaks.BindCollectionChanged((_, _) =>
|
||||
{
|
||||
Clear();
|
||||
foreach (var breakPeriod in beatmap.Breaks)
|
||||
Add(new BreakVisualisation(breakPeriod));
|
||||
Clear(disposeChildren: false);
|
||||
foreach (var breakPeriod in breaks)
|
||||
Add(pool.Get(v => v.BreakPeriod = breakPeriod));
|
||||
}, true);
|
||||
}
|
||||
|
||||
private partial class BreakVisualisation : Circle
|
||||
private partial class BreakVisualisation : PoolableDrawable
|
||||
{
|
||||
public BreakVisualisation(BreakPeriod breakPeriod)
|
||||
public BreakPeriod BreakPeriod
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
X = (float)breakPeriod.StartTime;
|
||||
Width = (float)breakPeriod.Duration;
|
||||
set
|
||||
{
|
||||
X = (float)value.StartTime;
|
||||
Width = (float)value.Duration;
|
||||
}
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours) => Colour = colours.Gray7;
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativePositionAxes = Axes.X;
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChild = new Circle { RelativeSizeAxes = Axes.Both };
|
||||
Colour = colours.Gray7;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -148,10 +148,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
bank = new LabelledTextBox
|
||||
{
|
||||
Label = "Bank Name",
|
||||
SelectAllOnFocus = true,
|
||||
},
|
||||
additionBank = new LabelledTextBox
|
||||
{
|
||||
Label = "Addition Bank",
|
||||
SelectAllOnFocus = true,
|
||||
},
|
||||
volume = new IndeterminateSliderWithTextBoxInput<int>("Volume", new BindableInt(100)
|
||||
{
|
||||
|
@ -132,7 +132,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
composerFocusMode.BindValueChanged(_ => timelineBackground.FadeTo(composerFocusMode.Value ? 0.5f : 1, 400, Easing.OutQuint), true);
|
||||
composerFocusMode.BindValueChanged(_ =>
|
||||
{
|
||||
if (!composerFocusMode.Value)
|
||||
timelineBackground.FadeIn(750, Easing.OutQuint);
|
||||
else
|
||||
timelineBackground.Delay(600).FadeTo(0.5f, 4000, Easing.OutQuint);
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -481,7 +481,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to test it.", () => attemptMutationOperation(() =>
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||
{
|
||||
if (!Save()) return false;
|
||||
|
||||
@ -521,7 +521,7 @@ namespace osu.Game.Screens.Edit
|
||||
/// Saves the currently edited beatmap.
|
||||
/// </summary>
|
||||
/// <returns>Whether the save was successful.</returns>
|
||||
protected bool Save()
|
||||
internal bool Save()
|
||||
{
|
||||
if (!canSave)
|
||||
{
|
||||
@ -1112,6 +1112,10 @@ namespace osu.Game.Screens.Edit
|
||||
var export = createExportMenu();
|
||||
saveRelatedMenuItems.AddRange(export.Items);
|
||||
yield return export;
|
||||
|
||||
var externalEdit = new EditorMenuItem("Edit externally", MenuItemType.Standard, editExternally);
|
||||
saveRelatedMenuItems.Add(externalEdit);
|
||||
yield return externalEdit;
|
||||
}
|
||||
|
||||
yield return new OsuMenuItemSpacer();
|
||||
@ -1129,11 +1133,35 @@ namespace osu.Game.Screens.Edit
|
||||
return new EditorMenuItem(CommonStrings.Export) { Items = exportItems };
|
||||
}
|
||||
|
||||
private void editExternally()
|
||||
{
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||
{
|
||||
if (!Save())
|
||||
return false;
|
||||
|
||||
startEdit();
|
||||
return true;
|
||||
})));
|
||||
}
|
||||
else
|
||||
{
|
||||
startEdit();
|
||||
}
|
||||
|
||||
void startEdit()
|
||||
{
|
||||
this.Push(new ExternalEditScreen());
|
||||
}
|
||||
}
|
||||
|
||||
private void exportBeatmap(bool legacy)
|
||||
{
|
||||
if (HasUnsavedChanges)
|
||||
{
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog("The beatmap will be saved in order to export it.", () => attemptAsyncMutationOperation(() =>
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptAsyncMutationOperation(() =>
|
||||
{
|
||||
if (!Save())
|
||||
return Task.CompletedTask;
|
||||
@ -1211,17 +1239,14 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
if (isNewBeatmap)
|
||||
{
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog("This beatmap will be saved in order to create another difficulty.", () =>
|
||||
dialogOverlay.Push(new SaveRequiredPopupDialog(() => attemptMutationOperation(() =>
|
||||
{
|
||||
attemptMutationOperation(() =>
|
||||
{
|
||||
if (!Save())
|
||||
return false;
|
||||
if (!Save())
|
||||
return false;
|
||||
|
||||
CreateNewDifficulty(rulesetInfo);
|
||||
return true;
|
||||
});
|
||||
}));
|
||||
CreateNewDifficulty(rulesetInfo);
|
||||
return true;
|
||||
})));
|
||||
|
||||
return;
|
||||
}
|
||||
@ -1260,7 +1285,11 @@ namespace osu.Game.Screens.Edit
|
||||
return new EditorMenuItem(EditorStrings.ChangeDifficulty) { Items = difficultyItems };
|
||||
}
|
||||
|
||||
protected void SwitchToDifficulty(BeatmapInfo nextBeatmap) => loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
|
||||
public void SwitchToDifficulty(BeatmapInfo nextBeatmap)
|
||||
{
|
||||
switchingDifficulty = true;
|
||||
loader?.ScheduleSwitchToExistingDifficulty(nextBeatmap, GetState(nextBeatmap.Ruleset));
|
||||
}
|
||||
|
||||
private void cancelExit()
|
||||
{
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Timing;
|
||||
@ -18,6 +19,11 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private readonly IBeatmapProcessor? rulesetBeatmapProcessor;
|
||||
|
||||
/// <summary>
|
||||
/// Kept for the purposes of reducing redundant regeneration of automatic breaks.
|
||||
/// </summary>
|
||||
private HashSet<(double, double)> objectDurationCache = new HashSet<(double, double)>();
|
||||
|
||||
public EditorBeatmapProcessor(EditorBeatmap beatmap, Ruleset ruleset)
|
||||
{
|
||||
Beatmap = beatmap;
|
||||
@ -38,6 +44,13 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
private void autoGenerateBreaks()
|
||||
{
|
||||
var objectDuration = Beatmap.HitObjects.Select(ho => (ho.StartTime, ho.GetEndTime())).ToHashSet();
|
||||
|
||||
if (objectDuration.SetEquals(objectDurationCache))
|
||||
return;
|
||||
|
||||
objectDurationCache = objectDuration;
|
||||
|
||||
Beatmap.Breaks.RemoveAll(b => b is not ManualBreakPeriod);
|
||||
|
||||
foreach (var manualBreak in Beatmap.Breaks.ToList())
|
||||
|
@ -13,13 +13,12 @@ namespace osu.Game.Screens.Edit
|
||||
[Cached]
|
||||
public abstract partial class EditorScreenWithTimeline : EditorScreen
|
||||
{
|
||||
public const float PADDING = 10;
|
||||
|
||||
public Container TimelineContent { get; private set; } = null!;
|
||||
public TimelineArea TimelineArea { get; private set; } = null!;
|
||||
|
||||
public Container MainContent { get; private set; } = null!;
|
||||
|
||||
private LoadingSpinner spinner = null!;
|
||||
private Container timelineContent = null!;
|
||||
|
||||
protected EditorScreenWithTimeline(EditorScreenMode type)
|
||||
: base(type)
|
||||
@ -60,7 +59,7 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
TimelineContent = new Container
|
||||
timelineContent = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
@ -108,7 +107,7 @@ namespace osu.Game.Screens.Edit
|
||||
MainContent.Add(content);
|
||||
content.FadeInFromZero(300, Easing.OutQuint);
|
||||
|
||||
LoadComponentAsync(new TimelineArea(CreateTimelineContent()), TimelineContent.Add);
|
||||
LoadComponentAsync(TimelineArea = new TimelineArea(CreateTimelineContent()), timelineContent.Add);
|
||||
});
|
||||
}
|
||||
|
||||
|
252
osu.Game/Screens/Edit/ExternalEditScreen.cs
Normal file
252
osu.Game/Screens/Edit/ExternalEditScreen.cs
Normal file
@ -0,0 +1,252 @@
|
||||
// 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.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterfaceV2;
|
||||
using osu.Game.Online.Multiplayer;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
internal partial class ExternalEditScreen : OsuScreen
|
||||
{
|
||||
[Resolved]
|
||||
private GameHost gameHost { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private BeatmapManager beatmapManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private Editor editor { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; } = null!;
|
||||
|
||||
private Task? fileMountOperation;
|
||||
|
||||
public ExternalEditOperation<BeatmapSetInfo>? EditOperation;
|
||||
|
||||
private FillFlowContainer flow = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new Container
|
||||
{
|
||||
Masking = true,
|
||||
CornerRadius = 20,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeDuration = 500,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colourProvider.Background5,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
flow = new FillFlowContainer
|
||||
{
|
||||
Margin = new MarginPadding(20),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Vertical,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Spacing = new Vector2(15),
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
fileMountOperation = begin();
|
||||
}
|
||||
|
||||
public override bool OnExiting(ScreenExitEvent e)
|
||||
{
|
||||
// Don't allow exiting until the file mount operation has completed.
|
||||
// This is mainly to simplify the flow (once the screen is pushed we are guaranteed an attempted mount).
|
||||
if (fileMountOperation?.IsCompleted == false)
|
||||
return true;
|
||||
|
||||
// If the operation completed successfully, ensure that we finish the operation before exiting.
|
||||
// The finish() call will subsequently call Exit() when done.
|
||||
if (EditOperation != null)
|
||||
{
|
||||
finish().FireAndForget();
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.OnExiting(e);
|
||||
}
|
||||
|
||||
private async Task begin()
|
||||
{
|
||||
showSpinner("Exporting for edit...");
|
||||
|
||||
await Task.Delay(500).ConfigureAwait(true);
|
||||
|
||||
try
|
||||
{
|
||||
EditOperation = await beatmapManager.BeginExternalEditing(editorBeatmap.BeatmapInfo.BeatmapSet!).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($@"Failed to initiate external edit operation: {ex}", LoggingTarget.Database);
|
||||
fileMountOperation = null;
|
||||
showSpinner("Export failed!");
|
||||
await Task.Delay(1000).ConfigureAwait(true);
|
||||
this.Exit();
|
||||
}
|
||||
|
||||
flow.Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = "Beatmap is mounted externally",
|
||||
Font = OsuFont.Default.With(size: 30),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
new OsuTextFlowContainer
|
||||
{
|
||||
Padding = new MarginPadding(5),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Width = 350,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Text = "Any changes made to the exported folder will be imported to the game, including file additions, modifications and deletions.",
|
||||
},
|
||||
new PurpleRoundedButton
|
||||
{
|
||||
Text = "Open folder",
|
||||
Width = 350,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = openDirectory,
|
||||
Enabled = { Value = false }
|
||||
},
|
||||
new DangerousRoundedButton
|
||||
{
|
||||
Text = "Finish editing and import changes",
|
||||
Width = 350,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Action = () => finish().FireAndForget(),
|
||||
Enabled = { Value = false }
|
||||
}
|
||||
};
|
||||
|
||||
Scheduler.AddDelayed(() =>
|
||||
{
|
||||
foreach (var b in flow.ChildrenOfType<RoundedButton>())
|
||||
b.Enabled.Value = true;
|
||||
openDirectory();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
private void openDirectory()
|
||||
{
|
||||
if (EditOperation == null)
|
||||
return;
|
||||
|
||||
// Ensure the trailing separator is present in order to show the folder contents.
|
||||
gameHost.OpenFileExternally(EditOperation.MountedPath.TrimDirectorySeparator() + Path.DirectorySeparatorChar);
|
||||
}
|
||||
|
||||
private async Task finish()
|
||||
{
|
||||
string originalDifficulty = editor.Beatmap.Value.Beatmap.BeatmapInfo.DifficultyName;
|
||||
|
||||
showSpinner("Cleaning up...");
|
||||
|
||||
Live<BeatmapSetInfo>? beatmap = null;
|
||||
|
||||
try
|
||||
{
|
||||
beatmap = await EditOperation!.Finish().ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.Log($@"Failed to finish external edit operation: {ex}", LoggingTarget.Database);
|
||||
showSpinner("Import failed!");
|
||||
await Task.Delay(1000).ConfigureAwait(true);
|
||||
}
|
||||
|
||||
// Setting to null will allow exit to succeed.
|
||||
EditOperation = null;
|
||||
|
||||
if (beatmap == null)
|
||||
this.Exit();
|
||||
else
|
||||
{
|
||||
// the `ImportAsUpdate()` flow will yield beatmap(sets) with online status of `None` if online lookup fails.
|
||||
// coerce such models to `LocallyModified` state instead to unify behaviour with normal editing flow.
|
||||
beatmap.PerformWrite(s =>
|
||||
{
|
||||
if (s.Status == BeatmapOnlineStatus.None)
|
||||
s.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
foreach (var difficulty in s.Beatmaps.Where(b => b.Status == BeatmapOnlineStatus.None))
|
||||
difficulty.Status = BeatmapOnlineStatus.LocallyModified;
|
||||
});
|
||||
|
||||
var closestMatchingBeatmap =
|
||||
beatmap.Value.Beatmaps.FirstOrDefault(b => b.DifficultyName == originalDifficulty)
|
||||
?? beatmap.Value.Beatmaps.First();
|
||||
|
||||
editor.SwitchToDifficulty(closestMatchingBeatmap);
|
||||
}
|
||||
}
|
||||
|
||||
private void showSpinner(string text)
|
||||
{
|
||||
foreach (var b in flow.ChildrenOfType<RoundedButton>())
|
||||
b.Enabled.Value = false;
|
||||
|
||||
flow.Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = text,
|
||||
Font = OsuFont.Default.With(size: 30),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
},
|
||||
new LoadingSpinner
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
State = { Value = Visibility.Visible }
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
@ -9,9 +9,9 @@ namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public partial class SaveRequiredPopupDialog : PopupDialog
|
||||
{
|
||||
public SaveRequiredPopupDialog(string headerText, Action saveAndAction)
|
||||
public SaveRequiredPopupDialog(Action saveAndAction)
|
||||
{
|
||||
HeaderText = headerText;
|
||||
HeaderText = "The beatmap will be saved to continue with this operation.";
|
||||
|
||||
Icon = FontAwesome.Regular.Save;
|
||||
|
||||
|
@ -9,7 +9,7 @@ using osu.Game.Localisation;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
internal partial class ColoursSection : SetupSection
|
||||
public partial class ColoursSection : SetupSection
|
||||
{
|
||||
public override LocalisableString Title => EditorSetupStrings.ColoursHeader;
|
||||
|
||||
|
@ -15,12 +15,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
public partial class DifficultySection : SetupSection
|
||||
{
|
||||
protected LabelledSliderBar<float> CircleSizeSlider { get; private set; } = null!;
|
||||
protected LabelledSliderBar<float> HealthDrainSlider { get; private set; } = null!;
|
||||
protected LabelledSliderBar<float> ApproachRateSlider { get; private set; } = null!;
|
||||
protected LabelledSliderBar<float> OverallDifficultySlider { get; private set; } = null!;
|
||||
protected LabelledSliderBar<double> BaseVelocitySlider { get; private set; } = null!;
|
||||
protected LabelledSliderBar<double> TickRateSlider { get; private set; } = null!;
|
||||
private LabelledSliderBar<float> circleSizeSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> healthDrainSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> approachRateSlider { get; set; } = null!;
|
||||
private LabelledSliderBar<float> overallDifficultySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> baseVelocitySlider { get; set; } = null!;
|
||||
private LabelledSliderBar<double> tickRateSlider { get; set; } = null!;
|
||||
|
||||
public override LocalisableString Title => EditorSetupStrings.DifficultyHeader;
|
||||
|
||||
@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
CircleSizeSlider = new LabelledSliderBar<float>
|
||||
circleSizeSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsCs,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -42,7 +42,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
HealthDrainSlider = new LabelledSliderBar<float>
|
||||
healthDrainSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsDrain,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
ApproachRateSlider = new LabelledSliderBar<float>
|
||||
approachRateSlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAr,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
OverallDifficultySlider = new LabelledSliderBar<float>
|
||||
overallDifficultySlider = new LabelledSliderBar<float>
|
||||
{
|
||||
Label = BeatmapsetsStrings.ShowStatsAccuracy,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Precision = 0.1f,
|
||||
}
|
||||
},
|
||||
BaseVelocitySlider = new LabelledSliderBar<double>
|
||||
baseVelocitySlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.BaseVelocity,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -94,7 +94,7 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
Precision = 0.01f,
|
||||
}
|
||||
},
|
||||
TickRateSlider = new LabelledSliderBar<double>
|
||||
tickRateSlider = new LabelledSliderBar<double>
|
||||
{
|
||||
Label = EditorSetupStrings.TickRate,
|
||||
FixedLabelWidth = LABEL_WIDTH,
|
||||
@ -120,12 +120,12 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
// for now, update these on commit rather than making BeatmapMetadata bindables.
|
||||
// after switching database engines we can reconsider if switching to bindables is a good direction.
|
||||
Beatmap.Difficulty.CircleSize = CircleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = HealthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = ApproachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = OverallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = BaseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = TickRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.CircleSize = circleSizeSlider.Current.Value;
|
||||
Beatmap.Difficulty.DrainRate = healthDrainSlider.Current.Value;
|
||||
Beatmap.Difficulty.ApproachRate = approachRateSlider.Current.Value;
|
||||
Beatmap.Difficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderMultiplier = baseVelocitySlider.Current.Value;
|
||||
Beatmap.Difficulty.SliderTickRate = tickRateSlider.Current.Value;
|
||||
|
||||
Beatmap.UpdateAllHitObjects();
|
||||
Beatmap.SaveState();
|
||||
|
@ -29,18 +29,14 @@ namespace osu.Game.Screens.Edit.Setup
|
||||
{
|
||||
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
|
||||
|
||||
var sectionsEnumerable = new List<SetupSection>
|
||||
{
|
||||
List<SetupSection> sectionsEnumerable =
|
||||
[
|
||||
new ResourcesSection(),
|
||||
new MetadataSection(),
|
||||
ruleset.CreateEditorDifficultySection() ?? new DifficultySection(),
|
||||
new ColoursSection(),
|
||||
new DesignSection(),
|
||||
};
|
||||
new MetadataSection()
|
||||
];
|
||||
|
||||
var rulesetSpecificSection = ruleset.CreateEditorSetupSection();
|
||||
if (rulesetSpecificSection != null)
|
||||
sectionsEnumerable.Add(rulesetSpecificSection);
|
||||
sectionsEnumerable.AddRange(ruleset.CreateEditorSetupSections());
|
||||
sectionsEnumerable.Add(new DesignSection());
|
||||
|
||||
Add(new Box
|
||||
{
|
||||
|
@ -51,7 +51,8 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
textBox = new LabelledTextBox
|
||||
{
|
||||
Label = "Time"
|
||||
Label = "Time",
|
||||
SelectAllOnFocus = true,
|
||||
},
|
||||
button = new RoundedButton
|
||||
{
|
||||
|
@ -75,6 +75,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
textBox = new LabelledTextBox
|
||||
{
|
||||
Label = labelText,
|
||||
SelectAllOnFocus = true,
|
||||
},
|
||||
slider = new SettingsSlider<T>
|
||||
{
|
||||
|
@ -79,6 +79,7 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
public BPMTextBox()
|
||||
{
|
||||
Label = "BPM";
|
||||
SelectAllOnFocus = true;
|
||||
|
||||
OnCommit += (_, isNew) =>
|
||||
{
|
||||
|
@ -17,13 +17,10 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
public partial class ScreenBackButton : ShearedButton
|
||||
{
|
||||
// todo: see https://github.com/ppy/osu-framework/issues/3271
|
||||
private const float torus_scale_factor = 1.2f;
|
||||
|
||||
public const float BUTTON_WIDTH = 240;
|
||||
|
||||
public ScreenBackButton()
|
||||
: base(BUTTON_WIDTH, 70)
|
||||
: base(BUTTON_WIDTH)
|
||||
{
|
||||
}
|
||||
|
||||
@ -42,14 +39,14 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Size = new Vector2(20f),
|
||||
Size = new Vector2(17f),
|
||||
Icon = FontAwesome.Solid.ChevronLeft,
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.TorusAlternate.With(size: 20 * torus_scale_factor),
|
||||
Font = OsuFont.TorusAlternate.With(size: 17),
|
||||
Text = CommonStrings.Back,
|
||||
UseFullGlyphHeight = false,
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK;
|
||||
|
||||
@ -24,11 +25,11 @@ namespace osu.Game.Screens.Footer
|
||||
private const int padding = 60;
|
||||
private const float delay_per_button = 30;
|
||||
|
||||
public const int HEIGHT = 60;
|
||||
public const int HEIGHT = 50;
|
||||
|
||||
private readonly List<OverlayContainer> overlays = new List<OverlayContainer>();
|
||||
|
||||
private ScreenBackButton backButton = null!;
|
||||
private Box background = null!;
|
||||
private FillFlowContainer<ScreenFooterButton> buttonsFlow = null!;
|
||||
private Container<ScreenFooterButton> removedButtonsContainer = null!;
|
||||
private LogoTrackingContainer logoTrackingContainer = null!;
|
||||
@ -36,6 +37,8 @@ namespace osu.Game.Screens.Footer
|
||||
[Cached]
|
||||
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Aquamarine);
|
||||
|
||||
public ScreenBackButton BackButton { get; private set; } = null!;
|
||||
|
||||
public Action? OnBack;
|
||||
|
||||
public ScreenFooter(BackReceptor? receptor = null)
|
||||
@ -48,7 +51,7 @@ namespace osu.Game.Screens.Footer
|
||||
if (receptor == null)
|
||||
Add(receptor = new BackReceptor());
|
||||
|
||||
receptor.OnBackPressed = () => backButton.TriggerClick();
|
||||
receptor.OnBackPressed = () => BackButton.TriggerClick();
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -56,7 +59,7 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
background = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background5
|
||||
@ -71,12 +74,12 @@ namespace osu.Game.Screens.Footer
|
||||
Spacing = new Vector2(7, 0),
|
||||
AutoSizeAxes = Axes.Both
|
||||
},
|
||||
backButton = new ScreenBackButton
|
||||
BackButton = new ScreenBackButton
|
||||
{
|
||||
Margin = new MarginPadding { Bottom = 10f, Left = 12f },
|
||||
Margin = new MarginPadding { Bottom = 15f, Left = 12f },
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Action = () => OnBack?.Invoke(),
|
||||
Action = onBackPressed,
|
||||
},
|
||||
removedButtonsContainer = new Container<ScreenFooterButton>
|
||||
{
|
||||
@ -115,8 +118,11 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
public void SetButtons(IReadOnlyList<ScreenFooterButton> buttons)
|
||||
{
|
||||
temporarilyHiddenButtons.Clear();
|
||||
overlays.Clear();
|
||||
|
||||
ClearActiveOverlayContainer();
|
||||
|
||||
var oldButtons = buttonsFlow.ToArray();
|
||||
|
||||
for (int i = 0; i < oldButtons.Length; i++)
|
||||
@ -127,9 +133,9 @@ namespace osu.Game.Screens.Footer
|
||||
removedButtonsContainer.Add(oldButton);
|
||||
|
||||
if (buttons.Count > 0)
|
||||
makeButtonDisappearToRightAndExpire(oldButton, i, oldButtons.Length);
|
||||
makeButtonDisappearToRight(oldButton, i, oldButtons.Length, true);
|
||||
else
|
||||
makeButtonDisappearToBottomAndExpire(oldButton, i, oldButtons.Length);
|
||||
makeButtonDisappearToBottom(oldButton, i, oldButtons.Length, true);
|
||||
}
|
||||
|
||||
for (int i = 0; i < buttons.Count; i++)
|
||||
@ -158,27 +164,120 @@ namespace osu.Game.Screens.Footer
|
||||
}
|
||||
}
|
||||
|
||||
private ShearedOverlayContainer? activeOverlay;
|
||||
private Container? contentContainer;
|
||||
private readonly List<ScreenFooterButton> temporarilyHiddenButtons = new List<ScreenFooterButton>();
|
||||
|
||||
public void SetActiveOverlayContainer(ShearedOverlayContainer overlay)
|
||||
{
|
||||
if (contentContainer != null)
|
||||
{
|
||||
throw new InvalidOperationException(@"Cannot set overlay content while one is already present. " +
|
||||
$@"The previous overlay whose content is {contentContainer.Child.GetType().Name} should be hidden first.");
|
||||
}
|
||||
|
||||
activeOverlay = overlay;
|
||||
|
||||
Debug.Assert(temporarilyHiddenButtons.Count == 0);
|
||||
|
||||
var targetButton = buttonsFlow.SingleOrDefault(b => b.Overlay == overlay);
|
||||
|
||||
temporarilyHiddenButtons.AddRange(targetButton != null
|
||||
? buttonsFlow.SkipWhile(b => b != targetButton).Skip(1)
|
||||
: buttonsFlow);
|
||||
|
||||
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
|
||||
makeButtonDisappearToBottom(temporarilyHiddenButtons[i], 0, 0, false);
|
||||
|
||||
var fallbackPosition = buttonsFlow.Any()
|
||||
? buttonsFlow.ToSpaceOfOtherDrawable(Vector2.Zero, this)
|
||||
: BackButton.ToSpaceOfOtherDrawable(BackButton.LayoutRectangle.TopRight + new Vector2(5f, 0f), this);
|
||||
|
||||
var targetPosition = targetButton?.ToSpaceOfOtherDrawable(targetButton.LayoutRectangle.TopRight, this) ?? fallbackPosition;
|
||||
|
||||
updateColourScheme(overlay.ColourProvider.ColourScheme);
|
||||
|
||||
var content = overlay.CreateFooterContent();
|
||||
|
||||
Add(contentContainer = new Container
|
||||
{
|
||||
Y = -15f,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding { Left = targetPosition.X },
|
||||
Child = content,
|
||||
});
|
||||
|
||||
if (temporarilyHiddenButtons.Count > 0)
|
||||
this.Delay(60).Schedule(() => content.Show());
|
||||
else
|
||||
content.Show();
|
||||
}
|
||||
|
||||
public void ClearActiveOverlayContainer()
|
||||
{
|
||||
if (contentContainer == null)
|
||||
return;
|
||||
|
||||
contentContainer.Child.Hide();
|
||||
|
||||
double timeUntilRun = contentContainer.Child.LatestTransformEndTime - Time.Current;
|
||||
|
||||
Container expireTarget = contentContainer;
|
||||
contentContainer = null;
|
||||
activeOverlay = null;
|
||||
|
||||
for (int i = 0; i < temporarilyHiddenButtons.Count; i++)
|
||||
makeButtonAppearFromBottom(temporarilyHiddenButtons[i], 0);
|
||||
|
||||
temporarilyHiddenButtons.Clear();
|
||||
|
||||
expireTarget.Delay(timeUntilRun).Expire();
|
||||
|
||||
updateColourScheme(OverlayColourScheme.Aquamarine);
|
||||
}
|
||||
|
||||
private void updateColourScheme(OverlayColourScheme colourScheme)
|
||||
{
|
||||
colourProvider.ChangeColourScheme(colourScheme);
|
||||
|
||||
background.FadeColour(colourProvider.Background5, 150, Easing.OutQuint);
|
||||
|
||||
foreach (var button in buttonsFlow)
|
||||
button.UpdateDisplay();
|
||||
}
|
||||
|
||||
private void makeButtonAppearFromLeft(ScreenFooterButton button, int index, int count, float startDelay)
|
||||
=> button.AppearFromLeft(startDelay + (count - index) * delay_per_button);
|
||||
|
||||
private void makeButtonAppearFromBottom(ScreenFooterButton button, int index)
|
||||
=> button.AppearFromBottom(index * delay_per_button);
|
||||
|
||||
private void makeButtonDisappearToRightAndExpire(ScreenFooterButton button, int index, int count)
|
||||
=> button.DisappearToRightAndExpire((count - index) * delay_per_button);
|
||||
private void makeButtonDisappearToRight(ScreenFooterButton button, int index, int count, bool expire)
|
||||
=> button.DisappearToRight((count - index) * delay_per_button, expire);
|
||||
|
||||
private void makeButtonDisappearToBottomAndExpire(ScreenFooterButton button, int index, int count)
|
||||
=> button.DisappearToBottomAndExpire((count - index) * delay_per_button);
|
||||
private void makeButtonDisappearToBottom(ScreenFooterButton button, int index, int count, bool expire)
|
||||
=> button.DisappearToBottom((count - index) * delay_per_button, expire);
|
||||
|
||||
private void showOverlay(OverlayContainer overlay)
|
||||
{
|
||||
foreach (var o in overlays)
|
||||
foreach (var o in overlays.Where(o => o != overlay))
|
||||
o.Hide();
|
||||
|
||||
overlay.ToggleVisibility();
|
||||
}
|
||||
|
||||
private void onBackPressed()
|
||||
{
|
||||
if (activeOverlay != null)
|
||||
{
|
||||
if (o == overlay)
|
||||
o.ToggleVisibility();
|
||||
else
|
||||
o.Hide();
|
||||
if (activeOverlay.OnBackButton())
|
||||
return;
|
||||
|
||||
activeOverlay.Hide();
|
||||
return;
|
||||
}
|
||||
|
||||
OnBack?.Invoke();
|
||||
}
|
||||
|
||||
public partial class BackReceptor : Drawable, IKeyBindingHandler<GlobalAction>
|
||||
|
@ -28,8 +28,8 @@ namespace osu.Game.Screens.Footer
|
||||
private const float shear = OsuGame.SHEAR;
|
||||
|
||||
protected const int CORNER_RADIUS = 10;
|
||||
protected const int BUTTON_HEIGHT = 90;
|
||||
protected const int BUTTON_WIDTH = 140;
|
||||
protected const int BUTTON_HEIGHT = 75;
|
||||
protected const int BUTTON_WIDTH = 116;
|
||||
|
||||
public Bindable<Visibility> OverlayState = new Bindable<Visibility>();
|
||||
|
||||
@ -40,7 +40,7 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
private Colour4 buttonAccentColour;
|
||||
|
||||
protected Colour4 AccentColour
|
||||
public Colour4 AccentColour
|
||||
{
|
||||
set
|
||||
{
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Screens.Footer
|
||||
}
|
||||
}
|
||||
|
||||
protected IconUsage Icon
|
||||
public IconUsage Icon
|
||||
{
|
||||
set => icon.Icon = value;
|
||||
}
|
||||
@ -116,19 +116,18 @@ namespace osu.Game.Screens.Footer
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
Y = 42,
|
||||
Y = 35,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Child = text = new OsuSpriteText
|
||||
{
|
||||
// figma design says the size is 16, but due to the issues with font sizes 19 matches better
|
||||
Font = OsuFont.TorusAlternate.With(size: 19),
|
||||
Font = OsuFont.TorusAlternate.With(size: 16),
|
||||
AlwaysPresent = true
|
||||
}
|
||||
},
|
||||
icon = new SpriteIcon
|
||||
{
|
||||
Y = 12,
|
||||
Size = new Vector2(20),
|
||||
Y = 10,
|
||||
Size = new Vector2(16),
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre
|
||||
},
|
||||
@ -140,7 +139,7 @@ namespace osu.Game.Screens.Footer
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.Centre,
|
||||
Y = -CORNER_RADIUS,
|
||||
Size = new Vector2(120, 6),
|
||||
Size = new Vector2(100, 5),
|
||||
Masking = true,
|
||||
CornerRadius = 3,
|
||||
Child = bar = new Box
|
||||
@ -167,12 +166,15 @@ namespace osu.Game.Screens.Footer
|
||||
if (Overlay != null)
|
||||
OverlayState.BindTo(Overlay.State);
|
||||
|
||||
OverlayState.BindValueChanged(_ => updateDisplay());
|
||||
Enabled.BindValueChanged(_ => updateDisplay(), true);
|
||||
OverlayState.BindValueChanged(_ => UpdateDisplay());
|
||||
Enabled.BindValueChanged(_ => UpdateDisplay(), true);
|
||||
|
||||
FinishTransforms(true);
|
||||
}
|
||||
|
||||
// use Content for tracking input as some buttons might be temporarily hidden with DisappearToBottom, and they become hidden by moving Content away from screen.
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Content.ReceivePositionalInputAt(screenSpacePos);
|
||||
|
||||
public GlobalAction? Hotkey;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
@ -187,11 +189,11 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateDisplay();
|
||||
UpdateDisplay();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e) => updateDisplay();
|
||||
protected override void OnHoverLost(HoverLostEvent e) => UpdateDisplay();
|
||||
|
||||
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
|
||||
{
|
||||
@ -203,7 +205,7 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
public virtual void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) { }
|
||||
|
||||
private void updateDisplay()
|
||||
public void UpdateDisplay()
|
||||
{
|
||||
Color4 backgroundColour = OverlayState.Value == Visibility.Visible ? buttonAccentColour : colourProvider.Background3;
|
||||
Color4 textColour = OverlayState.Value == Visibility.Visible ? colourProvider.Background6 : colourProvider.Content1;
|
||||
@ -228,6 +230,7 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
public void AppearFromLeft(double delay)
|
||||
{
|
||||
Content.FinishTransforms();
|
||||
Content.MoveToX(-300f)
|
||||
.FadeOut()
|
||||
.Delay(delay)
|
||||
@ -237,6 +240,7 @@ namespace osu.Game.Screens.Footer
|
||||
|
||||
public void AppearFromBottom(double delay)
|
||||
{
|
||||
Content.FinishTransforms();
|
||||
Content.MoveToY(100f)
|
||||
.FadeOut()
|
||||
.Delay(delay)
|
||||
@ -244,22 +248,26 @@ namespace osu.Game.Screens.Footer
|
||||
.FadeIn(240, Easing.OutCubic);
|
||||
}
|
||||
|
||||
public void DisappearToRightAndExpire(double delay)
|
||||
public void DisappearToRight(double delay, bool expire)
|
||||
{
|
||||
Content.FinishTransforms();
|
||||
Content.Delay(delay)
|
||||
.FadeOut(240, Easing.InOutCubic)
|
||||
.MoveToX(300f, 360, Easing.InOutCubic);
|
||||
|
||||
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
|
||||
if (expire)
|
||||
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
|
||||
}
|
||||
|
||||
public void DisappearToBottomAndExpire(double delay)
|
||||
public void DisappearToBottom(double delay, bool expire)
|
||||
{
|
||||
Content.FinishTransforms();
|
||||
Content.Delay(delay)
|
||||
.FadeOut(240, Easing.InOutCubic)
|
||||
.MoveToY(100f, 240, Easing.InOutCubic);
|
||||
|
||||
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
|
||||
if (expire)
|
||||
this.Delay(Content.LatestTransformEndTime - Time.Current).Expire();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Localisation;
|
||||
@ -37,9 +38,14 @@ namespace osu.Game.Screens.Menu
|
||||
{
|
||||
string text = "There are currently some background operations which will be aborted if you continue:\n\n";
|
||||
|
||||
foreach (var n in notifications.OngoingOperations)
|
||||
var ongoingOperations = notifications.OngoingOperations.ToArray();
|
||||
|
||||
foreach (var n in ongoingOperations.Take(10))
|
||||
text += $"{n.Text} ({n.Progress:0%})\n";
|
||||
|
||||
if (ongoingOperations.Length > 10)
|
||||
text += $"\nand {ongoingOperations.Length - 10} other operation(s).\n";
|
||||
|
||||
text += "\nLast chance to turn back";
|
||||
|
||||
BodyText = text;
|
||||
|
@ -9,7 +9,6 @@ using osu.Framework.Audio;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Extensions.LocalisationExtensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
@ -50,7 +49,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
private readonly Bindable<IReadOnlyList<Mod>> userMods = new Bindable<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
|
||||
private OnlinePlayScreenWaveContainer waves = null!;
|
||||
private MatchLeaderboard leaderboard = null!;
|
||||
private DailyChallengeLeaderboard leaderboard = null!;
|
||||
private RoomModSelectOverlay userModsSelectOverlay = null!;
|
||||
private Sample? sampleStart;
|
||||
private IDisposable? userModsSelectOverlayRegistration;
|
||||
@ -161,7 +160,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"3e3a44") // Temporary.
|
||||
Colour = colourProvider.Background4,
|
||||
},
|
||||
new GridContainer
|
||||
{
|
||||
@ -209,28 +208,17 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
feed = new DailyChallengeEventFeed
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
PresentScore = presentScore
|
||||
}
|
||||
],
|
||||
},
|
||||
},
|
||||
null,
|
||||
// Middle column (leaderboard)
|
||||
new GridContainer
|
||||
leaderboard = new DailyChallengeLeaderboard(room, playlistItem)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new SectionHeader("Leaderboard")
|
||||
},
|
||||
[leaderboard = new MatchLeaderboard { RelativeSizeAxes = Axes.Both }],
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
}
|
||||
PresentScore = presentScore,
|
||||
},
|
||||
// Spacer
|
||||
null,
|
||||
@ -272,7 +260,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Color4Extensions.FromHex(@"28242d") // Temporary.
|
||||
Colour = colourProvider.Background5,
|
||||
},
|
||||
footerButtons = new FillFlowContainer
|
||||
{
|
||||
@ -326,6 +314,12 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
metadataClient.MultiplayerRoomScoreSet += onRoomScoreSet;
|
||||
}
|
||||
|
||||
private void presentScore(long id)
|
||||
{
|
||||
if (this.IsCurrentScreen())
|
||||
this.Push(new PlaylistItemScoreResultsScreen(room.RoomID.Value!.Value, playlistItem, id));
|
||||
}
|
||||
|
||||
private void onRoomScoreSet(MultiplayerRoomScoreSetEvent e)
|
||||
{
|
||||
if (e.RoomID != room.RoomID.Value || e.PlaylistItemID != playlistItem.ID)
|
||||
@ -347,6 +341,9 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
breakdown.AddNewScore(ev);
|
||||
feed.AddNewScore(ev);
|
||||
|
||||
if (e.NewRank <= 50)
|
||||
Schedule(() => leaderboard.RefetchScores());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
@ -19,6 +20,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
private DailyChallengeEventFeedFlow flow = null!;
|
||||
|
||||
public Action<long>? PresentScore { get; init; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -48,6 +51,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
PresentScore = PresentScore,
|
||||
};
|
||||
flow.Add(row);
|
||||
row.Delay(15000).Then().FadeOut(300, Easing.OutQuint).Expire();
|
||||
@ -78,6 +82,8 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
private readonly NewScoreEvent newScore;
|
||||
|
||||
public Action<long>? PresentScore { get; init; }
|
||||
|
||||
public NewScoreEventRow(NewScoreEvent newScore)
|
||||
{
|
||||
this.newScore = newScore;
|
||||
@ -115,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
|
||||
text.AddUserLink(newScore.User);
|
||||
text.AddText(" got ");
|
||||
text.AddLink($"{newScore.TotalScore:N0} points", () => { }); // TODO: present the score here
|
||||
text.AddLink($"{newScore.TotalScore:N0} points", () => PresentScore?.Invoke(newScore.ScoreID));
|
||||
|
||||
if (newScore.NewRank != null)
|
||||
text.AddText($" and achieved rank #{newScore.NewRank.Value:N0}");
|
||||
|
@ -0,0 +1,175 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.SelectV2.Leaderboards;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.DailyChallenge
|
||||
{
|
||||
public partial class DailyChallengeLeaderboard : CompositeDrawable
|
||||
{
|
||||
public Action<long>? PresentScore { get; init; }
|
||||
|
||||
private readonly Room room;
|
||||
private readonly PlaylistItem playlistItem;
|
||||
|
||||
private FillFlowContainer<LeaderboardScoreV2> scoreFlow = null!;
|
||||
private Container userBestContainer = null!;
|
||||
private SectionHeader userBestHeader = null!;
|
||||
private LoadingLayer loadingLayer = null!;
|
||||
|
||||
private CancellationTokenSource? cancellationTokenSource;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private IBindable<WorkingBeatmap> beatmap { get; set; } = null!;
|
||||
|
||||
public DailyChallengeLeaderboard(Room room, PlaylistItem playlistItem)
|
||||
{
|
||||
this.room = room;
|
||||
this.playlistItem = playlistItem;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
RowDimensions =
|
||||
[
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.AutoSize),
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
],
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[] { new SectionHeader("Leaderboard") },
|
||||
new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = scoreFlow = new FillFlowContainer<LeaderboardScoreV2>
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Right = 20, },
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(5),
|
||||
Scale = new Vector2(0.8f),
|
||||
Width = 1 / 0.8f,
|
||||
}
|
||||
},
|
||||
loadingLayer = new LoadingLayer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
new Drawable[] { userBestHeader = new SectionHeader("Personal best") { Alpha = 0, } },
|
||||
new Drawable[]
|
||||
{
|
||||
userBestContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Padding = new MarginPadding { Right = 20, },
|
||||
Scale = new Vector2(0.8f),
|
||||
Width = 1 / 0.8f,
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
RefetchScores();
|
||||
}
|
||||
|
||||
public void RefetchScores()
|
||||
{
|
||||
var request = new IndexPlaylistScoresRequest(room.RoomID.Value!.Value, playlistItem.ID);
|
||||
|
||||
request.Success += req =>
|
||||
{
|
||||
var best = req.Scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo)).ToArray();
|
||||
var userBest = req.UserScore?.CreateScoreInfo(scoreManager, rulesets, playlistItem, beatmap.Value.BeatmapInfo);
|
||||
|
||||
cancellationTokenSource?.Cancel();
|
||||
cancellationTokenSource = null;
|
||||
cancellationTokenSource ??= new CancellationTokenSource();
|
||||
|
||||
if (best.Length == 0)
|
||||
{
|
||||
scoreFlow.Clear();
|
||||
loadingLayer.Hide();
|
||||
}
|
||||
else
|
||||
{
|
||||
LoadComponentsAsync(best.Select(s => new LeaderboardScoreV2(s, sheared: false)
|
||||
{
|
||||
Rank = s.Position,
|
||||
IsPersonalBest = s.UserID == api.LocalUser.Value.Id,
|
||||
Action = () => PresentScore?.Invoke(s.OnlineID),
|
||||
}), loaded =>
|
||||
{
|
||||
scoreFlow.Clear();
|
||||
scoreFlow.AddRange(loaded);
|
||||
scoreFlow.FadeTo(1, 400, Easing.OutQuint);
|
||||
loadingLayer.Hide();
|
||||
}, cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
userBestContainer.Clear();
|
||||
|
||||
if (userBest != null)
|
||||
{
|
||||
userBestContainer.Add(new LeaderboardScoreV2(userBest, sheared: false)
|
||||
{
|
||||
Rank = userBest.Position,
|
||||
IsPersonalBest = true,
|
||||
Action = () => PresentScore?.Invoke(userBest.OnlineID),
|
||||
});
|
||||
}
|
||||
|
||||
userBestHeader.FadeTo(userBest == null ? 0 : 1);
|
||||
};
|
||||
|
||||
loadingLayer.Show();
|
||||
scoreFlow.FadeTo(0.5f, 400, Easing.OutQuint);
|
||||
api.Queue(request);
|
||||
}
|
||||
}
|
||||
}
|
@ -80,6 +80,9 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
[Resolved(canBeNull: true)]
|
||||
protected OnlinePlayScreen ParentScreen { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private PreviewTrackManager previewTrackManager { get; set; } = null!;
|
||||
|
||||
[Cached]
|
||||
private readonly OnlinePlayBeatmapAvailabilityTracker beatmapAvailabilityTracker = new OnlinePlayBeatmapAvailabilityTracker();
|
||||
|
||||
@ -483,6 +486,8 @@ namespace osu.Game.Screens.OnlinePlay.Match
|
||||
{
|
||||
UserModsSelectOverlay.Hide();
|
||||
endHandlingTrack();
|
||||
|
||||
previewTrackManager.StopAnyPlaying(this);
|
||||
}
|
||||
|
||||
private void endHandlingTrack()
|
||||
|
@ -7,7 +7,7 @@ using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Multiplayer
|
||||
{
|
||||
public partial class MultiplayerResultsScreen : PlaylistsResultsScreen
|
||||
public partial class MultiplayerResultsScreen : PlaylistItemUserResultsScreen
|
||||
{
|
||||
public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem)
|
||||
: base(score, roomId, playlistItem)
|
||||
|
@ -17,10 +17,10 @@ using osu.Game.Screens.Ranking;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
public partial class PlaylistsResultsScreen : ResultsScreen
|
||||
public abstract partial class PlaylistItemResultsScreen : ResultsScreen
|
||||
{
|
||||
private readonly long roomId;
|
||||
private readonly PlaylistItem playlistItem;
|
||||
protected readonly long RoomId;
|
||||
protected readonly PlaylistItem PlaylistItem;
|
||||
|
||||
protected LoadingSpinner LeftSpinner { get; private set; } = null!;
|
||||
protected LoadingSpinner CentreSpinner { get; private set; } = null!;
|
||||
@ -30,19 +30,19 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
private MultiplayerScores? lowerScores;
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
protected IAPIProvider API { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private ScoreManager scoreManager { get; set; } = null!;
|
||||
protected ScoreManager ScoreManager { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesets { get; set; } = null!;
|
||||
protected RulesetStore Rulesets { get; private set; } = null!;
|
||||
|
||||
public PlaylistsResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
|
||||
protected PlaylistItemResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
|
||||
: base(score)
|
||||
{
|
||||
this.roomId = roomId;
|
||||
this.playlistItem = playlistItem;
|
||||
RoomId = roomId;
|
||||
PlaylistItem = playlistItem;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@ -74,13 +74,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
});
|
||||
}
|
||||
|
||||
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
protected abstract APIRequest<MultiplayerScore> CreateScoreRequest();
|
||||
|
||||
protected sealed override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
|
||||
{
|
||||
// This performs two requests:
|
||||
// 1. A request to show the user's score (and scores around).
|
||||
// 1. A request to show the relevant score (and scores around).
|
||||
// 2. If that fails, a request to index the room starting from the highest score.
|
||||
|
||||
var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id);
|
||||
var userScoreReq = CreateScoreRequest();
|
||||
|
||||
userScoreReq.Success += userScore =>
|
||||
{
|
||||
@ -111,11 +113,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
setPositions(lowerScores, userScore.Position.Value, 1);
|
||||
}
|
||||
|
||||
performSuccessCallback(scoresCallback, allScores);
|
||||
Schedule(() =>
|
||||
{
|
||||
PerformSuccessCallback(scoresCallback, allScores);
|
||||
hideLoadingSpinners();
|
||||
});
|
||||
};
|
||||
|
||||
// On failure, fallback to a normal index.
|
||||
userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback));
|
||||
userScoreReq.Failure += _ => API.Queue(createIndexRequest(scoresCallback));
|
||||
|
||||
return userScoreReq;
|
||||
}
|
||||
@ -147,8 +153,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
private APIRequest createIndexRequest(Action<IEnumerable<ScoreInfo>> scoresCallback, MultiplayerScores? pivot = null)
|
||||
{
|
||||
var indexReq = pivot != null
|
||||
? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params)
|
||||
: new IndexPlaylistScoresRequest(roomId, playlistItem.ID);
|
||||
? new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID, pivot.Cursor, pivot.Params)
|
||||
: new IndexPlaylistScoresRequest(RoomId, PlaylistItem.ID);
|
||||
|
||||
indexReq.Success += r =>
|
||||
{
|
||||
@ -163,7 +169,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
setPositions(r, pivot, -1);
|
||||
}
|
||||
|
||||
performSuccessCallback(scoresCallback, r.Scores, r);
|
||||
Schedule(() =>
|
||||
{
|
||||
PerformSuccessCallback(scoresCallback, r.Scores, r);
|
||||
hideLoadingSpinners(r);
|
||||
});
|
||||
};
|
||||
|
||||
indexReq.Failure += _ => hideLoadingSpinners(pivot);
|
||||
@ -177,26 +187,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
/// <param name="callback">The callback to invoke with the final <see cref="ScoreInfo"/>s.</param>
|
||||
/// <param name="scores">The <see cref="MultiplayerScore"/>s that were retrieved from <see cref="APIRequest"/>s.</param>
|
||||
/// <param name="pivot">An optional pivot around which the scores were retrieved.</param>
|
||||
private void performSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null) => Schedule(() =>
|
||||
protected virtual ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
|
||||
{
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(scoreManager, rulesets, playlistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
|
||||
|
||||
// Select a score if we don't already have one selected.
|
||||
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
|
||||
if (SelectedScore.Value == null)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
// Invoke callback to add the scores.
|
||||
callback.Invoke(scoreInfos);
|
||||
|
||||
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
|
||||
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
|
||||
|
||||
hideLoadingSpinners(pivot);
|
||||
});
|
||||
return scoreInfos;
|
||||
}
|
||||
|
||||
private void hideLoadingSpinners(MultiplayerScores? pivot = null)
|
||||
{
|
@ -0,0 +1,37 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows a selected arbitrary score for a playlist item, with scores around included.
|
||||
/// </summary>
|
||||
public partial class PlaylistItemScoreResultsScreen : PlaylistItemResultsScreen
|
||||
{
|
||||
private readonly long scoreId;
|
||||
|
||||
public PlaylistItemScoreResultsScreen(long roomId, PlaylistItem playlistItem, long scoreId)
|
||||
: base(null, roomId, playlistItem)
|
||||
{
|
||||
this.scoreId = scoreId;
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistScoreRequest(RoomId, PlaylistItem.ID, scoreId);
|
||||
|
||||
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
|
||||
{
|
||||
var scoreInfos = base.PerformSuccessCallback(callback, scores, pivot);
|
||||
|
||||
Schedule(() => SelectedScore.Value = scoreInfos.SingleOrDefault(score => score.OnlineID == scoreId));
|
||||
|
||||
return scoreInfos;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
// 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.Linq;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Scoring;
|
||||
|
||||
namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the user's best score for a given playlist item, with scores around included.
|
||||
/// </summary>
|
||||
public partial class PlaylistItemUserResultsScreen : PlaylistItemResultsScreen
|
||||
{
|
||||
public PlaylistItemUserResultsScreen(ScoreInfo? score, long roomId, PlaylistItem playlistItem)
|
||||
: base(score, roomId, playlistItem)
|
||||
{
|
||||
}
|
||||
|
||||
protected override APIRequest<MultiplayerScore> CreateScoreRequest() => new ShowPlaylistUserScoreRequest(RoomId, PlaylistItem.ID, API.LocalUser.Value.Id);
|
||||
|
||||
protected override ScoreInfo[] PerformSuccessCallback(Action<IEnumerable<ScoreInfo>> callback, List<MultiplayerScore> scores, MultiplayerScores? pivot = null)
|
||||
{
|
||||
var scoreInfos = scores.Select(s => s.CreateScoreInfo(ScoreManager, Rulesets, PlaylistItem, Beatmap.Value.BeatmapInfo)).OrderByTotalScore().ToArray();
|
||||
|
||||
// Select a score if we don't already have one selected.
|
||||
// Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll).
|
||||
if (SelectedScore.Value == null)
|
||||
{
|
||||
Schedule(() =>
|
||||
{
|
||||
// Prefer selecting the local user's score, or otherwise default to the first visible score.
|
||||
SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.OnlineID == API.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
|
||||
// Invoke callback to add the scores. Exclude the user's current score which was added previously.
|
||||
callback.Invoke(scoreInfos.Where(s => s.OnlineID != Score?.OnlineID));
|
||||
|
||||
return scoreInfos;
|
||||
}
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
protected override ResultsScreen CreateResults(ScoreInfo score)
|
||||
{
|
||||
Debug.Assert(Room.RoomID.Value != null);
|
||||
return new PlaylistsResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem)
|
||||
return new PlaylistItemUserResultsScreen(score, Room.RoomID.Value.Value, PlaylistItem)
|
||||
{
|
||||
AllowRetry = true,
|
||||
ShowUserStatistics = true,
|
||||
|
@ -114,7 +114,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
|
||||
RequestResults = item =>
|
||||
{
|
||||
Debug.Assert(RoomId.Value != null);
|
||||
ParentScreen?.Push(new PlaylistsResultsScreen(null, RoomId.Value.Value, item));
|
||||
ParentScreen?.Push(new PlaylistItemUserResultsScreen(null, RoomId.Value.Value, item));
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
@ -75,15 +76,13 @@ namespace osu.Game.Screens.Play
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Width = 0,
|
||||
Child = remainingTimeBox = new Container
|
||||
Child = remainingTimeBox = new Circle
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Height = 8,
|
||||
CornerRadius = 4,
|
||||
Masking = true,
|
||||
Child = new Box { RelativeSizeAxes = Axes.Both }
|
||||
}
|
||||
},
|
||||
remainingTimeCounter = new RemainingTimeCounter
|
||||
@ -119,6 +118,13 @@ namespace osu.Game.Screens.Play
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
remainingTimeBox.Height = Math.Min(8, remainingTimeBox.DrawWidth);
|
||||
}
|
||||
|
||||
private void initializeBreaks()
|
||||
{
|
||||
FinishTransforms(true);
|
||||
|
@ -267,7 +267,7 @@ namespace osu.Game.Screens.Select
|
||||
subscriptionBeatmaps = realm.RegisterForNotifications(r => r.All<BeatmapInfo>().Where(b => !b.Hidden), beatmapsChanged);
|
||||
}
|
||||
|
||||
private readonly HashSet<BeatmapSetInfo> setsRequiringUpdate = new HashSet<BeatmapSetInfo>();
|
||||
private readonly HashSet<Guid> setsRequiringUpdate = new HashSet<Guid>();
|
||||
private readonly HashSet<Guid> setsRequiringRemoval = new HashSet<Guid>();
|
||||
|
||||
private void beatmapSetsChanged(IRealmCollection<BeatmapSetInfo> sender, ChangeSet? changes)
|
||||
@ -280,6 +280,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
realmBeatmapSets.Clear();
|
||||
realmBeatmapSets.AddRange(sender.Select(r => r.ID));
|
||||
|
||||
setsRequiringRemoval.Clear();
|
||||
setsRequiringUpdate.Clear();
|
||||
|
||||
@ -289,18 +290,26 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
foreach (int i in changes.DeletedIndices.OrderDescending())
|
||||
{
|
||||
setsRequiringRemoval.Add(realmBeatmapSets[i]);
|
||||
Guid id = realmBeatmapSets[i];
|
||||
|
||||
setsRequiringRemoval.Add(id);
|
||||
setsRequiringUpdate.Remove(id);
|
||||
|
||||
realmBeatmapSets.RemoveAt(i);
|
||||
}
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
{
|
||||
realmBeatmapSets.Insert(i, sender[i].ID);
|
||||
setsRequiringUpdate.Add(sender[i].Detach());
|
||||
Guid id = sender[i].ID;
|
||||
|
||||
setsRequiringRemoval.Remove(id);
|
||||
setsRequiringUpdate.Add(id);
|
||||
|
||||
realmBeatmapSets.Insert(i, id);
|
||||
}
|
||||
|
||||
foreach (int i in changes.NewModifiedIndices)
|
||||
setsRequiringUpdate.Add(sender[i].Detach());
|
||||
setsRequiringUpdate.Add(sender[i].ID);
|
||||
}
|
||||
|
||||
Scheduler.AddOnce(processBeatmapChanges);
|
||||
@ -316,7 +325,7 @@ namespace osu.Game.Screens.Select
|
||||
{
|
||||
foreach (var set in setsRequiringRemoval) removeBeatmapSet(set);
|
||||
|
||||
foreach (var set in setsRequiringUpdate) updateBeatmapSet(set);
|
||||
foreach (var set in setsRequiringUpdate) updateBeatmapSet(fetchFromID(set)!);
|
||||
|
||||
if (setsRequiringRemoval.Count > 0 && SelectedBeatmapInfo != null)
|
||||
{
|
||||
@ -326,7 +335,7 @@ namespace osu.Game.Screens.Select
|
||||
// To handle the beatmap update flow, attempt to track selection changes across delete-insert transactions.
|
||||
// When an update occurs, the previous beatmap set is either soft or hard deleted.
|
||||
// Check if the current selection was potentially deleted by re-querying its validity.
|
||||
bool selectedSetMarkedDeleted = realm.Run(r => r.Find<BeatmapSetInfo>(SelectedBeatmapSet.ID)?.DeletePending != false);
|
||||
bool selectedSetMarkedDeleted = fetchFromID(SelectedBeatmapSet.ID)?.DeletePending != false;
|
||||
|
||||
if (selectedSetMarkedDeleted && setsRequiringUpdate.Any())
|
||||
{
|
||||
@ -334,7 +343,7 @@ namespace osu.Game.Screens.Select
|
||||
// This relies on the full update operation being in a single transaction, so please don't change that.
|
||||
foreach (var set in setsRequiringUpdate)
|
||||
{
|
||||
foreach (var beatmapInfo in set.Beatmaps)
|
||||
foreach (var beatmapInfo in fetchFromID(set)!.Beatmaps)
|
||||
{
|
||||
if (!((IBeatmapMetadataInfo)beatmapInfo.Metadata).Equals(SelectedBeatmapInfo.Metadata)) continue;
|
||||
|
||||
@ -349,7 +358,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
// If a direct selection couldn't be made, it's feasible that the difficulty name (or beatmap metadata) changed.
|
||||
// Let's attempt to follow set-level selection anyway.
|
||||
SelectBeatmap(setsRequiringUpdate.First().Beatmaps.First());
|
||||
SelectBeatmap(fetchFromID(setsRequiringUpdate.First())!.Beatmaps.First());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -361,6 +370,8 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
setsRequiringRemoval.Clear();
|
||||
setsRequiringUpdate.Clear();
|
||||
|
||||
BeatmapSetInfo? fetchFromID(Guid id) => realm.Realm.Find<BeatmapSetInfo>(id);
|
||||
}
|
||||
|
||||
private void beatmapsChanged(IRealmCollection<BeatmapInfo> sender, ChangeSet? changes)
|
||||
@ -417,16 +428,23 @@ namespace osu.Game.Screens.Select
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet) => Schedule(() =>
|
||||
public void UpdateBeatmapSet(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
updateBeatmapSet(beatmapSet);
|
||||
invalidateAfterChange();
|
||||
});
|
||||
beatmapSet = beatmapSet.Detach();
|
||||
|
||||
Schedule(() =>
|
||||
{
|
||||
updateBeatmapSet(beatmapSet);
|
||||
invalidateAfterChange();
|
||||
});
|
||||
}
|
||||
|
||||
private void updateBeatmapSet(BeatmapSetInfo beatmapSet)
|
||||
{
|
||||
beatmapSet = beatmapSet.Detach();
|
||||
|
||||
originalBeatmapSetsDetached.RemoveAll(set => set.ID == beatmapSet.ID);
|
||||
originalBeatmapSetsDetached.Add(beatmapSet.Detach());
|
||||
originalBeatmapSetsDetached.Add(beatmapSet);
|
||||
|
||||
var newSets = new List<CarouselBeatmapSet>();
|
||||
|
||||
|
@ -32,9 +32,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
{
|
||||
public partial class ScreenFooterButtonMods : ScreenFooterButton, IHasCurrentValue<IReadOnlyList<Mod>>
|
||||
{
|
||||
// todo: see https://github.com/ppy/osu-framework/issues/3271
|
||||
private const float torus_scale_factor = 1.2f;
|
||||
private const float bar_height = 37f;
|
||||
private const float bar_height = 30f;
|
||||
private const float mod_display_portion = 0.65f;
|
||||
|
||||
private readonly BindableWithCurrent<IReadOnlyList<Mod>> current = new BindableWithCurrent<IReadOnlyList<Mod>>(Array.Empty<Mod>());
|
||||
@ -112,7 +110,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
Origin = Anchor.Centre,
|
||||
Shear = -BUTTON_SHEAR,
|
||||
UseFullGlyphHeight = false,
|
||||
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold)
|
||||
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold)
|
||||
}
|
||||
},
|
||||
new Container
|
||||
@ -133,7 +131,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = -BUTTON_SHEAR,
|
||||
Scale = new Vector2(0.6f),
|
||||
Scale = new Vector2(0.5f),
|
||||
Current = { BindTarget = Current },
|
||||
ExpansionMode = ExpansionMode.AlwaysContracted,
|
||||
},
|
||||
@ -142,7 +140,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shear = -BUTTON_SHEAR,
|
||||
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
|
||||
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
|
||||
Mods = { BindTarget = Current },
|
||||
}
|
||||
}
|
||||
@ -335,7 +333,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
Text = ModSelectOverlayStrings.Unranked.ToUpper(),
|
||||
Margin = new MarginPadding { Horizontal = 15 },
|
||||
UseFullGlyphHeight = false,
|
||||
Font = OsuFont.Torus.With(size: 14 * torus_scale_factor, weight: FontWeight.Bold),
|
||||
Font = OsuFont.Torus.With(size: 14f, weight: FontWeight.Bold),
|
||||
Colour = Color4.Black,
|
||||
}
|
||||
};
|
||||
|
@ -42,7 +42,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
{
|
||||
randomSpriteText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.TorusAlternate.With(size: 19),
|
||||
Font = OsuFont.TorusAlternate.With(size: 16),
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@ -50,7 +50,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
},
|
||||
rewindSpriteText = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.TorusAlternate.With(size: 19),
|
||||
Font = OsuFont.TorusAlternate.With(size: 16),
|
||||
AlwaysPresent = true,
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@ -75,7 +75,7 @@ namespace osu.Game.Screens.SelectV2.Footer
|
||||
AlwaysPresent = true, // make sure the button is sized large enough to always show this
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.BottomCentre,
|
||||
Font = OsuFont.TorusAlternate.With(size: 19),
|
||||
Font = OsuFont.TorusAlternate.With(size: 16),
|
||||
});
|
||||
|
||||
fallingRewind.FadeOutFromOne(fade_time, Easing.In);
|
||||
|
@ -43,6 +43,9 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
public partial class LeaderboardScoreV2 : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
|
||||
{
|
||||
public int? Rank { get; init; }
|
||||
public bool IsPersonalBest { get; init; }
|
||||
|
||||
private const float expanded_right_content_width = 210;
|
||||
private const float grade_width = 40;
|
||||
private const float username_min_width = 125;
|
||||
@ -52,15 +55,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
private const float rank_label_visibility_width_cutoff = rank_label_width + height + username_min_width + statistics_regular_min_width + expanded_right_content_width;
|
||||
|
||||
private readonly ScoreInfo score;
|
||||
private readonly bool sheared;
|
||||
|
||||
private const int height = 60;
|
||||
private const int corner_radius = 10;
|
||||
private const int transition_duration = 200;
|
||||
|
||||
private readonly int? rank;
|
||||
|
||||
private readonly bool isPersonalBest;
|
||||
|
||||
private Colour4 foregroundColour;
|
||||
private Colour4 backgroundColour;
|
||||
private ColourInfo totalScoreBackgroundGradient;
|
||||
@ -104,13 +104,12 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
public ITooltip<ScoreInfo> GetCustomTooltip() => new LeaderboardScoreTooltip();
|
||||
public virtual ScoreInfo TooltipContent => score;
|
||||
|
||||
public LeaderboardScoreV2(ScoreInfo score, int? rank, bool isPersonalBest = false)
|
||||
public LeaderboardScoreV2(ScoreInfo score, bool sheared = true)
|
||||
{
|
||||
this.score = score;
|
||||
this.rank = rank;
|
||||
this.isPersonalBest = isPersonalBest;
|
||||
this.sheared = sheared;
|
||||
|
||||
Shear = new Vector2(OsuGame.SHEAR, 0);
|
||||
Shear = new Vector2(sheared ? OsuGame.SHEAR : 0, 0);
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = height;
|
||||
}
|
||||
@ -120,8 +119,8 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
var user = score.User;
|
||||
|
||||
foregroundColour = isPersonalBest ? colourProvider.Background1 : colourProvider.Background5;
|
||||
backgroundColour = isPersonalBest ? colourProvider.Background2 : colourProvider.Background4;
|
||||
foregroundColour = IsPersonalBest ? colourProvider.Background1 : colourProvider.Background5;
|
||||
backgroundColour = IsPersonalBest ? colourProvider.Background2 : colourProvider.Background4;
|
||||
totalScoreBackgroundGradient = ColourInfo.GradientHorizontal(backgroundColour.Opacity(0), backgroundColour);
|
||||
|
||||
statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s, score)
|
||||
@ -159,7 +158,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
AutoSizeAxes = Axes.X,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
Child = rankLabel = new RankLabel(rank)
|
||||
Child = rankLabel = new RankLabel(Rank, sheared)
|
||||
{
|
||||
Width = rank_label_width,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
@ -243,7 +242,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
User = score.User,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Colour = ColourInfo.GradientHorizontal(Colour4.White.Opacity(0.5f), Colour4.FromHex(@"222A27").Opacity(1)),
|
||||
@ -274,7 +273,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(1.1f),
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
})
|
||||
{
|
||||
@ -292,7 +291,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = Colour4.Black.Opacity(0.5f),
|
||||
},
|
||||
new RankLabel(rank)
|
||||
new RankLabel(Rank, sheared)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
@ -314,7 +313,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
flagBadgeAndDateContainer = new FillFlowContainer
|
||||
{
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(5),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@ -338,7 +337,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
nameLabel = new TruncatingSpriteText
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Text = user.Username,
|
||||
Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold)
|
||||
}
|
||||
@ -354,7 +353,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
Name = @"Statistics container",
|
||||
Padding = new MarginPadding { Right = 40 },
|
||||
Spacing = new Vector2(25, 0),
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
@ -412,7 +411,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
},
|
||||
RankContainer = new Container
|
||||
{
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
@ -470,7 +469,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
UseFullGlyphHeight = false,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Current = scoreManager.GetBindableTotalScoreString(score),
|
||||
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Light),
|
||||
},
|
||||
@ -478,7 +477,7 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
{
|
||||
Anchor = Anchor.TopRight,
|
||||
Origin = Anchor.TopRight,
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(2f, 0f),
|
||||
@ -656,14 +655,14 @@ namespace osu.Game.Screens.SelectV2.Leaderboards
|
||||
|
||||
private partial class RankLabel : Container, IHasTooltip
|
||||
{
|
||||
public RankLabel(int? rank)
|
||||
public RankLabel(int? rank, bool sheared)
|
||||
{
|
||||
if (rank >= 1000)
|
||||
TooltipText = $"#{rank:N0}";
|
||||
|
||||
Child = new OsuSpriteText
|
||||
{
|
||||
Shear = new Vector2(-OsuGame.SHEAR, 0),
|
||||
Shear = new Vector2(sheared ? -OsuGame.SHEAR : 0, 0),
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Font = OsuFont.GetFont(size: 20, weight: FontWeight.SemiBold, italics: true),
|
||||
|
@ -312,6 +312,8 @@ namespace osu.Game.Skinning
|
||||
public Task<Live<SkinInfo>> ImportAsUpdate(ProgressNotification notification, ImportTask task, SkinInfo original) =>
|
||||
skinImporter.ImportAsUpdate(notification, task, original);
|
||||
|
||||
public Task<ExternalEditOperation<SkinInfo>> BeginExternalEditing(SkinInfo model) => skinImporter.BeginExternalEditing(model);
|
||||
|
||||
public Task<Live<SkinInfo>> Import(ImportTask task, ImportParameters parameters = default, CancellationToken cancellationToken = default) =>
|
||||
skinImporter.Import(task, parameters, cancellationToken);
|
||||
|
||||
|
@ -99,6 +99,54 @@ namespace osu.Game.Tests.Visual.OnlinePlay
|
||||
});
|
||||
return true;
|
||||
|
||||
case IndexPlaylistScoresRequest roomLeaderboardRequest:
|
||||
roomLeaderboardRequest.TriggerSuccess(new IndexedMultiplayerScores
|
||||
{
|
||||
Scores =
|
||||
{
|
||||
new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
Accuracy = 1,
|
||||
Position = 1,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.S,
|
||||
MaxCombo = 1000,
|
||||
TotalScore = 1000000,
|
||||
User = new APIUser { Username = "best user" },
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
},
|
||||
new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
Accuracy = 0.7,
|
||||
Position = 2,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.B,
|
||||
MaxCombo = 100,
|
||||
TotalScore = 200000,
|
||||
User = new APIUser { Username = "worst user" },
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
},
|
||||
},
|
||||
UserScore = new MultiplayerScore
|
||||
{
|
||||
ID = currentScoreId++,
|
||||
Accuracy = 0.91,
|
||||
Position = 4,
|
||||
EndedAt = DateTimeOffset.Now,
|
||||
Passed = true,
|
||||
Rank = ScoreRank.A,
|
||||
MaxCombo = 100,
|
||||
TotalScore = 800000,
|
||||
User = localUser,
|
||||
Statistics = new Dictionary<HitResult, int>()
|
||||
},
|
||||
});
|
||||
return true;
|
||||
|
||||
case PartRoomRequest partRoomRequest:
|
||||
partRoomRequest.TriggerSuccess();
|
||||
return true;
|
||||
|
Loading…
Reference in New Issue
Block a user