mirror of
https://github.com/ppy/osu.git
synced 2025-03-19 04:17:27 +08:00
Merge pull request #12361 from Naxesss/verify-tab
Add basic AiMod-like features
This commit is contained in:
commit
d076be82a5
@ -0,0 +1,260 @@
|
||||
// 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.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckOffscreenObjectsTest
|
||||
{
|
||||
private static readonly Vector2 playfield_centre = OsuPlayfield.BASE_SIZE * 0.5f;
|
||||
|
||||
private CheckOffscreenObjects check;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckOffscreenObjects();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleInCenter()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre // Playfield is 640 x 480.
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(check.Run(beatmap), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleNearEdge()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(5, 5)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(check.Run(beatmap), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleNearEdgeStackedOffscreen()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(5, 5),
|
||||
StackHeight = 5
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenCircle(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCircleOffscreen()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new HitCircle
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(0, 0)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenCircle(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderInCenter()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(420, 240),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(-100, 0))
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(check.Run(beatmap), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNearEdge()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Assert.That(check.Run(beatmap), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderNearEdgeStackedOffscreen()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(new Vector2(0, -playfield_centre.Y + 5))
|
||||
}),
|
||||
StackHeight = 5
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenSlider(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenStart()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = new Vector2(0, 0),
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(playfield_centre)
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenSlider(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenEnd()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.Linear),
|
||||
new PathControlPoint(-playfield_centre)
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenSlider(beatmap);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderOffscreenPath()
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
HitObjects = new List<HitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 3000,
|
||||
Position = playfield_centre,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
// Circular arc shoots over the top of the screen.
|
||||
new PathControlPoint(new Vector2(0, 0), PathType.PerfectCurve),
|
||||
new PathControlPoint(new Vector2(-100, -200)),
|
||||
new PathControlPoint(new Vector2(100, -200))
|
||||
}),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
assertOffscreenSlider(beatmap);
|
||||
}
|
||||
|
||||
private void assertOffscreenCircle(IBeatmap beatmap)
|
||||
{
|
||||
var issues = check.Run(beatmap).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenCircle);
|
||||
}
|
||||
|
||||
private void assertOffscreenSlider(IBeatmap beatmap)
|
||||
{
|
||||
var issues = check.Run(beatmap).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckOffscreenObjects.IssueTemplateOffscreenSlider);
|
||||
}
|
||||
}
|
||||
}
|
115
osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
Normal file
115
osu.Game.Rulesets.Osu/Edit/Checks/CheckOffscreenObjects.cs
Normal file
@ -0,0 +1,115 @@
|
||||
// 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.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit.Checks
|
||||
{
|
||||
public class CheckOffscreenObjects : ICheck
|
||||
{
|
||||
// A close approximation for the bounding box of the screen in gameplay on 4:3 aspect ratio.
|
||||
// Uses gameplay space coordinates (512 x 384 playfield / 640 x 480 screen area).
|
||||
// See https://github.com/ppy/osu/pull/12361#discussion_r612199777 for reference.
|
||||
private const int min_x = -67;
|
||||
private const int min_y = -60;
|
||||
private const int max_x = 579;
|
||||
private const int max_y = 428;
|
||||
|
||||
// The amount of milliseconds to step through a slider path at a time
|
||||
// (higher = more performant, but higher false-negative chance).
|
||||
private const int path_step_size = 5;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Compose, "Offscreen hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateOffscreenCircle(this),
|
||||
new IssueTemplateOffscreenSlider(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap)
|
||||
{
|
||||
foreach (var hitobject in beatmap.HitObjects)
|
||||
{
|
||||
switch (hitobject)
|
||||
{
|
||||
case Slider slider:
|
||||
{
|
||||
foreach (var issue in sliderIssues(slider))
|
||||
yield return issue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case HitCircle circle:
|
||||
{
|
||||
if (isOffscreen(circle.StackedPosition, circle.Radius))
|
||||
yield return new IssueTemplateOffscreenCircle(this).Create(circle);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Steps through points on the slider to ensure the entire path is on-screen.
|
||||
/// Returns at most one issue.
|
||||
/// </summary>
|
||||
/// <param name="slider">The slider whose path to check.</param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<Issue> sliderIssues(Slider slider)
|
||||
{
|
||||
for (int i = 0; i < slider.Distance; i += path_step_size)
|
||||
{
|
||||
double progress = i / slider.Distance;
|
||||
Vector2 position = slider.StackedPositionAt(progress);
|
||||
|
||||
if (!isOffscreen(position, slider.Radius))
|
||||
continue;
|
||||
|
||||
// `SpanDuration` ensures we don't include reverses.
|
||||
double time = slider.StartTime + progress * slider.SpanDuration;
|
||||
yield return new IssueTemplateOffscreenSlider(this).Create(slider, time);
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
// Above loop may skip the last position in the slider due to step size.
|
||||
if (!isOffscreen(slider.StackedEndPosition, slider.Radius))
|
||||
yield break;
|
||||
|
||||
yield return new IssueTemplateOffscreenSlider(this).Create(slider, slider.EndTime);
|
||||
}
|
||||
|
||||
private bool isOffscreen(Vector2 position, double radius)
|
||||
{
|
||||
return position.X - radius < min_x || position.X + radius > max_x ||
|
||||
position.Y - radius < min_y || position.Y + radius > max_y;
|
||||
}
|
||||
|
||||
public class IssueTemplateOffscreenCircle : IssueTemplate
|
||||
{
|
||||
public IssueTemplateOffscreenCircle(ICheck check)
|
||||
: base(check, IssueType.Problem, "This circle goes offscreen on a 4:3 aspect ratio.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitCircle circle) => new Issue(circle, this);
|
||||
}
|
||||
|
||||
public class IssueTemplateOffscreenSlider : IssueTemplate
|
||||
{
|
||||
public IssueTemplateOffscreenSlider(ICheck check)
|
||||
: base(check, IssueType.Problem, "This slider goes offscreen here on a 4:3 aspect ratio.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(Slider slider, double offscreenTime) => new Issue(slider, this) { Time = offscreenTime };
|
||||
}
|
||||
}
|
||||
}
|
22
osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
Normal file
22
osu.Game.Rulesets.Osu/Edit/OsuBeatmapVerifier.cs
Normal file
@ -0,0 +1,22 @@
|
||||
// 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 osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Osu.Edit.Checks;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
public class OsuBeatmapVerifier : IBeatmapVerifier
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckOffscreenObjects()
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
|
||||
}
|
||||
}
|
@ -206,6 +206,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
|
||||
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
||||
|
||||
public override IBeatmapVerifier CreateBeatmapVerifier() => new OsuBeatmapVerifier();
|
||||
|
||||
public override string Description => "osu!";
|
||||
|
||||
public override string ShortName => SHORT_NAME;
|
||||
|
67
osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
Normal file
67
osu.Game.Tests/Editing/Checks/CheckBackgroundTest.cs
Normal file
@ -0,0 +1,67 @@
|
||||
// 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.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckBackgroundTest
|
||||
{
|
||||
private CheckBackground check;
|
||||
private IBeatmap beatmap;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckBackground();
|
||||
beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
BeatmapInfo = new BeatmapInfo
|
||||
{
|
||||
Metadata = new BeatmapMetadata { BackgroundFile = "abc123.jpg" },
|
||||
BeatmapSet = new BeatmapSetInfo
|
||||
{
|
||||
Files = new List<BeatmapSetFileInfo>(new[]
|
||||
{
|
||||
new BeatmapSetFileInfo { Filename = "abc123.jpg" }
|
||||
})
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundSetAndInFiles()
|
||||
{
|
||||
Assert.That(check.Run(beatmap), Is.Empty);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundSetAndNotInFiles()
|
||||
{
|
||||
beatmap.BeatmapInfo.BeatmapSet.Files.Clear();
|
||||
|
||||
var issues = check.Run(beatmap).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateDoesNotExist);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestBackgroundNotSet()
|
||||
{
|
||||
beatmap.Metadata.BackgroundFile = null;
|
||||
|
||||
var issues = check.Run(beatmap).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Single().Template is CheckBackground.IssueTemplateNoneSet);
|
||||
}
|
||||
}
|
||||
}
|
@ -70,6 +70,7 @@ namespace osu.Game.Input.Bindings
|
||||
new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode),
|
||||
new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode),
|
||||
new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode),
|
||||
new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.A }, GlobalAction.EditorVerifyMode),
|
||||
};
|
||||
|
||||
public IEnumerable<KeyBinding> InGameKeyBindings => new[]
|
||||
@ -247,5 +248,8 @@ namespace osu.Game.Input.Bindings
|
||||
|
||||
[Description("Beatmap Options")]
|
||||
ToggleBeatmapOptions,
|
||||
|
||||
[Description("Verify mode")]
|
||||
EditorVerifyMode,
|
||||
}
|
||||
}
|
||||
|
24
osu.Game/Rulesets/Edit/BeatmapVerifier.cs
Normal file
24
osu.Game/Rulesets/Edit/BeatmapVerifier.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A ruleset-agnostic beatmap verifier that identifies issues in common metadata or mapping standards.
|
||||
/// </summary>
|
||||
public class BeatmapVerifier : IBeatmapVerifier
|
||||
{
|
||||
private readonly List<ICheck> checks = new List<ICheck>
|
||||
{
|
||||
new CheckBackground(),
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap) => checks.SelectMany(check => check.Run(beatmap));
|
||||
}
|
||||
}
|
61
osu.Game/Rulesets/Edit/Checks/CheckBackground.cs
Normal file
61
osu.Game/Rulesets/Edit/Checks/CheckBackground.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 System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckBackground : ICheck
|
||||
{
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Resources, "Missing background");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateNoneSet(this),
|
||||
new IssueTemplateDoesNotExist(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap)
|
||||
{
|
||||
if (beatmap.Metadata.BackgroundFile == null)
|
||||
{
|
||||
yield return new IssueTemplateNoneSet(this).Create();
|
||||
|
||||
yield break;
|
||||
}
|
||||
|
||||
// If the background is set, also make sure it still exists.
|
||||
|
||||
var set = beatmap.BeatmapInfo.BeatmapSet;
|
||||
var file = set.Files.FirstOrDefault(f => f.Filename == beatmap.Metadata.BackgroundFile);
|
||||
|
||||
if (file != null)
|
||||
yield break;
|
||||
|
||||
yield return new IssueTemplateDoesNotExist(this).Create(beatmap.Metadata.BackgroundFile);
|
||||
}
|
||||
|
||||
public class IssueTemplateNoneSet : IssueTemplate
|
||||
{
|
||||
public IssueTemplateNoneSet(ICheck check)
|
||||
: base(check, IssueType.Problem, "No background has been set.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create() => new Issue(this);
|
||||
}
|
||||
|
||||
public class IssueTemplateDoesNotExist : IssueTemplate
|
||||
{
|
||||
public IssueTemplateDoesNotExist(ICheck check)
|
||||
: base(check, IssueType.Problem, "The background file \"{0}\" does not exist.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(string filename) => new Issue(this, filename);
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game/Rulesets/Edit/Checks/Components/CheckCategory.cs
Normal file
61
osu.Game/Rulesets/Edit/Checks/Components/CheckCategory.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.
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// The category of an issue.
|
||||
/// </summary>
|
||||
public enum CheckCategory
|
||||
{
|
||||
/// <summary>
|
||||
/// Anything to do with control points.
|
||||
/// </summary>
|
||||
Timing,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with artist, title, creator, etc.
|
||||
/// </summary>
|
||||
Metadata,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with non-audio files, e.g. background, skin, sprites, and video.
|
||||
/// </summary>
|
||||
Resources,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with audio files, e.g. song and hitsounds.
|
||||
/// </summary>
|
||||
Audio,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with files that don't fit into the above, e.g. unused, osu, or osb.
|
||||
/// </summary>
|
||||
Files,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with hitobjects unrelated to spread.
|
||||
/// </summary>
|
||||
Compose,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with difficulty levels or their progression.
|
||||
/// </summary>
|
||||
Spread,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with variables like CS, OD, AR, HP, and global SV.
|
||||
/// </summary>
|
||||
Settings,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with hitobject feedback.
|
||||
/// </summary>
|
||||
HitObjects,
|
||||
|
||||
/// <summary>
|
||||
/// Anything to do with storyboarding, breaks, video offset, etc.
|
||||
/// </summary>
|
||||
Events
|
||||
}
|
||||
}
|
24
osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs
Normal file
24
osu.Game/Rulesets/Edit/Checks/Components/CheckMetadata.cs
Normal file
@ -0,0 +1,24 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
public class CheckMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// The category this check belongs to. E.g. <see cref="CheckCategory.Metadata"/>, <see cref="CheckCategory.Timing"/>, or <see cref="CheckCategory.Compose"/>.
|
||||
/// </summary>
|
||||
public readonly CheckCategory Category;
|
||||
|
||||
/// <summary>
|
||||
/// Describes the issue(s) that this check looks for. Keep this brief, such that it fits into "No {description}". E.g. "Offscreen objects" / "Too short sliders".
|
||||
/// </summary>
|
||||
public readonly string Description;
|
||||
|
||||
public CheckMetadata(CheckCategory category, string description)
|
||||
{
|
||||
Category = category;
|
||||
Description = description;
|
||||
}
|
||||
}
|
||||
}
|
30
osu.Game/Rulesets/Edit/Checks/Components/ICheck.cs
Normal file
30
osu.Game/Rulesets/Edit/Checks/Components/ICheck.cs
Normal file
@ -0,0 +1,30 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// A specific check that can be run on a beatmap to verify or find issues.
|
||||
/// </summary>
|
||||
public interface ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// The metadata for this check.
|
||||
/// </summary>
|
||||
public CheckMetadata Metadata { get; }
|
||||
|
||||
/// <summary>
|
||||
/// All possible templates for issues that this check may return.
|
||||
/// </summary>
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Runs this check and returns any issues detected for the provided beatmap.
|
||||
/// </summary>
|
||||
/// <param name="beatmap">The beatmap to run the check on.</param>
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap);
|
||||
}
|
||||
}
|
77
osu.Game/Rulesets/Edit/Checks/Components/Issue.cs
Normal file
77
osu.Game/Rulesets/Edit/Checks/Components/Issue.cs
Normal file
@ -0,0 +1,77 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
public class Issue
|
||||
{
|
||||
/// <summary>
|
||||
/// The time which this issue is associated with, if any, otherwise null.
|
||||
/// </summary>
|
||||
public double? Time;
|
||||
|
||||
/// <summary>
|
||||
/// The hitobjects which this issue is associated with. Empty by default.
|
||||
/// </summary>
|
||||
public IReadOnlyList<HitObject> HitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// The template which this issue is using. This provides properties such as the <see cref="IssueType"/>, and the <see cref="IssueTemplate.UnformattedMessage"/>.
|
||||
/// </summary>
|
||||
public IssueTemplate Template;
|
||||
|
||||
/// <summary>
|
||||
/// The check that this issue originates from.
|
||||
/// </summary>
|
||||
public ICheck Check => Template.Check;
|
||||
|
||||
/// <summary>
|
||||
/// The arguments that give this issue its context, based on the <see cref="IssueTemplate"/>. These are then substituted into the <see cref="IssueTemplate.UnformattedMessage"/>.
|
||||
/// This could for instance include timestamps, which diff is being compared to, what some volume is, etc.
|
||||
/// </summary>
|
||||
public object[] Arguments;
|
||||
|
||||
public Issue(IssueTemplate template, params object[] args)
|
||||
{
|
||||
Time = null;
|
||||
HitObjects = Array.Empty<HitObject>();
|
||||
Template = template;
|
||||
Arguments = args;
|
||||
}
|
||||
|
||||
public Issue(double? time, IssueTemplate template, params object[] args)
|
||||
: this(template, args)
|
||||
{
|
||||
Time = time;
|
||||
}
|
||||
|
||||
public Issue(HitObject hitObject, IssueTemplate template, params object[] args)
|
||||
: this(template, args)
|
||||
{
|
||||
Time = hitObject.StartTime;
|
||||
HitObjects = new[] { hitObject };
|
||||
}
|
||||
|
||||
public Issue(IEnumerable<HitObject> hitObjects, IssueTemplate template, params object[] args)
|
||||
: this(template, args)
|
||||
{
|
||||
var hitObjectList = hitObjects.ToList();
|
||||
|
||||
Time = hitObjectList.FirstOrDefault()?.StartTime;
|
||||
HitObjects = hitObjectList;
|
||||
}
|
||||
|
||||
public override string ToString() => Template.GetMessage(Arguments);
|
||||
|
||||
public string GetEditorTimestamp()
|
||||
{
|
||||
return Time == null ? string.Empty : Time.Value.ToEditorFormattedString();
|
||||
}
|
||||
}
|
||||
}
|
74
osu.Game/Rulesets/Edit/Checks/Components/IssueTemplate.cs
Normal file
74
osu.Game/Rulesets/Edit/Checks/Components/IssueTemplate.cs
Normal file
@ -0,0 +1,74 @@
|
||||
// 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 Humanizer;
|
||||
using osu.Framework.Graphics;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks.Components
|
||||
{
|
||||
public class IssueTemplate
|
||||
{
|
||||
private static readonly Color4 problem_red = new Colour4(1.0f, 0.4f, 0.4f, 1.0f);
|
||||
private static readonly Color4 warning_yellow = new Colour4(1.0f, 0.8f, 0.2f, 1.0f);
|
||||
private static readonly Color4 negligible_green = new Colour4(0.33f, 0.8f, 0.5f, 1.0f);
|
||||
private static readonly Color4 error_gray = new Colour4(0.5f, 0.5f, 0.5f, 1.0f);
|
||||
|
||||
/// <summary>
|
||||
/// The check that this template originates from.
|
||||
/// </summary>
|
||||
public readonly ICheck Check;
|
||||
|
||||
/// <summary>
|
||||
/// The type of the issue.
|
||||
/// </summary>
|
||||
public readonly IssueType Type;
|
||||
|
||||
/// <summary>
|
||||
/// The unformatted message given when this issue is detected.
|
||||
/// This gets populated later when an issue is constructed with this template.
|
||||
/// E.g. "Inconsistent snapping (1/{0}) with [{1}] (1/{2})."
|
||||
/// </summary>
|
||||
public readonly string UnformattedMessage;
|
||||
|
||||
public IssueTemplate(ICheck check, IssueType type, string unformattedMessage)
|
||||
{
|
||||
Check = check;
|
||||
Type = type;
|
||||
UnformattedMessage = unformattedMessage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the formatted message given the arguments used to format it.
|
||||
/// </summary>
|
||||
/// <param name="args">The arguments used to format the message.</param>
|
||||
public string GetMessage(params object[] args) => UnformattedMessage.FormatWith(args);
|
||||
|
||||
/// <summary>
|
||||
/// Returns the colour corresponding to the type of this issue.
|
||||
/// </summary>
|
||||
public Colour4 Colour
|
||||
{
|
||||
get
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case IssueType.Problem:
|
||||
return problem_red;
|
||||
|
||||
case IssueType.Warning:
|
||||
return warning_yellow;
|
||||
|
||||
case IssueType.Negligible:
|
||||
return negligible_green;
|
||||
|
||||
case IssueType.Error:
|
||||
return error_gray;
|
||||
|
||||
default:
|
||||
return Color4.White;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs
Normal file
25
osu.Game/Rulesets/Edit/Checks/Components/IssueType.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// 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.Edit.Checks.Components
|
||||
{
|
||||
/// <summary>
|
||||
/// The type, or severity, of an issue.
|
||||
/// </summary>
|
||||
public enum IssueType
|
||||
{
|
||||
/// <summary> A must-fix in the vast majority of cases. </summary>
|
||||
Problem,
|
||||
|
||||
/// <summary> A possible mistake. Often requires critical thinking. </summary>
|
||||
Warning,
|
||||
|
||||
// TODO: Try/catch all checks run and return error templates if exceptions occur.
|
||||
/// <summary> An error occurred and a complete check could not be made. </summary>
|
||||
Error,
|
||||
|
||||
// TODO: Negligible issues should be hidden by default.
|
||||
/// <summary> A possible mistake so minor/unlikely that it can often be safely ignored. </summary>
|
||||
Negligible,
|
||||
}
|
||||
}
|
17
osu.Game/Rulesets/Edit/IBeatmapVerifier.cs
Normal file
17
osu.Game/Rulesets/Edit/IBeatmapVerifier.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
/// <summary>
|
||||
/// A class which can run against a beatmap and surface issues to the user which could go against known criteria or hinder gameplay.
|
||||
/// </summary>
|
||||
public interface IBeatmapVerifier
|
||||
{
|
||||
public IEnumerable<Issue> Run(IBeatmap beatmap);
|
||||
}
|
||||
}
|
@ -201,6 +201,8 @@ namespace osu.Game.Rulesets
|
||||
|
||||
public virtual HitObjectComposer CreateHitObjectComposer() => null;
|
||||
|
||||
public virtual IBeatmapVerifier CreateBeatmapVerifier() => null;
|
||||
|
||||
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle };
|
||||
|
||||
public virtual IResourceStore<byte[]> CreateResourceStore() => new NamespacedResourceStore<byte[]>(new DllResourceStore(GetType().Assembly), @"Resources");
|
||||
|
@ -35,6 +35,7 @@ using osu.Game.Screens.Edit.Compose;
|
||||
using osu.Game.Screens.Edit.Design;
|
||||
using osu.Game.Screens.Edit.Setup;
|
||||
using osu.Game.Screens.Edit.Timing;
|
||||
using osu.Game.Screens.Edit.Verify;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Users;
|
||||
using osuTK.Graphics;
|
||||
@ -444,6 +445,10 @@ namespace osu.Game.Screens.Edit
|
||||
menuBar.Mode.Value = EditorScreenMode.SongSetup;
|
||||
return true;
|
||||
|
||||
case GlobalAction.EditorVerifyMode:
|
||||
menuBar.Mode.Value = EditorScreenMode.Verify;
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@ -631,6 +636,10 @@ namespace osu.Game.Screens.Edit
|
||||
case EditorScreenMode.Timing:
|
||||
currentScreen = new TimingScreen();
|
||||
break;
|
||||
|
||||
case EditorScreenMode.Verify:
|
||||
currentScreen = new VerifyScreen();
|
||||
break;
|
||||
}
|
||||
|
||||
LoadComponentAsync(currentScreen, newScreen =>
|
||||
|
@ -18,5 +18,8 @@ namespace osu.Game.Screens.Edit
|
||||
|
||||
[Description("timing")]
|
||||
Timing,
|
||||
|
||||
[Description("verify")]
|
||||
Verify,
|
||||
}
|
||||
}
|
||||
|
140
osu.Game/Screens/Edit/EditorTable.cs
Normal file
140
osu.Game/Screens/Edit/EditorTable.cs
Normal file
@ -0,0 +1,140 @@
|
||||
// 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.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public abstract class EditorTable : TableContainer
|
||||
{
|
||||
private const float horizontal_inset = 20;
|
||||
|
||||
protected const float ROW_HEIGHT = 25;
|
||||
|
||||
protected const int TEXT_SIZE = 14;
|
||||
|
||||
protected readonly FillFlowContainer<RowBackground> BackgroundFlow;
|
||||
|
||||
protected EditorTable()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding { Horizontal = horizontal_inset };
|
||||
RowSize = new Dimension(GridSizeMode.Absolute, ROW_HEIGHT);
|
||||
|
||||
AddInternal(BackgroundFlow = new FillFlowContainer<RowBackground>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1f,
|
||||
Padding = new MarginPadding { Horizontal = -horizontal_inset },
|
||||
Margin = new MarginPadding { Top = ROW_HEIGHT }
|
||||
});
|
||||
}
|
||||
|
||||
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
|
||||
|
||||
private class HeaderText : OsuSpriteText
|
||||
{
|
||||
public HeaderText(string text)
|
||||
{
|
||||
Text = text.ToUpper();
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
|
||||
}
|
||||
}
|
||||
|
||||
public class RowBackground : OsuClickableContainer
|
||||
{
|
||||
public readonly object Item;
|
||||
|
||||
private const int fade_duration = 100;
|
||||
|
||||
private readonly Box hoveredBackground;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
public RowBackground(object item)
|
||||
{
|
||||
Item = item;
|
||||
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 25;
|
||||
|
||||
AlwaysPresent = true;
|
||||
|
||||
CornerRadius = 3;
|
||||
Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hoveredBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
};
|
||||
|
||||
// todo delete
|
||||
Action = () =>
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
private Color4 colourHover;
|
||||
private Color4 colourSelected;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
hoveredBackground.Colour = colourHover = colours.BlueDarker;
|
||||
colourSelected = colours.YellowDarker;
|
||||
}
|
||||
|
||||
private bool selected;
|
||||
|
||||
public bool Selected
|
||||
{
|
||||
get => selected;
|
||||
set
|
||||
{
|
||||
if (value == selected)
|
||||
return;
|
||||
|
||||
selected = value;
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
|
||||
|
||||
if (selected || IsHovered)
|
||||
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
|
||||
else
|
||||
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -8,59 +8,43 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Timing
|
||||
{
|
||||
public class ControlPointTable : TableContainer
|
||||
public class ControlPointTable : EditorTable
|
||||
{
|
||||
private const float horizontal_inset = 20;
|
||||
private const float row_height = 25;
|
||||
private const int text_size = 14;
|
||||
|
||||
private readonly FillFlowContainer backgroundFlow;
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||
|
||||
public ControlPointTable()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
Padding = new MarginPadding { Horizontal = horizontal_inset };
|
||||
RowSize = new Dimension(GridSizeMode.Absolute, row_height);
|
||||
|
||||
AddInternal(backgroundFlow = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Depth = 1f,
|
||||
Padding = new MarginPadding { Horizontal = -horizontal_inset },
|
||||
Margin = new MarginPadding { Top = row_height }
|
||||
});
|
||||
}
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
public IEnumerable<ControlPointGroup> ControlGroups
|
||||
{
|
||||
set
|
||||
{
|
||||
Content = null;
|
||||
backgroundFlow.Clear();
|
||||
BackgroundFlow.Clear();
|
||||
|
||||
if (value?.Any() != true)
|
||||
return;
|
||||
|
||||
foreach (var group in value)
|
||||
{
|
||||
backgroundFlow.Add(new RowBackground(group));
|
||||
BackgroundFlow.Add(new RowBackground(group)
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
selectedGroup.Value = group;
|
||||
clock.SeekSmoothlyTo(group.Time);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Columns = createHeaders();
|
||||
@ -68,6 +52,16 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedGroup.BindValueChanged(group =>
|
||||
{
|
||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == group.NewValue;
|
||||
}, true);
|
||||
}
|
||||
|
||||
private TableColumn[] createHeaders()
|
||||
{
|
||||
var columns = new List<TableColumn>
|
||||
@ -86,13 +80,13 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = $"#{index + 1}",
|
||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold),
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding(10)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = group.Time.ToEditorFormattedString(),
|
||||
Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold)
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold)
|
||||
},
|
||||
null,
|
||||
new ControlGroupAttributes(group),
|
||||
@ -163,111 +157,5 @@ namespace osu.Game.Screens.Edit.Timing
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected override Drawable CreateHeader(int index, TableColumn column) => new HeaderText(column?.Header ?? string.Empty);
|
||||
|
||||
private class HeaderText : OsuSpriteText
|
||||
{
|
||||
public HeaderText(string text)
|
||||
{
|
||||
Text = text.ToUpper();
|
||||
Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold);
|
||||
}
|
||||
}
|
||||
|
||||
public class RowBackground : OsuClickableContainer
|
||||
{
|
||||
private readonly ControlPointGroup controlGroup;
|
||||
private const int fade_duration = 100;
|
||||
|
||||
private readonly Box hoveredBackground;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<ControlPointGroup> selectedGroup { get; set; }
|
||||
|
||||
public RowBackground(ControlPointGroup controlGroup)
|
||||
{
|
||||
this.controlGroup = controlGroup;
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 25;
|
||||
|
||||
AlwaysPresent = true;
|
||||
|
||||
CornerRadius = 3;
|
||||
Masking = true;
|
||||
|
||||
Children = new Drawable[]
|
||||
{
|
||||
hoveredBackground = new Box
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Alpha = 0,
|
||||
},
|
||||
};
|
||||
|
||||
Action = () =>
|
||||
{
|
||||
selectedGroup.Value = controlGroup;
|
||||
clock.SeekSmoothlyTo(controlGroup.Time);
|
||||
};
|
||||
}
|
||||
|
||||
private Color4 colourHover;
|
||||
private Color4 colourSelected;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
hoveredBackground.Colour = colourHover = colours.BlueDarker;
|
||||
colourSelected = colours.YellowDarker;
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedGroup.BindValueChanged(group => { Selected = controlGroup == group.NewValue; }, true);
|
||||
}
|
||||
|
||||
private bool selected;
|
||||
|
||||
protected bool Selected
|
||||
{
|
||||
get => selected;
|
||||
set
|
||||
{
|
||||
if (value == selected)
|
||||
return;
|
||||
|
||||
selected = value;
|
||||
updateState();
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
updateState();
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
protected override void OnHoverLost(HoverLostEvent e)
|
||||
{
|
||||
updateState();
|
||||
base.OnHoverLost(e);
|
||||
}
|
||||
|
||||
private void updateState()
|
||||
{
|
||||
hoveredBackground.FadeColour(selected ? colourSelected : colourHover, 450, Easing.OutQuint);
|
||||
|
||||
if (selected || IsHovered)
|
||||
hoveredBackground.FadeIn(fade_duration, Easing.OutQuint);
|
||||
else
|
||||
hoveredBackground.FadeOut(fade_duration, Easing.OutQuint);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
46
osu.Game/Screens/Edit/Verify/IssueSettings.cs
Normal file
46
osu.Game/Screens/Edit/Verify/IssueSettings.cs
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Verify
|
||||
{
|
||||
public class IssueSettings : CompositeDrawable
|
||||
{
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Gray3,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Direction = FillDirection.Vertical,
|
||||
Children = createSections()
|
||||
},
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private IReadOnlyList<Drawable> createSections() => new Drawable[]
|
||||
{
|
||||
};
|
||||
}
|
||||
}
|
128
osu.Game/Screens/Edit/Verify/IssueTable.cs
Normal file
128
osu.Game/Screens/Edit/Verify/IssueTable.cs
Normal file
@ -0,0 +1,128 @@
|
||||
// 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 osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Verify
|
||||
{
|
||||
public class IssueTable : EditorTable
|
||||
{
|
||||
[Resolved]
|
||||
private Bindable<Issue> selectedIssue { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private EditorBeatmap editorBeatmap { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private Editor editor { get; set; }
|
||||
|
||||
public IEnumerable<Issue> Issues
|
||||
{
|
||||
set
|
||||
{
|
||||
Content = null;
|
||||
BackgroundFlow.Clear();
|
||||
|
||||
if (value == null)
|
||||
return;
|
||||
|
||||
foreach (var issue in value)
|
||||
{
|
||||
BackgroundFlow.Add(new RowBackground(issue)
|
||||
{
|
||||
Action = () =>
|
||||
{
|
||||
selectedIssue.Value = issue;
|
||||
|
||||
if (issue.Time != null)
|
||||
{
|
||||
clock.Seek(issue.Time.Value);
|
||||
editor.OnPressed(GlobalAction.EditorComposeMode);
|
||||
}
|
||||
|
||||
if (!issue.HitObjects.Any())
|
||||
return;
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
editorBeatmap.SelectedHitObjects.AddRange(issue.HitObjects);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Columns = createHeaders();
|
||||
Content = value.Select((g, i) => createContent(i, g)).ToArray().ToRectangular();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
selectedIssue.BindValueChanged(issue =>
|
||||
{
|
||||
foreach (var b in BackgroundFlow) b.Selected = b.Item == issue.NewValue;
|
||||
}, true);
|
||||
}
|
||||
|
||||
private TableColumn[] createHeaders()
|
||||
{
|
||||
var columns = new List<TableColumn>
|
||||
{
|
||||
new TableColumn(string.Empty, Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize)),
|
||||
new TableColumn("Type", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
|
||||
new TableColumn("Time", Anchor.CentreLeft, new Dimension(GridSizeMode.AutoSize, minSize: 60)),
|
||||
new TableColumn("Message", Anchor.CentreLeft),
|
||||
new TableColumn("Category", Anchor.CentreRight, new Dimension(GridSizeMode.AutoSize)),
|
||||
};
|
||||
|
||||
return columns.ToArray();
|
||||
}
|
||||
|
||||
private Drawable[] createContent(int index, Issue issue) => new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = $"#{index + 1}",
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium),
|
||||
Margin = new MarginPadding { Right = 10 }
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = issue.Template.Type.ToString(),
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Right = 10 },
|
||||
Colour = issue.Template.Colour
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = issue.GetEditorTimestamp(),
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding { Right = 10 },
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = issue.ToString(),
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Medium)
|
||||
},
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = issue.Check.Metadata.Category.ToString(),
|
||||
Font = OsuFont.GetFont(size: TEXT_SIZE, weight: FontWeight.Bold),
|
||||
Margin = new MarginPadding(10)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
133
osu.Game/Screens/Edit/Verify/VerifyScreen.cs
Normal file
133
osu.Game/Screens/Edit/Verify/VerifyScreen.cs
Normal file
@ -0,0 +1,133 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Screens.Edit.Verify
|
||||
{
|
||||
public class VerifyScreen : EditorScreen
|
||||
{
|
||||
[Cached]
|
||||
private Bindable<Issue> selectedIssue = new Bindable<Issue>();
|
||||
|
||||
public VerifyScreen()
|
||||
: base(EditorScreenMode.Verify)
|
||||
{
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
Child = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Padding = new MarginPadding(20),
|
||||
Child = new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 200),
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
new IssueList(),
|
||||
new IssueSettings(),
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public class IssueList : CompositeDrawable
|
||||
{
|
||||
private IssueTable table;
|
||||
|
||||
[Resolved]
|
||||
private EditorClock clock { get; set; }
|
||||
|
||||
[Resolved]
|
||||
protected EditorBeatmap Beatmap { get; private set; }
|
||||
|
||||
[Resolved]
|
||||
private Bindable<Issue> selectedIssue { get; set; }
|
||||
|
||||
private IBeatmapVerifier rulesetVerifier;
|
||||
private BeatmapVerifier generalVerifier;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
{
|
||||
generalVerifier = new BeatmapVerifier();
|
||||
rulesetVerifier = Beatmap.BeatmapInfo.Ruleset?.CreateInstance()?.CreateBeatmapVerifier();
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
{
|
||||
Colour = colours.Gray0,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
new OsuScrollContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = table = new IssueTable(),
|
||||
},
|
||||
new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Margin = new MarginPadding(20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new TriangleButton
|
||||
{
|
||||
Text = "Refresh",
|
||||
Action = refresh,
|
||||
Size = new Vector2(120, 40),
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void refresh()
|
||||
{
|
||||
var issues = generalVerifier.Run(Beatmap);
|
||||
|
||||
if (rulesetVerifier != null)
|
||||
issues = issues.Concat(rulesetVerifier.Run(Beatmap));
|
||||
|
||||
table.Issues = issues
|
||||
.OrderBy(issue => issue.Template.Type)
|
||||
.ThenBy(issue => issue.Check.Metadata.Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user