mirror of
https://github.com/ppy/osu.git
synced 2025-03-05 13:42:59 +08:00
Merge branch 'fix-selection-handler-visibility' into skin-bindables
This commit is contained in:
commit
fdd0713988
@ -0,0 +1,80 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Tests.Visual;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneTimingBasedNoteColouring : OsuTestScene
|
||||
{
|
||||
[Resolved]
|
||||
private RulesetConfigCache configCache { get; set; }
|
||||
|
||||
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
const double beat_length = 500;
|
||||
|
||||
var ruleset = new ManiaRuleset();
|
||||
|
||||
var beatmap = new ManiaBeatmap(new StageDefinition { Columns = 1 })
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Note { StartTime = 0 },
|
||||
new Note { StartTime = beat_length / 16 },
|
||||
new Note { StartTime = beat_length / 12 },
|
||||
new Note { StartTime = beat_length / 8 },
|
||||
new Note { StartTime = beat_length / 6 },
|
||||
new Note { StartTime = beat_length / 4 },
|
||||
new Note { StartTime = beat_length / 3 },
|
||||
new Note { StartTime = beat_length / 2 },
|
||||
new Note { StartTime = beat_length }
|
||||
},
|
||||
ControlPointInfo = new ControlPointInfo(),
|
||||
BeatmapInfo = { Ruleset = ruleset.RulesetInfo },
|
||||
};
|
||||
|
||||
foreach (var note in beatmap.HitObjects)
|
||||
{
|
||||
note.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
}
|
||||
|
||||
beatmap.ControlPointInfo.Add(0, new TimingControlPoint
|
||||
{
|
||||
BeatLength = beat_length
|
||||
});
|
||||
|
||||
Child = new Container
|
||||
{
|
||||
Clock = new FramedClock(new ManualClock()),
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Children = new[]
|
||||
{
|
||||
ruleset.CreateDrawableRulesetWith(beatmap)
|
||||
}
|
||||
};
|
||||
|
||||
var config = (ManiaRulesetConfigManager)configCache.GetConfigFor(Ruleset.Value.CreateInstance());
|
||||
config.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
|
||||
|
||||
AddStep("Enable", () => configTimingBasedNoteColouring.Value = true);
|
||||
AddStep("Disable", () => configTimingBasedNoteColouring.Value = false);
|
||||
}
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
|
||||
SetDefault(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
|
||||
SetDefault(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||
SetDefault(ManiaRulesetSetting.TimingBasedNoteColouring, false);
|
||||
}
|
||||
|
||||
public override TrackedSettings CreateTrackedSettings() => new TrackedSettings
|
||||
@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
||||
public enum ManiaRulesetSetting
|
||||
{
|
||||
ScrollTime,
|
||||
ScrollDirection
|
||||
ScrollDirection,
|
||||
TimingBasedNoteColouring
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,11 @@ namespace osu.Game.Rulesets.Mania
|
||||
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||
KeyboardStep = 5
|
||||
},
|
||||
new SettingsCheckbox
|
||||
{
|
||||
LabelText = "Timing-based note colouring",
|
||||
Current = config.GetBindable<bool>(ManiaRulesetSetting.TimingBasedNoteColouring),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -2,13 +2,19 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Diagnostics;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
using osu.Game.Rulesets.Mania.Skinning.Default;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Skinning;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
{
|
||||
@ -17,6 +23,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
/// </summary>
|
||||
public class DrawableNote : DrawableManiaHitObject<Note>, IKeyBindingHandler<ManiaAction>
|
||||
{
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[Resolved(canBeNull: true)]
|
||||
private IBeatmap beatmap { get; set; }
|
||||
|
||||
private readonly Bindable<bool> configTimingBasedNoteColouring = new Bindable<bool>();
|
||||
|
||||
protected virtual ManiaSkinComponents Component => ManiaSkinComponents.Note;
|
||||
|
||||
private readonly Drawable headPiece;
|
||||
@ -34,6 +48,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
});
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(ManiaRulesetConfigManager rulesetConfig)
|
||||
{
|
||||
rulesetConfig?.BindWith(ManiaRulesetSetting.TimingBasedNoteColouring, configTimingBasedNoteColouring);
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
HitObject.StartTimeBindable.BindValueChanged(_ => updateSnapColour());
|
||||
configTimingBasedNoteColouring.BindValueChanged(_ => updateSnapColour(), true);
|
||||
}
|
||||
|
||||
protected override void OnDirectionChanged(ValueChangedEvent<ScrollingDirection> e)
|
||||
{
|
||||
base.OnDirectionChanged(e);
|
||||
@ -73,5 +99,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
|
||||
public virtual void OnReleased(ManiaAction action)
|
||||
{
|
||||
}
|
||||
|
||||
private void updateSnapColour()
|
||||
{
|
||||
if (beatmap == null) return;
|
||||
|
||||
int snapDivisor = beatmap.ControlPointInfo.GetClosestBeatDivisor(HitObject.StartTime);
|
||||
|
||||
Colour = configTimingBasedNoteColouring.Value ? BindableBeatDivisor.GetColourFor(snapDivisor, colours) : Color4.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,7 +2,6 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Rulesets.Mania.Objects.Types;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
@ -2,53 +2,15 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public class OsuModBarrelRoll : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToDrawableHitObjects
|
||||
public class OsuModBarrelRoll : ModBarrelRoll<OsuHitObject>, IApplicableToDrawableHitObjects
|
||||
{
|
||||
private float currentRotation;
|
||||
|
||||
[SettingSource("Roll speed", "Rotations per minute")]
|
||||
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
|
||||
{
|
||||
MinValue = 0.02,
|
||||
MaxValue = 12,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = currentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
|
||||
{
|
||||
// scale the playfield to allow all hitobjects to stay within the visible region.
|
||||
drawableRuleset.Playfield.Scale = new Vector2(OsuPlayfield.BASE_SIZE.Y / OsuPlayfield.BASE_SIZE.X);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
|
||||
{
|
||||
foreach (var d in drawables)
|
||||
@ -58,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
switch (d)
|
||||
{
|
||||
case DrawableHitCircle circle:
|
||||
circle.CirclePiece.Rotation = -currentRotation;
|
||||
circle.CirclePiece.Rotation = -CurrentRotation;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
194
osu.Game.Tests/Editing/Checks/CheckConcurrentObjectsTest.cs
Normal file
194
osu.Game.Tests/Editing/Checks/CheckConcurrentObjectsTest.cs
Normal file
@ -0,0 +1,194 @@
|
||||
// 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.Checks;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckConcurrentObjectsTest
|
||||
{
|
||||
private CheckConcurrentObjects check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckConcurrentObjects();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesSeparate()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 150 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesConcurrent()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 100 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCirclesAlmostConcurrent()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 100 },
|
||||
new HitCircle { StartTime = 101 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersSeparate()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
getSliderMock(startTime: 500, endTime: 900.75d).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersConcurrent()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
getSliderMock(startTime: 300, endTime: 700.75d).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSlidersAlmostConcurrent()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
getSliderMock(startTime: 402, endTime: 902.75d).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderAndCircleConcurrent()
|
||||
{
|
||||
assertConcurrentDifferent(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
new HitCircle { StartTime = 300 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestManyObjectsConcurrent()
|
||||
{
|
||||
var hitobjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object,
|
||||
getSliderMock(startTime: 200, endTime: 500.75d).Object,
|
||||
new HitCircle { StartTime = 300 }
|
||||
};
|
||||
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(3));
|
||||
Assert.That(issues.Where(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent).ToList(), Has.Count.EqualTo(2));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesSeparateOnSameColumn()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
|
||||
getHoldNoteMock(startTime: 500, endTime: 900.75d, column: 1).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesConcurrentOnDifferentColumns()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
|
||||
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 2).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHoldNotesConcurrentOnSameColumn()
|
||||
{
|
||||
assertConcurrentSame(new List<HitObject>
|
||||
{
|
||||
getHoldNoteMock(startTime: 100, endTime: 400.75d, column: 1).Object,
|
||||
getHoldNoteMock(startTime: 300, endTime: 700.75d, column: 1).Object
|
||||
});
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
|
||||
{
|
||||
var mock = new Mock<Slider>();
|
||||
mock.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mock.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
|
||||
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private Mock<HoldNote> getHoldNoteMock(double startTime, double endTime, int column)
|
||||
{
|
||||
var mock = new Mock<HoldNote>();
|
||||
mock.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mock.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
mock.As<IHasColumn>().Setup(c => c.Column).Returns(column);
|
||||
|
||||
return mock;
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitobjects)
|
||||
{
|
||||
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertConcurrentSame(List<HitObject> hitobjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentSame));
|
||||
}
|
||||
|
||||
private void assertConcurrentDifferent(List<HitObject> hitobjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckConcurrentObjects.IssueTemplateConcurrentDifferent));
|
||||
}
|
||||
|
||||
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
|
||||
{
|
||||
return new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = hitobjects
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
155
osu.Game.Tests/Editing/Checks/CheckUnsnappedObjectsTest.cs
Normal file
155
osu.Game.Tests/Editing/Checks/CheckUnsnappedObjectsTest.cs
Normal file
@ -0,0 +1,155 @@
|
||||
// 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.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckUnsnappedObjectsTest
|
||||
{
|
||||
private CheckUnsnappedObjects check;
|
||||
private ControlPointInfo cpi;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckUnsnappedObjects();
|
||||
|
||||
cpi = new ControlPointInfo();
|
||||
cpi.Add(100, new TimingControlPoint { BeatLength = 100 });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleSnapped()
|
||||
{
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 100 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleUnsnapped1Ms()
|
||||
{
|
||||
assert1Ms(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 101 }
|
||||
});
|
||||
|
||||
assert1Ms(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 99 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleUnsnapped2Ms()
|
||||
{
|
||||
assert2Ms(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 102 }
|
||||
});
|
||||
|
||||
assert2Ms(new List<HitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 98 }
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderSnapped()
|
||||
{
|
||||
// Slider ends are naturally < 1 ms unsnapped because of how SV works.
|
||||
assertOk(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 100, endTime: 400.75d).Object
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderUnsnapped1Ms()
|
||||
{
|
||||
assert1Ms(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 101, endTime: 401.75d).Object
|
||||
}, count: 2);
|
||||
|
||||
// End is only off by 0.25 ms, hence count 1.
|
||||
assert1Ms(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 99, endTime: 399.75d).Object
|
||||
}, count: 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderUnsnapped2Ms()
|
||||
{
|
||||
assert2Ms(new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 102, endTime: 402.75d).Object
|
||||
}, count: 2);
|
||||
|
||||
// Start and end are 2 ms and 1.25 ms off respectively, hence two different issues in one object.
|
||||
var hitobjects = new List<HitObject>
|
||||
{
|
||||
getSliderMock(startTime: 98, endTime: 398.75d).Object
|
||||
};
|
||||
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(2));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
|
||||
}
|
||||
|
||||
private Mock<Slider> getSliderMock(double startTime, double endTime, int repeats = 0)
|
||||
{
|
||||
var mockSlider = new Mock<Slider>();
|
||||
mockSlider.SetupGet(s => s.StartTime).Returns(startTime);
|
||||
mockSlider.As<IHasRepeats>().Setup(r => r.RepeatCount).Returns(repeats);
|
||||
mockSlider.As<IHasDuration>().Setup(d => d.EndTime).Returns(endTime);
|
||||
|
||||
return mockSlider;
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitobjects)
|
||||
{
|
||||
Assert.That(check.Run(getPlayableBeatmap(hitobjects), null), Is.Empty);
|
||||
}
|
||||
|
||||
private void assert1Ms(List<HitObject> hitobjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateSmallUnsnap));
|
||||
}
|
||||
|
||||
private void assert2Ms(List<HitObject> hitobjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getPlayableBeatmap(hitobjects), null).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckUnsnappedObjects.IssueTemplateLargeUnsnap));
|
||||
}
|
||||
|
||||
private IBeatmap getPlayableBeatmap(List<HitObject> hitobjects)
|
||||
{
|
||||
return new Beatmap<HitObject>
|
||||
{
|
||||
ControlPointInfo = cpi,
|
||||
HitObjects = hitobjects
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
91
osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs
Normal file
91
osu.Game.Tests/NonVisual/ClosestBeatDivisorTest.cs
Normal file
@ -0,0 +1,91 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Tests.NonVisual
|
||||
{
|
||||
public class ClosestBeatDivisorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestExactDivisors()
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
|
||||
|
||||
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
|
||||
|
||||
assertClosestDivisors(divisors, divisors, cpi);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExactDivisorWithTempoChanges()
|
||||
{
|
||||
int offset = 0;
|
||||
int[] beatLengths = { 1000, 200, 100, 50 };
|
||||
|
||||
var cpi = new ControlPointInfo();
|
||||
|
||||
foreach (int beatLength in beatLengths)
|
||||
{
|
||||
cpi.Add(offset, new TimingControlPoint { BeatLength = beatLength });
|
||||
offset += beatLength * 2;
|
||||
}
|
||||
|
||||
double[] divisors = { 3, 1, 16, 12, 8, 6, 4, 3 };
|
||||
|
||||
assertClosestDivisors(divisors, divisors, cpi);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExactDivisorsHighBPMStream()
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(-50, new TimingControlPoint { BeatLength = 50 }); // 1200 BPM 1/4 (limit testing)
|
||||
|
||||
// A 1/4 stream should land on 1/1, 1/2 and 1/4 divisors.
|
||||
double[] divisors = { 4, 4, 4, 4, 4, 4, 4, 4 };
|
||||
double[] closestDivisors = { 4, 2, 4, 1, 4, 2, 4, 1 };
|
||||
|
||||
assertClosestDivisors(divisors, closestDivisors, cpi, step: 1 / 4d);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApproximateDivisors()
|
||||
{
|
||||
var cpi = new ControlPointInfo();
|
||||
cpi.Add(-1000, new TimingControlPoint { BeatLength = 1000 });
|
||||
|
||||
double[] divisors = { 3.03d, 0.97d, 14, 13, 7.94d, 6.08d, 3.93d, 2.96d, 2.02d, 64 };
|
||||
double[] closestDivisors = { 3, 1, 16, 12, 8, 6, 4, 3, 2, 1 };
|
||||
|
||||
assertClosestDivisors(divisors, closestDivisors, cpi);
|
||||
}
|
||||
|
||||
private void assertClosestDivisors(IReadOnlyList<double> divisors, IReadOnlyList<double> closestDivisors, ControlPointInfo cpi, double step = 1)
|
||||
{
|
||||
List<HitObject> hitobjects = new List<HitObject>();
|
||||
double offset = cpi.TimingPoints[0].Time;
|
||||
|
||||
for (int i = 0; i < divisors.Count; ++i)
|
||||
{
|
||||
double beatLength = cpi.TimingPointAt(offset).BeatLength;
|
||||
hitobjects.Add(new HitObject { StartTime = offset + beatLength / divisors[i] });
|
||||
offset += beatLength * step;
|
||||
}
|
||||
|
||||
var beatmap = new Beatmap
|
||||
{
|
||||
HitObjects = hitobjects,
|
||||
ControlPointInfo = cpi
|
||||
};
|
||||
|
||||
for (int i = 0; i < divisors.Count; ++i)
|
||||
Assert.AreEqual(closestDivisors[i], beatmap.ControlPointInfo.GetClosestBeatDivisor(beatmap.HitObjects[i].StartTime), $"at index {i}");
|
||||
}
|
||||
}
|
||||
}
|
@ -132,8 +132,8 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
AddStep("deselect", () => EditorBeatmap.SelectedHitObjects.Clear());
|
||||
|
||||
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
|
||||
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<EditorSelectionHandler>().First().Alpha == 0);
|
||||
AddUntilStep("timeline selection box is not visible", () => Editor.ChildrenOfType<Timeline>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
|
||||
AddUntilStep("composer selection box is not visible", () => Editor.ChildrenOfType<HitObjectComposer>().First().ChildrenOfType<SelectionBox>().First().Alpha == 0);
|
||||
}
|
||||
|
||||
AddStep("paste hitobject", () => Editor.Paste());
|
||||
|
@ -14,4 +14,10 @@ namespace osu.Game.Beatmaps
|
||||
Qualified = 3,
|
||||
Loved = 4,
|
||||
}
|
||||
|
||||
public static class BeatmapSetOnlineStatusExtensions
|
||||
{
|
||||
public static bool GrantsPerformancePoints(this BeatmapSetOnlineStatus status)
|
||||
=> status == BeatmapSetOnlineStatus.Ranked || status == BeatmapSetOnlineStatus.Approved;
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,8 @@ using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Screens.Edit;
|
||||
|
||||
namespace osu.Game.Beatmaps.ControlPoints
|
||||
{
|
||||
@ -160,6 +162,58 @@ namespace osu.Game.Beatmaps.ControlPoints
|
||||
groups.Remove(group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the time on the given beat divisor closest to the given time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the closest snapped time to.</param>
|
||||
/// <param name="beatDivisor">The beat divisor to snap to.</param>
|
||||
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
|
||||
public double GetClosestSnappedTime(double time, int beatDivisor, double? referenceTime = null)
|
||||
{
|
||||
var timingPoint = TimingPointAt(referenceTime ?? time);
|
||||
return getClosestSnappedTime(timingPoint, time, beatDivisor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the time on *ANY* valid beat divisor, favouring the divisor closest to the given time.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the closest snapped time to.</param>
|
||||
public double GetClosestSnappedTime(double time) => GetClosestSnappedTime(time, GetClosestBeatDivisor(time));
|
||||
|
||||
/// <summary>
|
||||
/// Returns the beat snap divisor closest to the given time. If two are equally close, the smallest divisor is returned.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the closest beat snap divisor to.</param>
|
||||
/// <param name="referenceTime">An optional reference point to use for timing point lookup.</param>
|
||||
public int GetClosestBeatDivisor(double time, double? referenceTime = null)
|
||||
{
|
||||
TimingControlPoint timingPoint = TimingPointAt(referenceTime ?? time);
|
||||
|
||||
int closestDivisor = 0;
|
||||
double closestTime = double.MaxValue;
|
||||
|
||||
foreach (int divisor in BindableBeatDivisor.VALID_DIVISORS)
|
||||
{
|
||||
double distanceFromSnap = Math.Abs(time - getClosestSnappedTime(timingPoint, time, divisor));
|
||||
|
||||
if (Precision.DefinitelyBigger(closestTime, distanceFromSnap))
|
||||
{
|
||||
closestDivisor = divisor;
|
||||
closestTime = distanceFromSnap;
|
||||
}
|
||||
}
|
||||
|
||||
return closestDivisor;
|
||||
}
|
||||
|
||||
private static double getClosestSnappedTime(TimingControlPoint timingPoint, double time, int beatDivisor)
|
||||
{
|
||||
var beatLength = timingPoint.BeatLength / beatDivisor;
|
||||
var beatLengths = (int)Math.Round((time - timingPoint.Time) / beatLength, MidpointRounding.AwayFromZero);
|
||||
|
||||
return timingPoint.Time + beatLengths * beatLength;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Binary searches one of the control point lists to find the active control point at <paramref name="time"/>.
|
||||
/// Includes logic for returning a specific point when no matching point is found.
|
||||
|
@ -47,52 +47,44 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
|
||||
{
|
||||
FillFlowContainer textSprites;
|
||||
|
||||
AddRangeInternal(new Drawable[]
|
||||
AddInternal(shakeContainer = new ShakeContainer
|
||||
{
|
||||
shakeContainer = new ShakeContainer
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Child = button = new HeaderButton { RelativeSizeAxes = Axes.Both },
|
||||
});
|
||||
|
||||
button.AddRange(new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Depth = -1,
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Masking = true,
|
||||
CornerRadius = 5,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
button = new HeaderButton { RelativeSizeAxes = Axes.Both },
|
||||
new Container
|
||||
textSprites = new FillFlowContainer
|
||||
{
|
||||
// cannot nest inside here due to the structure of button (putting things in its own content).
|
||||
// requires framework fix.
|
||||
Padding = new MarginPadding { Horizontal = 10 },
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
textSprites = new FillFlowContainer
|
||||
{
|
||||
Depth = -1,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeDuration = 500,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Direction = FillDirection.Vertical,
|
||||
},
|
||||
new SpriteIcon
|
||||
{
|
||||
Depth = -1,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Icon = FontAwesome.Solid.Download,
|
||||
Size = new Vector2(18),
|
||||
},
|
||||
}
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeDuration = 500,
|
||||
AutoSizeEasing = Easing.OutQuint,
|
||||
Direction = FillDirection.Vertical,
|
||||
},
|
||||
new DownloadProgressBar(BeatmapSet.Value)
|
||||
new SpriteIcon
|
||||
{
|
||||
Depth = -2,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.CentreRight,
|
||||
Origin = Anchor.CentreRight,
|
||||
Icon = FontAwesome.Solid.Download,
|
||||
Size = new Vector2(18),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
new DownloadProgressBar(BeatmapSet.Value)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
var scoreInfos = value.Scores.Select(s => s.CreateScoreInfo(rulesets)).ToList();
|
||||
var topScore = scoreInfos.First();
|
||||
|
||||
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked);
|
||||
scoreTable.DisplayScores(scoreInfos, topScore.Beatmap?.Status.GrantsPerformancePoints() == true);
|
||||
scoreTable.Show();
|
||||
|
||||
var userScore = value.UserScore;
|
||||
|
@ -111,7 +111,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores
|
||||
|
||||
accuracyColumn.Text = value.DisplayAccuracy;
|
||||
maxComboColumn.Text = $@"{value.MaxCombo:N0}x";
|
||||
ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0;
|
||||
|
||||
ppColumn.Alpha = value.Beatmap?.Status.GrantsPerformancePoints() == true ? 1 : 0;
|
||||
ppColumn.Text = $@"{value.PP:N0}";
|
||||
|
||||
statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn);
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps
|
||||
private readonly BeatmapSetType type;
|
||||
|
||||
public PaginatedBeatmapContainer(BeatmapSetType type, Bindable<User> user, string headerText)
|
||||
: base(user, headerText, "", CounterVisibilityState.AlwaysVisible)
|
||||
: base(user, headerText)
|
||||
{
|
||||
this.type = type;
|
||||
ItemsPerPage = 6;
|
||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
||||
public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection<APIUserMostPlayedBeatmap>
|
||||
{
|
||||
public PaginatedMostPlayedBeatmapContainer(Bindable<User> user)
|
||||
: base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible)
|
||||
: base(user, "Most Played Beatmaps")
|
||||
{
|
||||
ItemsPerPage = 5;
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
new PlayHistorySubsection(User),
|
||||
new PaginatedMostPlayedBeatmapContainer(User),
|
||||
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero),
|
||||
new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)"),
|
||||
new ReplaysSubsection(User)
|
||||
};
|
||||
}
|
||||
|
@ -38,8 +38,8 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
private OsuSpriteText missing;
|
||||
private readonly string missingText;
|
||||
|
||||
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden)
|
||||
: base(user, headerText, counterVisibilityState)
|
||||
protected PaginatedProfileSubsection(Bindable<User> user, string headerText = "", string missingText = "")
|
||||
: base(user, headerText, CounterVisibilityState.AlwaysVisible)
|
||||
{
|
||||
this.missingText = missingText;
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
|
||||
{
|
||||
private readonly ScoreType type;
|
||||
|
||||
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string headerText, CounterVisibilityState counterVisibilityState, string missingText = "")
|
||||
: base(user, headerText, missingText, counterVisibilityState)
|
||||
public PaginatedScoreContainer(ScoreType type, Bindable<User> user, string headerText)
|
||||
: base(user, headerText)
|
||||
{
|
||||
this.type = type;
|
||||
|
||||
@ -36,9 +36,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
|
||||
{
|
||||
switch (type)
|
||||
{
|
||||
case ScoreType.Best:
|
||||
return user.ScoresBestCount;
|
||||
|
||||
case ScoreType.Firsts:
|
||||
return user.ScoresFirstCount;
|
||||
|
||||
case ScoreType.Recent:
|
||||
return user.ScoresRecentCount;
|
||||
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
@ -50,9 +56,6 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks
|
||||
drawableItemIndex = 0;
|
||||
|
||||
base.OnItemsReceived(items);
|
||||
|
||||
if (type == ScoreType.Recent)
|
||||
SetCount(items.Count);
|
||||
}
|
||||
|
||||
protected override APIRequest<List<APILegacyScoreInfo>> CreateRequest() =>
|
||||
|
@ -16,8 +16,8 @@ namespace osu.Game.Overlays.Profile.Sections
|
||||
{
|
||||
Children = new[]
|
||||
{
|
||||
new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance", CounterVisibilityState.AlwaysHidden, "No performance records. :("),
|
||||
new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks", CounterVisibilityState.AlwaysVisible)
|
||||
new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance"),
|
||||
new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks")
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,11 @@ namespace osu.Game.Rulesets.Edit
|
||||
|
||||
// Audio
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality()
|
||||
new CheckAudioQuality(),
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
new CheckConcurrentObjects()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, WorkingBeatmap workingBeatmap)
|
||||
|
88
osu.Game/Rulesets/Edit/Checks/CheckConcurrentObjects.cs
Normal file
88
osu.Game/Rulesets/Edit/Checks/CheckConcurrentObjects.cs
Normal file
@ -0,0 +1,88 @@
|
||||
// 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.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckConcurrentObjects : ICheck
|
||||
{
|
||||
// We guarantee that the objects are either treated as concurrent or unsnapped when near the same beat divisor.
|
||||
private const double ms_leniency = CheckUnsnappedObjects.UNSNAP_MS_THRESHOLD;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Concurrent hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateConcurrentSame(this),
|
||||
new IssueTemplateConcurrentDifferent(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
{
|
||||
for (int i = 0; i < playableBeatmap.HitObjects.Count - 1; ++i)
|
||||
{
|
||||
var hitobject = playableBeatmap.HitObjects[i];
|
||||
|
||||
for (int j = i + 1; j < playableBeatmap.HitObjects.Count; ++j)
|
||||
{
|
||||
var nextHitobject = playableBeatmap.HitObjects[j];
|
||||
|
||||
// Accounts for rulesets with hitobjects separated by columns, such as Mania.
|
||||
// In these cases we only care about concurrent objects within the same column.
|
||||
if ((hitobject as IHasColumn)?.Column != (nextHitobject as IHasColumn)?.Column)
|
||||
continue;
|
||||
|
||||
// Two hitobjects cannot be concurrent without also being concurrent with all objects in between.
|
||||
// So if the next object is not concurrent, then we know no future objects will be either.
|
||||
if (!areConcurrent(hitobject, nextHitobject))
|
||||
break;
|
||||
|
||||
if (hitobject.GetType() == nextHitobject.GetType())
|
||||
yield return new IssueTemplateConcurrentSame(this).Create(hitobject, nextHitobject);
|
||||
else
|
||||
yield return new IssueTemplateConcurrentDifferent(this).Create(hitobject, nextHitobject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool areConcurrent(HitObject hitobject, HitObject nextHitobject) => nextHitobject.StartTime <= hitobject.GetEndTime() + ms_leniency;
|
||||
|
||||
public abstract class IssueTemplateConcurrent : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateConcurrent(ICheck check, string unformattedMessage)
|
||||
: base(check, IssueType.Problem, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, HitObject nextHitobject)
|
||||
{
|
||||
var hitobjects = new List<HitObject> { hitobject, nextHitobject };
|
||||
return new Issue(hitobjects, this, hitobject.GetType().Name, nextHitobject.GetType().Name)
|
||||
{
|
||||
Time = nextHitobject.StartTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateConcurrentSame : IssueTemplateConcurrent
|
||||
{
|
||||
public IssueTemplateConcurrentSame(ICheck check)
|
||||
: base(check, "{0}s are concurrent here.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateConcurrentDifferent : IssueTemplateConcurrent
|
||||
{
|
||||
public IssueTemplateConcurrentDifferent(ICheck check)
|
||||
: base(check, "{0} and {1} are concurrent here.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
100
osu.Game/Rulesets/Edit/Checks/CheckUnsnappedObjects.cs
Normal file
100
osu.Game/Rulesets/Edit/Checks/CheckUnsnappedObjects.cs
Normal file
@ -0,0 +1,100 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Beatmaps;
|
||||
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 CheckUnsnappedObjects : ICheck
|
||||
{
|
||||
public const double UNSNAP_MS_THRESHOLD = 2;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Timing, "Unsnapped hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateLargeUnsnap(this),
|
||||
new IssueTemplateSmallUnsnap(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap playableBeatmap, IWorkingBeatmap workingBeatmap)
|
||||
{
|
||||
var controlPointInfo = playableBeatmap.ControlPointInfo;
|
||||
|
||||
foreach (var hitobject in playableBeatmap.HitObjects)
|
||||
{
|
||||
double startUnsnap = hitobject.StartTime - controlPointInfo.GetClosestSnappedTime(hitobject.StartTime);
|
||||
string startPostfix = hitobject is IHasDuration ? "start" : "";
|
||||
foreach (var issue in getUnsnapIssues(hitobject, startUnsnap, hitobject.StartTime, startPostfix))
|
||||
yield return issue;
|
||||
|
||||
if (hitobject is IHasRepeats hasRepeats)
|
||||
{
|
||||
for (int repeatIndex = 0; repeatIndex < hasRepeats.RepeatCount; ++repeatIndex)
|
||||
{
|
||||
double spanDuration = hasRepeats.Duration / (hasRepeats.RepeatCount + 1);
|
||||
double repeatTime = hitobject.StartTime + spanDuration * (repeatIndex + 1);
|
||||
double repeatUnsnap = repeatTime - controlPointInfo.GetClosestSnappedTime(repeatTime);
|
||||
foreach (var issue in getUnsnapIssues(hitobject, repeatUnsnap, repeatTime, "repeat"))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
|
||||
if (hitobject is IHasDuration hasDuration)
|
||||
{
|
||||
double endUnsnap = hasDuration.EndTime - controlPointInfo.GetClosestSnappedTime(hasDuration.EndTime);
|
||||
foreach (var issue in getUnsnapIssues(hitobject, endUnsnap, hasDuration.EndTime, "end"))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> getUnsnapIssues(HitObject hitobject, double unsnap, double time, string postfix = "")
|
||||
{
|
||||
if (Math.Abs(unsnap) >= UNSNAP_MS_THRESHOLD)
|
||||
yield return new IssueTemplateLargeUnsnap(this).Create(hitobject, unsnap, time, postfix);
|
||||
else if (Math.Abs(unsnap) >= 1)
|
||||
yield return new IssueTemplateSmallUnsnap(this).Create(hitobject, unsnap, time, postfix);
|
||||
|
||||
// We don't care about unsnaps < 1 ms, as all object ends have these due to the way SV works.
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateUnsnap : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateUnsnap(ICheck check, IssueType type)
|
||||
: base(check, type, "{0} is unsnapped by {1:0.##} ms.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, double unsnap, double time, string postfix = "")
|
||||
{
|
||||
string objectName = hitobject.GetType().Name;
|
||||
if (!string.IsNullOrEmpty(postfix))
|
||||
objectName += " " + postfix;
|
||||
|
||||
return new Issue(hitobject, this, objectName, unsnap) { Time = time };
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLargeUnsnap : IssueTemplateUnsnap
|
||||
{
|
||||
public IssueTemplateLargeUnsnap(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateSmallUnsnap : IssueTemplateUnsnap
|
||||
{
|
||||
public IssueTemplateSmallUnsnap(ICheck check)
|
||||
: base(check, IssueType.Negligible)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
57
osu.Game/Rulesets/Mods/ModBarrelRoll.cs
Normal file
57
osu.Game/Rulesets/Mods/ModBarrelRoll.cs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
public abstract class ModBarrelRoll<TObject> : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<TObject>
|
||||
where TObject : HitObject
|
||||
{
|
||||
/// <summary>
|
||||
/// The current angle of rotation being applied by this mod.
|
||||
/// Generally should be used to apply inverse rotation to elements which should not be rotated.
|
||||
/// </summary>
|
||||
protected float CurrentRotation { get; private set; }
|
||||
|
||||
[SettingSource("Roll speed", "Rotations per minute")]
|
||||
public BindableNumber<double> SpinSpeed { get; } = new BindableDouble(0.5)
|
||||
{
|
||||
MinValue = 0.02,
|
||||
MaxValue = 12,
|
||||
Precision = 0.01,
|
||||
};
|
||||
|
||||
[SettingSource("Direction", "The direction of rotation")]
|
||||
public Bindable<RotationDirection> Direction { get; } = new Bindable<RotationDirection>(RotationDirection.Clockwise);
|
||||
|
||||
public override string Name => "Barrel Roll";
|
||||
public override string Acronym => "BR";
|
||||
public override string Description => "The whole playfield is on a wheel!";
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
public override string SettingDescription => $"{SpinSpeed.Value} rpm {Direction.Value.GetDescription().ToLowerInvariant()}";
|
||||
|
||||
public void Update(Playfield playfield)
|
||||
{
|
||||
playfield.Rotation = CurrentRotation = (Direction.Value == RotationDirection.Counterclockwise ? -1 : 1) * 360 * (float)(playfield.Time.Current / 60000 * SpinSpeed.Value);
|
||||
}
|
||||
|
||||
public void ApplyToDrawableRuleset(DrawableRuleset<TObject> drawableRuleset)
|
||||
{
|
||||
// scale the playfield to allow all hitobjects to stay within the visible region.
|
||||
|
||||
var playfieldSize = drawableRuleset.Playfield.DrawSize;
|
||||
var minSide = MathF.Min(playfieldSize.X, playfieldSize.Y);
|
||||
var maxSide = MathF.Max(playfieldSize.X, playfieldSize.Y);
|
||||
drawableRuleset.Playfield.Scale = new Vector2(minSide / maxSide);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +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.
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Objects.Types
|
||||
namespace osu.Game.Rulesets.Objects.Types
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of hit object which lies in one of a number of predetermined columns.
|
@ -85,6 +85,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The beatmap.
|
||||
/// </summary>
|
||||
[Cached(typeof(IBeatmap))]
|
||||
public readonly Beatmap<TObject> Beatmap;
|
||||
|
||||
public override IEnumerable<HitObject> Objects => Beatmap.HitObjects;
|
||||
|
@ -16,7 +16,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
{
|
||||
public class DrawableRadioButton : TriangleButton
|
||||
public class DrawableRadioButton : OsuButton
|
||||
{
|
||||
/// <summary>
|
||||
/// Invoked when this <see cref="DrawableRadioButton"/> has been selected.
|
||||
@ -49,8 +49,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons
|
||||
selectedBackgroundColour = colours.BlueDark;
|
||||
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
|
||||
Triangles.Alpha = 0;
|
||||
|
||||
Content.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
|
@ -15,7 +15,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
{
|
||||
internal class DrawableTernaryButton : TriangleButton
|
||||
internal class DrawableTernaryButton : OsuButton
|
||||
{
|
||||
private Color4 defaultBackgroundColour;
|
||||
private Color4 defaultBubbleColour;
|
||||
@ -43,8 +43,6 @@ namespace osu.Game.Screens.Edit.Components.TernaryButtons
|
||||
selectedBackgroundColour = colours.BlueDark;
|
||||
selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f);
|
||||
|
||||
Triangles.Alpha = 0;
|
||||
|
||||
Content.EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Type = EdgeEffectType.Shadow,
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Input;
|
||||
|
||||
@ -16,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
public class SelectionBox : CompositeDrawable
|
||||
{
|
||||
public const float BORDER_RADIUS = 3;
|
||||
|
||||
public Func<float, bool> OnRotation;
|
||||
public Func<Vector2, Anchor, bool> OnScale;
|
||||
public Func<Direction, bool> OnFlip;
|
||||
@ -92,21 +95,32 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
}
|
||||
}
|
||||
|
||||
private string text;
|
||||
|
||||
public string Text
|
||||
{
|
||||
get => text;
|
||||
set
|
||||
{
|
||||
if (value == text)
|
||||
return;
|
||||
|
||||
text = value;
|
||||
if (selectionDetailsText != null)
|
||||
selectionDetailsText.Text = value;
|
||||
}
|
||||
}
|
||||
|
||||
private Container dragHandles;
|
||||
private FillFlowContainer buttons;
|
||||
|
||||
public const float BORDER_RADIUS = 3;
|
||||
private OsuSpriteText selectionDetailsText;
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
recreate();
|
||||
}
|
||||
private void load() => recreate();
|
||||
|
||||
protected override bool OnKeyDown(KeyDownEvent e)
|
||||
{
|
||||
@ -144,6 +158,26 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Container
|
||||
{
|
||||
Name = "info text",
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.YellowDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
selectionDetailsText = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(2),
|
||||
Colour = colours.Gray0,
|
||||
Font = OsuFont.Default.With(size: 11),
|
||||
Text = text,
|
||||
}
|
||||
}
|
||||
},
|
||||
new Container
|
||||
{
|
||||
Masking = true,
|
||||
|
@ -9,13 +9,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osuTK;
|
||||
@ -42,10 +41,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
private readonly List<SelectionBlueprint<T>> selectedBlueprints;
|
||||
|
||||
private Drawable content;
|
||||
|
||||
private OsuSpriteText selectionDetailsText;
|
||||
|
||||
protected SelectionBox SelectionBox { get; private set; }
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
@ -62,33 +57,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
InternalChild = content = new Container
|
||||
{
|
||||
Children = new Drawable[]
|
||||
{
|
||||
// todo: should maybe be inside the SelectionBox?
|
||||
new Container
|
||||
{
|
||||
Name = "info text",
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.YellowDark,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
selectionDetailsText = new OsuSpriteText
|
||||
{
|
||||
Padding = new MarginPadding(2),
|
||||
Colour = colours.Gray0,
|
||||
Font = OsuFont.Default.With(size: 11)
|
||||
}
|
||||
}
|
||||
},
|
||||
SelectionBox = CreateSelectionBox(),
|
||||
}
|
||||
};
|
||||
InternalChild = SelectionBox = CreateSelectionBox();
|
||||
|
||||
SelectedItems.CollectionChanged += (sender, args) =>
|
||||
{
|
||||
@ -315,9 +284,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
int count = SelectedItems.Count;
|
||||
|
||||
selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty;
|
||||
SelectionBox.Text = count > 0 ? count.ToString() : string.Empty;
|
||||
SelectionBox.FadeTo(count > 0 ? 1 : 0);
|
||||
|
||||
content.FadeTo(count > 0 ? 1 : 0);
|
||||
OnSelectionChanged();
|
||||
}
|
||||
|
||||
@ -337,20 +306,15 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
return;
|
||||
|
||||
// Move the rectangle to cover the items
|
||||
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
|
||||
var bottomRight = new Vector2(float.MinValue, float.MinValue);
|
||||
RectangleF selectionRect = ToLocalSpace(selectedBlueprints[0].SelectionQuad).AABBFloat;
|
||||
|
||||
foreach (var blueprint in selectedBlueprints)
|
||||
{
|
||||
topLeft = Vector2.ComponentMin(topLeft, ToLocalSpace(blueprint.SelectionQuad.TopLeft));
|
||||
bottomRight = Vector2.ComponentMax(bottomRight, ToLocalSpace(blueprint.SelectionQuad.BottomRight));
|
||||
}
|
||||
for (int i = 1; i < selectedBlueprints.Count; i++)
|
||||
selectionRect = RectangleF.Union(selectionRect, ToLocalSpace(selectedBlueprints[i].SelectionQuad).AABBFloat);
|
||||
|
||||
topLeft -= new Vector2(5);
|
||||
bottomRight += new Vector2(5);
|
||||
selectionRect = selectionRect.Inflate(5f);
|
||||
|
||||
content.Size = bottomRight - topLeft;
|
||||
content.Position = topLeft;
|
||||
SelectionBox.Position = selectionRect.Location;
|
||||
SelectionBox.Size = selectionRect.Size;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
@ -301,13 +301,7 @@ namespace osu.Game.Screens.Edit
|
||||
return list.Count - 1;
|
||||
}
|
||||
|
||||
public double SnapTime(double time, double? referenceTime)
|
||||
{
|
||||
var timingPoint = ControlPointInfo.TimingPointAt(referenceTime ?? time);
|
||||
var beatLength = timingPoint.BeatLength / BeatDivisor;
|
||||
|
||||
return timingPoint.Time + (int)Math.Round((time - timingPoint.Time) / beatLength, MidpointRounding.AwayFromZero) * beatLength;
|
||||
}
|
||||
public double SnapTime(double time, double? referenceTime) => ControlPointInfo.GetClosestSnappedTime(time, BeatDivisor, referenceTime);
|
||||
|
||||
public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor;
|
||||
|
||||
|
@ -144,9 +144,15 @@ namespace osu.Game.Users
|
||||
[JsonProperty(@"unranked_beatmapset_count")]
|
||||
public int UnrankedBeatmapsetCount;
|
||||
|
||||
[JsonProperty(@"scores_best_count")]
|
||||
public int ScoresBestCount;
|
||||
|
||||
[JsonProperty(@"scores_first_count")]
|
||||
public int ScoresFirstCount;
|
||||
|
||||
[JsonProperty(@"scores_recent_count")]
|
||||
public int ScoresRecentCount;
|
||||
|
||||
[JsonProperty(@"beatmap_playcounts_count")]
|
||||
public int BeatmapPlaycountsCount;
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="DiffPlex" Version="1.7.0" />
|
||||
<PackageReference Include="Humanizer" Version="2.9.9" />
|
||||
<PackageReference Include="Humanizer" Version="2.8.26" />
|
||||
<PackageReference Include="MessagePack" Version="2.2.85" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="5.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Protocols.MessagePack" Version="5.0.5" />
|
||||
|
@ -89,7 +89,7 @@
|
||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<PackageReference Include="DiffPlex" Version="1.6.3" />
|
||||
<PackageReference Include="Humanizer" Version="2.9.9" />
|
||||
<PackageReference Include="Humanizer" Version="2.8.26" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||
|
Loading…
Reference in New Issue
Block a user