mirror of
https://github.com/ppy/osu.git
synced 2024-11-11 10:07:52 +08:00
Merge pull request #25371 from RatinFX/open-editor-timestamp
Implement opening editor timestamp links
This commit is contained in:
commit
d83b2e24e7
@ -0,0 +1,94 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Mania.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneOpenEditorTimestampInMania : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new ManiaRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestNormalSelection()
|
||||
{
|
||||
addStepClickLink("00:05:920 (5920|3,6623|3,6857|2,7326|1)");
|
||||
AddAssert("selected group", () => checkSnapAndSelectColumn(5_920, new List<(int, int)>
|
||||
{ (5_920, 3), (6_623, 3), (6_857, 2), (7_326, 1) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:42:716 (42716|3,43420|2,44123|0,44357|1,45295|1)");
|
||||
AddAssert("selected ungrouped", () => checkSnapAndSelectColumn(42_716, new List<(int, int)>
|
||||
{ (42_716, 3), (43_420, 2), (44_123, 0), (44_357, 1), (45_295, 1) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
AddStep("add notes to row", () =>
|
||||
{
|
||||
if (EditorBeatmap.HitObjects.Any(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column is 1 or 2 or 3))
|
||||
return;
|
||||
|
||||
ManiaHitObject first = (ManiaHitObject)EditorBeatmap.HitObjects.First(x => x is ManiaHitObject m && m.StartTime == 11_545 && m.Column == 0);
|
||||
ManiaHitObject second = new Note { Column = 1, StartTime = first.StartTime };
|
||||
ManiaHitObject third = new Note { Column = 2, StartTime = first.StartTime };
|
||||
ManiaHitObject forth = new Note { Column = 3, StartTime = first.StartTime };
|
||||
EditorBeatmap.AddRange(new[] { second, third, forth });
|
||||
});
|
||||
addStepClickLink("00:11:545 (11545|0,11545|1,11545|2,11545|3)");
|
||||
AddAssert("selected in row", () => checkSnapAndSelectColumn(11_545, new List<(int, int)>
|
||||
{ (11_545, 0), (11_545, 1), (11_545, 2), (11_545, 3) }
|
||||
));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("01:36:623 (96623|1,97560|1,97677|1,97795|1,98966|1)");
|
||||
AddAssert("selected in column", () => checkSnapAndSelectColumn(96_623, new List<(int, int)>
|
||||
{ (96_623, 1), (97_560, 1), (97_677, 1), (97_795, 1), (98_966, 1) }
|
||||
));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnusualSelection()
|
||||
{
|
||||
addStepClickLink("00:00:000 (0|1)", "wrong offset");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:000 (2)", "std link");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:000 (1,2)", "std link");
|
||||
AddAssert("snap to 1, select none", () => checkSnapAndSelectColumn(2_170));
|
||||
}
|
||||
|
||||
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
|
||||
{
|
||||
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
|
||||
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
|
||||
}
|
||||
|
||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||
|
||||
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)))
|
||||
: !EditorBeatmap.SelectedHitObjects.Any();
|
||||
|
||||
return EditorClock.CurrentTime == startTime
|
||||
&& EditorBeatmap.SelectedHitObjects.Count == (columnPairs?.Count ?? 0)
|
||||
&& checkColumns;
|
||||
}
|
||||
|
||||
private bool isNoteAt(HitObject hitObject, double time, int column) =>
|
||||
hitObject is ManiaHitObject maniaHitObject
|
||||
&& maniaHitObject.StartTime == time
|
||||
&& maniaHitObject.Column == column;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Tools;
|
||||
@ -50,5 +51,37 @@ namespace osu.Game.Rulesets.Mania.Edit
|
||||
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', EditorBeatmap.SelectedHitObjects.Cast<ManiaHitObject>().OrderBy(h => h.StartTime).Select(h => $"{h.StartTime}|{h.Column}"));
|
||||
|
||||
// 123|0,456|1,789|2 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+\|\d+(,\d+\|\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
public override void SelectFromTimestamp(double timestamp, string objectDescription)
|
||||
{
|
||||
if (!selection_regex.IsMatch(objectDescription))
|
||||
return;
|
||||
|
||||
List<ManiaHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<ManiaHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] objectDescriptions = objectDescription.Split(',').ToArray();
|
||||
|
||||
for (int i = 0; i < objectDescriptions.Length; i++)
|
||||
{
|
||||
string[] split = objectDescriptions[i].Split('|').ToArray();
|
||||
if (split.Length != 2)
|
||||
continue;
|
||||
|
||||
if (!double.TryParse(split[0], out double time) || !int.TryParse(split[1], out int column))
|
||||
continue;
|
||||
|
||||
ManiaHitObject current = remainingHitObjects.FirstOrDefault(h => h.StartTime == time && h.Column == column);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Add(current);
|
||||
|
||||
if (i < objectDescriptions.Length - 1)
|
||||
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Editor
|
||||
{
|
||||
public partial class TestSceneOpenEditorTimestampInOsu : EditorTestScene
|
||||
{
|
||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||
|
||||
[Test]
|
||||
public void TestNormalSelection()
|
||||
{
|
||||
addStepClickLink("00:02:170 (1,2,3)");
|
||||
checkSelection(() => 2_170, 1, 2, 3);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:04:748 (2,3,4,1,2)");
|
||||
checkSelection(() => 4_748, 2, 3, 4, 1, 2);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:02:170 (1,1,1)");
|
||||
checkSelection(() => 2_170, 1, 1, 1);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:02:873 (2,2,2,2)");
|
||||
checkSelection(() => 2_873, 2, 2, 2, 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUnusualSelection()
|
||||
{
|
||||
HitObject firstObject = null!;
|
||||
|
||||
AddStep("retrieve first object", () => firstObject = EditorBeatmap.HitObjects.First());
|
||||
|
||||
addStepClickLink("00:00:000 (0)", "invalid combo");
|
||||
checkSelection(() => firstObject.StartTime);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:000 (1)", "wrong offset");
|
||||
checkSelection(() => firstObject.StartTime, 1);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:956 (2,3,4)", "wrong offset");
|
||||
checkSelection(() => firstObject.StartTime, 2, 3, 4);
|
||||
|
||||
addReset();
|
||||
addStepClickLink("00:00:956 (956|1,956|2)", "mania link");
|
||||
checkSelection(() => firstObject.StartTime);
|
||||
}
|
||||
|
||||
private void addReset() => addStepClickLink("00:00:000", "reset", false);
|
||||
|
||||
private void addStepClickLink(string timestamp, string step = "", bool displayTimestamp = true)
|
||||
{
|
||||
AddStep(displayTimestamp ? $"{step} {timestamp}" : step, () => Editor.HandleTimestamp(timestamp));
|
||||
AddUntilStep("wait for seek", () => EditorClock.SeekingOrStopped.Value);
|
||||
}
|
||||
|
||||
private void checkSelection(Func<double> startTime, params int[] comboNumbers)
|
||||
=> AddUntilStep($"seeked & selected {(comboNumbers.Any() ? string.Join(",", comboNumbers) : "nothing")}", () =>
|
||||
{
|
||||
bool checkCombos = comboNumbers.Any()
|
||||
? hasCombosInOrder(EditorBeatmap.SelectedHitObjects, comboNumbers)
|
||||
: !EditorBeatmap.SelectedHitObjects.Any();
|
||||
|
||||
return EditorClock.CurrentTime == startTime()
|
||||
&& EditorBeatmap.SelectedHitObjects.Count == comboNumbers.Length
|
||||
&& checkCombos;
|
||||
});
|
||||
|
||||
private bool hasCombosInOrder(IEnumerable<HitObject> selected, params int[] comboNumbers)
|
||||
{
|
||||
List<HitObject> hitObjects = selected.ToList();
|
||||
if (hitObjects.Count != comboNumbers.Length)
|
||||
return false;
|
||||
|
||||
return !hitObjects.Select(x => (OsuHitObject)x)
|
||||
.Where((x, i) => x.IndexInCurrentCombo + 1 != comboNumbers[i])
|
||||
.Any();
|
||||
}
|
||||
}
|
||||
}
|
@ -6,6 +6,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Caching;
|
||||
@ -106,6 +107,34 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
public override string ConvertSelectionToString()
|
||||
=> string.Join(',', selectedHitObjects.Cast<OsuHitObject>().OrderBy(h => h.StartTime).Select(h => (h.IndexInCurrentCombo + 1).ToString()));
|
||||
|
||||
// 1,2,3,4 ...
|
||||
private static readonly Regex selection_regex = new Regex(@"^\d+(,\d+)*$", RegexOptions.Compiled);
|
||||
|
||||
public override void SelectFromTimestamp(double timestamp, string objectDescription)
|
||||
{
|
||||
if (!selection_regex.IsMatch(objectDescription))
|
||||
return;
|
||||
|
||||
List<OsuHitObject> remainingHitObjects = EditorBeatmap.HitObjects.Cast<OsuHitObject>().Where(h => h.StartTime >= timestamp).ToList();
|
||||
string[] splitDescription = objectDescription.Split(',').ToArray();
|
||||
|
||||
for (int i = 0; i < splitDescription.Length; i++)
|
||||
{
|
||||
if (!int.TryParse(splitDescription[i], out int combo) || combo < 1)
|
||||
continue;
|
||||
|
||||
OsuHitObject current = remainingHitObjects.FirstOrDefault(h => h.IndexInCurrentCombo + 1 == combo);
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
EditorBeatmap.SelectedHitObjects.Add(current);
|
||||
|
||||
if (i < splitDescription.Length - 1)
|
||||
remainingHitObjects = remainingHitObjects.Where(h => h != current && h.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
}
|
||||
|
||||
private DistanceSnapGrid distanceSnapGrid;
|
||||
private Container distanceSnapGridContainer;
|
||||
|
||||
|
151
osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs
Normal file
151
osu.Game.Tests/Visual/Editing/TestSceneOpenEditorTimestamp.cs
Normal file
@ -0,0 +1,151 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Tests.Resources;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
{
|
||||
public partial class TestSceneOpenEditorTimestamp : OsuGameTestScene
|
||||
{
|
||||
private Editor editor => (Editor)Game.ScreenStack.CurrentScreen;
|
||||
private EditorBeatmap editorBeatmap => editor.ChildrenOfType<EditorBeatmap>().Single();
|
||||
private EditorClock editorClock => editor.ChildrenOfType<EditorClock>().Single();
|
||||
|
||||
[Test]
|
||||
public void TestErrorNotifications()
|
||||
{
|
||||
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
|
||||
|
||||
addStepClickLink("00:00:000", waitForSeek: false);
|
||||
AddAssert("received 'must be in edit'",
|
||||
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
|
||||
() => Is.EqualTo(1));
|
||||
|
||||
AddStep("enter song select", () => Game.ChildrenOfType<ButtonSystem>().Single().OnSolo.Invoke());
|
||||
AddUntilStep("entered song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect);
|
||||
|
||||
addStepClickLink("00:00:000 (1)", waitForSeek: false);
|
||||
AddAssert("received 'must be in edit'",
|
||||
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.MustBeInEditorToHandleLinks),
|
||||
() => Is.EqualTo(2));
|
||||
|
||||
setUpEditor(rulesetInfo);
|
||||
AddAssert("ruleset is osu!", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
|
||||
|
||||
addStepClickLink("00:000", "invalid link", waitForSeek: false);
|
||||
AddAssert("received 'failed to process'",
|
||||
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
|
||||
() => Is.EqualTo(1));
|
||||
|
||||
addStepClickLink("50000:00:000", "too long link", waitForSeek: false);
|
||||
AddAssert("received 'failed to process'",
|
||||
() => Game.Notifications.AllNotifications.Count(x => x.Text == EditorStrings.FailedToParseEditorLink),
|
||||
() => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHandleCurrentScreenChanges()
|
||||
{
|
||||
RulesetInfo rulesetInfo = new OsuRuleset().RulesetInfo;
|
||||
|
||||
setUpEditor(rulesetInfo);
|
||||
AddAssert("is osu! ruleset", () => editorBeatmap.BeatmapInfo.Ruleset.Equals(rulesetInfo));
|
||||
|
||||
addStepClickLink("100:00:000", "long link");
|
||||
AddUntilStep("moved to end of track", () => editorClock.CurrentTime, () => Is.EqualTo(editorClock.TrackLength));
|
||||
|
||||
addStepScreenModeTo(EditorScreenMode.SongSetup);
|
||||
addStepClickLink("00:00:000");
|
||||
assertOnScreenAt(EditorScreenMode.SongSetup, 0);
|
||||
|
||||
addStepClickLink("00:05:000 (0|0)");
|
||||
assertMovedScreenTo(EditorScreenMode.Compose);
|
||||
|
||||
addStepScreenModeTo(EditorScreenMode.Design);
|
||||
addStepClickLink("00:10:000");
|
||||
assertOnScreenAt(EditorScreenMode.Design, 10_000);
|
||||
|
||||
addStepClickLink("00:15:000 (1)");
|
||||
assertMovedScreenTo(EditorScreenMode.Compose);
|
||||
|
||||
addStepScreenModeTo(EditorScreenMode.Timing);
|
||||
addStepClickLink("00:20:000");
|
||||
assertOnScreenAt(EditorScreenMode.Timing, 20_000);
|
||||
|
||||
addStepClickLink("00:25:000 (0,1)");
|
||||
assertMovedScreenTo(EditorScreenMode.Compose);
|
||||
|
||||
addStepScreenModeTo(EditorScreenMode.Verify);
|
||||
addStepClickLink("00:30:000");
|
||||
assertOnScreenAt(EditorScreenMode.Verify, 30_000);
|
||||
|
||||
addStepClickLink("00:35:000 (0,1)");
|
||||
assertMovedScreenTo(EditorScreenMode.Compose);
|
||||
|
||||
addStepClickLink("00:00:000");
|
||||
assertOnScreenAt(EditorScreenMode.Compose, 0);
|
||||
}
|
||||
|
||||
private 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);
|
||||
}
|
||||
|
||||
private void addStepScreenModeTo(EditorScreenMode screenMode) =>
|
||||
AddStep("change screen to " + screenMode, () => editor.Mode.Value = screenMode);
|
||||
|
||||
private void assertOnScreenAt(EditorScreenMode screen, double time)
|
||||
{
|
||||
AddAssert($"stayed on {screen} at {time}", () =>
|
||||
editor.Mode.Value == screen
|
||||
&& editorClock.CurrentTime == time
|
||||
);
|
||||
}
|
||||
|
||||
private void assertMovedScreenTo(EditorScreenMode screen, string text = "moved to") =>
|
||||
AddAssert($"{text} {screen}", () => editor.Mode.Value == screen);
|
||||
|
||||
private void setUpEditor(RulesetInfo ruleset)
|
||||
{
|
||||
BeatmapSetInfo beatmapSet = null!;
|
||||
|
||||
AddStep("Import test beatmap", () =>
|
||||
Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).WaitSafely()
|
||||
);
|
||||
AddStep("Retrieve beatmap", () =>
|
||||
beatmapSet = Game.BeatmapManager.QueryBeatmapSet(set => !set.Protected).AsNonNull().Value.Detach()
|
||||
);
|
||||
AddStep("Present beatmap", () => Game.PresentBeatmap(beatmapSet));
|
||||
AddUntilStep("Wait for song select", () =>
|
||||
Game.Beatmap.Value.BeatmapSetInfo.Equals(beatmapSet)
|
||||
&& Game.ScreenStack.CurrentScreen is PlaySongSelect songSelect
|
||||
&& songSelect.IsLoaded
|
||||
);
|
||||
AddStep("Switch ruleset", () => Game.Ruleset.Value = ruleset);
|
||||
AddStep("Open editor for ruleset", () =>
|
||||
((PlaySongSelect)Game.ScreenStack.CurrentScreen)
|
||||
.Edit(beatmapSet.Beatmaps.Last(beatmap => beatmap.Ruleset.Name == ruleset.Name))
|
||||
);
|
||||
AddUntilStep("Wait for editor open", () => editor.ReadyForUse);
|
||||
}
|
||||
}
|
||||
}
|
@ -119,6 +119,16 @@ namespace osu.Game.Localisation
|
||||
/// </summary>
|
||||
public static LocalisableString LimitedDistanceSnap => new TranslatableString(getKey(@"limited_distance_snap_grid"), @"Limit distance snap placement to current time");
|
||||
|
||||
/// <summary>
|
||||
/// "Must be in edit mode to handle editor links"
|
||||
/// </summary>
|
||||
public static LocalisableString MustBeInEditorToHandleLinks => new TranslatableString(getKey(@"must_be_in_editor_to_handle_links"), @"Must be in edit mode to handle editor links");
|
||||
|
||||
/// <summary>
|
||||
/// "Failed to parse editor link"
|
||||
/// </summary>
|
||||
public static LocalisableString FailedToParseEditorLink => new TranslatableString(getKey(@"failed_to_parse_edtior_link"), @"Failed to parse editor link");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using System.Collections.Generic;
|
||||
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 @@ namespace osu.Game.Online.Chat
|
||||
@"(?:#(?:[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 @@ namespace osu.Game.Online.Chat
|
||||
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);
|
||||
|
@ -58,6 +58,7 @@ using osu.Game.Performance;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens;
|
||||
using osu.Game.Screens.Edit;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osu.Game.Screens.OnlinePlay.Multiplayer;
|
||||
using osu.Game.Screens.Play;
|
||||
@ -433,6 +434,9 @@ namespace osu.Game
|
||||
break;
|
||||
|
||||
case LinkAction.OpenEditorTimestamp:
|
||||
HandleTimestamp(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.JoinMultiplayerMatch:
|
||||
case LinkAction.Spectate:
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
||||
@ -550,6 +554,25 @@ namespace osu.Game
|
||||
/// <param name="version">The build version of the update stream</param>
|
||||
public void ShowChangelogBuild(string updateStream, string version) => waitForReady(() => changelogOverlay, _ => changelogOverlay.ShowBuild(updateStream, version));
|
||||
|
||||
/// <summary>
|
||||
/// Seeks to the provided <paramref name="timestamp"/> if the editor is currently open.
|
||||
/// Can also select objects as indicated by the <paramref name="timestamp"/> (depends on ruleset implementation).
|
||||
/// </summary>
|
||||
public void HandleTimestamp(string timestamp)
|
||||
{
|
||||
if (ScreenStack.CurrentScreen is not Editor editor)
|
||||
{
|
||||
Schedule(() => Notifications.Post(new SimpleErrorNotification
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Text = EditorStrings.MustBeInEditorToHandleLinks
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
editor.HandleTimestamp(timestamp);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Present a skin select immediately.
|
||||
/// </summary>
|
||||
|
50
osu.Game/Rulesets/Edit/EditorTimestampParser.cs
Normal file
50
osu.Game/Rulesets/Edit/EditorTimestampParser.cs
Normal file
@ -0,0 +1,50 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit
|
||||
{
|
||||
public static class EditorTimestampParser
|
||||
{
|
||||
// 00:00:000 (...) - test
|
||||
// original osu-web regex: https://github.com/ppy/osu-web/blob/3b1698639244cfdaf0b41c68bfd651ea729ec2e3/resources/js/utils/beatmapset-discussion-helper.ts#L78
|
||||
public static readonly Regex TIME_REGEX = new Regex(@"\b(((?<minutes>\d{2,}):(?<seconds>[0-5]\d)[:.](?<milliseconds>\d{3}))(?<selection>\s\([^)]+\))?)", RegexOptions.Compiled);
|
||||
|
||||
public static bool TryParse(string timestamp, [NotNullWhen(true)] out TimeSpan? parsedTime, out string? parsedSelection)
|
||||
{
|
||||
Match match = TIME_REGEX.Match(timestamp);
|
||||
|
||||
if (!match.Success)
|
||||
{
|
||||
parsedTime = null;
|
||||
parsedSelection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool result = true;
|
||||
|
||||
result &= int.TryParse(match.Groups[@"minutes"].Value, out int timeMin);
|
||||
result &= int.TryParse(match.Groups[@"seconds"].Value, out int timeSec);
|
||||
result &= int.TryParse(match.Groups[@"milliseconds"].Value, out int timeMsec);
|
||||
|
||||
// somewhat sane limit for timestamp duration (10 hours).
|
||||
result &= timeMin < 600;
|
||||
|
||||
if (!result)
|
||||
{
|
||||
parsedTime = null;
|
||||
parsedSelection = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedTime = TimeSpan.FromMinutes(timeMin) + TimeSpan.FromSeconds(timeSec) + TimeSpan.FromMilliseconds(timeMsec);
|
||||
parsedSelection = match.Groups[@"selection"].Value.Trim();
|
||||
if (!string.IsNullOrEmpty(parsedSelection))
|
||||
parsedSelection = parsedSelection[1..^1];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
@ -526,8 +526,20 @@ namespace osu.Game.Rulesets.Edit
|
||||
/// </summary>
|
||||
public abstract bool CursorInPlacementArea { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string representing the current selection.
|
||||
/// The inverse method to <see cref="SelectFromTimestamp"/>.
|
||||
/// </summary>
|
||||
public virtual string ConvertSelectionToString() => string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Selects objects based on the supplied <paramref name="timestamp"/> and <paramref name="objectDescription"/>.
|
||||
/// The inverse method to <see cref="ConvertSelectionToString"/>.
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The time instant to seek to, in milliseconds.</param>
|
||||
/// <param name="objectDescription">The ruleset-specific description of objects to select at the given timestamp.</param>
|
||||
public virtual void SelectFromTimestamp(double timestamp, string objectDescription) { }
|
||||
|
||||
#region IPositionSnapProvider
|
||||
|
||||
public abstract SnapResult FindSnappedPositionAndTime(Vector2 screenSpacePosition, SnapType snapType = SnapType.All);
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
@ -50,6 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
|
||||
Beatmap.HitObjectAdded += AddBlueprintFor;
|
||||
Beatmap.HitObjectRemoved += RemoveBlueprintFor;
|
||||
Beatmap.SelectedHitObjects.CollectionChanged += updateSelectionLifetime;
|
||||
|
||||
if (Composer != null)
|
||||
{
|
||||
@ -144,6 +146,25 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
SelectedItems.AddRange(Beatmap.HitObjects.Except(SelectedItems).ToArray());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that newly-selected hitobjects are kept alive
|
||||
/// and drops that keep-alive from newly-deselected objects.
|
||||
/// </summary>
|
||||
private void updateSelectionLifetime(object sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
foreach (HitObject newSelection in e.NewItems)
|
||||
Composer.Playfield.SetKeepAlive(newSelection, true);
|
||||
}
|
||||
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
foreach (HitObject oldSelection in e.OldItems)
|
||||
Composer.Playfield.SetKeepAlive(oldSelection, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnBlueprintSelected(SelectionBlueprint<HitObject> blueprint)
|
||||
{
|
||||
base.OnBlueprintSelected(blueprint);
|
||||
@ -166,6 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
{
|
||||
Beatmap.HitObjectAdded -= AddBlueprintFor;
|
||||
Beatmap.HitObjectRemoved -= RemoveBlueprintFor;
|
||||
Beatmap.SelectedHitObjects.CollectionChanged -= updateSelectionLifetime;
|
||||
}
|
||||
|
||||
usageEventBuffer?.Dispose();
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Audio.Track;
|
||||
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;
|
||||
@ -39,6 +40,7 @@ using osu.Game.Overlays.Notifications;
|
||||
using osu.Game.Overlays.OSD;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Screens.Edit.Components.Menus;
|
||||
using osu.Game.Screens.Edit.Compose;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
@ -1150,6 +1152,45 @@ namespace osu.Game.Screens.Edit
|
||||
loader?.CancelPendingDifficultySwitch();
|
||||
}
|
||||
|
||||
public void HandleTimestamp(string timestamp)
|
||||
{
|
||||
if (!EditorTimestampParser.TryParse(timestamp, out var timeSpan, out string selection))
|
||||
{
|
||||
Schedule(() => notifications?.Post(new SimpleErrorNotification
|
||||
{
|
||||
Icon = FontAwesome.Solid.ExclamationTriangle,
|
||||
Text = EditorStrings.FailedToParseEditorLink
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
if (clock.IsRunning)
|
||||
clock.Stop();
|
||||
|
||||
double position = timeSpan.Value.TotalMilliseconds;
|
||||
|
||||
if (string.IsNullOrEmpty(selection))
|
||||
{
|
||||
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);
|
||||
|
||||
Mode.Value = EditorScreenMode.Compose;
|
||||
|
||||
// Delegate handling the selection to the ruleset.
|
||||
currentScreen.Dependencies.Get<HitObjectComposer>().SelectFromTimestamp(position, selection);
|
||||
}
|
||||
|
||||
public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime);
|
||||
|
||||
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||
|
Loading…
Reference in New Issue
Block a user