1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 18:33:20 +08:00

Merge branch 'master' into lounge-redesign

This commit is contained in:
smoogipoo 2021-07-14 12:47:16 +09:00
commit c5977f3318
36 changed files with 1218 additions and 113 deletions

View File

@ -52,10 +52,10 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.713.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="Realm" Version="10.3.0" />
</ItemGroup>
</Project>

View File

@ -68,6 +68,8 @@ namespace osu.Desktop.Updater
return false;
}
scheduleRecheck = false;
if (notification == null)
{
notification = new UpdateProgressNotification(this) { State = ProgressNotificationState.Active };
@ -98,7 +100,6 @@ namespace osu.Desktop.Updater
// could fail if deltas are unavailable for full update path (https://github.com/Squirrel/Squirrel.Windows/issues/959)
// try again without deltas.
await checkForUpdateAsync(false, notification).ConfigureAwait(false);
scheduleRecheck = false;
}
else
{
@ -110,6 +111,7 @@ namespace osu.Desktop.Updater
catch (Exception)
{
// we'll ignore this and retry later. can be triggered by no internet connection or thread abortion.
scheduleRecheck = true;
}
finally
{

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Edit;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class CatchEditorTestSceneContainer : Container
{
[Cached(typeof(Playfield))]
public readonly ScrollingPlayfield Playfield;
protected override Container<Drawable> Content { get; }
public CatchEditorTestSceneContainer()
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
Width = CatchPlayfield.WIDTH;
Height = 1000;
Padding = new MarginPadding
{
Bottom = 100
};
InternalChildren = new Drawable[]
{
new ScrollingTestContainer(ScrollingDirection.Down)
{
TimeRange = 1000,
RelativeSizeAxes = Axes.Both,
Child = Playfield = new TestCatchPlayfield
{
RelativeSizeAxes = Axes.Both
}
},
new PlayfieldBorder
{
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Full },
Clock = new FramedClock(new StopwatchClock(true))
},
Content = new Container
{
RelativeSizeAxes = Axes.Both
}
};
}
private class TestCatchPlayfield : CatchEditorPlayfield
{
public TestCatchPlayfield()
: base(new BeatmapDifficulty { CircleSize = 0 })
{
}
}
}
}

View File

@ -0,0 +1,79 @@
// 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.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Testing;
using osu.Framework.Timing;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchPlacementBlueprintTestScene : PlacementBlueprintTestScene
{
protected const double TIME_SNAP = 100;
protected DrawableCatchHitObject LastObject;
protected new ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchPlacementBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
contentContainer.Playfield.Clock = new FramedClock(new ManualClock());
}
[SetUp]
public void Setup() => Schedule(() =>
{
HitObjectContainer.Clear();
ResetPlacement();
LastObject = null;
});
protected void AddMoveStep(double time, float x) => AddStep($"move to time={time}, x={x}", () =>
{
float y = HitObjectContainer.PositionAtTime(time);
Vector2 pos = HitObjectContainer.ToScreenSpace(new Vector2(x, y + HitObjectContainer.DrawHeight));
InputManager.MoveMouseTo(pos);
});
protected void AddClickStep(MouseButton button) => AddStep($"click {button}", () =>
{
InputManager.Click(button);
});
protected IEnumerable<FruitOutline> FruitOutlines => Content.ChildrenOfType<FruitOutline>();
// Unused because AddHitObject is overriden
protected override Container CreateHitObjectContainer() => new Container();
protected override void AddHitObject(DrawableHitObject hitObject)
{
LastObject = (DrawableCatchHitObject)hitObject;
contentContainer.Playfield.HitObjectContainer.Add(hitObject);
}
protected override SnapResult SnapForBlueprint(PlacementBlueprint blueprint)
{
var result = base.SnapForBlueprint(blueprint);
result.Time = Math.Round(HitObjectContainer.TimeAtScreenSpacePosition(result.ScreenSpacePosition) / TIME_SNAP) * TIME_SNAP;
return result;
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Tests.Visual;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public abstract class CatchSelectionBlueprintTestScene : SelectionBlueprintTestScene
{
protected ScrollingHitObjectContainer HitObjectContainer => contentContainer.Playfield.HitObjectContainer;
protected override Container<Drawable> Content => contentContainer;
private readonly CatchEditorTestSceneContainer contentContainer;
protected CatchSelectionBlueprintTestScene()
{
base.Content.Add(contentContainer = new CatchEditorTestSceneContainer());
}
}
}

View File

@ -0,0 +1,87 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Edit.Blueprints.Components;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneBananaShowerPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableBananaShower((BananaShower)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new BananaShowerPlacementBlueprint();
protected override void AddHitObject(DrawableHitObject hitObject)
{
// Create nested bananas (but positions are not randomized because beatmap processing is not done).
hitObject.HitObject.ApplyDefaults(new ControlPointInfo(), Beatmap.Value.BeatmapInfo.BaseDifficulty);
base.AddHitObject(hitObject);
}
[Test]
public void TestBasicPlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is placed", () => LastObject is DrawableBananaShower);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestReversePlacement()
{
const double start_time = 100;
const double end_time = 500;
AddMoveStep(end_time, 0);
AddClickStep(MouseButton.Left);
AddMoveStep(start_time, 0);
AddClickStep(MouseButton.Right);
AddAssert("start time is correct", () => Precision.AlmostEquals(LastObject.HitObject.StartTime, start_time));
AddAssert("end time is correct", () => Precision.AlmostEquals(LastObject.HitObject.GetEndTime(), end_time));
}
[Test]
public void TestFinishWithZeroDuration()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddClickStep(MouseButton.Right);
AddAssert("banana shower is not placed", () => LastObject == null);
AddAssert("state is waiting", () => CurrentBlueprint?.PlacementActive == PlacementBlueprint.PlacementState.Waiting);
}
[Test]
public void TestOpacity()
{
AddMoveStep(100, 0);
AddClickStep(MouseButton.Left);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
AddMoveStep(200, 0);
AddUntilStep("outline is opaque", () => Precision.AlmostEquals(timeSpanOutline.Alpha, 1));
AddMoveStep(100, 0);
AddUntilStep("outline is semitransparent", () => Precision.DefinitelyBigger(1, timeSpanOutline.Alpha));
}
private TimeSpanOutline timeSpanOutline => Content.ChildrenOfType<TimeSpanOutline>().Single();
}
}

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using NUnit.Framework;
using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawables;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osuTK.Input;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneFruitPlacementBlueprint : CatchPlacementBlueprintTestScene
{
protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableFruit((Fruit)hitObject);
protected override PlacementBlueprint CreateBlueprint() => new FruitPlacementBlueprint();
[Test]
public void TestFruitPlacementPosition()
{
const double time = 300;
const float x = CatchPlayfield.CENTER_X;
AddMoveStep(time, x);
AddClickStep(MouseButton.Left);
AddAssert("outline position is correct", () =>
{
var outline = FruitOutlines.Single();
return Precision.AlmostEquals(outline.X, x) &&
Precision.AlmostEquals(outline.Y, HitObjectContainer.PositionAtTime(time));
});
AddAssert("fruit time is correct", () => Precision.AlmostEquals(LastObject.StartTimeBindable.Value, time));
AddAssert("fruit position is correct", () => Precision.AlmostEquals(LastObject.X, x));
}
}
}

View File

@ -0,0 +1,38 @@
// 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.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Edit.Blueprints;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osuTK;
namespace osu.Game.Rulesets.Catch.Tests.Editor
{
public class TestSceneJuiceStreamSelectionBlueprint : CatchSelectionBlueprintTestScene
{
public TestSceneJuiceStreamSelectionBlueprint()
{
var hitObject = new JuiceStream
{
OriginalX = 100,
StartTime = 100,
Path = new SliderPath(PathType.PerfectCurve, new[]
{
Vector2.Zero,
new Vector2(200, 100),
new Vector2(0, 200),
}),
};
var controlPoint = new ControlPointInfo();
controlPoint.Add(0, new TimingControlPoint
{
BeatLength = 100
});
hitObject.ApplyDefaults(controlPoint, new BeatmapDifficulty { CircleSize = 0 });
AddBlueprint(new JuiceStreamSelectionBlueprint(hitObject));
}
}
}

View File

@ -0,0 +1,145 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSlidersTest
{
private CheckTooShortSliders check;
[SetUp]
public void Setup()
{
check = new CheckTooShortSliders();
}
[Test]
public void TestLongSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(100, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(25, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderExpert()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertOk(new List<HitObject> { slider }, DifficultyRating.Expert);
}
[Test]
public void TestTooShortSlider()
{
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 0,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
[Test]
public void TestTooShortSliderWithRepeats()
{
// Would be ok if we looked at the duration, but not if we look at the span duration.
Slider slider = new Slider
{
StartTime = 0,
RepeatCount = 2,
Path = new SliderPath(new[]
{
new PathControlPoint(new Vector2(0, 0)),
new PathControlPoint(new Vector2(10, 0))
})
};
slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
assertTooShort(new List<HitObject> { slider });
}
private void assertOk(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
Assert.That(check.Run(getContext(hitObjects, difficultyRating)), Is.Empty);
}
private void assertTooShort(List<HitObject> hitObjects, DifficultyRating difficultyRating = DifficultyRating.Easy)
{
var issues = check.Run(getContext(hitObjects, difficultyRating)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSliders.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, DifficultyRating difficultyRating)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap), difficultyRating);
}
}
}

View File

@ -0,0 +1,116 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Edit.Checks;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
{
[TestFixture]
public class CheckTooShortSpinnersTest
{
private CheckTooShortSpinners check;
private BeatmapDifficulty difficulty;
[SetUp]
public void Setup()
{
check = new CheckTooShortSpinners();
difficulty = new BeatmapDifficulty();
}
[Test]
public void TestLongSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 4000 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 750 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertOk(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestVeryShortSpinner()
{
// Spinners at a certain duration only get 1000 points if approached by auto at a certain angle, making it difficult to determine.
Spinner spinner = new Spinner { StartTime = 0, Duration = 475 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertVeryShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinner()
{
Spinner spinner = new Spinner { StartTime = 0, Duration = 400 };
spinner.ApplyDefaults(new ControlPointInfo(), difficulty);
assertTooShort(new List<HitObject> { spinner }, difficulty);
}
[Test]
public void TestTooShortSpinnerVaryingOd()
{
const double duration = 450;
var difficultyLowOd = new BeatmapDifficulty { OverallDifficulty = 1 };
Spinner spinnerLowOd = new Spinner { StartTime = 0, Duration = duration };
spinnerLowOd.ApplyDefaults(new ControlPointInfo(), difficultyLowOd);
var difficultyHighOd = new BeatmapDifficulty { OverallDifficulty = 10 };
Spinner spinnerHighOd = new Spinner { StartTime = 0, Duration = duration };
spinnerHighOd.ApplyDefaults(new ControlPointInfo(), difficultyHighOd);
assertOk(new List<HitObject> { spinnerLowOd }, difficultyLowOd);
assertTooShort(new List<HitObject> { spinnerHighOd }, difficultyHighOd);
}
private void assertOk(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
Assert.That(check.Run(getContext(hitObjects, beatmapDifficulty)), Is.Empty);
}
private void assertVeryShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateVeryShort);
}
private void assertTooShort(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var issues = check.Run(getContext(hitObjects, beatmapDifficulty)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckTooShortSpinners.IssueTemplateTooShort);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects, BeatmapDifficulty beatmapDifficulty)
{
var beatmap = new Beatmap<HitObject>
{
HitObjects = hitObjects,
BeatmapInfo = new BeatmapInfo { BaseDifficulty = beatmapDifficulty }
};
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSliders : ICheck
{
/// <summary>
/// The shortest acceptable duration between the head and tail of the slider (so ignoring repeats).
/// </summary>
private const double span_duration_threshold = 125; // 240 BPM 1/2
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short sliders");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
if (context.InterpretedDifficulty > DifficultyRating.Easy)
yield break;
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (hitObject is Slider slider && slider.SpanDuration < span_duration_threshold)
yield return new IssueTemplateTooShort(this).Create(slider);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This slider is too short ({0:0} ms), expected at least {1:0} ms.")
{
}
public Issue Create(Slider slider) => new Issue(slider, this, slider.SpanDuration, span_duration_threshold);
}
}
}

View File

@ -0,0 +1,61 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Edit.Checks
{
public class CheckTooShortSpinners : ICheck
{
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Spread, "Too short spinners");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateTooShort(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
double od = context.Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
// These are meant to reflect the duration necessary for auto to score at least 1000 points on the spinner.
// It's difficult to eliminate warnings here, as auto achieving 1000 points depends on the approach angle on some spinners.
double warningThreshold = 500 + (od < 5 ? (5 - od) * -21.8 : (od - 5) * 20); // Anything above this is always ok.
double problemThreshold = 450 + (od < 5 ? (5 - od) * -17 : (od - 5) * 17); // Anything below this is never ok.
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (!(hitObject is Spinner spinner))
continue;
if (spinner.Duration < problemThreshold)
yield return new IssueTemplateTooShort(this).Create(spinner);
else if (spinner.Duration < warningThreshold)
yield return new IssueTemplateVeryShort(this).Create(spinner);
}
}
public class IssueTemplateTooShort : IssueTemplate
{
public IssueTemplateTooShort(ICheck check)
: base(check, IssueType.Problem, "This spinner is too short. Auto cannot achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
public class IssueTemplateVeryShort : IssueTemplate
{
public IssueTemplateVeryShort(ICheck check)
: base(check, IssueType.Warning, "This spinner may be too short. Ensure auto can achieve 1000 points on this.")
{
}
public Issue Create(Spinner spinner) => new Issue(spinner, this);
}
}
}

View File

@ -15,10 +15,12 @@ namespace osu.Game.Rulesets.Osu.Edit
{
// Compose
new CheckOffscreenObjects(),
new CheckTooShortSpinners(),
// Spread
new CheckTimeDistanceEquality(),
new CheckLowDiffOverlaps()
new CheckLowDiffOverlaps(),
new CheckTooShortSliders(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -4,7 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Mods
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
/// <summary>
/// Number of previous hitobjects to be shifted together when another object is being moved.
/// </summary>
private const int preceding_hitobjects_to_shift = 10;
private Random rng;
public void ApplyToBeatmap(IBeatmap beatmap)
@ -49,8 +54,9 @@ namespace osu.Game.Rulesets.Osu.Mods
var current = new RandomObjectInfo(hitObject);
// rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams
if (i % 3 == 0)
// rateOfChangeMultiplier only changes every 5 iterations in a combo
// to prevent shaky-line-shaped streams
if (hitObject.IndexInCurrentCombo % 5 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (hitObject is Spinner)
@ -61,13 +67,35 @@ namespace osu.Game.Rulesets.Osu.Mods
applyRandomisation(rateOfChangeMultiplier, previous, current);
hitObject.Position = current.PositionRandomised;
// Move hit objects back into the playfield if they are outside of it
Vector2 shift = Vector2.Zero;
// update end position as it may have changed as a result of the position update.
current.EndPositionRandomised = current.PositionRandomised;
switch (hitObject)
{
case HitCircle circle:
shift = clampHitCircleToPlayfield(circle, current);
break;
if (hitObject is Slider slider)
moveSliderIntoPlayfield(slider, current);
case Slider slider:
shift = clampSliderToPlayfield(slider, current);
break;
}
if (shift != Vector2.Zero)
{
var toBeShifted = new List<OsuHitObject>();
for (int j = i - 1; j >= i - preceding_hitobjects_to_shift && j >= 0; j--)
{
// only shift hit circles
if (!(hitObjects[j] is HitCircle)) break;
toBeShifted.Add(hitObjects[j]);
}
if (toBeShifted.Count > 0)
applyDecreasingShift(toBeShifted, shift);
}
previous = current;
}
@ -94,7 +122,9 @@ namespace osu.Game.Rulesets.Osu.Mods
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal;
// Allow maximum jump angle when jump distance is more than half of playfield diagonal length
var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * Math.Min(1f, distanceToPrev / (playfield_diagonal * 0.5f));
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
if (current.AngleRad < 0)
@ -109,56 +139,120 @@ namespace osu.Game.Rulesets.Osu.Mods
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
var position = previous.EndPositionRandomised + posRelativeToPrev;
current.PositionRandomised = previous.EndPositionRandomised + posRelativeToPrev;
}
// Move hit objects back into the playfield if they are outside of it,
// which would sometimes happen during big jumps otherwise.
position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X);
position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y);
/// <summary>
/// Move the randomised position of a hit circle so that it fits inside the playfield.
/// </summary>
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampHitCircleToPlayfield(HitCircle circle, RandomObjectInfo objectInfo)
{
var previousPosition = objectInfo.PositionRandomised;
objectInfo.EndPositionRandomised = objectInfo.PositionRandomised = clampToPlayfieldWithPadding(
objectInfo.PositionRandomised,
(float)circle.Radius
);
current.PositionRandomised = position;
circle.Position = objectInfo.PositionRandomised;
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo)
/// <returns>The deviation from the original randomised position in order to fit within the playfield.</returns>
private Vector2 clampSliderToPlayfield(Slider slider, RandomObjectInfo objectInfo)
{
var minMargin = getMinSliderMargin(slider);
var possibleMovementBounds = calculatePossibleMovementBounds(slider);
slider.Position = new Vector2(
Math.Clamp(slider.Position.X, minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right),
Math.Clamp(slider.Position.Y, minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom)
);
var previousPosition = objectInfo.PositionRandomised;
currentObjectInfo.PositionRandomised = slider.Position;
currentObjectInfo.EndPositionRandomised = slider.EndPosition;
// Clamp slider position to the placement area
// If the slider is larger than the playfield, force it to stay at the original position
var newX = possibleMovementBounds.Width < 0
? objectInfo.PositionOriginal.X
: Math.Clamp(previousPosition.X, possibleMovementBounds.Left, possibleMovementBounds.Right);
shiftNestedObjects(slider, currentObjectInfo.PositionRandomised - currentObjectInfo.PositionOriginal);
var newY = possibleMovementBounds.Height < 0
? objectInfo.PositionOriginal.Y
: Math.Clamp(previousPosition.Y, possibleMovementBounds.Top, possibleMovementBounds.Bottom);
slider.Position = objectInfo.PositionRandomised = new Vector2(newX, newY);
objectInfo.EndPositionRandomised = slider.EndPosition;
shiftNestedObjects(slider, objectInfo.PositionRandomised - objectInfo.PositionOriginal);
return objectInfo.PositionRandomised - previousPosition;
}
/// <summary>
/// Calculates the min. distances from the <see cref="Slider"/>'s position to the playfield border for the slider to be fully inside of the playfield.
/// Decreasingly shift a list of <see cref="OsuHitObject"/>s by a specified amount.
/// The first item in the list is shifted by the largest amount, while the last item is shifted by the smallest amount.
/// </summary>
private MarginPadding getMinSliderMargin(Slider slider)
/// <param name="hitObjects">The list of hit objects to be shifted.</param>
/// <param name="shift">The amount to be shifted.</param>
private void applyDecreasingShift(IList<OsuHitObject> hitObjects, Vector2 shift)
{
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
// The first object is shifted by a vector slightly smaller than shift
// The last object is shifted by a vector slightly larger than zero
Vector2 position = hitObject.Position + shift * ((hitObjects.Count - i) / (float)(hitObjects.Count + 1));
hitObject.Position = clampToPlayfieldWithPadding(position, (float)hitObject.Radius);
}
}
/// <summary>
/// Calculates a <see cref="RectangleF"/> which contains all of the possible movements of the slider (in relative X/Y coordinates)
/// such that the entire slider is inside the playfield.
/// </summary>
/// <remarks>
/// If the slider is larger than the playfield, the returned <see cref="RectangleF"/> may have negative width/height.
/// </remarks>
private RectangleF calculatePossibleMovementBounds(Slider slider)
{
var pathPositions = new List<Vector2>();
slider.Path.GetPathToProgress(pathPositions, 0, 1);
var minMargin = new MarginPadding();
float minX = float.PositiveInfinity;
float maxX = float.NegativeInfinity;
float minY = float.PositiveInfinity;
float maxY = float.NegativeInfinity;
// Compute the bounding box of the slider.
foreach (var pos in pathPositions)
{
minMargin.Left = Math.Max(minMargin.Left, -pos.X);
minMargin.Right = Math.Max(minMargin.Right, pos.X);
minMargin.Top = Math.Max(minMargin.Top, -pos.Y);
minMargin.Bottom = Math.Max(minMargin.Bottom, pos.Y);
minX = MathF.Min(minX, pos.X);
maxX = MathF.Max(maxX, pos.X);
minY = MathF.Min(minY, pos.Y);
maxY = MathF.Max(maxY, pos.Y);
}
minMargin.Left = Math.Min(minMargin.Left, OsuPlayfield.BASE_SIZE.X - minMargin.Right);
minMargin.Top = Math.Min(minMargin.Top, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom);
// Take the circle radius into account.
var radius = (float)slider.Radius;
return minMargin;
minX -= radius;
minY -= radius;
maxX += radius;
maxY += radius;
// Given the bounding box of the slider (via min/max X/Y),
// the amount that the slider can move to the left is minX (with the sign flipped, since positive X is to the right),
// and the amount that it can move to the right is WIDTH - maxX.
// Same calculation applies for the Y axis.
float left = -minX;
float right = OsuPlayfield.BASE_SIZE.X - maxX;
float top = -minY;
float bottom = OsuPlayfield.BASE_SIZE.Y - maxY;
return new RectangleF(left, top, right - left, bottom - top);
}
/// <summary>
@ -177,6 +271,20 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
/// <summary>
/// Clamp a position to playfield, keeping a specified distance from the edges.
/// </summary>
/// <param name="position">The position to be clamped.</param>
/// <param name="padding">The minimum distance allowed from playfield edges.</param>
/// <returns>The clamped position.</returns>
private Vector2 clampToPlayfieldWithPadding(Vector2 position, float padding)
{
return new Vector2(
Math.Clamp(position.X, padding, OsuPlayfield.BASE_SIZE.X - padding),
Math.Clamp(position.Y, padding, OsuPlayfield.BASE_SIZE.Y - padding)
);
}
private class RandomObjectInfo
{
public float AngleRad { get; set; }

View File

@ -0,0 +1,94 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using Moq;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Checks;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using osuTK;
namespace osu.Game.Tests.Editing.Checks
{
[TestFixture]
public class CheckZeroLengthObjectsTest
{
private CheckZeroLengthObjects check;
[SetUp]
public void Setup()
{
check = new CheckZeroLengthObjects();
}
[Test]
public void TestCircle()
{
assertOk(new List<HitObject>
{
new HitCircle { StartTime = 1000, Position = new Vector2(0, 0) }
});
}
[Test]
public void TestRegularSlider()
{
assertOk(new List<HitObject>
{
getSliderMock(1000).Object
});
}
[Test]
public void TestZeroLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(0).Object
});
}
[Test]
public void TestNegativeLengthSlider()
{
assertZeroLength(new List<HitObject>
{
getSliderMock(-1000).Object
});
}
private Mock<Slider> getSliderMock(double duration)
{
var mockSlider = new Mock<Slider>();
mockSlider.As<IHasDuration>().Setup(d => d.Duration).Returns(duration);
return mockSlider;
}
private void assertOk(List<HitObject> hitObjects)
{
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
}
private void assertZeroLength(List<HitObject> hitObjects)
{
var issues = check.Run(getContext(hitObjects)).ToList();
Assert.That(issues, Has.Count.EqualTo(1));
Assert.That(issues.First().Template is CheckZeroLengthObjects.IssueTemplateZeroLength);
}
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
{
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
}
}
}

View File

@ -21,6 +21,7 @@ using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens;
using osu.Game.Screens.OnlinePlay.Components;
using osu.Game.Screens.OnlinePlay.Lounge;
using osu.Game.Screens.OnlinePlay.Match.Components;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
@ -184,6 +185,26 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("play started", () => !multiplayerScreen.IsCurrentScreen());
}
[Test]
public void TestSubScreenExitedWhenDisconnectedFromMultiplayerServer()
{
createRoom(() => new Room
{
Name = { Value = "Test Room" },
Playlist =
{
new PlaylistItem
{
Beatmap = { Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.RulesetID == 0)).BeatmapInfo },
Ruleset = { Value = new OsuRuleset().RulesetInfo },
}
}
});
AddStep("disconnect", () => client.Disconnect());
AddUntilStep("back in lounge", () => this.ChildrenOfType<LoungeSubScreen>().FirstOrDefault()?.IsCurrentScreen() == true);
}
[Test]
public void TestLeaveNavigation()
{

View File

@ -4,7 +4,6 @@
using System.Linq;
using NUnit.Framework;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Testing;
using osu.Game.Overlays.Settings;
using osu.Game.Overlays;
@ -17,28 +16,65 @@ namespace osu.Game.Tests.Visual.Settings
[Test]
public void TestRestoreDefaultValueButtonVisibility()
{
TestSettingsTextBox textBox = null;
SettingsTextBox textBox = null;
RestoreDefaultValueButton<string> restoreDefaultValueButton = null;
AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox
AddStep("create settings item", () =>
{
Current = new Bindable<string>
Child = textBox = new SettingsTextBox
{
Default = "test",
Value = "test"
}
Current = new Bindable<string>
{
Default = "test",
Value = "test"
}
};
restoreDefaultValueButton = textBox.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
});
AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value from default", () => textBox.Current.Value = "non-default");
AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0);
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddStep("restore default", () => textBox.Current.SetDefault());
AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0);
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
private class TestSettingsTextBox : SettingsTextBox
/// <summary>
/// Ensures that the reset to default button uses the correct implementation of IsDefault to determine whether it should be shown or not.
/// Values have been chosen so that after being set, Value != Default (but they are close enough that the difference is negligible compared to Precision).
/// </summary>
[TestCase(4.2f)]
[TestCase(9.9f)]
public void TestRestoreDefaultValueButtonPrecision(float initialValue)
{
public Drawable RestoreDefaultValueButton => this.ChildrenOfType<RestoreDefaultValueButton<string>>().Single();
BindableFloat current = null;
SettingsSlider<float> sliderBar = null;
RestoreDefaultValueButton<float> restoreDefaultValueButton = null;
AddStep("create settings item", () =>
{
Child = sliderBar = new SettingsSlider<float>
{
Current = current = new BindableFloat(initialValue)
{
MinValue = 0f,
MaxValue = 10f,
Precision = 0.1f,
}
};
restoreDefaultValueButton = sliderBar.ChildrenOfType<RestoreDefaultValueButton<float>>().Single();
});
AddAssert("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
AddStep("change value to next closest", () => sliderBar.Current.Value += current.Precision * 0.6f);
AddUntilStep("restore button shown", () => restoreDefaultValueButton.Alpha > 0);
AddStep("restore default", () => sliderBar.Current.SetDefault());
AddUntilStep("restore button hidden", () => restoreDefaultValueButton.Alpha == 0);
}
}
}
}

View File

@ -31,10 +31,11 @@ namespace osu.Game.Tests.Visual.SongSelect
private readonly TestBeatmapDifficultyCache testDifficultyCache = new TestBeatmapDifficultyCache();
[Test]
public void TestLocal([Values("Beatmap", "Some long title and stuff")]
string title,
[Values("Trial", "Some1's very hardest difficulty")]
string version)
public void TestLocal(
[Values("Beatmap", "Some long title and stuff")]
string title,
[Values("Trial", "Some1's very hardest difficulty")]
string version)
{
showMetadataForBeatmap(() => CreateWorkingBeatmap(new Beatmap
{

View File

@ -157,6 +157,23 @@ namespace osu.Game.Tests.Visual.UserInterface
checkBindableAtValue("Circle Size", 3);
}
[Test]
public void TestResetToDefaults()
{
setBeatmapWithDifficultyParameters(5);
setSliderValue("Circle Size", 3);
setExtendedLimits(true);
checkSliderAtValue("Circle Size", 3);
checkBindableAtValue("Circle Size", 3);
AddStep("reset mod settings", () => modDifficultyAdjust.ResetSettingsToDefaults());
checkSliderAtValue("Circle Size", 5);
checkBindableAtValue("Circle Size", null);
}
private void resetToDefault(string name)
{
AddStep($"Reset {name} to default", () =>

View File

@ -95,6 +95,15 @@ _**italic with underscore, bold with asterisk**_";
});
}
[Test]
public void TestAutoLink()
{
AddStep("Add autolink", () =>
{
markdownContainer.Text = "<https://discord.gg/ppy>";
});
}
[Test]
public void TestInlineCode()
{

View File

@ -26,6 +26,12 @@ namespace osu.Game.Graphics.Containers.Markdown
title = linkInline.Title;
}
public OsuMarkdownLinkText(AutolinkInline autolinkInline)
: base(autolinkInline)
{
text = autolinkInline.Url;
}
[BackgroundDependencyLoader]
private void load()
{

View File

@ -17,6 +17,9 @@ namespace osu.Game.Graphics.Containers.Markdown
protected override void AddLinkText(string text, LinkInline linkInline)
=> AddDrawable(new OsuMarkdownLinkText(text, linkInline));
protected override void AddAutoLink(AutolinkInline autolinkInline)
=> AddDrawable(new OsuMarkdownLinkText(autolinkInline));
protected override void AddImage(LinkInline linkInline) => AddDrawable(new OsuMarkdownImage(linkInline));
// TODO : Change font to monospace

View File

@ -3,6 +3,7 @@
using osu.Framework.Extensions.Color4Extensions;
using osu.Game.Beatmaps;
using osu.Game.Overlays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
using osuTK.Graphics;
@ -198,8 +199,14 @@ namespace osu.Game.Graphics
public readonly Color4 GrayE = Color4Extensions.FromHex(@"eee");
public readonly Color4 GrayF = Color4Extensions.FromHex(@"fff");
// in latest editor design logic, need to figure out where these sit...
/// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Lime"/>'s <see cref="OverlayColourProvider.Colour1"/>.
/// </summary>
public readonly Color4 Lime1 = Color4Extensions.FromHex(@"b2ff66");
/// <summary>
/// Equivalent to <see cref="OverlayColourProvider.Orange"/>'s <see cref="OverlayColourProvider.Colour1"/>.
/// </summary>
public readonly Color4 Orange1 = Color4Extensions.FromHex(@"ffd966");
// Content Background

View File

@ -14,6 +14,7 @@ namespace osu.Game.Overlays
public static OverlayColourProvider Red { get; } = new OverlayColourProvider(OverlayColourScheme.Red);
public static OverlayColourProvider Pink { get; } = new OverlayColourProvider(OverlayColourScheme.Pink);
public static OverlayColourProvider Orange { get; } = new OverlayColourProvider(OverlayColourScheme.Orange);
public static OverlayColourProvider Lime { get; } = new OverlayColourProvider(OverlayColourScheme.Lime);
public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green);
public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple);
public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue);
@ -68,6 +69,9 @@ namespace osu.Game.Overlays
case OverlayColourScheme.Orange:
return 46 / 360f;
case OverlayColourScheme.Lime:
return 90 / 360f;
case OverlayColourScheme.Green:
return 115 / 360f;
@ -85,6 +89,7 @@ namespace osu.Game.Overlays
Red,
Pink,
Orange,
Lime,
Green,
Purple,
Blue

View File

@ -20,15 +20,26 @@ namespace osu.Game.Overlays
{
public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
// this is done to ensure a click on this button doesn't trigger focus on a parent element which contains the button.
public override bool AcceptsFocus => true;
// this is intentionally not using BindableWithCurrent, as it can use the wrong IsDefault implementation when passed a BindableNumber.
// using GetBoundCopy() ensures that the received bindable is of the exact same type as the source bindable and uses the proper IsDefault implementation.
private Bindable<T> current;
public Bindable<T> Current
{
get => current.Current;
set => current.Current = value;
get => current;
set
{
current?.UnbindAll();
current = value.GetBoundCopy();
current.ValueChanged += _ => UpdateState();
current.DefaultChanged += _ => UpdateState();
current.DisabledChanged += _ => UpdateState();
UpdateState();
}
}
private Color4 buttonColour;
@ -62,18 +73,14 @@ namespace osu.Game.Overlays
Action += () =>
{
if (!current.Disabled) current.SetDefault();
if (!current.Disabled)
current.SetDefault();
};
}
protected override void LoadComplete()
{
base.LoadComplete();
Current.ValueChanged += _ => UpdateState();
Current.DisabledChanged += _ => UpdateState();
Current.DefaultChanged += _ => UpdateState();
UpdateState();
}

View File

@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Edit
// Compose
new CheckUnsnappedObjects(),
new CheckConcurrentObjects()
new CheckConcurrentObjects(),
new CheckZeroLengthObjects(),
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)

View File

@ -0,0 +1,47 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Rulesets.Edit.Checks.Components;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Edit.Checks
{
public class CheckZeroLengthObjects : ICheck
{
/// <summary>
/// The duration can be this low before being treated as having no length, in case of precision errors. Unit is milliseconds.
/// </summary>
private const double leniency = 0.5d;
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Zero-length hitobjects");
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
{
new IssueTemplateZeroLength(this)
};
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
{
foreach (var hitObject in context.Beatmap.HitObjects)
{
if (!(hitObject is IHasDuration hasDuration))
continue;
if (hasDuration.Duration < leniency)
yield return new IssueTemplateZeroLength(this).Create(hitObject, hasDuration.Duration);
}
}
public class IssueTemplateZeroLength : IssueTemplate
{
public IssueTemplateZeroLength(ICheck check)
: base(check, IssueType.Problem, "{0} has a duration of {1:0}.")
{
}
public Issue Create(HitObject hitobject, double duration) => new Issue(hitobject, this, hitobject.GetType(), duration);
}
}
}

View File

@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Edit
/// </summary>
protected virtual bool AlwaysShowWhenSelected => false;
protected override bool ShouldBeAlive => (DrawableObject.IsAlive && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected override bool ShouldBeAlive => (DrawableObject?.IsAlive == true && DrawableObject.IsPresent) || (AlwaysShowWhenSelected && State == SelectionState.Selected);
protected HitObjectSelectionBlueprint(HitObject hitObject)
: base(hitObject)

View File

@ -71,6 +71,12 @@ namespace osu.Game.Rulesets.Mods
}
public DifficultyBindable()
: this(null)
{
}
public DifficultyBindable(float? defaultValue = null)
: base(defaultValue)
{
ExtendedLimits.BindValueChanged(_ => updateMaxValue());
}
@ -93,15 +99,35 @@ namespace osu.Game.Rulesets.Mods
CurrentNumber.MaxValue = ExtendedLimits.Value && extendedMaxValue != null ? extendedMaxValue.Value : maxValue;
}
public new DifficultyBindable GetBoundCopy() => new DifficultyBindable
public override void BindTo(Bindable<float?> them)
{
BindTarget = this,
CurrentNumber = { BindTarget = CurrentNumber },
ExtendedLimits = { BindTarget = ExtendedLimits },
ReadCurrentFromDifficulty = ReadCurrentFromDifficulty,
// the following is only safe as long as these values are effectively constants.
MaxValue = maxValue,
ExtendedMaxValue = extendedMaxValue
};
if (!(them is DifficultyBindable otherDifficultyBindable))
throw new InvalidOperationException($"Cannot bind to a non-{nameof(DifficultyBindable)}.");
ReadCurrentFromDifficulty = otherDifficultyBindable.ReadCurrentFromDifficulty;
// the following max value copies are only safe as long as these values are effectively constants.
MaxValue = otherDifficultyBindable.maxValue;
ExtendedMaxValue = otherDifficultyBindable.extendedMaxValue;
ExtendedLimits.BindTarget = otherDifficultyBindable.ExtendedLimits;
// the actual values need to be copied after the max value constraints.
CurrentNumber.BindTarget = otherDifficultyBindable.CurrentNumber;
base.BindTo(them);
}
public override void UnbindFrom(IUnbindable them)
{
if (!(them is DifficultyBindable otherDifficultyBindable))
throw new InvalidOperationException($"Cannot unbind from a non-{nameof(DifficultyBindable)}.");
base.UnbindFrom(them);
CurrentNumber.UnbindFrom(otherDifficultyBindable.CurrentNumber);
ExtendedLimits.UnbindFrom(otherDifficultyBindable.ExtendedLimits);
}
public new DifficultyBindable GetBoundCopy() => new DifficultyBindable { BindTarget = this };
}
}

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Audio;
@ -72,25 +71,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
{
var localUser = Client.LocalUser;
if (localUser == null)
return;
int newCountReady = Room?.Users.Count(u => u.State == MultiplayerUserState.Ready) ?? 0;
int newCountTotal = Room?.Users.Count(u => u.State != MultiplayerUserState.Spectating) ?? 0;
Debug.Assert(Room != null);
int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready);
int newCountTotal = Room.Users.Count(u => u.State != MultiplayerUserState.Spectating);
string countText = $"({newCountReady} / {newCountTotal} ready)";
switch (localUser.State)
switch (localUser?.State)
{
case MultiplayerUserState.Idle:
default:
button.Text = "Ready";
updateButtonColour(true);
break;
case MultiplayerUserState.Spectating:
case MultiplayerUserState.Ready:
string countText = $"({newCountReady} / {newCountTotal} ready)";
if (Room?.Host?.Equals(localUser) == true)
{
button.Text = $"Start match {countText}";
@ -108,7 +102,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
bool enableButton = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value;
// When the local user is the host and spectating the match, the "start match" state should be enabled if any users are ready.
if (localUser.State == MultiplayerUserState.Spectating)
if (localUser?.State == MultiplayerUserState.Spectating)
enableButton &= Room?.Host?.Equals(localUser) == true && newCountReady > 0;
button.Enabled.Value = enableButton;

View File

@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@ -57,14 +56,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
private void updateState()
{
var localUser = Client.LocalUser;
if (localUser == null)
return;
Debug.Assert(Room != null);
switch (localUser.State)
switch (Client.LocalUser?.State)
{
default:
button.Text = "Spectate";
@ -81,7 +73,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
break;
}
button.Enabled.Value = Client.Room?.State != MultiplayerRoomState.Closed && !operationInProgress.Value;
button.Enabled.Value = Client.Room != null
&& Client.Room.State != MultiplayerRoomState.Closed
&& !operationInProgress.Value;
}
private class ButtonWithTrianglesExposed : TriangleButton

View File

@ -48,6 +48,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
[Resolved]
private OngoingOperationTracker ongoingOperationTracker { get; set; }
[Resolved]
private Bindable<Room> currentRoom { get; set; }
private MultiplayerMatchSettingsOverlay settingsOverlay;
private readonly IBindable<bool> isConnected = new Bindable<bool>();
@ -273,6 +276,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
if (!connected.NewValue)
Schedule(this.Exit);
}, true);
currentRoom.BindValueChanged(room =>
{
if (room.NewValue == null)
{
// the room has gone away.
// this could mean something happened during the join process, or an external connection issue occurred.
// one specific scenario is where the underlying room is created, but the signalr server returns an error during the join process. this triggers a PartRoom operation (see https://github.com/ppy/osu/blob/7654df94f6f37b8382be7dfcb4f674e03bd35427/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs#L97)
Schedule(this.Exit);
}
}, true);
}
protected override void UpdateMods()
@ -310,7 +324,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer
public override bool OnExiting(IScreen next)
{
if (client.Room == null)
// the room may not be left immediately after a disconnection due to async flow,
// so checking the IsConnected status is also required.
if (client.Room == null || !client.IsConnected.Value)
{
// room has not been created yet; exit immediately.
return base.OnExiting(next);

View File

@ -17,11 +17,11 @@ namespace osu.Game.Tests.Visual
public abstract class PlacementBlueprintTestScene : OsuManualInputManagerTestScene, IPlacementHandler
{
protected readonly Container HitObjectContainer;
private PlacementBlueprint currentBlueprint;
protected PlacementBlueprint CurrentBlueprint { get; private set; }
protected PlacementBlueprintTestScene()
{
Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
base.Content.Add(HitObjectContainer = CreateHitObjectContainer().With(c => c.Clock = new FramedClock(new StopwatchClock())));
}
[BackgroundDependencyLoader]
@ -63,9 +63,9 @@ namespace osu.Game.Tests.Visual
protected void ResetPlacement()
{
if (currentBlueprint != null)
Remove(currentBlueprint);
Add(currentBlueprint = CreateBlueprint());
if (CurrentBlueprint != null)
Remove(CurrentBlueprint);
Add(CurrentBlueprint = CreateBlueprint());
}
public void Delete(HitObject hitObject)
@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual
{
base.Update();
currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint));
CurrentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(CurrentBlueprint));
}
protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) =>

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 JetBrains.Annotations;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Timing;
@ -23,7 +24,7 @@ namespace osu.Game.Tests.Visual
});
}
protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, DrawableHitObject drawableObject)
protected void AddBlueprint(HitObjectSelectionBlueprint blueprint, [CanBeNull] DrawableHitObject drawableObject = null)
{
Add(blueprint.With(d =>
{

View File

@ -35,8 +35,8 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.707.0" />
<PackageReference Include="Realm" Version="10.3.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
<PackageReference Include="Sentry" Version="3.6.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />

View File

@ -70,7 +70,7 @@
<Reference Include="System.Net.Http" />
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework.iOS" Version="2021.713.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.706.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
@ -93,12 +93,12 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2021.707.0" />
<PackageReference Include="ppy.osu.Framework" Version="2021.713.0" />
<PackageReference Include="SharpCompress" Version="0.28.3" />
<PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="ppy.osu.Framework.NativeLibs" Version="2021.115.0" ExcludeAssets="all" />
<PackageReference Include="Realm" Version="10.2.1" />
<PackageReference Include="Realm" Version="10.3.0" />
</ItemGroup>
</Project>