Merge remote-tracking branch 'origin/master' into netstandard

This commit is contained in:
smoogipoo
2018-03-24 14:49:46 +09:00
465 changed files with 12513 additions and 4969 deletions

View File

@ -4,17 +4,21 @@
using System;
using System.Collections.Generic;
using System.Linq;
using OpenTK.Graphics;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Logging;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit.Layers.Selection;
using osu.Game.Rulesets.Edit.Tools;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit.Screens.Compose;
using osu.Game.Screens.Edit.Screens.Compose.Layers;
using osu.Game.Screens.Edit.Screens.Compose.RadioButtons;
namespace osu.Game.Rulesets.Edit
@ -25,6 +29,14 @@ namespace osu.Game.Rulesets.Edit
protected ICompositionTool CurrentTool { get; private set; }
private RulesetContainer rulesetContainer;
private readonly List<Container> layerContainers = new List<Container>();
private readonly Bindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private readonly BindableBeatDivisor beatDivisor = new BindableBeatDivisor();
private IAdjustableClock adjustableClock;
protected HitObjectComposer(Ruleset ruleset)
{
this.ruleset = ruleset;
@ -32,13 +44,20 @@ namespace osu.Game.Rulesets.Edit
RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
private void load(OsuGameBase osuGame)
[BackgroundDependencyLoader(true)]
private void load([NotNull] OsuGameBase osuGame, [NotNull] IAdjustableClock adjustableClock, [NotNull] IFrameBasedClock framedClock, [CanBeNull] BindableBeatDivisor beatDivisor)
{
RulesetContainer rulesetContainer;
this.adjustableClock = adjustableClock;
if (beatDivisor != null)
this.beatDivisor.BindTo(beatDivisor);
beatmap.BindTo(osuGame.Beatmap);
try
{
rulesetContainer = CreateRulesetContainer(ruleset, osuGame.Beatmap.Value);
rulesetContainer = CreateRulesetContainer(ruleset, beatmap.Value);
rulesetContainer.Clock = framedClock;
}
catch (Exception e)
{
@ -46,6 +65,26 @@ namespace osu.Game.Rulesets.Edit
return;
}
HitObjectMaskLayer hitObjectMaskLayer = new HitObjectMaskLayer(this);
SelectionLayer selectionLayer = new SelectionLayer(rulesetContainer.Playfield);
var layerBelowRuleset = new BorderLayer
{
RelativeSizeAxes = Axes.Both,
Child = CreateLayerContainer()
};
var layerAboveRuleset = CreateLayerContainer();
layerAboveRuleset.Children = new Drawable[]
{
selectionLayer, // Below object overlays for input
hitObjectMaskLayer,
selectionLayer.CreateProxy() // Proxy above object overlays for selections
};
layerContainers.Add(layerBelowRuleset);
layerContainers.Add(layerAboveRuleset);
RadioButtonCollection toolboxCollection;
InternalChild = new GridContainer
{
@ -66,20 +105,13 @@ namespace osu.Game.Rulesets.Edit
},
new Container
{
Name = "Content",
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderColour = Color4.White,
BorderThickness = 2,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true,
},
layerBelowRuleset,
rulesetContainer,
new SelectionLayer(rulesetContainer.Playfield)
layerAboveRuleset
}
}
},
@ -90,7 +122,10 @@ namespace osu.Game.Rulesets.Edit
}
};
rulesetContainer.Clock = new InterpolatingFramedClock((IAdjustableClock)osuGame.Beatmap.Value.Track ?? new StopwatchClock());
selectionLayer.ObjectSelected += hitObjectMaskLayer.AddOverlay;
selectionLayer.ObjectDeselected += hitObjectMaskLayer.RemoveOverlay;
selectionLayer.SelectionCleared += hitObjectMaskLayer.RemoveSelectionOverlay;
selectionLayer.SelectionFinished += hitObjectMaskLayer.AddSelectionOverlay;
toolboxCollection.Items =
new[] { new RadioButton("Select", () => setCompositionTool(null)) }
@ -102,10 +137,141 @@ namespace osu.Game.Rulesets.Edit
toolboxCollection.Items[0].Select();
}
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
layerContainers.ForEach(l =>
{
l.Anchor = rulesetContainer.Playfield.Anchor;
l.Origin = rulesetContainer.Playfield.Origin;
l.Position = rulesetContainer.Playfield.Position;
l.Size = rulesetContainer.Playfield.Size;
});
}
protected override bool OnWheel(InputState state)
{
if (state.Mouse.WheelDelta > 0)
SeekBackward(true);
else
SeekForward(true);
return true;
}
/// <summary>
/// Seeks the current time one beat-snapped beat-length backwards.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat.</param>
public void SeekBackward(bool snapped = false) => seek(-1, snapped);
/// <summary>
/// Seeks the current time one beat-snapped beat-length forwards.
/// </summary>
/// <param name="snapped">Whether to snap to the closest beat.</param>
public void SeekForward(bool snapped = false) => seek(1, snapped);
private void seek(int direction, bool snapped)
{
var cpi = beatmap.Value.Beatmap.ControlPointInfo;
var timingPoint = cpi.TimingPointAt(adjustableClock.CurrentTime);
if (direction < 0 && timingPoint.Time == adjustableClock.CurrentTime)
{
// When going backwards and we're at the boundary of two timing points, we compute the seek distance with the timing point which we are seeking into
int activeIndex = cpi.TimingPoints.IndexOf(timingPoint);
while (activeIndex > 0 && adjustableClock.CurrentTime == timingPoint.Time)
timingPoint = cpi.TimingPoints[--activeIndex];
}
double seekAmount = timingPoint.BeatLength / beatDivisor;
double seekTime = adjustableClock.CurrentTime + seekAmount * direction;
if (!snapped || cpi.TimingPoints.Count == 0)
{
adjustableClock.Seek(seekTime);
return;
}
// We will be snapping to beats within timingPoint
seekTime -= timingPoint.Time;
// Determine the index from timingPoint of the closest beat to seekTime, accounting for scrolling direction
int closestBeat;
if (direction > 0)
closestBeat = (int)Math.Floor(seekTime / seekAmount);
else
closestBeat = (int)Math.Ceiling(seekTime / seekAmount);
seekTime = timingPoint.Time + closestBeat * seekAmount;
// Due to the rounding above, we may end up on the current beat. This will effectively cause 0 seeking to happen, but we don't want this.
// Instead, we'll go to the next beat in the direction when this is the case
if (Precision.AlmostEquals(adjustableClock.CurrentTime, seekTime))
{
closestBeat += direction > 0 ? 1 : -1;
seekTime = timingPoint.Time + closestBeat * seekAmount;
}
if (seekTime < timingPoint.Time && timingPoint != cpi.TimingPoints.First())
seekTime = timingPoint.Time;
var nextTimingPoint = cpi.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
adjustableClock.Seek(seekTime);
}
public void SeekTo(double seekTime, bool snapped = false)
{
if (!snapped)
{
adjustableClock.Seek(seekTime);
return;
}
var timingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(seekTime);
double beatSnapLength = timingPoint.BeatLength / beatDivisor;
// We will be snapping to beats within the timing point
seekTime -= timingPoint.Time;
// Determine the index from the current timing point of the closest beat to seekTime
int closestBeat = (int)Math.Round(seekTime / beatSnapLength);
seekTime = timingPoint.Time + closestBeat * beatSnapLength;
// Depending on beatSnapLength, we may snap to a beat that is beyond timingPoint's end time, but we want to instead snap to
// the next timing point's start time
var nextTimingPoint = beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time);
if (seekTime > nextTimingPoint?.Time)
seekTime = nextTimingPoint.Time;
adjustableClock.Seek(seekTime);
}
private void setCompositionTool(ICompositionTool tool) => CurrentTool = tool;
protected virtual RulesetContainer CreateRulesetContainer(Ruleset ruleset, WorkingBeatmap beatmap) => ruleset.CreateRulesetContainerWith(beatmap, true);
protected abstract IReadOnlyList<ICompositionTool> CompositionTools { get; }
/// <summary>
/// Creates a <see cref="HitObjectMask"/> for a specific <see cref="DrawableHitObject"/>.
/// </summary>
/// <param name="hitObject">The <see cref="DrawableHitObject"/> to create the overlay for.</param>
public virtual HitObjectMask CreateMaskFor(DrawableHitObject hitObject) => null;
/// <summary>
/// Creates a <see cref="SelectionBox"/> which outlines <see cref="DrawableHitObject"/>s
/// and handles all hitobject movement/pattern adjustments.
/// </summary>
/// <param name="overlays">The <see cref="DrawableHitObject"/> overlays.</param>
public virtual SelectionBox CreateSelectionOverlay(IReadOnlyList<HitObjectMask> overlays) => new SelectionBox(overlays);
/// <summary>
/// Creates a <see cref="ScalableContainer"/> which provides a layer above or below the <see cref="Playfield"/>.
/// </summary>
protected virtual ScalableContainer CreateLayerContainer() => new ScalableContainer { RelativeSizeAxes = Axes.Both };
}
}

View File

@ -0,0 +1,21 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Edit
{
/// <summary>
/// A mask placed above a <see cref="DrawableHitObject"/> adding editing functionality.
/// </summary>
public class HitObjectMask : Container
{
public readonly DrawableHitObject HitObject;
public HitObjectMask(DrawableHitObject hitObject)
{
HitObject = hitObject;
}
}
}

View File

@ -1,105 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
/// <summary>
/// Represents a marker visible on the border of a <see cref="HandleContainer"/> which exposes
/// properties that are used to resize a <see cref="HitObjectSelectionBox"/>.
/// </summary>
public class Handle : CompositeDrawable
{
private const float marker_size = 10;
/// <summary>
/// Invoked when this <see cref="Handle"/> requires the current drag rectangle.
/// </summary>
public Func<RectangleF> GetDragRectangle;
/// <summary>
/// Invoked when this <see cref="Handle"/> wants to update the drag rectangle.
/// </summary>
public Action<RectangleF> UpdateDragRectangle;
/// <summary>
/// Invoked when this <see cref="Handle"/> has finished updates to the drag rectangle.
/// </summary>
public Action FinishDrag;
private Color4 normalColour;
private Color4 hoverColour;
public Handle()
{
Size = new Vector2(marker_size);
InternalChild = new CircularContainer
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = normalColour = colours.Yellow;
hoverColour = colours.YellowDarker;
}
protected override bool OnDragStart(InputState state) => true;
protected override bool OnDrag(InputState state)
{
var currentRectangle = GetDragRectangle();
float left = currentRectangle.Left;
float right = currentRectangle.Right;
float top = currentRectangle.Top;
float bottom = currentRectangle.Bottom;
// Apply modifications to the capture rectangle
if ((Anchor & Anchor.y0) > 0)
top += state.Mouse.Delta.Y;
else if ((Anchor & Anchor.y2) > 0)
bottom += state.Mouse.Delta.Y;
if ((Anchor & Anchor.x0) > 0)
left += state.Mouse.Delta.X;
else if ((Anchor & Anchor.x2) > 0)
right += state.Mouse.Delta.X;
UpdateDragRectangle(RectangleF.FromLTRB(left, top, right, bottom));
return true;
}
protected override bool OnDragEnd(InputState state)
{
FinishDrag();
return true;
}
protected override bool OnHover(InputState state)
{
this.FadeColour(hoverColour, 100);
return true;
}
protected override void OnHoverLost(InputState state)
{
this.FadeColour(normalColour, 100);
}
}
}

View File

@ -1,92 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
/// <summary>
/// A <see cref="CompositeDrawable"/> that has <see cref="Handle"/>s around its border.
/// </summary>
public class HandleContainer : CompositeDrawable
{
/// <summary>
/// Invoked when a <see cref="Handle"/> requires the current drag rectangle.
/// </summary>
public Func<RectangleF> GetDragRectangle;
/// <summary>
/// Invoked when a <see cref="Handle"/> wants to update the drag rectangle.
/// </summary>
public Action<RectangleF> UpdateDragRectangle;
/// <summary>
/// Invoked when a <see cref="Handle"/> has finished updates to the drag rectangle.
/// </summary>
public Action FinishDrag;
public HandleContainer()
{
InternalChildren = new Drawable[]
{
new Handle
{
Anchor = Anchor.TopLeft,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.TopCentre,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.TopRight,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.BottomLeft,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.CentreRight,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.BottomRight,
Origin = Anchor.Centre
},
new Handle
{
Anchor = Anchor.BottomCentre,
Origin = Anchor.Centre
},
new OriginHandle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre
}
};
InternalChildren.OfType<Handle>().ForEach(m =>
{
m.GetDragRectangle = () => GetDragRectangle();
m.UpdateDragRectangle = r => UpdateDragRectangle(r);
m.FinishDrag = () => FinishDrag();
});
}
}
}

View File

@ -1,179 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Configuration;
using RectangleF = osu.Framework.Graphics.Primitives.RectangleF;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
/// <summary>
/// A box that represents a drag selection.
/// </summary>
public class HitObjectSelectionBox : CompositeDrawable
{
public readonly Bindable<SelectionInfo> Selection = new Bindable<SelectionInfo>();
/// <summary>
/// The <see cref="DrawableHitObject"/>s that can be selected through a drag-selection.
/// </summary>
public IEnumerable<DrawableHitObject> CapturableObjects;
private readonly Container borderMask;
private readonly Drawable background;
private readonly HandleContainer handles;
private Color4 captureFinishedColour;
private readonly Vector2 startPos;
/// <summary>
/// Creates a new <see cref="HitObjectSelectionBox"/>.
/// </summary>
/// <param name="startPos">The point at which the drag was initiated, in the parent's coordinates.</param>
public HitObjectSelectionBox(Vector2 startPos)
{
this.startPos = startPos;
InternalChildren = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(-1),
Child = borderMask = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
BorderColour = Color4.White,
BorderThickness = 2,
MaskingSmoothness = 1,
Child = background = new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.1f,
AlwaysPresent = true
},
}
},
handles = new HandleContainer
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
GetDragRectangle = () => dragRectangle,
UpdateDragRectangle = updateDragRectangle,
FinishDrag = FinishCapture
}
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
captureFinishedColour = colours.Yellow;
}
/// <summary>
/// The secondary corner of the drag selection box. A rectangle will be fit between the starting position and this value.
/// </summary>
public Vector2 DragEndPosition { set => updateDragRectangle(RectangleF.FromLTRB(startPos.X, startPos.Y, value.X, value.Y)); }
private RectangleF dragRectangle;
private void updateDragRectangle(RectangleF rectangle)
{
dragRectangle = rectangle;
Position = new Vector2(
Math.Min(rectangle.Left, rectangle.Right),
Math.Min(rectangle.Top, rectangle.Bottom));
Size = new Vector2(
Math.Max(rectangle.Left, rectangle.Right) - Position.X,
Math.Max(rectangle.Top, rectangle.Bottom) - Position.Y);
}
private readonly List<DrawableHitObject> capturedHitObjects = new List<DrawableHitObject>();
/// <summary>
/// Processes hitobjects to determine which ones are captured by the drag selection.
/// Captured hitobjects will be enclosed by the drag selection upon <see cref="FinishCapture"/>.
/// </summary>
public void BeginCapture()
{
capturedHitObjects.Clear();
foreach (var obj in CapturableObjects)
{
if (!obj.IsAlive || !obj.IsPresent)
continue;
if (ScreenSpaceDrawQuad.Contains(obj.SelectionPoint))
capturedHitObjects.Add(obj);
}
}
/// <summary>
/// Encloses hitobjects captured through <see cref="BeginCapture"/> in the drag selection box.
/// </summary>
public void FinishCapture()
{
if (capturedHitObjects.Count == 0)
{
Hide();
return;
}
// Move the rectangle to cover the hitobjects
var topLeft = new Vector2(float.MaxValue, float.MaxValue);
var bottomRight = new Vector2(float.MinValue, float.MinValue);
foreach (var obj in capturedHitObjects)
{
topLeft = Vector2.ComponentMin(topLeft, Parent.ToLocalSpace(obj.SelectionQuad.TopLeft));
bottomRight = Vector2.ComponentMax(bottomRight, Parent.ToLocalSpace(obj.SelectionQuad.BottomRight));
}
topLeft -= new Vector2(5);
bottomRight += new Vector2(5);
this.MoveTo(topLeft, 200, Easing.OutQuint)
.ResizeTo(bottomRight - topLeft, 200, Easing.OutQuint);
dragRectangle = RectangleF.FromLTRB(topLeft.X, topLeft.Y, bottomRight.X, bottomRight.Y);
borderMask.BorderThickness = 3;
borderMask.FadeColour(captureFinishedColour, 200);
// Transform into markers to let the user modify the drag selection further.
background.Delay(50).FadeOut(200);
handles.FadeIn(200);
Selection.Value = new SelectionInfo
{
Objects = capturedHitObjects,
SelectionQuad = Parent.ToScreenSpace(dragRectangle)
};
}
private bool isActive = true;
public override bool HandleKeyboardInput => isActive;
public override bool HandleMouseInput => isActive;
public override void Hide()
{
isActive = false;
this.FadeOut(400, Easing.OutQuint).Expire();
Selection.Value = null;
}
}
}

View File

@ -1,50 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using OpenTK;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
/// <summary>
/// Represents the origin of a <see cref="HandleContainer"/>.
/// </summary>
public class OriginHandle : CompositeDrawable
{
private const float marker_size = 10;
private const float line_width = 2;
public OriginHandle()
{
Size = new Vector2(marker_size);
InternalChildren = new[]
{
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
Height = line_width
},
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Y,
Width = line_width
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Colour = colours.Yellow;
}
}
}

View File

@ -1,22 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Objects.Drawables;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
public class SelectionInfo
{
/// <summary>
/// The objects that are captured by the selection.
/// </summary>
public IEnumerable<DrawableHitObject> Objects;
/// <summary>
/// The screen space quad of the selection box surrounding <see cref="Objects"/>.
/// </summary>
public Quad SelectionQuad;
}
}

View File

@ -1,61 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Edit.Layers.Selection
{
public class SelectionLayer : CompositeDrawable
{
public readonly Bindable<SelectionInfo> Selection = new Bindable<SelectionInfo>();
private readonly Playfield playfield;
public SelectionLayer(Playfield playfield)
{
this.playfield = playfield;
RelativeSizeAxes = Axes.Both;
}
private HitObjectSelectionBox selectionBoxBox;
protected override bool OnDragStart(InputState state)
{
// Hide the previous drag box - we won't be working with it any longer
selectionBoxBox?.Hide();
AddInternal(selectionBoxBox = new HitObjectSelectionBox(ToLocalSpace(state.Mouse.NativeState.Position))
{
CapturableObjects = playfield.HitObjects.Objects,
});
Selection.BindTo(selectionBoxBox.Selection);
return true;
}
protected override bool OnDrag(InputState state)
{
selectionBoxBox.DragEndPosition = ToLocalSpace(state.Mouse.NativeState.Position);
selectionBoxBox.BeginCapture();
return true;
}
protected override bool OnDragEnd(InputState state)
{
selectionBoxBox.FinishCapture();
return true;
}
protected override bool OnClick(InputState state)
{
selectionBoxBox?.Hide();
return true;
}
}
}

View File

@ -0,0 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
namespace osu.Game.Rulesets.Edit.Types
{
public interface IHasEditablePosition : IHasPosition
{
void OffsetPosition(Vector2 offset);
}
}

View File

@ -9,7 +9,10 @@ using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Judgements
{
@ -18,43 +21,37 @@ namespace osu.Game.Rulesets.Judgements
/// </summary>
public class DrawableJudgement : Container
{
private const float judgement_size = 80;
protected readonly Judgement Judgement;
protected readonly SpriteText JudgementText;
public readonly DrawableHitObject JudgedObject;
protected SpriteText JudgementText;
/// <summary>
/// Creates a drawable which visualises a <see cref="Judgements.Judgement"/>.
/// </summary>
/// <param name="judgement">The judgement to visualise.</param>
public DrawableJudgement(Judgement judgement)
public DrawableJudgement(Judgement judgement, DrawableHitObject judgedObject)
{
Judgement = judgement;
JudgedObject = judgedObject;
AutoSizeAxes = Axes.Both;
Children = new[]
{
JudgementText = new OsuSpriteText
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Text = judgement.Result.GetDescription().ToUpper(),
Font = @"Venera",
Scale = new Vector2(0.85f, 1),
TextSize = 12
}
};
Size = new Vector2(judgement_size);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
switch (Judgement.Result)
Child = new SkinnableDrawable($"Play/{Judgement.Result}", _ => JudgementText = new OsuSpriteText
{
case HitResult.Miss:
Colour = colours.Red;
break;
}
Text = Judgement.Result.GetDescription().ToUpper(),
Font = @"Venera",
Colour = Judgement.Result == HitResult.Miss ? colours.Red : Color4.White,
Scale = new Vector2(0.85f, 1),
TextSize = 12
}, restrictSize: false);
}
protected override void LoadComplete()

View File

@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Autoplay";
public override string ShortenedName => "AT";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_auto;
public override string Description => "Watch a perfect automated play through the song";
public override string Description => "Watch a perfect automated play through the song.";
public override double ScoreMultiplier => 0;
public bool AllowFail => false;
public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) };

View File

@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Mods
public override string ShortenedName => "CN";
public override bool HasImplementation => false;
public override FontAwesome Icon => FontAwesome.fa_osu_mod_cinema;
public override string Description => "Watch the video without visual distractions.";
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Daycore";
public override string ShortenedName => "DC";
public override FontAwesome Icon => FontAwesome.fa_question;
public override string Description => "whoaaaaa";
public override string Description => "Whoaaaaa...";
public override void ApplyToClock(IAdjustableClock clock)
{

View File

@ -7,18 +7,16 @@ using osu.Game.Graphics;
namespace osu.Game.Rulesets.Mods
{
public class ModDoubleTime : Mod, IApplicableToClock
public abstract class ModDoubleTime : Mod, IApplicableToClock
{
public override string Name => "Double Time";
public override string ShortenedName => "DT";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_doubletime;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Zoooooooooom";
public override string Description => "Zoooooooooom...";
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModHalfTime) };
public override double ScoreMultiplier => 1.12;
public virtual void ApplyToClock(IAdjustableClock clock)
{
clock.Rate = 1.5;

View File

@ -13,7 +13,6 @@ namespace osu.Game.Rulesets.Mods
public override string ShortenedName => "EZ";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_easy;
public override ModType Type => ModType.DifficultyReduction;
public override string Description => "Reduces overall difficulty - larger circles, more forgiving HP drain, less accuracy required.";
public override double ScoreMultiplier => 0.5;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModHardRock) };

View File

@ -13,12 +13,10 @@ namespace osu.Game.Rulesets.Mods
public override string ShortenedName => "HT";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_halftime;
public override ModType Type => ModType.DifficultyReduction;
public override string Description => "Less zoom";
public override string Description => "Less zoom...";
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModDoubleTime) };
public override double ScoreMultiplier => 1.12;
public virtual void ApplyToClock(IAdjustableClock clock)
{
clock.Rate = 0.75;

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mods
public override FontAwesome Icon => FontAwesome.fa_osu_mod_hardrock;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Everything just got a bit harder...";
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModEasy) };
public void ApplyToDifficulty(BeatmapDifficulty difficulty)

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => "Nightcore";
public override string ShortenedName => "NC";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_nightcore;
public override string Description => "uguuuuuuuu";
public override string Description => "Uguuuuuuuu...";
public override void ApplyToClock(IAdjustableClock clock)
{

View File

@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods
{
public abstract class ModNoFail : Mod, IApplicableFailOverride
{
public override string Name => "NoFail";
public override string Name => "No Fail";
public override string ShortenedName => "NF";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_nofail;
public override ModType Type => ModType.DifficultyReduction;

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Graphics;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mods
@ -9,6 +10,7 @@ namespace osu.Game.Rulesets.Mods
{
public override string Name => "Perfect";
public override string ShortenedName => "PF";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_perfect;
public override string Description => "SS or quit.";
protected override bool FailCondition(ScoreProcessor scoreProcessor) => scoreProcessor.Accuracy.Value != 1;

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mods
public override string ShortenedName => "SD";
public override FontAwesome Icon => FontAwesome.fa_osu_mod_suddendeath;
public override ModType Type => ModType.DifficultyIncrease;
public override string Description => "Miss a note and fail.";
public override string Description => "Miss and fail.";
public override double ScoreMultiplier => 1;
public override bool Ranked => true;
public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) };

View File

@ -8,7 +8,7 @@ namespace osu.Game.Rulesets.Mods
public override string Name => string.Empty;
public override string ShortenedName => string.Empty;
public override string Description => string.Empty;
public override double ScoreMultiplier => 0.0;
public override double ScoreMultiplier => 0;
public Mod[] Mods;
}

View File

@ -3,24 +3,22 @@
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Game.Rulesets.Judgements;
using Container = osu.Framework.Graphics.Containers.Container;
using osu.Game.Rulesets.Objects.Types;
using OpenTK.Graphics;
using osu.Game.Audio;
using System.Linq;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using OpenTK;
using osu.Framework.Graphics.Primitives;
using osu.Game.Audio;
using osu.Game.Graphics;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Objects.Drawables
{
public abstract class DrawableHitObject : Container, IHasAccentColour
public abstract class DrawableHitObject : SkinReloadableDrawable, IHasAccentColour
{
public readonly HitObject HitObject;
@ -32,11 +30,13 @@ namespace osu.Game.Rulesets.Objects.Drawables
// Todo: Rulesets should be overriding the resources instead, but we need to figure out where/when to apply overrides first
protected virtual string SampleNamespace => null;
protected List<SampleChannel> Samples = new List<SampleChannel>();
protected SkinnableSound Samples;
protected virtual IEnumerable<SampleInfo> GetSamples() => HitObject.Samples;
private List<DrawableHitObject> nestedHitObjects;
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects;
private readonly Lazy<List<DrawableHitObject>> nestedHitObjects = new Lazy<List<DrawableHitObject>>();
public bool HasNestedHitObjects => nestedHitObjects.IsValueCreated;
public IReadOnlyList<DrawableHitObject> NestedHitObjects => nestedHitObjects.Value;
public event Action<DrawableHitObject, Judgement> OnJudgement;
public event Action<DrawableHitObject, Judgement> OnJudgementRemoved;
@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been hit.
/// </summary>
public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (NestedHitObjects?.All(n => n.IsHit) ?? true);
public bool IsHit => Judgements.Any(j => j.Final && j.IsHit) && (!HasNestedHitObjects || NestedHitObjects.All(n => n.IsHit));
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> and all of its nested <see cref="DrawableHitObject"/>s have been judged.
/// </summary>
public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (NestedHitObjects?.All(h => h.AllJudged) ?? true);
public bool AllJudged => (!ProvidesJudgement || judgementFinalized) && (!HasNestedHitObjects || NestedHitObjects.All(h => h.AllJudged));
/// <summary>
/// Whether this <see cref="DrawableHitObject"/> can be judged.
@ -83,31 +83,22 @@ namespace osu.Game.Rulesets.Objects.Drawables
}
[BackgroundDependencyLoader]
private void load(AudioManager audio)
private void load()
{
var samples = GetSamples();
var samples = GetSamples().ToArray();
if (samples.Any())
{
if (HitObject.SampleControlPoint == null)
throw new ArgumentNullException(nameof(HitObject.SampleControlPoint), $"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
foreach (SampleInfo s in samples)
AddInternal(Samples = new SkinnableSound(samples.Select(s => new SampleInfo
{
SampleInfo localSampleInfo = new SampleInfo
{
Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank,
Name = s.Name,
Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume
};
SampleChannel channel = localSampleInfo.GetChannel(audio.Sample, SampleNamespace);
if (channel == null)
continue;
Samples.Add(channel);
}
Bank = s.Bank ?? HitObject.SampleControlPoint.SampleBank,
Name = s.Name,
Volume = s.Volume > 0 ? s.Volume : HitObject.SampleControlPoint.SampleVolume,
Namespace = SampleNamespace
}).ToArray()));
}
}
@ -139,7 +130,7 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// <summary>
/// Plays all the hitsounds for this <see cref="DrawableHitObject"/>.
/// </summary>
public void PlaySamples() => Samples.ForEach(s => s?.Play());
public void PlaySamples() => Samples?.Play();
protected override void Update()
{
@ -169,14 +160,11 @@ namespace osu.Game.Rulesets.Objects.Drawables
protected virtual void AddNested(DrawableHitObject h)
{
if (nestedHitObjects == null)
nestedHitObjects = new List<DrawableHitObject>();
h.OnJudgement += (d, j) => OnJudgement?.Invoke(d, j);
h.OnJudgementRemoved += (d, j) => OnJudgementRemoved?.Invoke(d, j);
h.ApplyCustomUpdateState += (d, j) => ApplyCustomUpdateState?.Invoke(d, j);
nestedHitObjects.Add(h);
nestedHitObjects.Value.Add(h);
}
/// <summary>
@ -220,11 +208,9 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (AllJudged)
return false;
if (NestedHitObjects != null)
{
if (HasNestedHitObjects)
foreach (var d in NestedHitObjects)
judgementOccurred |= d.UpdateJudgement(userTriggered);
}
if (!ProvidesJudgement || judgementFinalized || judgementOccurred)
return judgementOccurred;

View File

@ -9,6 +9,7 @@ using System.Globalization;
using osu.Game.Beatmaps.Formats;
using osu.Game.Audio;
using System.Linq;
using osu.Framework.MathUtils;
namespace osu.Game.Rulesets.Objects.Legacy
{
@ -41,9 +42,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
else if ((type & ConvertHitObjectType.Slider) > 0)
{
var pos = new Vector2(int.Parse(split[0]), int.Parse(split[1]));
CurveType curveType = CurveType.Catmull;
double length = 0;
var points = new List<Vector2> { new Vector2(int.Parse(split[0]), int.Parse(split[1])) };
var points = new List<Vector2> { Vector2.Zero };
string[] pointsplit = split[5].Split('|');
foreach (string t in pointsplit)
@ -69,9 +72,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
}
string[] temp = t.Split(':');
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)));
points.Add(new Vector2((int)Convert.ToDouble(temp[0], CultureInfo.InvariantCulture), (int)Convert.ToDouble(temp[1], CultureInfo.InvariantCulture)) - pos);
}
// osu-stable special-cased colinear perfect curves to a CurveType.Linear
bool isLinear(List<Vector2> p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y));
if (points.Count == 3 && curveType == CurveType.PerfectCurve && isLinear(points))
curveType = CurveType.Linear;
int repeatCount = Convert.ToInt32(split[6], CultureInfo.InvariantCulture);
if (repeatCount > 9000)
@ -134,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(new Vector2(int.Parse(split[0]), int.Parse(split[1])), combo, points, length, curveType, repeatCount, nodeSamples);
result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples);
}
else if ((type & ConvertHitObjectType.Spinner) > 0)
{
@ -180,8 +188,8 @@ namespace osu.Game.Rulesets.Objects.Legacy
string[] split = str.Split(':');
var bank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[0]);
var addbank = (LegacyDecoder.LegacySampleBank)Convert.ToInt32(split[1]);
var bank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[0]);
var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Convert.ToInt32(split[1]);
// Let's not implement this for now, because this doesn't fit nicely into the bank structure
//string sampleFile = split2.Length > 4 ? split2[4] : string.Empty;

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using OpenTK;
using osu.Game.Rulesets.Objects.Types;
using System.Collections.Generic;
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
Position = position,
NewCombo = newCombo,
ControlPoints = controlPoints,
Distance = length,
Distance = Math.Max(0, length),
CurveType = curveType,
RepeatSamples = repeatSamples,
RepeatCount = repeatCount

View File

@ -0,0 +1,26 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A HitObject that is part of a combo and has extended information about its position relative to other combo objects.
/// </summary>
public interface IHasComboIndex : IHasCombo
{
/// <summary>
/// The offset of this hitobject in the current combo.
/// </summary>
int IndexInCurrentCombo { get; set; }
/// <summary>
/// The offset of this hitobject in the current combo.
/// </summary>
int ComboIndex { get; set; }
/// <summary>
/// Whether this is the last object in the current combo.
/// </summary>
bool LastInCombo { get; set; }
}
}

View File

@ -0,0 +1,26 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
namespace osu.Game.Rulesets.Objects.Types
{
/// <summary>
/// A HitObject that is part of a combo and has extended information about its position relative to other combo objects.
/// </summary>
public interface IHasComboInformation : IHasCombo
{
/// <summary>
/// The offset of this hitobject in the current combo.
/// </summary>
int IndexInCurrentCombo { get; set; }
/// <summary>
/// The offset of this combo in relation to the beatmap.
/// </summary>
int ComboIndex { get; set; }
/// <summary>
/// Whether this is the last object in the current combo.
/// </summary>
bool LastInCombo { get; set; }
}
}

View File

@ -30,21 +30,19 @@ namespace osu.Game.Rulesets.Objects.Types
public static class HasCurveExtensions
{
/// <summary>
/// Computes the position on the curve at a given progress, accounting for repeat logic.
/// <para>
/// Ranges from [0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.
/// </para>
/// Computes the position on the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
public static Vector2 PositionAt(this IHasCurve obj, double progress)
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>The position on the curve.</returns>
public static Vector2 CurvePositionAt(this IHasCurve obj, double progress)
=> obj.Curve.PositionAt(obj.ProgressAt(progress));
/// <summary>
/// Finds the progress along the curve, accounting for repeat logic.
/// Computes the progress along the curve relative to how much of the <see cref="HitObject"/> has been completed.
/// </summary>
/// <param name="obj">The curve.</param>
/// <param name="progress">[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</param>
/// <param name="progress">[0, 1] where 0 is the start time of the <see cref="HitObject"/> and 1 is the end time of the <see cref="HitObject"/>.</param>
/// <returns>[0, 1] where 0 is the beginning of the curve and 1 is the end of the curve.</returns>
public static double ProgressAt(this IHasCurve obj, double progress)
{

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using osu.Framework.Input;
using osu.Framework.MathUtils;
using osu.Game.Input.Handlers;
using OpenTK;
using OpenTK.Input;
@ -17,14 +16,15 @@ namespace osu.Game.Rulesets.Replays
/// The ReplayHandler will take a replay and handle the propagation of updates to the input stack.
/// It handles logic of any frames which *must* be executed.
/// </summary>
public abstract class FramedReplayInputHandler : ReplayInputHandler
public abstract class FramedReplayInputHandler<TFrame> : ReplayInputHandler
where TFrame : ReplayFrame
{
private readonly Replay replay;
protected List<ReplayFrame> Frames => replay.Frames;
public ReplayFrame CurrentFrame => !hasFrames ? null : Frames[currentFrameIndex];
public ReplayFrame NextFrame => !hasFrames ? null : Frames[nextFrameIndex];
public TFrame CurrentFrame => !HasFrames ? null : (TFrame)Frames[currentFrameIndex];
public TFrame NextFrame => !HasFrames ? null : (TFrame)Frames[nextFrameIndex];
private int currentFrameIndex;
@ -46,31 +46,14 @@ namespace osu.Game.Rulesets.Replays
return true;
}
public void SetPosition(Vector2 pos)
{
}
protected Vector2? Position
{
get
{
if (!hasFrames)
return null;
return Interpolation.ValueAt(currentTime, CurrentFrame.Position, NextFrame.Position, CurrentFrame.Time, NextFrame.Time);
}
}
public override List<InputState> GetPendingStates() => new List<InputState>();
public bool AtLastFrame => currentFrameIndex == Frames.Count - 1;
public bool AtFirstFrame => currentFrameIndex == 0;
public Vector2 Size => new Vector2(512, 384);
private const double sixty_frame_time = 1000.0 / 60;
private double currentTime;
protected double CurrentTime { get; private set; }
private int currentDirection;
/// <summary>
@ -79,14 +62,16 @@ namespace osu.Game.Rulesets.Replays
/// </summary>
public bool FrameAccuratePlayback = true;
private bool hasFrames => Frames.Count > 0;
protected bool HasFrames => Frames.Count > 0;
private bool inImportantSection =>
FrameAccuratePlayback &&
HasFrames && FrameAccuratePlayback &&
//a button is in a pressed state
((currentDirection > 0 ? CurrentFrame : NextFrame)?.IsImportant ?? false) &&
IsImportant(currentDirection > 0 ? CurrentFrame : NextFrame) &&
//the next frame is within an allowable time span
Math.Abs(currentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
Math.Abs(CurrentTime - NextFrame?.Time ?? 0) <= sixty_frame_time * 1.2;
protected virtual bool IsImportant(TFrame frame) => false;
/// <summary>
/// Update the current frame based on an incoming time value.
@ -97,10 +82,10 @@ namespace osu.Game.Rulesets.Replays
/// <returns>The usable time value. If null, we should not advance time as we do not have enough data.</returns>
public override double? SetFrameFromTime(double time)
{
currentDirection = time.CompareTo(currentTime);
currentDirection = time.CompareTo(CurrentTime);
if (currentDirection == 0) currentDirection = 1;
if (hasFrames)
if (HasFrames)
{
// check if the next frame is in the "future" for the current playback direction
if (currentDirection != time.CompareTo(NextFrame.Time))
@ -114,12 +99,12 @@ namespace osu.Game.Rulesets.Replays
// If going backwards, we need to execute once _before_ the frame time to reverse any judgements
// that would occur as a result of this frame in forward playback
if (currentDirection == -1)
return currentTime = CurrentFrame.Time - 1;
return currentTime = CurrentFrame.Time;
return CurrentTime = CurrentFrame.Time - 1;
return CurrentTime = CurrentFrame.Time;
}
}
return currentTime = time;
return CurrentTime = time;
}
protected class ReplayMouseState : MouseState

View File

@ -0,0 +1,38 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
namespace osu.Game.Rulesets.Replays.Legacy
{
public class LegacyReplayFrame : ReplayFrame
{
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
public float? MouseX;
public float? MouseY;
public bool MouseLeft => MouseLeft1 || MouseLeft2;
public bool MouseRight => MouseRight1 || MouseRight2;
public bool MouseLeft1 => (ButtonState & ReplayButtonState.Left1) > 0;
public bool MouseRight1 => (ButtonState & ReplayButtonState.Right1) > 0;
public bool MouseLeft2 => (ButtonState & ReplayButtonState.Left2) > 0;
public bool MouseRight2 => (ButtonState & ReplayButtonState.Right2) > 0;
public ReplayButtonState ButtonState;
public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)
: base(time)
{
MouseX = mouseX;
MouseY = mouseY;
ButtonState = buttonState;
}
public override string ToString()
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
}
}

View File

@ -3,7 +3,7 @@
using System;
namespace osu.Game.Rulesets.Replays
namespace osu.Game.Rulesets.Replays.Legacy
{
[Flags]
public enum ReplayButtonState

View File

@ -9,7 +9,6 @@ namespace osu.Game.Rulesets.Replays
public class Replay
{
public User User;
public List<ReplayFrame> Frames = new List<ReplayFrame>();
}
}

View File

@ -1,70 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using OpenTK;
namespace osu.Game.Rulesets.Replays
{
public class ReplayFrame
{
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight);
public float? MouseX;
public float? MouseY;
public bool MouseLeft => MouseLeft1 || MouseLeft2;
public bool MouseRight => MouseRight1 || MouseRight2;
public bool MouseLeft1
{
get { return (ButtonState & ReplayButtonState.Left1) > 0; }
set { setButtonState(ReplayButtonState.Left1, value); }
}
public bool MouseRight1
{
get { return (ButtonState & ReplayButtonState.Right1) > 0; }
set { setButtonState(ReplayButtonState.Right1, value); }
}
public bool MouseLeft2
{
get { return (ButtonState & ReplayButtonState.Left2) > 0; }
set { setButtonState(ReplayButtonState.Left2, value); }
}
public bool MouseRight2
{
get { return (ButtonState & ReplayButtonState.Right2) > 0; }
set { setButtonState(ReplayButtonState.Right2, value); }
}
private void setButtonState(ReplayButtonState singleButton, bool pressed)
{
if (pressed)
ButtonState |= singleButton;
else
ButtonState &= ~singleButton;
}
public double Time;
public ReplayButtonState ButtonState;
protected ReplayFrame()
public ReplayFrame()
{
}
public ReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState)
public ReplayFrame(double time)
{
MouseX = mouseX;
MouseY = mouseY;
ButtonState = buttonState;
Time = time;
}
public override string ToString()
{
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Replays.Legacy;
namespace osu.Game.Rulesets.Replays.Types
{
/// <summary>
/// A type of <see cref="ReplayFrame"/> which can be converted from a <see cref="LegacyReplayFrame"/>.
/// </summary>
public interface IConvertibleReplayFrame
{
/// <summary>
/// Populates this <see cref="ReplayFrame"/> using values from a <see cref="LegacyReplayFrame"/>.
/// </summary>
/// <param name="legacyFrame">The <see cref="LegacyReplayFrame"/> to extract values from.</param>
/// <param name="score">The score.</param>
/// <param name="beatmap">The beatmap.</param>
void ConvertFrom(LegacyReplayFrame legacyFrame, Beatmap beatmap);
}
}

View File

@ -11,6 +11,7 @@ using osu.Game.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays.Types;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
@ -63,7 +64,7 @@ namespace osu.Game.Rulesets
/// <summary>
/// Do not override this unless you are a legacy mode.
/// </summary>
public virtual int LegacyID => -1;
public virtual int? LegacyID => null;
/// <summary>
/// A unique short name to reference this ruleset in online requests.
@ -89,6 +90,13 @@ namespace osu.Game.Rulesets
/// <returns>A descriptive name of the variant.</returns>
public virtual string GetVariantName(int variant) => string.Empty;
/// <summary>
/// For rulesets which support legacy (osu-stable) replay conversion, this method will create an empty replay frame
/// for conversion use.
/// </summary>
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null;
/// <summary>
/// Create a ruleset info based on this ruleset.
/// </summary>

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets
loadRulesetFromFile(file);
}
public RulesetStore(Func<OsuDbContext> factory)
public RulesetStore(IDatabaseContextFactory factory)
: base(factory)
{
AddMissingRulesets();
@ -56,47 +56,50 @@ namespace osu.Game.Rulesets
protected void AddMissingRulesets()
{
var context = GetContext();
var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
//add all legacy modes in correct order
foreach (var r in instances.Where(r => r.LegacyID >= 0).OrderBy(r => r.LegacyID))
using (var usage = ContextFactory.GetForWrite())
{
if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
var context = usage.Context;
context.SaveChanges();
var instances = loaded_assemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList();
//add any other modes
foreach (var r in instances.Where(r => r.LegacyID < 0))
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
context.RulesetInfo.Add(r.RulesetInfo);
context.SaveChanges();
//perform a consistency check
foreach (var r in context.RulesetInfo)
{
try
//add all legacy modes in correct order
foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID))
{
var instance = r.CreateInstance();
r.Name = instance.Description;
r.ShortName = instance.ShortName;
r.Available = true;
if (context.RulesetInfo.SingleOrDefault(rsi => rsi.ID == r.RulesetInfo.ID) == null)
context.RulesetInfo.Add(r.RulesetInfo);
}
catch
context.SaveChanges();
//add any other modes
foreach (var r in instances.Where(r => r.LegacyID == null))
if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo == r.RulesetInfo.InstantiationInfo) == null)
context.RulesetInfo.Add(r.RulesetInfo);
context.SaveChanges();
//perform a consistency check
foreach (var r in context.RulesetInfo)
{
r.Available = false;
try
{
var instance = r.CreateInstance();
r.Name = instance.Description;
r.ShortName = instance.ShortName;
r.Available = true;
}
catch
{
r.Available = false;
}
}
context.SaveChanges();
AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList();
}
context.SaveChanges();
AvailableRulesets = context.RulesetInfo.Where(r => r.Available).ToList();
}
private static void loadRulesetFromFile(string file)

View File

@ -0,0 +1,152 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.IO;
using osu.Game.Beatmaps;
using osu.Game.IO.Legacy;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Legacy;
using osu.Game.Users;
using SharpCompress.Compressors.LZMA;
namespace osu.Game.Rulesets.Scoring.Legacy
{
public class LegacyScoreParser
{
private readonly RulesetStore rulesets;
private readonly BeatmapManager beatmaps;
public LegacyScoreParser(RulesetStore rulesets, BeatmapManager beatmaps)
{
this.rulesets = rulesets;
this.beatmaps = beatmaps;
}
private Beatmap currentBeatmap;
private Ruleset currentRuleset;
public Score Parse(Stream stream)
{
Score score;
using (SerializationReader sr = new SerializationReader(stream))
{
score = new Score { Ruleset = rulesets.GetRuleset(sr.ReadByte()) };
currentRuleset = score.Ruleset.CreateInstance();
/* score.Pass = true;*/
var version = sr.ReadInt32();
/* score.FileChecksum = */
var beatmapHash = sr.ReadString();
score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash);
currentBeatmap = beatmaps.GetWorkingBeatmap(score.Beatmap).Beatmap;
/* score.PlayerName = */
score.User = new User { Username = sr.ReadString() };
/* var localScoreChecksum = */
sr.ReadString();
/* score.Count300 = */
sr.ReadUInt16();
/* score.Count100 = */
sr.ReadUInt16();
/* score.Count50 = */
sr.ReadUInt16();
/* score.CountGeki = */
sr.ReadUInt16();
/* score.CountKatu = */
sr.ReadUInt16();
/* score.CountMiss = */
sr.ReadUInt16();
score.TotalScore = sr.ReadInt32();
score.MaxCombo = sr.ReadUInt16();
/* score.Perfect = */
sr.ReadBoolean();
/* score.EnabledMods = (Mods)*/
sr.ReadInt32();
/* score.HpGraphString = */
sr.ReadString();
/* score.Date = */
sr.ReadDateTime();
var compressedReplay = sr.ReadByteArray();
if (version >= 20140721)
/*OnlineId =*/
sr.ReadInt64();
else if (version >= 20121008)
/*OnlineId =*/
sr.ReadInt32();
using (var replayInStream = new MemoryStream(compressedReplay))
{
byte[] properties = new byte[5];
if (replayInStream.Read(properties, 0, 5) != 5)
throw new IOException("input .lzma is too short");
long outSize = 0;
for (int i = 0; i < 8; i++)
{
int v = replayInStream.ReadByte();
if (v < 0)
throw new IOException("Can't Read 1");
outSize |= (long)(byte)v << (8 * i);
}
long compressedSize = replayInStream.Length - replayInStream.Position;
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
using (var reader = new StreamReader(lzma))
{
score.Replay = new Replay { User = score.User };
readLegacyReplay(score.Replay, reader);
}
}
}
return score;
}
private void readLegacyReplay(Replay replay, StreamReader reader)
{
float lastTime = 0;
foreach (var l in reader.ReadToEnd().Split(','))
{
var split = l.Split('|');
if (split.Length < 4)
continue;
if (split[0] == "-12345")
{
// Todo: The seed is provided in split[3], which we'll need to use at some point
continue;
}
var diff = float.Parse(split[0]);
lastTime += diff;
// Todo: At some point we probably want to rewind and play back the negative-time frames
// but for now we'll achieve equal playback to stable by skipping negative frames
if (diff < 0)
continue;
replay.Frames.Add(convertFrame(new LegacyReplayFrame(lastTime, float.Parse(split[1]), float.Parse(split[2]), (ReplayButtonState)int.Parse(split[3]))));
}
}
private ReplayFrame convertFrame(LegacyReplayFrame legacyFrame)
{
var convertible = currentRuleset.CreateConvertibleReplayFrame();
if (convertible == null)
throw new InvalidOperationException($"Legacy replay cannot be converted for the ruleset: {currentRuleset.Description}");
convertible.ConvertFrom(legacyFrame, currentBeatmap);
var frame = (ReplayFrame)convertible;
frame.Time = legacyFrame.Time;
return frame;
}
}
}

View File

@ -2,7 +2,9 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Scoring
@ -23,9 +25,15 @@ namespace osu.Game.Rulesets.Scoring
protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
{
Beatmap = CreateBeatmapConverter().Convert(beatmap);
Score = score;
var converter = CreateBeatmapConverter();
foreach (var mod in score.Mods.OfType<IApplicableToBeatmapConverter<TObject>>())
mod.ApplyToBeatmapConverter(converter);
Beatmap = converter.Convert(beatmap);
var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods);
diffCalc.Calculate(attributes);
}

View File

@ -2,20 +2,16 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.IO;
using osu.Framework.Platform;
using osu.Game.Beatmaps;
using osu.Game.Database;
using osu.Game.IO.Legacy;
using osu.Game.IPC;
using osu.Game.Rulesets.Replays;
using osu.Game.Users;
using SharpCompress.Compressors.LZMA;
using osu.Game.Rulesets.Scoring.Legacy;
namespace osu.Game.Rulesets.Scoring
{
public class ScoreStore : DatabaseBackedStore
public class ScoreStore : DatabaseBackedStore, ICanAcceptFiles
{
private readonly Storage storage;
@ -24,10 +20,12 @@ namespace osu.Game.Rulesets.Scoring
private const string replay_folder = @"replays";
public event Action<Score> ScoreImported;
// ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised)
private ScoreIPCChannel ipc;
public ScoreStore(Storage storage, Func<OsuDbContext> factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory)
public ScoreStore(Storage storage, DatabaseContextFactory factory, IIpcHost importHost = null, BeatmapManager beatmaps = null, RulesetStore rulesets = null) : base(factory)
{
this.storage = storage;
this.beatmaps = beatmaps;
@ -37,128 +35,22 @@ namespace osu.Game.Rulesets.Scoring
ipc = new ScoreIPCChannel(importHost, this);
}
public Score ReadReplayFile(string replayFilename)
public string[] HandledExtensions => new[] { ".osr" };
public void Import(params string[] paths)
{
Score score;
using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename)))
using (SerializationReader sr = new SerializationReader(s))
foreach (var path in paths)
{
score = new Score
{
Ruleset = rulesets.GetRuleset(sr.ReadByte())
};
/* score.Pass = true;*/
var version = sr.ReadInt32();
/* score.FileChecksum = */
var beatmapHash = sr.ReadString();
score.Beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == beatmapHash);
/* score.PlayerName = */
score.User = new User { Username = sr.ReadString() };
/* var localScoreChecksum = */
sr.ReadString();
/* score.Count300 = */
sr.ReadUInt16();
/* score.Count100 = */
sr.ReadUInt16();
/* score.Count50 = */
sr.ReadUInt16();
/* score.CountGeki = */
sr.ReadUInt16();
/* score.CountKatu = */
sr.ReadUInt16();
/* score.CountMiss = */
sr.ReadUInt16();
score.TotalScore = sr.ReadInt32();
score.MaxCombo = sr.ReadUInt16();
/* score.Perfect = */
sr.ReadBoolean();
/* score.EnabledMods = (Mods)*/
sr.ReadInt32();
/* score.HpGraphString = */
sr.ReadString();
/* score.Date = */
sr.ReadDateTime();
var compressedReplay = sr.ReadByteArray();
if (version >= 20140721)
/*OnlineId =*/
sr.ReadInt64();
else if (version >= 20121008)
/*OnlineId =*/
sr.ReadInt32();
using (var replayInStream = new MemoryStream(compressedReplay))
{
byte[] properties = new byte[5];
if (replayInStream.Read(properties, 0, 5) != 5)
throw new IOException("input .lzma is too short");
long outSize = 0;
for (int i = 0; i < 8; i++)
{
int v = replayInStream.ReadByte();
if (v < 0)
throw new IOException("Can't Read 1");
outSize |= (long)(byte)v << (8 * i);
}
long compressedSize = replayInStream.Length - replayInStream.Position;
using (var lzma = new LzmaStream(properties, replayInStream, compressedSize, outSize))
using (var reader = new StreamReader(lzma))
{
score.Replay = createLegacyReplay(reader);
score.Replay.User = score.User;
}
}
var score = ReadReplayFile(path);
if (score != null)
ScoreImported?.Invoke(score);
}
return score;
}
/// <summary>
/// Creates a legacy replay which is read from a stream.
/// </summary>
/// <param name="reader">The stream reader.</param>
/// <returns>The legacy replay.</returns>
private Replay createLegacyReplay(StreamReader reader)
public Score ReadReplayFile(string replayFilename)
{
var frames = new List<ReplayFrame>();
float lastTime = 0;
foreach (var l in reader.ReadToEnd().Split(','))
{
var split = l.Split('|');
if (split.Length < 4)
continue;
if (split[0] == "-12345")
{
// Todo: The seed is provided in split[3], which we'll need to use at some point
continue;
}
var diff = float.Parse(split[0]);
lastTime += diff;
// Todo: At some point we probably want to rewind and play back the negative-time frames
// but for now we'll achieve equal playback to stable by skipping negative frames
if (diff < 0)
continue;
frames.Add(new ReplayFrame(
lastTime,
float.Parse(split[1]),
float.Parse(split[2]),
(ReplayButtonState)int.Parse(split[3])
));
}
return new Replay { Frames = frames };
using (Stream s = storage.GetStream(Path.Combine(replay_folder, replayFilename)))
return new LegacyScoreParser(rulesets, beatmaps).Parse(s);
}
}
}

View File

@ -0,0 +1,24 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Judgements;
namespace osu.Game.Rulesets.UI
{
public class JudgementContainer<T> : Container<T>
where T : DrawableJudgement
{
public override void Add(T judgement)
{
if (judgement == null) throw new ArgumentNullException(nameof(judgement));
// remove any existing judgements for the judged object.
// this can be the case when rewinding.
RemoveAll(c => c.JudgedObject == judgement.JudgedObject);
base.Add(judgement);
}
}
}

View File

@ -3,52 +3,37 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
using osu.Framework.Allocation;
namespace osu.Game.Rulesets.UI
{
public abstract class Playfield : Container
public abstract class Playfield : ScalableContainer
{
/// <summary>
/// The HitObjects contained in this Playfield.
/// </summary>
public HitObjectContainer HitObjects { get; private set; }
public Container<Drawable> ScaledContent;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
private List<Playfield> nestedPlayfields;
/// <summary>
/// All the <see cref="Playfield"/>s nested inside this playfield.
/// </summary>
public IReadOnlyList<Playfield> NestedPlayfields => nestedPlayfields;
private List<Playfield> nestedPlayfields;
/// <summary>
/// A container for keeping track of DrawableHitObjects.
/// </summary>
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width.</param>
protected Playfield(float? customWidth = null)
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
protected Playfield(float? customWidth = null, float? customHeight = null)
: base(customWidth, customHeight)
{
RelativeSizeAxes = Axes.Both;
AddInternal(ScaledContent = new ScaledContainer
{
CustomWidth = customWidth,
RelativeSizeAxes = Axes.Both,
Children = new[]
{
content = new Container
{
RelativeSizeAxes = Axes.Both,
}
}
});
}
[BackgroundDependencyLoader]
@ -94,22 +79,5 @@ namespace osu.Game.Rulesets.UI
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
private class ScaledContainer : Container
{
/// <summary>
/// A value (in game pixels that we should scale our content to match).
/// </summary>
public float? CustomWidth;
//dividing by the customwidth will effectively scale our content to the required container size.
protected override Vector2 DrawScale => CustomWidth.HasValue ? new Vector2(DrawSize.X / CustomWidth.Value) : base.DrawScale;
protected override void Update()
{
base.Update();
RelativeChildSize = new Vector2(DrawScale.X, RelativeChildSize.Y);
}
}
}
}

View File

@ -17,6 +17,7 @@ using osu.Framework.Configuration;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using osu.Game.Configuration;
using osu.Game.Input.Handlers;
using osu.Game.Overlays;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.Replays;
@ -33,11 +34,6 @@ namespace osu.Game.Rulesets.UI
/// </summary>
public abstract class RulesetContainer : Container
{
/// <summary>
/// Whether to apply adjustments to the child <see cref="Playfield"/> based on our own size.
/// </summary>
public bool AspectAdjust = true;
/// <summary>
/// The selected variant.
/// </summary>
@ -115,7 +111,7 @@ namespace osu.Game.Rulesets.UI
/// <returns>The input manager.</returns>
public abstract PassThroughInputManager CreateInputManager();
protected virtual FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null;
public Replay Replay { get; private set; }
@ -325,7 +321,7 @@ namespace osu.Game.Rulesets.UI
{
base.Update();
Playfield.Size = AspectAdjust ? GetPlayfieldAspectAdjust() : Vector2.One;
Playfield.Size = GetAspectAdjustedSize() * PlayfieldArea;
}
/// <summary>
@ -336,10 +332,17 @@ namespace osu.Game.Rulesets.UI
protected virtual BeatmapProcessor<TObject> CreateBeatmapProcessor() => new BeatmapProcessor<TObject>();
/// <summary>
/// In some cases we want to apply changes to the relative size of our contained <see cref="Playfield"/> based on custom conditions.
/// Computes the size of the <see cref="Playfield"/> in relative coordinate space after aspect adjustments.
/// </summary>
/// <returns></returns>
protected virtual Vector2 GetPlayfieldAspectAdjust() => new Vector2(0.75f); //a sane default
/// <returns>The aspect-adjusted size.</returns>
protected virtual Vector2 GetAspectAdjustedSize() => Vector2.One;
/// <summary>
/// The area of this <see cref="RulesetContainer"/> that is available for the <see cref="Playfield"/> to use.
/// Must be specified in relative coordinate space to this <see cref="RulesetContainer"/>.
/// This affects the final size of the <see cref="Playfield"/> but does not affect the <see cref="Playfield"/>'s scale.
/// </summary>
protected virtual Vector2 PlayfieldArea => new Vector2(0.75f); // A sane default
/// <summary>
/// Creates a converter to convert Beatmap to a specific mode.

View File

@ -91,8 +91,6 @@ namespace osu.Game.Rulesets.UI
#region Clock control
protected override bool ShouldProcessClock => false; // We handle processing the clock ourselves
private ManualClock clock;
private IFrameBasedClock parentClock;
@ -103,6 +101,7 @@ namespace osu.Game.Rulesets.UI
//our clock will now be our parent's clock, but we want to replace this to allow manual control.
parentClock = Clock;
ProcessCustomClock = false;
Clock = new FramedClock(clock = new ManualClock
{
CurrentTime = parentClock.CurrentTime,

View File

@ -0,0 +1,86 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using OpenTK;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
/// </summary>
public class ScalableContainer : Container
{
/// <summary>
/// The scaled content.
/// </summary>
public readonly Container ScaledContent;
protected override Container<Drawable> Content => content;
private readonly Container content;
/// <summary>
/// A <see cref="Container"/> which can have its internal coordinate system scaled to a specific size.
/// </summary>
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
public ScalableContainer(float? customWidth = null, float? customHeight = null)
{
AddInternal(ScaledContent = new ScaledContainer
{
CustomWidth = customWidth,
CustomHeight = customHeight,
RelativeSizeAxes = Axes.Both,
Child = content = new Container { RelativeSizeAxes = Axes.Both }
});
}
private class ScaledContainer : Container
{
/// <summary>
/// The value to scale the width of the content to match.
/// If null, <see cref="CustomHeight"/> is used.
/// </summary>
public float? CustomWidth;
/// <summary>
/// The value to scale the height of the content to match.
/// if null, <see cref="CustomWidth"/> is used.
/// </summary>
public float? CustomHeight;
/// <summary>
/// The scale that is required for the size of the content to match <see cref="CustomWidth"/> and <see cref="CustomHeight"/>.
/// </summary>
private Vector2 sizeScale
{
get
{
if (CustomWidth.HasValue && CustomHeight.HasValue)
return Vector2.Divide(DrawSize, new Vector2(CustomWidth.Value, CustomHeight.Value));
if (CustomWidth.HasValue)
return new Vector2(DrawSize.X / CustomWidth.Value);
if (CustomHeight.HasValue)
return new Vector2(DrawSize.Y / CustomHeight.Value);
return Vector2.One;
}
}
/// <summary>
/// Scale the content to the required container size by multiplying by <see cref="sizeScale"/>.
/// </summary>
protected override Vector2 DrawScale => sizeScale * base.DrawScale;
protected override void Update()
{
base.Update();
RelativeChildSize = new Vector2(CustomWidth.HasValue ? sizeScale.X : RelativeChildSize.X, CustomHeight.HasValue ? sizeScale.Y : RelativeChildSize.Y);
}
}
}
}

View File

@ -62,9 +62,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// Creates a new <see cref="ScrollingPlayfield"/>.
/// </summary>
/// <param name="direction">The direction in which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null)
: base(customWidth)
/// <param name="customWidth">The width to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customHeight"/> is desired. If <paramref name="customHeight"/> is also null, no scaling will occur.
/// </param>
/// <param name="customHeight">The height to scale the internal coordinate space to.
/// May be null if scaling based on <paramref name="customWidth"/> is desired. If <paramref name="customWidth"/> is also null, no scaling will occur.
/// </param>
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null, float? customHeight = null)
: base(customWidth, customHeight)
{
this.direction = direction;
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
var controlPoint = controlPointAt(obj.HitObject.StartTime);
obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier;
if (obj.NestedHitObjects != null)
if (obj.HasNestedHitObjects)
{
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);

View File

@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
}
}
if (obj.NestedHitObjects != null)
if (obj.HasNestedHitObjects)
{
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);