1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-11 10:07:52 +08:00

Merge branch 'master' into editor-fix-button-states-after-paste

This commit is contained in:
Dean Herbert 2020-10-09 20:51:09 +09:00 committed by GitHub
commit 681e88af40
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 1112 additions and 405 deletions

View File

@ -52,6 +52,6 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1009.0" />
</ItemGroup>
</Project>

View File

@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
/// <summary>
/// Whether an update has been downloaded but not yet applied.
/// </summary>
private bool updatePending;
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
{
if (updatePending)
{
// the user may have dismissed the completion notice, so show it again.
notificationOverlay.Post(new UpdateCompleteNotification(this));
return true;
}
// no updates available. bail and retry later.
return;
return false;
}
if (notification == null)
{
@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
}
catch (Exception e)
{
@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
return true;
}
protected override void Dispose(bool isDisposing)
@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
private class UpdateCompleteNotification : ProgressCompletionNotification
{
[Resolved]
private OsuGame game { get; set; }
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
{
Text = @"Update ready to install. Click to restart!";
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
};
}
}
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
return new ProgressCompletionNotification
{
Text = @"Update ready to install. Click to restart!",
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
}
};
return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuGame game)
private void load(OsuColour colours)
{
this.game = game;
IconContent.AddRange(new Drawable[]
{
new Box

View File

@ -141,6 +141,35 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.LargeTickHit,
HitResult.SmallTickHit,
HitResult.LargeBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "large droplet";
case HitResult.SmallTickHit:
return "small droplet";
case HitResult.LargeBonus:
return "banana";
}
return base.GetDisplayNameForHitResult(result);
}
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);

View File

@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
RandomZ = snapshot.RandomZ;
}
public override void PostProcess()
{
base.PostProcess();
Objects.Sort();
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
}
public struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>, IComparable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
public int CompareTo(ConvertValue other)
{
var result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
return Column.CompareTo(other.Column);
}
}
}

View File

@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
}
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Perfect,
HitResult.Great,
HitResult.Good,
HitResult.Ok,
HitResult.Meh,
HitResult.LargeTickHit,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "hold tick";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow

View File

@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
OriginPosition = body.PathOffset;
}
public void RecyclePath() => body.RecyclePath();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}

View File

@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
{
protected readonly SliderBodyPiece BodyPiece;
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
protected readonly PathControlPointVisualiser ControlPointVisualiser;
protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
private readonly DrawableSlider slider;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
var sliderObject = (Slider)slider.HitObject;
this.slider = slider;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
{
RemoveControlPointsRequested = removeControlPoints
}
};
}
@ -66,13 +68,35 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
}
protected override void Update()
{
base.Update();
BodyPiece.UpdateFrom(HitObject);
if (IsSelected)
BodyPiece.UpdateFrom(HitObject);
}
protected override void OnSelected()
{
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints
});
base.OnSelected();
}
protected override void OnDeselected()
{
base.OnDeselected();
// throw away frame buffers on deselection.
ControlPointVisualiser?.Expire();
BodyPiece.RecyclePath();
}
private Vector2 rightClickPosition;
@ -182,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.UpdateHitObject(HitObject);
editorBeatmap?.Update(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -7,6 +7,7 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit
SelectionBox.CanRotate = canOperate;
SelectionBox.CanScaleX = canOperate;
SelectionBox.CanScaleY = canOperate;
SelectionBox.CanReverse = canOperate;
}
protected override void OnOperationEnded()
@ -41,6 +43,54 @@ namespace osu.Game.Rulesets.Osu.Edit
/// </summary>
private Vector2? referenceOrigin;
public override bool HandleReverse()
{
var hitObjects = selectedMovableObjects;
double endTime = hitObjects.Max(h => h.GetEndTime());
double startTime = hitObjects.Min(h => h.StartTime);
bool moreThanOneObject = hitObjects.Length > 1;
foreach (var h in hitObjects)
{
if (moreThanOneObject)
h.StartTime = endTime - (h.GetEndTime() - startTime);
if (h is Slider slider)
{
var points = slider.Path.ControlPoints.ToArray();
Vector2 endPos = points.Last().Position.Value;
slider.Path.ControlPoints.Clear();
slider.Position += endPos;
PathType? lastType = null;
for (var i = 0; i < points.Length; i++)
{
var p = points[i];
p.Position.Value -= endPos;
// propagate types forwards to last null type
if (i == points.Length - 1)
p.Type.Value = lastType;
else if (p.Type.Value != null)
{
var newType = p.Type.Value;
p.Type.Value = lastType;
lastType = newType;
}
slider.Path.ControlPoints.Insert(0, p);
}
}
}
return true;
}
public override bool HandleFlip(Direction direction)
{
var hitObjects = selectedMovableObjects;

View File

@ -42,7 +42,11 @@ namespace osu.Game.Rulesets.Osu.Mods
private double lastSliderHeadFadeOutStartTime;
private double lastSliderHeadFadeOutDuration;
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true);
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false);
private void applyState(DrawableHitObject drawable, bool increaseVisibility)
{
if (!(drawable is DrawableOsuHitObject d))
return;
@ -86,14 +90,23 @@ namespace osu.Game.Rulesets.Osu.Mods
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
}
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
Drawable fadeTarget = circle;
if (increaseVisibility)
{
// only fade the circle piece (not the approach circle) for the increased visibility object.
fadeTarget = circle.CirclePiece;
}
else
{
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
}
// fade out immediately after fade in.
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
circle.FadeOut(fadeOutDuration);
fadeTarget.FadeOut(fadeOutDuration);
break;
case DrawableSlider slider:

View File

@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.Ok,
HitResult.Meh,
HitResult.LargeTickHit,
HitResult.SmallTickHit,
HitResult.SmallBonus,
HitResult.LargeBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "slider tick";
case HitResult.SmallTickHit:
return "slider end";
case HitResult.SmallBonus:
return "spinner spin";
case HitResult.LargeBonus:
return "spinner bonus";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any() && !(first is Swell))
if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
return first;
}).ToList();

View File

@ -54,30 +54,30 @@ namespace osu.Game.Rulesets.Taiko.Edit
{
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
{
if (h.IsStrong != state)
{
h.IsStrong = state;
EditorBeatmap.UpdateHitObject(h);
EditorBeatmap.Update(h);
}
}
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
public void SetRimState(bool state)
{
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
h.Type = state ? HitType.Rim : HitType.Centre;
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
@ -89,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
protected override void UpdateTernaryStates()
{
base.UpdateTernaryStates();

View File

@ -158,7 +158,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
if (HitObject.CanBeStrong)
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()

View File

@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => Duration = value - StartTime;
}
public override bool CanBeStrong => false;
public double Duration { get; set; }
/// <summary>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
/// <summary>
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
/// </summary>
public virtual bool CanBeStrong => true;
/// <summary>
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
@ -37,7 +43,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong
{
get => IsStrongBindable.Value;
set => IsStrongBindable.Value = value;
set
{
if (value && !CanBeStrong)
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
IsStrongBindable.Value = value;
}
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -159,6 +159,33 @@ namespace osu.Game.Rulesets.Taiko
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.SmallTickHit:
return "drum tick";
case HitResult.SmallBonus:
return "strong bonus";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class TransactionalCommitComponentTest
{
private TestHandler handler;
[SetUp]
public void SetUp()
{
handler = new TestHandler();
}
[Test]
public void TestCommitTransaction()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestSaveOutsideOfTransactionTriggersUpdates()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(2));
}
[Test]
public void TestEventsFire()
{
int transactionBegan = 0;
int transactionEnded = 0;
int stateSaved = 0;
handler.TransactionBegan += () => transactionBegan++;
handler.TransactionEnded += () => transactionEnded++;
handler.SaveStateTriggered += () => stateSaved++;
handler.BeginChange();
Assert.That(transactionBegan, Is.EqualTo(1));
handler.EndChange();
Assert.That(transactionEnded, Is.EqualTo(1));
Assert.That(stateSaved, Is.EqualTo(0));
handler.SaveState();
Assert.That(stateSaved, Is.EqualTo(1));
}
[Test]
public void TestSaveDuringTransactionDoesntTriggerUpdate()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestEndWithoutBeginThrows()
{
handler.BeginChange();
handler.EndChange();
Assert.That(() => handler.EndChange(), Throws.TypeOf<InvalidOperationException>());
}
private class TestHandler : TransactionalCommitComponent
{
public int StateUpdateCount { get; private set; }
protected override void UpdateState()
{
StateUpdateCount++;
}
}
}
}

View File

@ -193,6 +193,7 @@ namespace osu.Game.Tests.Visual.Background
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
{
Ruleset = new OsuRuleset().RulesetInfo,
User = new User { Username = "osu!" },
Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo
})));

View File

@ -5,7 +5,6 @@ using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Screens.Edit.Compose.Components;
using osu.Game.Screens.Edit.Compose.Components.Timeline;
using osuTK;
namespace osu.Game.Tests.Visual.Editing
@ -13,7 +12,7 @@ namespace osu.Game.Tests.Visual.Editing
[TestFixture]
public class TestSceneTimelineTickDisplay : TimelineTestScene
{
public override Drawable CreateTestComponent() => new TimelineTickDisplay();
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
[BackgroundDependencyLoader]
private void load()

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Threading;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Editing
};
});
AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
AddUntilStep("wait for load", () => graph.Loaded.IsSet);
}
[Test]
@ -98,12 +99,18 @@ namespace osu.Game.Tests.Visual.Editing
};
});
AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
AddUntilStep("wait for load", () => graph.Loaded.IsSet);
}
public class TestWaveformGraph : WaveformGraph
{
public new Waveform ResampledWaveform => base.ResampledWaveform;
public readonly ManualResetEventSlim Loaded = new ManualResetEventSlim();
protected override void OnWaveformRegenerated(Waveform waveform)
{
base.OnWaveformRegenerated(waveform);
Loaded.Set();
}
}
}
}

View File

@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking
createTest(CreateDistributedHitEvents());
}
[Test]
public void TestAroundCentre()
{
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
}
[Test]
public void TestZeroTimeOffset()
{

View File

@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
/// <summary>
/// The statistics that appear in the table, in order of appearance.
/// </summary>
private readonly List<HitResult> statisticResultTypes = new List<HitResult>();
private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>();
private bool showPerformancePoints;
@ -101,15 +101,24 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
};
// All statistics across all scores, unordered.
var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet();
var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.Result)).ToHashSet();
var ruleset = scores.First().Ruleset.CreateInstance();
foreach (var result in OrderAttributeUtils.GetValuesInOrder<HitResult>())
{
if (!allScoreStatistics.Contains(result))
continue;
columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
statisticResultTypes.Add(result);
// for the time being ignore bonus result types.
// this is not being sent from the API and will be empty in all cases.
if (result.IsBonus())
continue;
string displayName = ruleset.GetDisplayNameForHitResult(result);
columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60)));
statisticResultTypes.Add((result, displayName));
}
if (showPerformancePoints)
@ -163,18 +172,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
};
var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result);
var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result);
foreach (var result in statisticResultTypes)
{
if (!availableStatistics.TryGetValue(result, out var stat))
stat = (result, 0, null);
if (!availableStatistics.TryGetValue(result.result, out var stat))
stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName);
content.Add(new OsuSpriteText
{
Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}",
Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}",
Font = OsuFont.GetFont(size: text_size),
Colour = stat.count == 0 ? Color4.Gray : Color4.White
Colour = stat.Count == 0 ? Color4.Gray : Color4.White
});
}

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -15,7 +14,6 @@ using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osuTK;
@ -117,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
ppColumn.Text = $@"{value.PP:N0}";
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount));
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
modsColumn.Mods = value.Mods;
if (scoreManager != null)
@ -125,9 +123,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
}
}
private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width)
private TextColumn createStatisticsColumn(HitResultDisplayStatistic stat) => new TextColumn(stat.DisplayName, smallFont, bottom_columns_min_width)
{
Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}"
Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}"
};
private class InfoColumn : CompositeDrawable

View File

@ -150,7 +150,7 @@ namespace osu.Game.Overlays
{
if (IsUserPaused) return;
if (CurrentTrack.IsDummyDevice)
if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending)
{
if (beatmap.Disabled)
return;

View File

@ -4,9 +4,11 @@
using System.Threading.Tasks;
using osu.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform;
using osu.Framework.Screens;
using osu.Game.Configuration;
using osu.Game.Overlays.Notifications;
using osu.Game.Overlays.Settings.Sections.Maintenance;
using osu.Game.Updater;
@ -21,6 +23,9 @@ namespace osu.Game.Overlays.Settings.Sections.General
private SettingsButton checkForUpdatesButton;
[Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; }
[BackgroundDependencyLoader(true)]
private void load(Storage storage, OsuConfigManager config, OsuGame game)
{
@ -38,7 +43,19 @@ namespace osu.Game.Overlays.Settings.Sections.General
Action = () =>
{
checkForUpdatesButton.Enabled.Value = false;
Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true));
Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() =>
{
if (!t.Result)
{
notifications?.Post(new SimpleNotification
{
Text = $"You are running the latest release ({game.Version})",
Icon = FontAwesome.Solid.CheckCircle,
});
}
checkForUpdatesButton.Enabled.Value = true;
}));
}
});
}

View File

@ -89,9 +89,23 @@ namespace osu.Game.Rulesets.Edit
}
}
protected virtual void OnDeselected() => Hide();
protected virtual void OnDeselected()
{
// selection blueprints are AlwaysPresent while the related DrawableHitObject is visible
// set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children.
foreach (var d in InternalChildren)
d.Hide();
protected virtual void OnSelected() => Show();
Hide();
}
protected virtual void OnSelected()
{
foreach (var d in InternalChildren)
d.Show();
Show();
}
// When not selected, input is only required for the blueprint itself to receive IsHovering
protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected;

View File

@ -38,7 +38,15 @@ namespace osu.Game.Rulesets.Mods
public virtual void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
if (IncreaseFirstObjectVisibility.Value)
drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1);
{
drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h));
var firstObject = drawables.FirstOrDefault();
if (firstObject != null)
firstObject.ApplyCustomUpdateState += ApplyFirstObjectIncreaseVisibilityState;
drawables = drawables.Skip(1);
}
foreach (var dho in drawables)
dho.ApplyCustomUpdateState += ApplyHiddenState;
@ -65,6 +73,20 @@ namespace osu.Game.Rulesets.Mods
}
}
/// <summary>
/// Apply a special visibility state to the first object in a beatmap, if the user chooses to turn on the "increase first object visibility" setting.
/// </summary>
/// <param name="hitObject">The hit object to apply the state change to.</param>
/// <param name="state">The state of the hit object.</param>
protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state)
{
}
/// <summary>
/// Apply a hidden state to the provided object.
/// </summary>
/// <param name="hitObject">The hit object to apply the state change to.</param>
/// <param name="state">The state of the hit object.</param>
protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state)
{
}

View File

@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Objects
c.Changed += invalidate;
break;
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (var c in args.OldItems.Cast<PathControlPoint>())
c.Changed -= invalidate;

View File

@ -23,8 +23,10 @@ using osu.Game.Scoring;
using osu.Game.Skinning;
using osu.Game.Users;
using JetBrains.Annotations;
using osu.Framework.Extensions;
using osu.Framework.Testing;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Utils;
namespace osu.Game.Rulesets
{
@ -241,5 +243,52 @@ namespace osu.Game.Rulesets
/// <returns>The <see cref="StatisticRow"/>s to display. Each <see cref="StatisticRow"/> may contain 0 or more <see cref="StatisticItem"/>.</returns>
[NotNull]
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticRow>();
/// <summary>
/// Get all valid <see cref="HitResult"/>s for this ruleset.
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
/// </summary>
/// <returns>
/// All valid <see cref="HitResult"/>s along with a display-friendly name.
/// </returns>
public IEnumerable<(HitResult result, string displayName)> GetHitResults()
{
var validResults = GetValidHitResults();
// enumerate over ordered list to guarantee return order is stable.
foreach (var result in OrderAttributeUtils.GetValuesInOrder<HitResult>())
{
switch (result)
{
// hard blocked types, should never be displayed even if the ruleset tells us to.
case HitResult.None:
case HitResult.IgnoreHit:
case HitResult.IgnoreMiss:
// display is handled as a completion count with corresponding "hit" type.
case HitResult.LargeTickMiss:
case HitResult.SmallTickMiss:
continue;
}
if (result == HitResult.Miss || validResults.Contains(result))
yield return (result, GetDisplayNameForHitResult(result));
}
}
/// <summary>
/// Get all valid <see cref="HitResult"/>s for this ruleset.
/// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset.
/// </summary>
/// <remarks>
/// <see cref="HitResult.Miss"/> is implicitly included. Special types like <see cref="HitResult.IgnoreHit"/> are ignored even when specified.
/// </remarks>
protected virtual IEnumerable<HitResult> GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder<HitResult>();
/// <summary>
/// Get a display friendly name for the specified result type.
/// </summary>
/// <param name="result">The result type to get the name for.</param>
/// <returns>The display name.</returns>
public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription();
}
}

View File

@ -0,0 +1,41 @@
// 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.Rulesets.Scoring;
namespace osu.Game.Scoring
{
/// <summary>
/// Compiled result data for a specific <see cref="HitResult"/> in a score.
/// </summary>
public class HitResultDisplayStatistic
{
/// <summary>
/// The associated result type.
/// </summary>
public HitResult Result { get; }
/// <summary>
/// The count of successful hits of this type.
/// </summary>
public int Count { get; }
/// <summary>
/// The maximum achievable hits of this type. May be null if undetermined.
/// </summary>
public int? MaxCount { get; }
/// <summary>
/// A custom display name for the result type. May be provided by rulesets to give better clarity.
/// </summary>
public string DisplayName { get; }
public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName)
{
Result = result;
Count = count;
MaxCount = maxCount;
DisplayName = displayName;
}
}
}

View File

@ -213,22 +213,19 @@ namespace osu.Game.Scoring
set => isLegacyScore = value;
}
public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay()
public IEnumerable<HitResultDisplayStatistic> GetStatisticsForDisplay()
{
foreach (var key in OrderAttributeUtils.GetValuesInOrder<HitResult>())
foreach (var r in Ruleset.CreateInstance().GetHitResults())
{
if (key.IsBonus())
continue;
int value = Statistics.GetOrDefault(r.result);
int value = Statistics.GetOrDefault(key);
switch (key)
switch (r.result)
{
case HitResult.SmallTickHit:
{
int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss);
if (total > 0)
yield return (key, value, total);
yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
@ -237,7 +234,7 @@ namespace osu.Game.Scoring
{
int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss);
if (total > 0)
yield return (key, value, total);
yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName);
break;
}
@ -247,8 +244,7 @@ namespace osu.Game.Scoring
break;
default:
if (value > 0 || key == HitResult.Miss)
yield return (key, value, null);
yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName);
break;
}

View File

@ -1,9 +1,9 @@
// 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 osuTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osuTK;
namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
{
@ -12,16 +12,23 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations
/// </summary>
public class PointVisualisation : Box
{
public const float WIDTH = 1;
public PointVisualisation(double startTime)
: this()
{
X = (float)startTime;
}
public PointVisualisation()
{
Origin = Anchor.TopCentre;
RelativeSizeAxes = Axes.Y;
Width = 1;
EdgeSmoothness = new Vector2(1, 0);
RelativePositionAxes = Axes.X;
X = (float)startTime;
RelativeSizeAxes = Axes.Y;
Width = WIDTH;
EdgeSmoothness = new Vector2(WIDTH, 0);
}
}
}

View File

@ -203,7 +203,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// handle positional change etc.
foreach (var obj in selectedHitObjects)
Beatmap.UpdateHitObject(obj);
Beatmap.Update(obj);
changeHandler?.EndChange();
isDraggingBlueprint = false;
@ -436,8 +436,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
{
// Apply the start time at the newly snapped-to position
double offset = result.Time.Value - draggedObject.StartTime;
foreach (HitObject obj in Beatmap.SelectedHitObjects)
{
obj.StartTime += offset;
Beatmap.Update(obj);
}
}
return true;

View File

@ -201,7 +201,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
protected override void AddBlueprintFor(HitObject hitObject)
{
refreshTool();
base.AddBlueprintFor(hitObject);
// on successful placement, the new combo button should be reset as this is the most common user interaction.
if (Beatmap.SelectedHitObjects.Count == 0)
NewCombo.Value = TernaryState.False;
}
private void createPlacement()

View File

@ -17,10 +17,28 @@ namespace osu.Game.Screens.Edit.Compose.Components
public Action<float> OnRotation;
public Action<Vector2, Anchor> OnScale;
public Action<Direction> OnFlip;
public Action OnReverse;
public Action OperationStarted;
public Action OperationEnded;
private bool canReverse;
/// <summary>
/// Whether pattern reversing support should be enabled.
/// </summary>
public bool CanReverse
{
get => canReverse;
set
{
if (canReverse == value) return;
canReverse = value;
recreate();
}
}
private bool canRotate;
/// <summary>
@ -125,6 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (CanScaleX && CanScaleY) addFullScaleComponents();
if (CanScaleY) addYScaleComponents();
if (CanRotate) addRotationComponents();
if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke());
}
private void addRotationComponents()

View File

@ -101,6 +101,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
OnRotation = angle => HandleRotation(angle),
OnScale = (amount, anchor) => HandleScale(amount, anchor),
OnFlip = direction => HandleFlip(direction),
OnReverse = () => HandleReverse(),
};
/// <summary>
@ -139,7 +140,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// Handles the selected <see cref="DrawableHitObject"/>s being rotated.
/// </summary>
/// <param name="angle">The delta angle to apply to the selection.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be rotated.</returns>
public virtual bool HandleRotation(float angle) => false;
/// <summary>
@ -147,16 +148,22 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// </summary>
/// <param name="scale">The delta scale to apply, in playfield local coordinates.</param>
/// <param name="anchor">The point of reference where the scale is originating from.</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be scaled.</returns>
public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false;
/// <summary>
/// Handled the selected <see cref="DrawableHitObject"/>s being flipped.
/// Handles the selected <see cref="DrawableHitObject"/>s being flipped.
/// </summary>
/// <param name="direction">The direction to flip</param>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be moved.</returns>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be flipped.</returns>
public virtual bool HandleFlip(Direction direction) => false;
/// <summary>
/// Handles the selected <see cref="DrawableHitObject"/>s being reversed pattern-wise.
/// </summary>
/// <returns>Whether any <see cref="DrawableHitObject"/>s could be reversed.</returns>
public virtual bool HandleReverse() => false;
public bool OnPressed(PlatformAction action)
{
switch (action.ActionMethod)
@ -236,9 +243,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
private void deleteSelected()
{
ChangeHandler?.BeginChange();
EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject));
ChangeHandler?.EndChange();
}
#endregion
@ -305,7 +310,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void AddHitSample(string sampleName)
{
ChangeHandler?.BeginChange();
EditorBeatmap?.BeginChange();
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
@ -316,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
h.Samples.Add(new HitSampleInfo { Name = sampleName });
}
ChangeHandler?.EndChange();
EditorBeatmap?.EndChange();
}
/// <summary>
@ -326,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <exception cref="InvalidOperationException">Throws if any selected object doesn't implement <see cref="IHasComboInformation"/></exception>
public void SetNewCombo(bool state)
{
ChangeHandler?.BeginChange();
EditorBeatmap?.BeginChange();
foreach (var h in EditorBeatmap.SelectedHitObjects)
{
@ -335,10 +340,10 @@ namespace osu.Game.Screens.Edit.Compose.Components
if (comboInfo == null || comboInfo.NewCombo == state) continue;
comboInfo.NewCombo = state;
EditorBeatmap?.UpdateHitObject(h);
EditorBeatmap?.Update(h);
}
ChangeHandler?.EndChange();
EditorBeatmap?.EndChange();
}
/// <summary>
@ -347,12 +352,12 @@ namespace osu.Game.Screens.Edit.Compose.Components
/// <param name="sampleName">The name of the hit sample.</param>
public void RemoveHitSample(string sampleName)
{
ChangeHandler?.BeginChange();
EditorBeatmap?.BeginChange();
foreach (var h in EditorBeatmap.SelectedHitObjects)
h.SamplesBindable.RemoveAll(s => s.Name == sampleName);
ChangeHandler?.EndChange();
EditorBeatmap?.EndChange();
}
#endregion

View File

@ -392,6 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
repeatHitObject.RepeatCount = proposedCount;
beatmap.Update(hitObject);
break;
case IHasDuration endTimeHitObject:
@ -401,10 +402,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
return;
endTimeHitObject.Duration = snappedTime - hitObject.StartTime;
beatmap.Update(hitObject);
break;
}
beatmap.UpdateHitObject(hitObject);
}
}

View File

@ -1,9 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Caching;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
@ -12,7 +14,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations;
namespace osu.Game.Screens.Edit.Compose.Components.Timeline
{
public class TimelineTickDisplay : TimelinePart
public class TimelineTickDisplay : TimelinePart<PointVisualisation>
{
[Resolved]
private EditorBeatmap beatmap { get; set; }
@ -31,15 +33,63 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
RelativeSizeAxes = Axes.Both;
}
private readonly Cached tickCache = new Cached();
[BackgroundDependencyLoader]
private void load()
{
beatDivisor.BindValueChanged(_ => createLines(), true);
beatDivisor.BindValueChanged(_ => tickCache.Invalidate());
}
private void createLines()
/// <summary>
/// The visible time/position range of the timeline.
/// </summary>
private (float min, float max) visibleRange = (float.MinValue, float.MaxValue);
/// <summary>
/// The next time/position value to the left of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMinTick;
/// <summary>
/// The next time/position value to the right of the display when tick regeneration needs to be run.
/// </summary>
private float? nextMaxTick;
[Resolved(canBeNull: true)]
private Timeline timeline { get; set; }
protected override void Update()
{
Clear();
base.Update();
if (timeline != null)
{
var newRange = (
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X,
(ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X);
if (visibleRange != newRange)
{
visibleRange = newRange;
// actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries.
if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick))
tickCache.Invalidate();
}
}
if (!tickCache.IsValid)
createTicks();
}
private void createTicks()
{
int drawableIndex = 0;
int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last();
nextMinTick = null;
nextMaxTick = null;
for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++)
{
@ -50,41 +100,70 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline
for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value)
{
var indexInBeat = beat % beatDivisor.Value;
float xPos = (float)t;
if (indexInBeat == 0)
{
Add(new PointVisualisation(t)
{
Colour = BindableBeatDivisor.GetColourFor(1, colours),
Origin = Anchor.TopCentre,
});
}
if (t < visibleRange.min)
nextMinTick = xPos;
else if (t > visibleRange.max)
nextMaxTick ??= xPos;
else
{
// if this is the first beat in the beatmap, there is no next min tick
if (beat == 0 && i == 0)
nextMinTick = float.MinValue;
var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value);
var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value);
var colour = BindableBeatDivisor.GetColourFor(divisor, colours);
var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Height = height,
Origin = Anchor.TopCentre,
});
// even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn.
var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f;
Add(new PointVisualisation(t)
{
Colour = colour,
Anchor = Anchor.BottomLeft,
Origin = Anchor.BottomCentre,
Height = height,
});
var topPoint = getNextUsablePoint();
topPoint.X = xPos;
topPoint.Colour = colour;
topPoint.Height = height;
topPoint.Anchor = Anchor.TopLeft;
topPoint.Origin = Anchor.TopCentre;
var bottomPoint = getNextUsablePoint();
bottomPoint.X = xPos;
bottomPoint.Colour = colour;
bottomPoint.Anchor = Anchor.BottomLeft;
bottomPoint.Origin = Anchor.BottomCentre;
bottomPoint.Height = height;
}
beat++;
}
}
int usedDrawables = drawableIndex;
// save a few drawables beyond the currently used for edge cases.
while (drawableIndex < Math.Min(usedDrawables + 16, Count))
Children[drawableIndex++].Hide();
// expire any excess
while (drawableIndex < Count)
Children[drawableIndex++].Expire();
tickCache.Validate();
Drawable getNextUsablePoint()
{
PointVisualisation point;
if (drawableIndex >= Count)
Add(point = new PointVisualisation());
else
point = Children[drawableIndex];
drawableIndex++;
point.Show();
return point;
}
}
}
}

View File

@ -469,10 +469,17 @@ namespace osu.Game.Screens.Edit
private void confirmExit()
{
// stop the track if playing to allow the parent screen to choose a suitable playback mode.
Beatmap.Value.Track.Stop();
if (isNewBeatmap)
{
// confirming exit without save means we should delete the new beatmap completely.
beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet);
// in theory this shouldn't be required but due to EF core not sharing instance states 100%
// MusicController is unaware of the changed DeletePending state.
Beatmap.SetDefault();
}
exitConfirmed = true;
@ -509,14 +516,14 @@ namespace osu.Game.Screens.Edit
foreach (var h in objects)
h.StartTime += timeOffset;
changeHandler.BeginChange();
editorBeatmap.BeginChange();
editorBeatmap.SelectedHitObjects.Clear();
editorBeatmap.AddRange(objects);
editorBeatmap.SelectedHitObjects.AddRange(objects);
changeHandler.EndChange();
editorBeatmap.EndChange();
}
protected void Undo() => changeHandler.RestoreState(-1);

View File

@ -8,7 +8,6 @@ using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing;
@ -18,7 +17,7 @@ using osu.Game.Skinning;
namespace osu.Game.Screens.Edit
{
public class EditorBeatmap : Component, IBeatmap, IBeatSnapProvider
public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider
{
/// <summary>
/// Invoked when a <see cref="HitObject"/> is added to this <see cref="EditorBeatmap"/>.
@ -89,9 +88,11 @@ namespace osu.Game.Screens.Edit
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
private readonly List<HitObject> batchPendingInserts = new List<HitObject>();
private bool isBatchApplying;
private readonly List<HitObject> batchPendingDeletes = new List<HitObject>();
private readonly HashSet<HitObject> batchPendingUpdates = new HashSet<HitObject>();
/// <summary>
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
@ -99,11 +100,10 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObjects">The <see cref="HitObject"/>s to add.</param>
public void AddRange(IEnumerable<HitObject> hitObjects)
{
ApplyBatchChanges(_ =>
{
foreach (var h in hitObjects)
Add(h);
});
BeginChange();
foreach (var h in hitObjects)
Add(h);
EndChange();
}
/// <summary>
@ -131,26 +131,28 @@ namespace osu.Game.Screens.Edit
mutableHitObjects.Insert(index, hitObject);
if (isBatchApplying)
batchPendingInserts.Add(hitObject);
else
{
// must be run after any change to hitobject ordering
beatmapProcessor?.PreProcess();
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
HitObjectAdded?.Invoke(hitObject);
}
BeginChange();
batchPendingInserts.Add(hitObject);
EndChange();
}
/// <summary>
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
/// </summary>
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
public void UpdateHitObject([NotNull] HitObject hitObject)
public void Update([NotNull] HitObject hitObject)
{
pendingUpdates.Add(hitObject);
// updates are debounced regardless of whether a batch is active.
batchPendingUpdates.Add(hitObject);
}
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateAllHitObjects()
{
foreach (var h in HitObjects)
batchPendingUpdates.Add(h);
}
/// <summary>
@ -175,11 +177,10 @@ namespace osu.Game.Screens.Edit
/// <param name="hitObjects">The <see cref="HitObject"/>s to remove.</param>
public void RemoveRange(IEnumerable<HitObject> hitObjects)
{
ApplyBatchChanges(_ =>
{
foreach (var h in hitObjects)
Remove(h);
});
BeginChange();
foreach (var h in hitObjects)
Remove(h);
EndChange();
}
/// <summary>
@ -203,50 +204,45 @@ namespace osu.Game.Screens.Edit
bindable.UnbindAll();
startTimeBindables.Remove(hitObject);
if (isBatchApplying)
batchPendingDeletes.Add(hitObject);
else
{
// must be run after any change to hitobject ordering
beatmapProcessor?.PreProcess();
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
HitObjectRemoved?.Invoke(hitObject);
}
BeginChange();
batchPendingDeletes.Add(hitObject);
EndChange();
}
private readonly List<HitObject> batchPendingInserts = new List<HitObject>();
private readonly List<HitObject> batchPendingDeletes = new List<HitObject>();
/// <summary>
/// Apply a batch of operations in one go, without performing Pre/Postprocessing each time.
/// </summary>
/// <param name="applyFunction">The function which will apply the batch changes.</param>
public void ApplyBatchChanges(Action<EditorBeatmap> applyFunction)
protected override void Update()
{
if (isBatchApplying)
throw new InvalidOperationException("Attempting to perform a batch application from within an existing batch");
base.Update();
isBatchApplying = true;
if (batchPendingUpdates.Count > 0)
UpdateState();
}
applyFunction(this);
protected override void UpdateState()
{
if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0)
return;
beatmapProcessor?.PreProcess();
foreach (var h in batchPendingDeletes) processHitObject(h);
foreach (var h in batchPendingInserts) processHitObject(h);
foreach (var h in batchPendingUpdates) processHitObject(h);
beatmapProcessor?.PostProcess();
foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h);
foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h);
// callbacks may modify the lists so let's be safe about it
var deletes = batchPendingDeletes.ToArray();
batchPendingDeletes.Clear();
var inserts = batchPendingInserts.ToArray();
batchPendingInserts.Clear();
isBatchApplying = false;
var updates = batchPendingUpdates.ToArray();
batchPendingUpdates.Clear();
foreach (var h in deletes) HitObjectRemoved?.Invoke(h);
foreach (var h in inserts) HitObjectAdded?.Invoke(h);
foreach (var h in updates) HitObjectUpdated?.Invoke(h);
}
/// <summary>
@ -254,28 +250,6 @@ namespace osu.Game.Screens.Edit
/// </summary>
public void Clear() => RemoveRange(HitObjects.ToArray());
protected override void Update()
{
base.Update();
// debounce updates as they are common and may come from input events, which can run needlessly many times per update frame.
if (pendingUpdates.Count > 0)
{
beatmapProcessor?.PreProcess();
foreach (var hitObject in pendingUpdates)
processHitObject(hitObject);
beatmapProcessor?.PostProcess();
// explicitly needs to be fired after PostProcess
foreach (var hitObject in pendingUpdates)
HitObjectUpdated?.Invoke(hitObject);
pendingUpdates.Clear();
}
}
private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
private void trackStartTime(HitObject hitObject)
@ -289,7 +263,7 @@ namespace osu.Game.Screens.Edit
var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime);
mutableHitObjects.Insert(insertionIndex + 1, hitObject);
UpdateHitObject(hitObject);
Update(hitObject);
};
}
@ -315,14 +289,5 @@ namespace osu.Game.Screens.Edit
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
public int BeatDivisor => beatDivisor?.Value ?? 1;
/// <summary>
/// Update all hit objects with potentially changed difficulty or control point data.
/// </summary>
public void UpdateBeatmap()
{
foreach (var h in HitObjects)
pendingUpdates.Add(h);
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit
/// <summary>
/// Tracks changes to the <see cref="Editor"/>.
/// </summary>
public class EditorChangeHandler : IEditorChangeHandler
public class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler
{
public readonly Bindable<bool> CanUndo = new Bindable<bool>();
public readonly Bindable<bool> CanRedo = new Bindable<bool>();
@ -41,7 +41,6 @@ namespace osu.Game.Screens.Edit
}
private readonly EditorBeatmap editorBeatmap;
private int bulkChangesStarted;
private bool isRestoring;
public const int MAX_SAVED_STATES = 50;
@ -54,9 +53,9 @@ namespace osu.Game.Screens.Edit
{
this.editorBeatmap = editorBeatmap;
editorBeatmap.HitObjectAdded += hitObjectAdded;
editorBeatmap.HitObjectRemoved += hitObjectRemoved;
editorBeatmap.HitObjectUpdated += hitObjectUpdated;
editorBeatmap.TransactionBegan += BeginChange;
editorBeatmap.TransactionEnded += EndChange;
editorBeatmap.SaveStateTriggered += SaveState;
patcher = new LegacyEditorBeatmapPatcher(editorBeatmap);
@ -64,28 +63,8 @@ namespace osu.Game.Screens.Edit
SaveState();
}
private void hitObjectAdded(HitObject obj) => SaveState();
private void hitObjectRemoved(HitObject obj) => SaveState();
private void hitObjectUpdated(HitObject obj) => SaveState();
public void BeginChange() => bulkChangesStarted++;
public void EndChange()
protected override void UpdateState()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
SaveState();
}
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
if (isRestoring)
return;
@ -120,7 +99,7 @@ namespace osu.Game.Screens.Edit
/// <param name="direction">The direction to restore in. If less than 0, an older state will be used. If greater than 0, a newer state will be used.</param>
public void RestoreState(int direction)
{
if (bulkChangesStarted > 0)
if (TransactionActive)
return;
if (savedStates.Count == 0)

View File

@ -68,19 +68,20 @@ namespace osu.Game.Screens.Edit
toRemove.Sort();
toAdd.Sort();
editorBeatmap.ApplyBatchChanges(eb =>
{
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
eb.RemoveAt(toRemove[i]);
editorBeatmap.BeginChange();
if (toAdd.Count > 0)
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
eb.Insert(i, newBeatmap.HitObjects[i]);
}
});
// Apply the changes.
for (int i = toRemove.Count - 1; i >= 0; i--)
editorBeatmap.RemoveAt(toRemove[i]);
if (toAdd.Count > 0)
{
IBeatmap newBeatmap = readBeatmap(newState);
foreach (var i in toAdd)
editorBeatmap.Insert(i, newBeatmap.HitObjects[i]);
}
editorBeatmap.EndChange();
}
private string readString(byte[] state) => Encoding.UTF8.GetString(state);

View File

@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup
Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value;
Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value;
editorBeatmap.UpdateBeatmap();
editorBeatmap.UpdateAllHitObjects();
}
}
}

View File

@ -0,0 +1,73 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics;
namespace osu.Game.Screens.Edit
{
/// <summary>
/// A component that tracks a batch change, only applying after all active changes are completed.
/// </summary>
public abstract class TransactionalCommitComponent : Component
{
/// <summary>
/// Fires whenever a transaction begins. Will not fire on nested transactions.
/// </summary>
public event Action TransactionBegan;
/// <summary>
/// Fires when the last transaction completes.
/// </summary>
public event Action TransactionEnded;
/// <summary>
/// Fires when <see cref="SaveState"/> is called and results in a non-transactional state save.
/// </summary>
public event Action SaveStateTriggered;
public bool TransactionActive => bulkChangesStarted > 0;
private int bulkChangesStarted;
/// <summary>
/// Signal the beginning of a change.
/// </summary>
public void BeginChange()
{
if (bulkChangesStarted++ == 0)
TransactionBegan?.Invoke();
}
/// <summary>
/// Signal the end of a change.
/// </summary>
/// <exception cref="InvalidOperationException">Throws if <see cref="BeginChange"/> was not first called.</exception>
public void EndChange()
{
if (bulkChangesStarted == 0)
throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}.");
if (--bulkChangesStarted == 0)
{
UpdateState();
TransactionEnded?.Invoke();
}
}
/// <summary>
/// Force an update of the state with no attached transaction.
/// This is a no-op if a transaction is already active. Should generally be used as a safety measure to ensure granular changes are not left outside a transaction.
/// </summary>
public void SaveState()
{
if (bulkChangesStarted > 0)
return;
SaveStateTriggered?.Invoke();
UpdateState();
}
protected abstract void UpdateState();
}
}

View File

@ -3,7 +3,6 @@
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Colour;
@ -117,7 +116,7 @@ namespace osu.Game.Screens.Ranking.Contracted
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount))
ChildrenEnumerable = score.GetStatisticsForDisplay().Where(s => !s.Result.IsBonus()).Select(createStatistic)
},
new FillFlowContainer
{
@ -199,8 +198,8 @@ namespace osu.Game.Screens.Ranking.Contracted
};
}
private Drawable createStatistic(HitResult result, int count, int? maxCount)
=> createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}");
private Drawable createStatistic(HitResultDisplayStatistic result)
=> createStatistic(result.DisplayName, result.MaxCount == null ? $"{result.Count}" : $"{result.Count}/{result.MaxCount}");
private Drawable createStatistic(string key, string value) => new Container
{

View File

@ -64,155 +64,168 @@ namespace osu.Game.Screens.Ranking.Expanded
new CounterStatistic("pp", (int)(score.PP ?? 0)),
};
var bottomStatistics = new List<StatisticDisplay>();
var bottomStatistics = new List<HitResultStatistic>();
foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay())
bottomStatistics.Add(new HitResultStatistic(key, value, maxCount));
foreach (var result in score.GetStatisticsForDisplay())
bottomStatistics.Add(new HitResultStatistic(result));
statisticDisplays.AddRange(topStatistics);
statisticDisplays.AddRange(bottomStatistics);
InternalChild = new FillFlowContainer
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
new FillFlowContainer
{
new FillFlowContainer
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(20),
Children = new Drawable[]
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
new FillFlowContainer
{
new OsuSpriteText
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X,
Height = 230,
Child = new AccuracyCircle(score)
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
}
},
scoreCounter = new TotalScoreCounter
{
Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 },
Alpha = 0,
AlwaysPresent = true
},
starAndModDisplay = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)),
Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new OsuSpriteText
{
new StarRatingDisplay(beatmap)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
}
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)),
Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold),
MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2,
Truncate = true,
},
new Container
{
new OsuSpriteText
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Margin = new MarginPadding { Top = 40 },
RelativeSizeAxes = Axes.X,
Height = 230,
Child = new AccuracyCircle(score)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = beatmap.Version,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
},
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
}
},
scoreCounter = new TotalScoreCounter
{
Margin = new MarginPadding { Top = 0, Bottom = 5 },
Current = { Value = 0 },
Alpha = 0,
AlwaysPresent = true
},
starAndModDisplay = new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Spacing = new Vector2(5, 0),
Children = new Drawable[]
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}.With(t =>
{
if (!string.IsNullOrEmpty(creator))
new StarRatingDisplay(beatmap)
{
t.AddText("mapped by ");
t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
}
})
}
},
}
},
new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
},
}
},
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Direction = FillDirection.Vertical,
AutoSizeAxes = Axes.Both,
Children = new Drawable[]
{
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = beatmap.Version,
Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold),
},
new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12))
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
}.With(t =>
{
if (!string.IsNullOrEmpty(creator))
{
t.AddText("mapped by ");
t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold));
}
})
}
},
}
},
new FillFlowContainer
{
new GridContainer
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { topStatistics.Cast<Drawable>().ToArray() },
RowDimensions = new[]
new GridContainer
{
new Dimension(GridSizeMode.AutoSize),
}
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { bottomStatistics.Cast<Drawable>().ToArray() },
RowDimensions = new[]
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { topStatistics.Cast<Drawable>().ToArray() },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
},
new GridContainer
{
new Dimension(GridSizeMode.AutoSize),
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
},
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() },
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
}
}
}
}
},
new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"
}
},
new OsuSpriteText
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.BottomCentre,
Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold),
Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}"
}
};

View File

@ -2,26 +2,26 @@
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Extensions;
using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
namespace osu.Game.Screens.Ranking.Expanded.Statistics
{
public class HitResultStatistic : CounterStatistic
{
private readonly HitResult result;
public readonly HitResult Result;
public HitResultStatistic(HitResult result, int count, int? maxCount = null)
: base(result.GetDescription(), count, maxCount)
public HitResultStatistic(HitResultDisplayStatistic result)
: base(result.DisplayName, result.Count, result.MaxCount)
{
this.result = result;
Result = result.Result;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
HeaderText.Colour = colours.ForHitResult(result);
HeaderText.Colour = colours.ForHitResult(Result);
}
}
}

View File

@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Height of the panel when contracted.
/// </summary>
private const float contracted_height = 355;
private const float contracted_height = 385;
/// <summary>
/// Width of the panel when expanded.
@ -39,7 +39,7 @@ namespace osu.Game.Screens.Ranking
/// <summary>
/// Height of the panel when expanded.
/// </summary>
private const float expanded_height = 560;
private const float expanded_height = 586;
/// <summary>
/// Height of the top layer when the panel is expanded.
@ -105,11 +105,16 @@ namespace osu.Game.Screens.Ranking
[BackgroundDependencyLoader]
private void load()
{
// ScorePanel doesn't include the top extruding area in its own size.
// Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale.
const float vertical_fudge = 20;
InternalChild = content = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(40),
Y = vertical_fudge,
Children = new Drawable[]
{
topLayerContainer = new Container

View File

@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Statistics
foreach (var e in hitEvents)
{
int binOffset = (int)(e.TimeOffset / binSize);
int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero);
bins[timing_distribution_centre_bin_index + binOffset]++;
}

View File

@ -34,6 +34,12 @@ namespace osu.Game.Tests.Beatmaps
var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray());
var expectedResult = read(name);
foreach (var m in ourResult.Mappings)
m.PostProcess();
foreach (var m in expectedResult.Mappings)
m.PostProcess();
Assert.Multiple(() =>
{
int mappingCounter = 0;
@ -239,6 +245,13 @@ namespace osu.Game.Tests.Beatmaps
set => Objects = value;
}
/// <summary>
/// Invoked after this <see cref="ConvertMapping{TConvertValue}"/> is populated to post-process the contained data.
/// </summary>
public virtual void PostProcess()
{
}
public virtual bool Equals(ConvertMapping<TConvertValue> other) => StartTime == other?.StartTime;
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Updater
version = game.Version;
}
protected override async Task PerformUpdateCheck()
protected override async Task<bool> PerformUpdateCheck()
{
try
{
@ -53,12 +53,17 @@ namespace osu.Game.Updater
return true;
}
});
return true;
}
}
catch
{
// we shouldn't crash on a web failure. or any failure for the matter.
return true;
}
return false;
}
private string getBestUrl(GitHubRelease release)

View File

@ -57,25 +57,31 @@ namespace osu.Game.Updater
private readonly object updateTaskLock = new object();
private Task updateCheckTask;
private Task<bool> updateCheckTask;
public async Task CheckForUpdateAsync()
public async Task<bool> CheckForUpdateAsync()
{
if (!CanCheckForUpdate)
return;
return false;
Task waitTask;
Task<bool> waitTask;
lock (updateTaskLock)
waitTask = (updateCheckTask ??= PerformUpdateCheck());
await waitTask;
bool hasUpdates = await waitTask;
lock (updateTaskLock)
updateCheckTask = null;
return hasUpdates;
}
protected virtual Task PerformUpdateCheck() => Task.CompletedTask;
/// <summary>
/// Performs an asynchronous check for application updates.
/// </summary>
/// <returns>Whether any update is waiting. May return true if an error occured (there is potentially an update available).</returns>
protected virtual Task<bool> PerformUpdateCheck() => Task.FromResult(false);
private class UpdateCompleteNotification : SimpleNotification
{

View File

@ -24,7 +24,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1004.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="Sentry" Version="2.1.6" />
<PackageReference Include="SharpCompress" Version="0.26.0" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1004.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.1009.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
</ItemGroup>
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
@ -80,7 +80,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1004.0" />
<PackageReference Include="ppy.osu.Framework" Version="2020.1009.0" />
<PackageReference Include="SharpCompress" Version="0.26.0" />
<PackageReference Include="NUnit" Version="3.12.0" />
<PackageReference Include="SharpRaven" Version="2.4.0" />