2017-02-07 12:59:30 +08:00
|
|
|
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
|
|
|
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
2016-10-13 09:10:15 +08:00
|
|
|
|
|
2017-03-11 23:34:21 +08:00
|
|
|
|
using OpenTK;
|
2017-02-09 13:56:39 +08:00
|
|
|
|
using osu.Game.Beatmaps;
|
2017-03-11 23:34:21 +08:00
|
|
|
|
using osu.Game.Modes.Objects;
|
|
|
|
|
using osu.Game.Modes.Osu.Objects;
|
2017-02-09 13:56:39 +08:00
|
|
|
|
using osu.Game.Modes.Osu.Objects.Drawables;
|
2017-03-11 23:34:21 +08:00
|
|
|
|
using System.Collections.Generic;
|
2017-03-13 18:15:25 +08:00
|
|
|
|
using osu.Game.Modes.Objects.Types;
|
2017-03-14 13:48:32 +08:00
|
|
|
|
using System.Linq;
|
2016-10-13 09:10:15 +08:00
|
|
|
|
|
2017-03-11 23:34:21 +08:00
|
|
|
|
namespace osu.Game.Modes.Osu.Beatmaps
|
2016-10-13 09:10:15 +08:00
|
|
|
|
{
|
2017-03-11 23:34:21 +08:00
|
|
|
|
internal class OsuBeatmapConverter : IBeatmapConverter<OsuHitObject>
|
2016-10-13 09:10:15 +08:00
|
|
|
|
{
|
2017-03-14 11:54:09 +08:00
|
|
|
|
public Beatmap<OsuHitObject> Convert(Beatmap original)
|
2017-03-11 23:34:21 +08:00
|
|
|
|
{
|
|
|
|
|
return new Beatmap<OsuHitObject>(original)
|
|
|
|
|
{
|
2017-03-14 16:01:21 +08:00
|
|
|
|
HitObjects = convertHitObjects(original.HitObjects, original.BeatmapInfo?.StackLeniency ?? 0.7f)
|
2017-03-11 23:34:21 +08:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-14 13:48:32 +08:00
|
|
|
|
private List<OsuHitObject> convertHitObjects(List<HitObject> hitObjects, float stackLeniency)
|
|
|
|
|
{
|
2017-03-14 14:45:27 +08:00
|
|
|
|
List<OsuHitObject> converted = hitObjects.Select(convertHitObject).ToList();
|
2017-03-14 13:48:32 +08:00
|
|
|
|
|
|
|
|
|
updateStacking(converted, stackLeniency);
|
|
|
|
|
|
|
|
|
|
return converted;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-13 18:15:25 +08:00
|
|
|
|
private OsuHitObject convertHitObject(HitObject original)
|
|
|
|
|
{
|
2017-03-14 12:24:07 +08:00
|
|
|
|
IHasCurve curveData = original as IHasCurve;
|
|
|
|
|
IHasEndTime endTimeData = original as IHasEndTime;
|
|
|
|
|
IHasPosition positionData = original as IHasPosition;
|
|
|
|
|
IHasCombo comboData = original as IHasCombo;
|
|
|
|
|
|
|
|
|
|
if (curveData != null)
|
2017-03-13 18:15:25 +08:00
|
|
|
|
{
|
|
|
|
|
return new Slider
|
|
|
|
|
{
|
|
|
|
|
StartTime = original.StartTime,
|
2017-04-06 10:41:16 +08:00
|
|
|
|
Samples = original.Samples,
|
2017-03-15 11:52:25 +08:00
|
|
|
|
CurveObject = curveData,
|
2017-03-14 12:24:07 +08:00
|
|
|
|
Position = positionData?.Position ?? Vector2.Zero,
|
2017-03-15 11:52:25 +08:00
|
|
|
|
NewCombo = comboData?.NewCombo ?? false
|
2017-03-13 18:15:25 +08:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-14 12:24:07 +08:00
|
|
|
|
if (endTimeData != null)
|
2017-03-13 18:15:25 +08:00
|
|
|
|
{
|
|
|
|
|
return new Spinner
|
|
|
|
|
{
|
|
|
|
|
StartTime = original.StartTime,
|
2017-04-06 10:41:16 +08:00
|
|
|
|
Samples = original.Samples,
|
2017-03-13 18:15:25 +08:00
|
|
|
|
Position = new Vector2(512, 384) / 2,
|
2017-03-15 11:52:25 +08:00
|
|
|
|
EndTime = endTimeData.EndTime
|
2017-03-13 18:15:25 +08:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new HitCircle
|
|
|
|
|
{
|
|
|
|
|
StartTime = original.StartTime,
|
2017-04-06 10:41:16 +08:00
|
|
|
|
Samples = original.Samples,
|
2017-03-14 12:24:07 +08:00
|
|
|
|
Position = positionData?.Position ?? Vector2.Zero,
|
|
|
|
|
NewCombo = comboData?.NewCombo ?? false
|
2017-03-13 18:15:25 +08:00
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-11 23:34:21 +08:00
|
|
|
|
private void updateStacking(List<OsuHitObject> hitObjects, float stackLeniency, int startIndex = 0, int endIndex = -1)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
if (endIndex == -1)
|
|
|
|
|
endIndex = hitObjects.Count - 1;
|
|
|
|
|
|
2017-03-23 12:52:38 +08:00
|
|
|
|
const int stack_distance = 3;
|
2017-02-09 13:56:39 +08:00
|
|
|
|
float stackThreshold = DrawableOsuHitObject.TIME_PREEMPT * stackLeniency;
|
|
|
|
|
|
|
|
|
|
// Reset stacking inside the update range
|
|
|
|
|
for (int i = startIndex; i <= endIndex; i++)
|
|
|
|
|
hitObjects[i].StackHeight = 0;
|
|
|
|
|
|
|
|
|
|
// Extend the end index to include objects they are stacked on
|
|
|
|
|
int extendedEndIndex = endIndex;
|
|
|
|
|
for (int i = endIndex; i >= startIndex; i--)
|
|
|
|
|
{
|
|
|
|
|
int stackBaseIndex = i;
|
|
|
|
|
for (int n = stackBaseIndex + 1; n < hitObjects.Count; n++)
|
|
|
|
|
{
|
|
|
|
|
OsuHitObject stackBaseObject = hitObjects[stackBaseIndex];
|
|
|
|
|
if (stackBaseObject is Spinner) break;
|
|
|
|
|
|
|
|
|
|
OsuHitObject objectN = hitObjects[n];
|
2017-03-13 18:15:25 +08:00
|
|
|
|
if (objectN is Spinner)
|
|
|
|
|
continue;
|
2017-02-09 13:56:39 +08:00
|
|
|
|
|
2017-03-13 18:15:25 +08:00
|
|
|
|
double endTime = (stackBaseObject as IHasEndTime)?.EndTime ?? stackBaseObject.StartTime;
|
|
|
|
|
|
|
|
|
|
if (objectN.StartTime - endTime > stackThreshold)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
//We are no longer within stacking range of the next object.
|
|
|
|
|
break;
|
|
|
|
|
|
2017-03-23 12:52:38 +08:00
|
|
|
|
if (Vector2.Distance(stackBaseObject.Position, objectN.Position) < stack_distance ||
|
|
|
|
|
stackBaseObject is Slider && Vector2.Distance(stackBaseObject.EndPosition, objectN.Position) < stack_distance)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
stackBaseIndex = n;
|
|
|
|
|
|
|
|
|
|
// HitObjects after the specified update range haven't been reset yet
|
|
|
|
|
objectN.StackHeight = 0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (stackBaseIndex > extendedEndIndex)
|
|
|
|
|
{
|
|
|
|
|
extendedEndIndex = stackBaseIndex;
|
|
|
|
|
if (extendedEndIndex == hitObjects.Count - 1)
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//Reverse pass for stack calculation.
|
|
|
|
|
int extendedStartIndex = startIndex;
|
|
|
|
|
for (int i = extendedEndIndex; i > startIndex; i--)
|
|
|
|
|
{
|
|
|
|
|
int n = i;
|
|
|
|
|
/* We should check every note which has not yet got a stack.
|
|
|
|
|
* Consider the case we have two interwound stacks and this will make sense.
|
|
|
|
|
*
|
|
|
|
|
* o <-1 o <-2
|
|
|
|
|
* o <-3 o <-4
|
|
|
|
|
*
|
|
|
|
|
* We first process starting from 4 and handle 2,
|
|
|
|
|
* then we come backwards on the i loop iteration until we reach 3 and handle 1.
|
|
|
|
|
* 2 and 1 will be ignored in the i loop because they already have a stack value.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
OsuHitObject objectI = hitObjects[i];
|
|
|
|
|
if (objectI.StackHeight != 0 || objectI is Spinner) continue;
|
|
|
|
|
|
|
|
|
|
/* If this object is a hitcircle, then we enter this "special" case.
|
|
|
|
|
* It either ends with a stack of hitcircles only, or a stack of hitcircles that are underneath a slider.
|
|
|
|
|
* Any other case is handled by the "is Slider" code below this.
|
|
|
|
|
*/
|
|
|
|
|
if (objectI is HitCircle)
|
|
|
|
|
{
|
|
|
|
|
while (--n >= 0)
|
|
|
|
|
{
|
|
|
|
|
OsuHitObject objectN = hitObjects[n];
|
|
|
|
|
if (objectN is Spinner) continue;
|
|
|
|
|
|
2017-03-13 18:15:25 +08:00
|
|
|
|
double endTime = (objectN as IHasEndTime)?.EndTime ?? objectN.StartTime;
|
|
|
|
|
|
|
|
|
|
if (objectI.StartTime - endTime > stackThreshold)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
//We are no longer within stacking range of the previous object.
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
// HitObjects before the specified update range haven't been reset yet
|
|
|
|
|
if (n < extendedStartIndex)
|
|
|
|
|
{
|
|
|
|
|
objectN.StackHeight = 0;
|
|
|
|
|
extendedStartIndex = n;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* This is a special case where hticircles are moved DOWN and RIGHT (negative stacking) if they are under the *last* slider in a stacked pattern.
|
|
|
|
|
* o==o <- slider is at original location
|
|
|
|
|
* o <- hitCircle has stack of -1
|
|
|
|
|
* o <- hitCircle has stack of -2
|
|
|
|
|
*/
|
2017-03-23 12:52:38 +08:00
|
|
|
|
if (objectN is Slider && Vector2.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
int offset = objectI.StackHeight - objectN.StackHeight + 1;
|
|
|
|
|
for (int j = n + 1; j <= i; j++)
|
|
|
|
|
{
|
|
|
|
|
//For each object which was declared under this slider, we will offset it to appear *below* the slider end (rather than above).
|
|
|
|
|
OsuHitObject objectJ = hitObjects[j];
|
2017-03-23 12:52:38 +08:00
|
|
|
|
if (Vector2.Distance(objectN.EndPosition, objectJ.Position) < stack_distance)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
objectJ.StackHeight -= offset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//We have hit a slider. We should restart calculation using this as the new base.
|
|
|
|
|
//Breaking here will mean that the slider still has StackCount of 0, so will be handled in the i-outer-loop.
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-23 12:52:38 +08:00
|
|
|
|
if (Vector2.Distance(objectN.Position, objectI.Position) < stack_distance)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
//Keep processing as if there are no sliders. If we come across a slider, this gets cancelled out.
|
|
|
|
|
//NOTE: Sliders with start positions stacking are a special case that is also handled here.
|
|
|
|
|
|
|
|
|
|
objectN.StackHeight = objectI.StackHeight + 1;
|
|
|
|
|
objectI = objectN;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if (objectI is Slider)
|
|
|
|
|
{
|
|
|
|
|
/* We have hit the first slider in a possible stack.
|
|
|
|
|
* From this point on, we ALWAYS stack positive regardless.
|
|
|
|
|
*/
|
|
|
|
|
while (--n >= startIndex)
|
|
|
|
|
{
|
|
|
|
|
OsuHitObject objectN = hitObjects[n];
|
|
|
|
|
if (objectN is Spinner) continue;
|
|
|
|
|
|
|
|
|
|
if (objectI.StartTime - objectN.StartTime > stackThreshold)
|
|
|
|
|
//We are no longer within stacking range of the previous object.
|
|
|
|
|
break;
|
|
|
|
|
|
2017-03-23 12:52:38 +08:00
|
|
|
|
if (Vector2.Distance(objectN.EndPosition, objectI.Position) < stack_distance)
|
2017-02-09 13:56:39 +08:00
|
|
|
|
{
|
|
|
|
|
objectN.StackHeight = objectI.StackHeight + 1;
|
|
|
|
|
objectI = objectN;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-13 09:10:15 +08:00
|
|
|
|
}
|
|
|
|
|
}
|