// Copyright (c) ppy Pty Ltd . 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 JetBrains.Annotations; 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.Framework.Threading; using osu.Framework.Timing; 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.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 : HitObjectComposer, IPlacementHandler where TObject : HitObject { protected IRulesetConfigManager Config { get; private set; } protected readonly Ruleset Ruleset; [Resolved] protected IFrameBasedClock EditorClock { get; private set; } [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } [Resolved] private IAdjustableClock adjustableClock { get; set; } [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } private IBeatmapProcessor beatmapProcessor; private DrawableEditRulesetWrapper drawableRulesetWrapper; private ComposeBlueprintContainer blueprintContainer; private Container distanceSnapGridContainer; private DistanceSnapGrid distanceSnapGrid; private readonly List layerContainers = new List(); private InputManager inputManager; private RadioButtonCollection toolboxCollection; protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; RelativeSizeAxes = Axes.Both; } [BackgroundDependencyLoader] private void load(IFrameBasedClock framedClock) { beatmapProcessor = Ruleset.CreateBeatmapProcessor(EditorBeatmap.PlayableBeatmap); EditorBeatmap.HitObjectAdded += addHitObject; EditorBeatmap.HitObjectRemoved += removeHitObject; EditorBeatmap.StartTimeChanged += UpdateHitObject; Config = Dependencies.Get().GetConfigFor(Ruleset); try { drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) { Clock = framedClock, ProcessCustomClock = false }; } catch (Exception e) { Logger.Error(e, "Could not load beatmap sucessfully!"); return; } var layerBelowRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] { distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }, new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } }); var layerAboveRuleset = drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(blueprintContainer = CreateBlueprintContainer()); layerContainers.Add(layerBelowRuleset); layerContainers.Add(layerAboveRuleset); 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, Children = new Drawable[] { layerBelowRuleset, drawableRulesetWrapper, layerAboveRuleset } } }, }, ColumnDimensions = new[] { new Dimension(GridSizeMode.Absolute, 200), } }; toolboxCollection.Items = CompositionTools .Prepend(new SelectTool()) .Select(t => new RadioButton(t.Name, () => toolSelected(t))) .ToList(); setSelectTool(); blueprintContainer.SelectionChanged += selectionChanged; } protected override bool OnKeyDown(KeyDownEvent e) { if (e.Key >= Key.Number1 && e.Key <= Key.Number9) { var item = toolboxCollection.Items.Skip(e.Key - Key.Number1).FirstOrDefault(); if (item != null) { item.Select(); return true; } } return base.OnKeyDown(e); } protected override void LoadComplete() { base.LoadComplete(); inputManager = GetContainingInputManager(); } private double lastGridUpdateTime; protected override void Update() { base.Update(); if (EditorClock.CurrentTime != lastGridUpdateTime && blueprintContainer.CurrentTool != null) showGridFor(Enumerable.Empty()); } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); layerContainers.ForEach(l => { l.Anchor = drawableRulesetWrapper.Playfield.Anchor; l.Origin = drawableRulesetWrapper.Playfield.Origin; l.Position = drawableRulesetWrapper.Playfield.Position; l.Size = drawableRulesetWrapper.Playfield.Size; }); } private void selectionChanged(IEnumerable selectedHitObjects) { var hitObjects = selectedHitObjects.ToArray(); if (hitObjects.Any()) { // ensure in selection mode if a selection is made. setSelectTool(); showGridFor(hitObjects); } else distanceSnapGridContainer.Hide(); } private void setSelectTool() => toolboxCollection.Items.First().Select(); private void toolSelected(HitObjectCompositionTool tool) { blueprintContainer.CurrentTool = tool; if (tool is SelectTool) distanceSnapGridContainer.Hide(); else { EditorBeatmap.SelectedHitObjects.Clear(); showGridFor(Enumerable.Empty()); } } private void showGridFor(IEnumerable selectedHitObjects) { distanceSnapGridContainer.Clear(); distanceSnapGrid = CreateDistanceSnapGrid(selectedHitObjects); if (distanceSnapGrid != null) { distanceSnapGridContainer.Child = distanceSnapGrid; distanceSnapGridContainer.Show(); } lastGridUpdateTime = EditorClock.CurrentTime; } private ScheduledDelegate scheduledUpdate; public override void UpdateHitObject(HitObject hitObject) { scheduledUpdate?.Cancel(); scheduledUpdate = Schedule(() => { beatmapProcessor?.PreProcess(); hitObject?.ApplyDefaults(EditorBeatmap.ControlPointInfo, EditorBeatmap.BeatmapInfo.BaseDifficulty); beatmapProcessor?.PostProcess(); }); } private void addHitObject(HitObject hitObject) => UpdateHitObject(hitObject); private void removeHitObject(HitObject hitObject) => UpdateHitObject(null); public override IEnumerable HitObjects => drawableRulesetWrapper.Playfield.AllHitObjects; public override bool CursorInPlacementArea => drawableRulesetWrapper.Playfield.ReceivePositionalInputAt(inputManager.CurrentState.Mouse.Position); protected abstract IReadOnlyList CompositionTools { get; } protected abstract ComposeBlueprintContainer CreateBlueprintContainer(); protected abstract DrawableRuleset CreateDrawableRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null); public void BeginPlacement(HitObject hitObject) { if (distanceSnapGrid != null) hitObject.StartTime = GetSnappedPosition(distanceSnapGrid.ToLocalSpace(inputManager.CurrentState.Mouse.Position), hitObject.StartTime).time; } public void EndPlacement(HitObject hitObject) { EditorBeatmap.Add(hitObject); adjustableClock.Seek(hitObject.StartTime); showGridFor(Enumerable.Empty()); } public void Delete(HitObject hitObject) => EditorBeatmap.Remove(hitObject); public override (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time) => distanceSnapGrid?.GetSnappedPosition(position) ?? (position, time); 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) => DurationToDistance(referenceTime, beatSnapProvider.SnapTime(DistanceToDuration(referenceTime, distance), referenceTime)); protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); if (EditorBeatmap != null) { EditorBeatmap.HitObjectAdded -= addHitObject; EditorBeatmap.HitObjectRemoved -= removeHitObject; } } } [Cached(typeof(HitObjectComposer))] [Cached(typeof(IDistanceSnapProvider))] public abstract class HitObjectComposer : CompositeDrawable, IDistanceSnapProvider { internal HitObjectComposer() { RelativeSizeAxes = Axes.Both; } /// /// All the s. /// public abstract IEnumerable HitObjects { get; } /// /// Whether the user's cursor is currently in an area of the that is valid for placement. /// public abstract bool CursorInPlacementArea { get; } /// /// Creates the applicable for a selection. /// /// The selection. /// The for . [CanBeNull] protected virtual DistanceSnapGrid CreateDistanceSnapGrid([NotNull] IEnumerable selectedHitObjects) => null; /// /// Updates a , invoking and re-processing the beatmap. /// /// The to update. public abstract void UpdateHitObject([CanBeNull] HitObject hitObject); public abstract (Vector2 position, double time) GetSnappedPosition(Vector2 position, double time); 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); } }