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:
commit
c5977f3318
@ -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>
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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 })
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
48
osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs
Normal file
48
osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSliders.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
Normal file
61
osu.Game.Rulesets.Osu/Edit/Checks/CheckTooShortSpinners.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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; }
|
||||
|
94
osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs
Normal file
94
osu.Game.Tests/Editing/Checks/CheckZeroLengthObjectsTest.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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", () =>
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
new CheckConcurrentObjects()
|
||||
new CheckConcurrentObjects(),
|
||||
new CheckZeroLengthObjects(),
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
|
47
osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs
Normal file
47
osu.Game/Rulesets/Edit/Checks/CheckZeroLengthObjects.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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) =>
|
||||
|
@ -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 =>
|
||||
{
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user