1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-12 03:57:25 +08:00
osu-lazer/osu.Game/Rulesets/Edit/HitObjectComposer.cs
Dean Herbert 6c7475f085 Fix snapped distances potentially exceeding the source distance
This results in slider placement including "excess" length, where the
curve is not applied to the placed path. This is generally not what we
want.

I considered adding a bool parameter (or enum) to change the
floor/rounding mode, but on further examination I think this is what we
always expect from this function.
2020-08-25 18:58:01 +09:00

355 lines
14 KiB
C#

// 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.Collections.Specialized;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Events;
using osu.Framework.Logging;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Screens.Edit;
using osu.Game.Screens.Edit.Components.RadioButtons;
using osu.Game.Screens.Edit.Compose;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
using Key = osuTK.Input.Key;
namespace osu.Game.Rulesets.Edit
{
[Cached(Type = typeof(IPlacementHandler))]
public abstract class HitObjectComposer<TObject> : HitObjectComposer, IPlacementHandler
where TObject : HitObject
{
protected IRulesetConfigManager Config { get; private set; }
protected readonly Ruleset Ruleset;
[Resolved]
protected EditorClock EditorClock { get; private set; }
[Resolved]
protected EditorBeatmap EditorBeatmap { get; private set; }
[Resolved]
protected IBeatSnapProvider BeatSnapProvider { get; private set; }
protected ComposeBlueprintContainer BlueprintContainer { get; private set; }
private DrawableEditRulesetWrapper<TObject> drawableRulesetWrapper;
protected readonly Container LayerBelowRuleset = new Container { RelativeSizeAxes = Axes.Both };
private InputManager inputManager;
private RadioButtonCollection toolboxCollection;
protected HitObjectComposer(Ruleset ruleset)
{
Ruleset = ruleset;
}
[BackgroundDependencyLoader]
private void load()
{
Config = Dependencies.Get<RulesetConfigCache>().GetConfigFor(Ruleset);
try
{
drawableRulesetWrapper = new DrawableEditRulesetWrapper<TObject>(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap))
{
Clock = EditorClock,
ProcessCustomClock = false
};
}
catch (Exception e)
{
Logger.Error(e, "Could not load beatmap sucessfully!");
return;
}
InternalChild = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new FillFlowContainer
{
Name = "Sidebar",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 10 },
Children = new Drawable[]
{
new ToolboxGroup { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }
}
},
new Container
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
// layers below playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[]
{
LayerBelowRuleset,
new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both }
}),
drawableRulesetWrapper,
// layers above playfield
drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer()
.WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects))
}
}
},
},
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 200),
}
};
toolboxCollection.Items = CompositionTools
.Prepend(new SelectTool())
.Select(t => new RadioButton(t.Name, () => toolSelected(t)))
.ToList();
setSelectTool();
EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged;
}
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override Playfield Playfield => drawableRulesetWrapper.Playfield;
public override IEnumerable<DrawableHitObject> HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects;
public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position);
/// <summary>
/// Defines all available composition tools, listed on the left side of the editor screen as button controls.
/// This should usually define one tool for each <see cref="HitObject"/> type used in the target ruleset.
/// </summary>
/// <remarks>
/// A "select" tool is automatically added as the first tool.
/// </remarks>
protected abstract IReadOnlyList<HitObjectCompositionTool> CompositionTools { get; }
/// <summary>
/// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic.
/// </summary>
/// <param name="hitObjects">A live collection of all <see cref="DrawableHitObject"/>s in the editor beatmap.</param>
protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable<DrawableHitObject> hitObjects)
=> new ComposeBlueprintContainer(hitObjects);
/// <summary>
/// Construct a drawable ruleset for the provided ruleset.
/// </summary>
/// <remarks>
/// Can be overridden to add editor-specific logical changes to a <see cref="Ruleset"/>'s standard <see cref="DrawableRuleset{TObject}"/>.
/// For example, hit animations or judgement logic may be changed to give a better editor user experience.
/// </remarks>
/// <param name="ruleset">The ruleset used to construct its drawable counterpart.</param>
/// <param name="beatmap">The loaded beatmap.</param>
/// <param name="mods">The mods to be applied.</param>
/// <returns>An editor-relevant <see cref="DrawableRuleset{TObject}"/>.</returns>
protected virtual DrawableRuleset<TObject> CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod> mods = null)
=> (DrawableRuleset<TObject>)ruleset.CreateDrawableRulesetWith(beatmap, mods);
#region Tool selection logic
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key >= Key.Number1 && e.Key <= Key.Number9)
{
var item = toolboxCollection.Items.ElementAtOrDefault(e.Key - Key.Number1);
if (item != null)
{
item.Select();
return true;
}
}
return base.OnKeyDown(e);
}
private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs)
{
if (EditorBeatmap.SelectedHitObjects.Any())
{
// ensure in selection mode if a selection is made.
setSelectTool();
}
}
private void setSelectTool() => toolboxCollection.Items.First().Select();
private void toolSelected(HitObjectCompositionTool tool)
{
BlueprintContainer.CurrentTool = tool;
if (!(tool is SelectTool))
EditorBeatmap.SelectedHitObjects.Clear();
}
#endregion
#region IPlacementHandler
public void BeginPlacement(HitObject hitObject)
{
EditorBeatmap.PlacementObject.Value = hitObject;
}
public void EndPlacement(HitObject hitObject, bool commit)
{
EditorBeatmap.PlacementObject.Value = null;
if (commit)
{
EditorBeatmap.Add(hitObject);
if (EditorClock.CurrentTime < hitObject.StartTime)
EditorClock.SeekTo(hitObject.StartTime);
}
}
public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject);
#endregion
#region IPositionSnapProvider
/// <summary>
/// Retrieve the relevant <see cref="Playfield"/> at a specified screen-space position.
/// In cases where a ruleset doesn't require custom logic (due to nested playfields, for example)
/// this will return the ruleset's main playfield.
/// </summary>
/// <param name="screenSpacePosition">The screen-space position to query.</param>
/// <returns>The most relevant <see cref="Playfield"/>.</returns>
protected virtual Playfield PlayfieldAtScreenSpacePosition(Vector2 screenSpacePosition) => drawableRulesetWrapper.Playfield;
public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition)
{
var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition);
double? targetTime = null;
if (playfield is ScrollingPlayfield scrollingPlayfield)
{
targetTime = scrollingPlayfield.TimeAtScreenSpacePosition(screenSpacePosition);
// apply beat snapping
targetTime = BeatSnapProvider.SnapTime(targetTime.Value);
// convert back to screen space
screenSpacePosition = scrollingPlayfield.ScreenSpacePositionAtTime(targetTime.Value);
}
return new SnapResult(screenSpacePosition, targetTime, playfield);
}
public override float GetBeatSnapDistanceAt(double referenceTime)
{
DifficultyControlPoint difficultyPoint = EditorBeatmap.ControlPointInfo.DifficultyPointAt(referenceTime);
return (float)(100 * EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / BeatSnapProvider.BeatDivisor);
}
public override float DurationToDistance(double referenceTime, double duration)
{
double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return (float)(duration / beatLength * GetBeatSnapDistanceAt(referenceTime));
}
public override double DistanceToDuration(double referenceTime, float distance)
{
double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return distance / GetBeatSnapDistanceAt(referenceTime) * beatLength;
}
public override double GetSnappedDurationFromDistance(double referenceTime, float distance)
=> BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime) - referenceTime;
public override float GetSnappedDistanceFromDistance(double referenceTime, float distance)
{
double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance);
double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime);
// we don't want to exceed the actual duration and snap to a point in the future.
if (snappedEndTime > actualDuration)
snappedEndTime -= BeatSnapProvider.GetBeatLengthAtTime(referenceTime);
return DurationToDistance(referenceTime, snappedEndTime - referenceTime);
}
#endregion
}
/// <summary>
/// A non-generic definition of a HitObject composer class.
/// Generally used to access certain methods without requiring a generic type for <see cref="HitObjectComposer{T}" />.
/// </summary>
[Cached(typeof(HitObjectComposer))]
[Cached(typeof(IPositionSnapProvider))]
public abstract class HitObjectComposer : CompositeDrawable, IPositionSnapProvider
{
protected HitObjectComposer()
{
RelativeSizeAxes = Axes.Both;
}
/// <summary>
/// The target ruleset's playfield.
/// </summary>
public abstract Playfield Playfield { get; }
/// <summary>
/// All <see cref="DrawableHitObject"/>s in currently loaded beatmap.
/// </summary>
public abstract IEnumerable<DrawableHitObject> HitObjects { get; }
/// <summary>
/// Whether the user's cursor is currently in an area of the <see cref="HitObjectComposer"/> that is valid for placement.
/// </summary>
public abstract bool CursorInPlacementArea { get; }
#region IPositionSnapProvider
public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition);
public abstract float GetBeatSnapDistanceAt(double referenceTime);
public abstract float DurationToDistance(double referenceTime, double duration);
public abstract double DistanceToDuration(double referenceTime, float distance);
public abstract double GetSnappedDurationFromDistance(double referenceTime, float distance);
public abstract float GetSnappedDistanceFromDistance(double referenceTime, float distance);
#endregion
}
}