mirror of https://github.com/ppy/osu
HitObject Selection logic and separation for gamemodes
+ moved time_regex into EditorTimestampParser
This commit is contained in:
parent
44f127c8a8
commit
aa87e0a44d
|
@ -3,6 +3,7 @@
|
|||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
|
@ -11,6 +12,7 @@
|
|||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
|
@ -49,6 +51,21 @@ protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
|||
};
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
=> string.Join(ObjectSeparator, EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
|
||||
public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo)
|
||||
{
|
||||
if (hitObject is not ManiaHitObject maniaHitObject)
|
||||
return false;
|
||||
|
||||
double[] split = objectInfo.Split('|').Select(double.Parse).ToArray();
|
||||
if (split.Length != 2)
|
||||
return false;
|
||||
|
||||
double timeValue = split[0];
|
||||
double columnValue = split[1];
|
||||
return Math.Abs(maniaHitObject.StartTime - timeValue) < 0.5
|
||||
&& Math.Abs(maniaHitObject.Column - columnValue) < 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,7 +104,18 @@ protected override ComposeBlueprintContainer CreateBlueprintContainer()
|
|||
=> new OsuBlueprintContainer(this);
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
=> string.Join(ObjectSeparator, selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
|
||||
public override bool HandleHitObjectSelection(HitObject hitObject, string objectInfo)
|
||||
{
|
||||
if (hitObject is not OsuHitObject osuHitObject)
|
||||
return false;
|
||||
|
||||
if (!int.TryParse(objectInfo, out int comboValue) || comboValue < 1)
|
||||
return false;
|
||||
|
||||
return osuHitObject.IndexInCurrentCombo + 1 == comboValue;
|
||||
}
|
||||
|
||||
private DistanceSnapGrid distanceSnapGrid;
|
||||
private Container distanceSnapGridContainer;
|
||||
|
|
|
@ -29,11 +29,14 @@ public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene
|
|||
protected EditorBeatmap EditorBeatmap => Editor.ChildrenOfType<EditorBeatmap>().Single();
|
||||
protected EditorClock EditorClock => Editor.ChildrenOfType<EditorClock>().Single();
|
||||
|
||||
protected void AddStepClickLink(string timestamp, string step = "")
|
||||
protected void AddStepClickLink(string timestamp, string step = "", bool waitForSeek = true)
|
||||
{
|
||||
AddStep($"{step} {timestamp}", () =>
|
||||
Game.HandleLink(new LinkDetails(LinkAction.OpenEditorTimestamp, timestamp))
|
||||
);
|
||||
|
||||
if (waitForSeek)
|
||||
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
|
||||
}
|
||||
|
||||
protected void AddStepScreenModeTo(EditorScreenMode screenMode)
|
||||
|
@ -76,7 +79,7 @@ private bool hasCombosInOrder(IEnumerable<HitObject> selected, params int[] comb
|
|||
.Any();
|
||||
}
|
||||
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)> columnPairs = null)
|
||||
private bool checkSnapAndSelectColumn(double startTime, IReadOnlyCollection<(int, int)>? columnPairs = null)
|
||||
{
|
||||
bool checkColumns = columnPairs != null
|
||||
? EditorBeatmap.SelectedHitObjects.All(x => columnPairs.Any(col => isNoteAt(x, col.Item1, col.Item2)))
|
||||
|
@ -123,7 +126,7 @@ public void TestErrorNotifications()
|
|||
{
|
||||
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
|
||||
|
||||
AddStepClickLink("00:00:000");
|
||||
AddStepClickLink("00:00:000", waitForSeek: false);
|
||||
AddAssert("recieved 'must be in edit'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 1
|
||||
);
|
||||
|
@ -131,7 +134,7 @@ public void TestErrorNotifications()
|
|||
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
|
||||
AddAssert("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
|
||||
AddStepClickLink("00:00:000 (1)");
|
||||
AddStepClickLink("00:00:000 (1)", waitForSeek: false);
|
||||
AddAssert("recieved 'must be in edit'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEdit) == 2
|
||||
);
|
||||
|
@ -139,27 +142,12 @@ public void TestErrorNotifications()
|
|||
SetUpEditor(rulesetInfo);
|
||||
AddAssert("is editor Osu", () => EditorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
|
||||
|
||||
AddStepClickLink("00:000", "invalid link");
|
||||
AddStepClickLink("00:000", "invalid link", waitForSeek: false);
|
||||
AddAssert("recieved 'failed to process'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 1
|
||||
);
|
||||
|
||||
AddStepClickLink("00:00:00:000", "invalid link");
|
||||
AddAssert("recieved 'failed to process'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 2
|
||||
);
|
||||
|
||||
AddStepClickLink("00:00:000 ()", "invalid link");
|
||||
AddAssert("recieved 'failed to process'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 3
|
||||
);
|
||||
|
||||
AddStepClickLink("00:00:000 (-1)", "invalid link");
|
||||
AddAssert("recieved 'failed to process'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToProcessTimestamp) == 4
|
||||
);
|
||||
|
||||
AddStepClickLink("50000:00:000", "too long link");
|
||||
AddStepClickLink("50000:00:000", "too long link", waitForSeek: false);
|
||||
AddAssert("recieved 'too long'", () =>
|
||||
Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.TooLongTimestamp) == 1
|
||||
);
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
|
||||
namespace osu.Game.Online.Chat
|
||||
{
|
||||
|
@ -41,10 +42,6 @@ public static class MessageFormatter
|
|||
@"(?:#(?:[a-z0-9$_\+!\*\',;:\(\)@&=\/~-]|%[0-9a-f]{2})*)?)?)",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
// 00:00:000 (1,2,3) - test
|
||||
// regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10
|
||||
private static readonly Regex time_regex = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)");
|
||||
|
||||
// #osu
|
||||
private static readonly Regex channel_regex = new Regex(@"(#[a-zA-Z]+[a-zA-Z0-9]+)");
|
||||
|
||||
|
@ -274,7 +271,7 @@ private static MessageFormatterResult format(string toFormat, int startIndex = 0
|
|||
handleAdvanced(advanced_link_regex, result, startIndex);
|
||||
|
||||
// handle editor times
|
||||
handleMatches(time_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
|
||||
handleMatches(EditorTimestampParser.TIME_REGEX, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}edit/{{0}}", result, startIndex, LinkAction.OpenEditorTimestamp);
|
||||
|
||||
// handle channels
|
||||
handleMatches(channel_regex, "{0}", $@"{OsuGameBase.OSU_PROTOCOL}chan/{{0}}", result, startIndex, LinkAction.OpenChannel);
|
||||
|
|
|
@ -563,10 +563,10 @@ public void SeekToTimestamp(string timestamp)
|
|||
if (ScreenStack.CurrentScreen is not Editor editor)
|
||||
{
|
||||
Schedule(() => Notifications.Post(new SimpleNotification
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Text = EditorStrings.MustBeInEdit
|
||||
}));
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -7,41 +7,43 @@
|
|||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public static class EditorTimestampParser
|
||||
{
|
||||
private static readonly Regex timestamp_regex = new Regex(@"^(\d+:\d+:\d+)(?: \((\d+(?:[|,]\d+)*)\))?$", RegexOptions.Compiled);
|
||||
// 00:00:000 (1,2,3) - test
|
||||
// regex from https://github.com/ppy/osu-web/blob/651a9bac2b60d031edd7e33b8073a469bf11edaa/resources/assets/coffee/_classes/beatmap-discussion-helper.coffee#L10
|
||||
public static readonly Regex TIME_REGEX = new Regex(@"\b(((\d{2,}):([0-5]\d)[:.](\d{3}))(\s\((?:\d+[,|])*\d+\))?)");
|
||||
|
||||
public static string[] GetRegexGroups(string timestamp)
|
||||
{
|
||||
Match match = timestamp_regex.Match(timestamp);
|
||||
return match.Success
|
||||
? match.Groups.Values.Where(x => x is not Match).Select(x => x.Value).ToArray()
|
||||
Match match = TIME_REGEX.Match(timestamp);
|
||||
string[] result = match.Success
|
||||
? match.Groups.Values.Where(x => x is not Match && !x.Value.Contains(':')).Select(x => x.Value).ToArray()
|
||||
: Array.Empty<string>();
|
||||
return result;
|
||||
}
|
||||
|
||||
public static double GetTotalMilliseconds(string timeGroup)
|
||||
public static double GetTotalMilliseconds(params string[] timesGroup)
|
||||
{
|
||||
int[] times = timeGroup.Split(':').Select(int.Parse).ToArray();
|
||||
int[] times = timesGroup.Select(int.Parse).ToArray();
|
||||
|
||||
Debug.Assert(times.Length == 3);
|
||||
|
||||
return (times[0] * 60 + times[1]) * 1_000 + times[2];
|
||||
}
|
||||
|
||||
public static List<HitObject> GetSelectedHitObjects(IReadOnlyList<HitObject> editorHitObjects, string objectsGroup, double position)
|
||||
public static List<HitObject> GetSelectedHitObjects(HitObjectComposer composer, IReadOnlyList<HitObject> editorHitObjects, string objectsGroup, double position)
|
||||
{
|
||||
List<HitObject> hitObjects = editorHitObjects.Where(x => x.StartTime >= position).ToList();
|
||||
List<HitObject> selectedObjects = new List<HitObject>();
|
||||
|
||||
string[] objectsToSelect = objectsGroup.Split(',').ToArray();
|
||||
string[] objectsToSelect = objectsGroup.Split(composer.ObjectSeparator).ToArray();
|
||||
|
||||
foreach (string objectInfo in objectsToSelect)
|
||||
{
|
||||
HitObject? current = hitObjects.FirstOrDefault(x => shouldHitObjectBeSelected(x, objectInfo));
|
||||
HitObject? current = hitObjects.FirstOrDefault(x => composer.HandleHitObjectSelection(x, objectInfo));
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
@ -67,35 +69,5 @@ public static List<HitObject> GetSelectedHitObjects(IReadOnlyList<HitObject> edi
|
|||
|
||||
return selectedObjects;
|
||||
}
|
||||
|
||||
private static bool shouldHitObjectBeSelected(HitObject hitObject, string objectInfo)
|
||||
{
|
||||
switch (hitObject)
|
||||
{
|
||||
// (combo)
|
||||
case IHasComboInformation comboInfo:
|
||||
{
|
||||
if (!double.TryParse(objectInfo, out double comboValue) || comboValue < 1)
|
||||
return false;
|
||||
|
||||
return comboInfo.IndexInCurrentCombo + 1 == comboValue;
|
||||
}
|
||||
|
||||
// (time|column)
|
||||
case IHasColumn column:
|
||||
{
|
||||
double[] split = objectInfo.Split('|').Select(double.Parse).ToArray();
|
||||
if (split.Length != 2)
|
||||
return false;
|
||||
|
||||
double timeValue = split[0];
|
||||
double columnValue = split[1];
|
||||
return hitObject.StartTime == timeValue && column.Column == columnValue;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -528,6 +528,19 @@ protected HitObjectComposer(Ruleset ruleset)
|
|||
|
||||
public virtual string ConvertSelectionToString() => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// The custom logic that decides whether a HitObject should be selected when clicking an editor timestamp link
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hitObject being checked</param>
|
||||
/// <param name="objectInfo">A single hitObject's information created with <see cref="ConvertSelectionToString"/></param>
|
||||
/// <returns>Whether a HitObject should be selected or not</returns>
|
||||
public virtual bool HandleHitObjectSelection(HitObject hitObject, string objectInfo) => false;
|
||||
|
||||
/// <summary>
|
||||
/// A character that separates the selection in <see cref="ConvertSelectionToString"/>
|
||||
/// </summary>
|
||||
public virtual char ObjectSeparator => ',';
|
||||
|
||||
#region IPositionSnapProvider
|
||||
|
||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// 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.
|
||||
|
||||
#nullable disable
|
||||
|
@ -14,6 +14,7 @@
|
|||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
|
@ -1142,7 +1143,7 @@ public void HandleTimestamp(string timestamp)
|
|||
{
|
||||
string[] groups = EditorTimestampParser.GetRegexGroups(timestamp);
|
||||
|
||||
if (groups.Length != 2 || string.IsNullOrEmpty(groups[0]))
|
||||
if (groups.Length != 4 || string.IsNullOrEmpty(groups[0]))
|
||||
{
|
||||
Schedule(() => notifications.Post(new SimpleNotification
|
||||
{
|
||||
|
@ -1152,13 +1153,14 @@ public void HandleTimestamp(string timestamp)
|
|||
return;
|
||||
}
|
||||
|
||||
string timeGroup = groups[0];
|
||||
string objectsGroup = groups[1];
|
||||
string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty;
|
||||
string timeMin = groups[0];
|
||||
string timeSec = groups[1];
|
||||
string timeMss = groups[2];
|
||||
string objectsGroup = groups[3].Replace("(", "").Replace(")", "").Trim();
|
||||
|
||||
// Currently, lazer chat highlights infinite-long editor links like `10000000000:00:000 (1)`
|
||||
// Limit timestamp link length at 30000 min (50 hr) to avoid parsing issues
|
||||
if (timeMinutes.Length > 5 || double.Parse(timeMinutes) > 30_000)
|
||||
if (string.IsNullOrEmpty(timeMin) || timeMin.Length > 5 || double.Parse(timeMin) > 30_000)
|
||||
{
|
||||
Schedule(() => notifications.Post(new SimpleNotification
|
||||
{
|
||||
|
@ -1168,38 +1170,36 @@ public void HandleTimestamp(string timestamp)
|
|||
return;
|
||||
}
|
||||
|
||||
double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup);
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
// Only seeking is necessary
|
||||
double position = EditorTimestampParser.GetTotalMilliseconds(timeMin, timeSec, timeMss);
|
||||
|
||||
if (string.IsNullOrEmpty(objectsGroup))
|
||||
{
|
||||
if (clock.IsRunning)
|
||||
clock.Stop();
|
||||
|
||||
clock.Seek(position);
|
||||
clock.SeekSmoothlyTo(position);
|
||||
return;
|
||||
}
|
||||
|
||||
// Seek to the next closest HitObject instead
|
||||
HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position);
|
||||
|
||||
if (nextObject != null)
|
||||
position = nextObject.StartTime;
|
||||
|
||||
clock.SeekSmoothlyTo(position);
|
||||
|
||||
if (Mode.Value != EditorScreenMode.Compose)
|
||||
Mode.Value = EditorScreenMode.Compose;
|
||||
|
||||
// Seek to the next closest HitObject
|
||||
HitObject nextObject = editorBeatmap.HitObjects.FirstOrDefault(x => x.StartTime >= position);
|
||||
|
||||
if (nextObject != null && nextObject.StartTime > 0)
|
||||
position = nextObject.StartTime;
|
||||
|
||||
List<HitObject> selected = EditorTimestampParser.GetSelectedHitObjects(editorBeatmap.HitObjects.ToList(), objectsGroup, position);
|
||||
List<HitObject> selected = EditorTimestampParser.GetSelectedHitObjects(
|
||||
currentScreen.Dependencies.Get<HitObjectComposer>(),
|
||||
editorBeatmap.HitObjects.ToList(),
|
||||
objectsGroup,
|
||||
position
|
||||
);
|
||||
|
||||
if (selected.Any())
|
||||
editorBeatmap.SelectedHitObjects.AddRange(selected);
|
||||
|
||||
if (clock.IsRunning)
|
||||
clock.Stop();
|
||||
|
||||
clock.Seek(position);
|
||||
}
|
||||
|
||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||
|
|
Loading…
Reference in New Issue