mirror of
https://github.com/ppy/osu.git
synced 2025-01-13 07:22:54 +08:00
Added OpenEditorTimestamp base implementation
This commit is contained in:
parent
bc9cdb4ce0
commit
43ab7f4942
@ -119,6 +119,21 @@ 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 MustBeInEdit => new TranslatableString(getKey(@"must_be_in_edit"), @"Must be in edit mode to handle editor links");
|
||||
|
||||
/// <summary>
|
||||
/// "Failed to process timestamp"
|
||||
/// </summary>
|
||||
public static LocalisableString FailedToProcessTimestamp => new TranslatableString(getKey(@"failed_to_process_timestamp"), @"Failed to process timestamp");
|
||||
|
||||
/// <summary>
|
||||
/// "The timestamp was too long to process"
|
||||
/// </summary>
|
||||
public static LocalisableString TooLongTimestamp => new TranslatableString(getKey(@"too_long_timestamp"), @"The timestamp was too long to process");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
SeekToTimestamp(argString);
|
||||
break;
|
||||
|
||||
case LinkAction.JoinMultiplayerMatch:
|
||||
case LinkAction.Spectate:
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleNotification
|
||||
@ -550,6 +554,50 @@ 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>
|
||||
/// Seek to a given timestamp in the Editor and select relevant HitObjects if needed
|
||||
/// </summary>
|
||||
/// <param name="timestamp">The timestamp and the selected objects</param>
|
||||
public void SeekToTimestamp(string timestamp)
|
||||
{
|
||||
if (ScreenStack.CurrentScreen is not Editor editor)
|
||||
{
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification
|
||||
{
|
||||
Text = EditorStrings.MustBeInEdit,
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
string[] groups = EditorTimestampParser.GetRegexGroups(timestamp);
|
||||
|
||||
if (groups.Length != 2 || string.IsNullOrEmpty(groups[0]))
|
||||
{
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification
|
||||
{
|
||||
Text = EditorStrings.FailedToProcessTimestamp
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
string timeGroup = groups[0];
|
||||
string objectsGroup = groups[1];
|
||||
string timeMinutes = timeGroup.Split(':').FirstOrDefault() ?? string.Empty;
|
||||
|
||||
// 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)
|
||||
{
|
||||
waitForReady(() => Notifications, _ => Notifications.Post(new SimpleErrorNotification
|
||||
{
|
||||
Text = EditorStrings.TooLongTimestamp
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
editor.SeekToTimestamp(timeGroup, objectsGroup);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Present a skin select immediately.
|
||||
/// </summary>
|
||||
|
@ -39,6 +39,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;
|
||||
@ -1137,6 +1138,39 @@ namespace osu.Game.Screens.Edit
|
||||
loader?.CancelPendingDifficultySwitch();
|
||||
}
|
||||
|
||||
public void SeekToTimestamp(string timeGroup, string objectsGroup)
|
||||
{
|
||||
double position = EditorTimestampParser.GetTotalMilliseconds(timeGroup);
|
||||
editorBeatmap.SelectedHitObjects.Clear();
|
||||
|
||||
if (string.IsNullOrEmpty(objectsGroup))
|
||||
{
|
||||
if (clock.IsRunning)
|
||||
clock.Stop();
|
||||
|
||||
clock.Seek(position);
|
||||
return;
|
||||
}
|
||||
|
||||
if (Mode.Value != EditorScreenMode.Compose)
|
||||
Mode.Value = EditorScreenMode.Compose;
|
||||
|
||||
// Seek to the next closest HitObject's position
|
||||
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);
|
||||
|
||||
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);
|
||||
|
||||
public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime);
|
||||
|
101
osu.Game/Screens/Edit/EditorTimestampParser.cs
Normal file
101
osu.Game/Screens/Edit/EditorTimestampParser.cs
Normal file
@ -0,0 +1,101 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Screens.Edit
|
||||
{
|
||||
public static class EditorTimestampParser
|
||||
{
|
||||
private static readonly Regex timestamp_regex = new Regex(@"^(\d+:\d+:\d+)(?: \((\d+(?:[|,]\d+)*)\))?$", RegexOptions.Compiled);
|
||||
|
||||
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()
|
||||
: Array.Empty<string>();
|
||||
}
|
||||
|
||||
public static double GetTotalMilliseconds(string timeGroup)
|
||||
{
|
||||
int[] times = timeGroup.Split(':').Select(int.Parse).ToArray();
|
||||
|
||||
Debug.Assert(times.Length == 3);
|
||||
|
||||
return (times[0] * 60 + times[1]) * 1_000 + times[2];
|
||||
}
|
||||
|
||||
public static List<HitObject> GetSelectedHitObjects(IEnumerable<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();
|
||||
|
||||
foreach (string objectInfo in objectsToSelect)
|
||||
{
|
||||
HitObject? current = hitObjects.FirstOrDefault(x => shouldHitObjectBeSelected(x, objectInfo));
|
||||
|
||||
if (current == null)
|
||||
continue;
|
||||
|
||||
selectedObjects.Add(current);
|
||||
hitObjects = hitObjects.Where(x => x != current && x.StartTime >= current.StartTime).ToList();
|
||||
}
|
||||
|
||||
// Stable behavior
|
||||
// - always selects next closest object when `objectsGroup` only has one, non-Column item
|
||||
if (objectsToSelect.Length != 1 || objectsGroup.Contains('|'))
|
||||
return selectedObjects;
|
||||
|
||||
HitObject? nextClosest = editorHitObjects.FirstOrDefault(x => x.StartTime >= position);
|
||||
if (nextClosest == null)
|
||||
return selectedObjects;
|
||||
|
||||
if (nextClosest.StartTime <= (selectedObjects.FirstOrDefault()?.StartTime ?? position))
|
||||
{
|
||||
selectedObjects.Clear();
|
||||
selectedObjects.Add(nextClosest);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user