2019-01-24 08:43:03 +00:00
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
2018-04-13 09:19:50 +00:00
2017-12-01 06:08:12 +00:00
using System ;
2017-12-01 11:39:58 +00:00
using System.Linq ;
2017-09-18 03:48:33 +00:00
using osu.Game.Beatmaps ;
2017-09-15 11:54:34 +00:00
using osu.Game.Rulesets.Catch.Objects ;
2017-12-01 06:08:12 +00:00
using osu.Game.Rulesets.Catch.UI ;
2020-11-24 10:57:37 +00:00
using osu.Game.Rulesets.Objects.Types ;
2022-04-28 08:46:00 +00:00
using osu.Game.Utils ;
2018-04-13 09:19:50 +00:00
2017-09-15 11:54:34 +00:00
namespace osu.Game.Rulesets.Catch.Beatmaps
{
2018-04-19 13:04:12 +00:00
public class CatchBeatmapProcessor : BeatmapProcessor
2017-09-15 11:54:34 +00:00
{
2018-06-29 03:45:48 +00:00
public const int RNG_SEED = 1337 ;
2021-06-23 05:11:25 +00:00
public bool HardRockOffsets { get ; set ; }
2018-04-19 13:04:12 +00:00
public CatchBeatmapProcessor ( IBeatmap beatmap )
: base ( beatmap )
2017-09-15 11:54:34 +00:00
{
2018-04-19 13:04:12 +00:00
}
2023-11-23 04:41:01 +00:00
public override void PreProcess ( )
{
IHasComboInformation ? lastObj = null ;
// For sanity, ensures that both the first hitobject and the first hitobject after a banana shower start a new combo.
// This is normally enforced by the legacy decoder, but is not enforced by the editor.
foreach ( var obj in Beatmap . HitObjects . OfType < IHasComboInformation > ( ) )
{
if ( obj is not BananaShower & & ( lastObj = = null | | lastObj is BananaShower ) )
obj . NewCombo = true ;
lastObj = obj ;
}
base . PreProcess ( ) ;
}
2018-06-29 03:45:48 +00:00
public override void PostProcess ( )
{
2018-04-19 13:04:12 +00:00
base . PostProcess ( ) ;
2018-04-13 09:19:50 +00:00
2019-07-31 09:44:01 +00:00
ApplyPositionOffsets ( Beatmap ) ;
2018-06-29 03:45:48 +00:00
2018-03-20 06:45:40 +00:00
int index = 0 ;
2019-04-01 03:16:05 +00:00
2018-04-19 13:04:12 +00:00
foreach ( var obj in Beatmap . HitObjects . OfType < CatchHitObject > ( ) )
2018-06-26 11:13:55 +00:00
{
2020-02-19 06:37:12 +00:00
obj . IndexInBeatmap = index ;
foreach ( var nested in obj . NestedHitObjects . OfType < CatchHitObject > ( ) )
nested . IndexInBeatmap = index ;
2018-06-26 11:13:55 +00:00
if ( obj . LastInCombo & & obj . NestedHitObjects . LastOrDefault ( ) is IHasComboInformation lastNested )
lastNested . LastInCombo = true ;
2020-02-19 06:37:12 +00:00
index + + ;
2018-06-26 11:13:55 +00:00
}
2017-09-15 11:54:34 +00:00
}
2018-04-13 09:19:50 +00:00
2021-06-23 05:11:25 +00:00
public void ApplyPositionOffsets ( IBeatmap beatmap )
2018-05-25 10:11:29 +00:00
{
2022-04-28 08:46:00 +00:00
var rng = new LegacyRandom ( RNG_SEED ) ;
2018-05-25 10:11:29 +00:00
2019-07-31 09:44:01 +00:00
float? lastPosition = null ;
double lastStartTime = 0 ;
2019-08-01 04:33:00 +00:00
foreach ( var obj in beatmap . HitObjects . OfType < CatchHitObject > ( ) )
2018-05-25 10:11:29 +00:00
{
2019-08-01 04:33:00 +00:00
obj . XOffset = 0 ;
2018-05-25 10:11:29 +00:00
switch ( obj )
{
2019-07-31 09:44:01 +00:00
case Fruit fruit :
2021-06-23 05:11:25 +00:00
if ( HardRockOffsets )
2019-07-31 09:44:01 +00:00
applyHardRockOffset ( fruit , ref lastPosition , ref lastStartTime , rng ) ;
break ;
2018-05-25 10:11:29 +00:00
case BananaShower bananaShower :
2018-06-29 06:01:33 +00:00
foreach ( var banana in bananaShower . NestedHitObjects . OfType < Banana > ( ) )
2018-05-25 10:11:29 +00:00
{
2020-07-01 15:21:45 +00:00
banana . XOffset = ( float ) ( rng . NextDouble ( ) * CatchPlayfield . WIDTH ) ;
2018-06-13 09:39:26 +00:00
rng . Next ( ) ; // osu!stable retrieved a random banana type
rng . Next ( ) ; // osu!stable retrieved a random banana rotation
2018-06-13 12:10:54 +00:00
rng . Next ( ) ; // osu!stable retrieved a random banana colour
2018-05-25 10:11:29 +00:00
}
2019-02-28 04:31:40 +00:00
2018-05-25 10:11:29 +00:00
break ;
2019-04-01 03:16:05 +00:00
2018-05-25 10:11:29 +00:00
case JuiceStream juiceStream :
2020-03-11 09:37:58 +00:00
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
2021-08-25 16:42:57 +00:00
lastPosition = juiceStream . OriginalX + juiceStream . Path . ControlPoints [ ^ 1 ] . Position . X ;
2020-03-11 09:37:58 +00:00
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream . StartTime ;
2018-05-25 10:11:29 +00:00
foreach ( var nested in juiceStream . NestedHitObjects )
{
2019-08-01 05:57:17 +00:00
var catchObject = ( CatchHitObject ) nested ;
catchObject . XOffset = 0 ;
if ( catchObject is TinyDroplet )
2020-12-09 08:58:53 +00:00
catchObject . XOffset = Math . Clamp ( rng . Next ( - 20 , 20 ) , - catchObject . OriginalX , CatchPlayfield . WIDTH - catchObject . OriginalX ) ;
2019-08-01 05:57:17 +00:00
else if ( catchObject is Droplet )
2018-06-13 10:52:04 +00:00
rng . Next ( ) ; // osu!stable retrieved a random droplet rotation
2018-05-25 10:11:29 +00:00
}
2019-02-28 04:31:40 +00:00
2018-05-25 10:11:29 +00:00
break ;
}
}
2020-03-11 09:36:37 +00:00
initialiseHyperDash ( beatmap ) ;
2018-05-25 10:11:29 +00:00
}
2022-04-28 08:46:00 +00:00
private static void applyHardRockOffset ( CatchHitObject hitObject , ref float? lastPosition , ref double lastStartTime , LegacyRandom rng )
2019-07-31 09:44:01 +00:00
{
2020-12-09 08:58:53 +00:00
float offsetPosition = hitObject . OriginalX ;
2019-07-31 09:44:01 +00:00
double startTime = hitObject . StartTime ;
2024-03-01 21:19:45 +00:00
if ( lastPosition = = null | |
// some objects can get assigned position zero, making stable incorrectly go inside this if branch on the next object. to maintain behaviour and compatibility, do the same here.
// reference: https://github.com/peppy/osu-stable-reference/blob/3ea48705eb67172c430371dcfc8a16a002ed0d3d/osu!/GameplayElements/HitObjects/Fruits/HitFactoryFruits.cs#L45-L50
// todo: should be revisited and corrected later probably.
lastPosition = = 0 )
2019-07-31 09:44:01 +00:00
{
2019-08-01 04:33:00 +00:00
lastPosition = offsetPosition ;
2019-07-31 09:44:01 +00:00
lastStartTime = startTime ;
return ;
}
2019-08-01 04:33:00 +00:00
float positionDiff = offsetPosition - lastPosition . Value ;
2020-03-11 09:43:08 +00:00
// Todo: BUG!! Stable calculated time deltas as ints, which affects randomisation. This should be changed to a double.
int timeDiff = ( int ) ( startTime - lastStartTime ) ;
2019-07-31 09:44:01 +00:00
if ( timeDiff > 1000 )
{
2019-08-01 04:33:00 +00:00
lastPosition = offsetPosition ;
2019-07-31 09:44:01 +00:00
lastStartTime = startTime ;
return ;
}
if ( positionDiff = = 0 )
{
2019-08-01 04:33:00 +00:00
applyRandomOffset ( ref offsetPosition , timeDiff / 4d , rng ) ;
2020-12-09 08:58:53 +00:00
hitObject . XOffset = offsetPosition - hitObject . OriginalX ;
2019-07-31 09:44:01 +00:00
return ;
}
2020-03-11 09:43:08 +00:00
// ReSharper disable once PossibleLossOfFraction
2020-07-01 15:21:45 +00:00
if ( Math . Abs ( positionDiff ) < timeDiff / 3 )
2019-08-01 04:33:00 +00:00
applyOffset ( ref offsetPosition , positionDiff ) ;
2019-07-31 09:44:01 +00:00
2020-12-09 08:58:53 +00:00
hitObject . XOffset = offsetPosition - hitObject . OriginalX ;
2019-07-31 09:44:01 +00:00
2019-08-01 04:33:00 +00:00
lastPosition = offsetPosition ;
2019-07-31 09:44:01 +00:00
lastStartTime = startTime ;
}
/// <summary>
/// Applies a random offset in a random direction to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="maxOffset">The maximum offset, cannot exceed 20px.</param>
/// <param name="rng">The random number generator.</param>
2022-04-28 08:46:00 +00:00
private static void applyRandomOffset ( ref float position , double maxOffset , LegacyRandom rng )
2019-07-31 09:44:01 +00:00
{
bool right = rng . NextBool ( ) ;
2020-07-01 15:21:45 +00:00
float rand = Math . Min ( 20 , ( float ) rng . Next ( 0 , Math . Max ( 0 , maxOffset ) ) ) ;
2019-07-31 09:44:01 +00:00
if ( right )
{
// Clamp to the right bound
2020-07-01 15:21:45 +00:00
if ( position + rand < = CatchPlayfield . WIDTH )
2019-07-31 09:44:01 +00:00
position + = rand ;
else
position - = rand ;
}
else
{
// Clamp to the left bound
if ( position - rand > = 0 )
position - = rand ;
else
position + = rand ;
}
}
/// <summary>
/// Applies an offset to a position, ensuring that the final position remains within the boundary of the playfield.
/// </summary>
/// <param name="position">The position which the offset should be applied to.</param>
/// <param name="amount">The amount to offset by.</param>
private static void applyOffset ( ref float position , float amount )
{
if ( amount > 0 )
{
// Clamp to the right bound
2020-08-20 11:25:40 +00:00
if ( position + amount < CatchPlayfield . WIDTH )
2019-07-31 09:44:01 +00:00
position + = amount ;
}
else
{
// Clamp to the left bound
if ( position + amount > 0 )
position + = amount ;
}
}
2020-03-11 09:36:37 +00:00
private static void initialiseHyperDash ( IBeatmap beatmap )
2017-11-28 12:22:57 +00:00
{
2023-02-03 05:07:21 +00:00
var palpableObjects = CatchBeatmap . GetPalpableObjects ( beatmap . HitObjects )
. Where ( h = > h is Fruit | | ( h is Droplet & & h is not TinyDroplet ) )
. ToArray ( ) ;
2018-09-13 15:15:46 +00:00
2021-10-02 03:34:29 +00:00
double halfCatcherWidth = Catcher . CalculateCatchWidth ( beatmap . Difficulty ) / 2 ;
2020-08-20 17:21:16 +00:00
// Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins.
// This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible.
// For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size.
halfCatcherWidth / = Catcher . ALLOWED_CATCH_RANGE ;
2018-09-12 17:48:35 +00:00
int lastDirection = 0 ;
double lastExcess = halfCatcherWidth ;
2018-04-13 09:19:50 +00:00
2023-02-03 05:07:21 +00:00
for ( int i = 0 ; i < palpableObjects . Length - 1 ; i + + )
2018-09-12 17:48:35 +00:00
{
2020-11-24 10:57:37 +00:00
var currentObject = palpableObjects [ i ] ;
var nextObject = palpableObjects [ i + 1 ] ;
2018-04-13 09:19:50 +00:00
2020-03-11 09:36:37 +00:00
// Reset variables in-case values have changed (e.g. after applying HR)
currentObject . HyperDashTarget = null ;
currentObject . DistanceToHyperDash = 0 ;
2020-12-09 08:58:53 +00:00
int thisDirection = nextObject . EffectiveX > currentObject . EffectiveX ? 1 : - 1 ;
2023-12-06 05:50:03 +00:00
// Int truncation added to match osu!stable.
2023-12-04 05:32:14 +00:00
double timeToNext = ( int ) nextObject . StartTime - ( int ) currentObject . StartTime - 1000f / 60f / 4 ; // 1/4th of a frame of grace time, taken from osu-stable
2020-12-09 08:58:53 +00:00
double distanceToNext = Math . Abs ( nextObject . EffectiveX - currentObject . EffectiveX ) - ( lastDirection = = thisDirection ? lastExcess : halfCatcherWidth ) ;
2021-10-26 11:09:48 +00:00
float distanceToHyper = ( float ) ( timeToNext * Catcher . BASE_DASH_SPEED - distanceToNext ) ;
2019-04-01 03:16:05 +00:00
2018-09-12 17:48:35 +00:00
if ( distanceToHyper < 0 )
2017-12-01 06:08:12 +00:00
{
2017-12-01 10:24:48 +00:00
currentObject . HyperDashTarget = nextObject ;
2017-12-01 11:39:58 +00:00
lastExcess = halfCatcherWidth ;
2017-12-01 06:08:12 +00:00
}
else
{
2018-09-12 17:48:35 +00:00
currentObject . DistanceToHyperDash = distanceToHyper ;
2019-11-20 12:19:49 +00:00
lastExcess = Math . Clamp ( distanceToHyper , 0 , halfCatcherWidth ) ;
2017-12-01 06:08:12 +00:00
}
2018-04-13 09:19:50 +00:00
2017-12-01 06:08:12 +00:00
lastDirection = thisDirection ;
}
2017-11-28 12:22:57 +00:00
}
2017-09-15 11:54:34 +00:00
}
}