2019-01-24 16:43:03 +08: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 17:19:50 +08:00
2017-12-01 14:08:12 +08:00
using System ;
2017-12-01 19:39:58 +08:00
using System.Linq ;
2017-09-18 11:48:33 +08:00
using osu.Game.Beatmaps ;
2017-09-15 19:54:34 +08:00
using osu.Game.Rulesets.Catch.Objects ;
2017-12-01 14:08:12 +08:00
using osu.Game.Rulesets.Catch.UI ;
2020-11-24 18:57:37 +08:00
using osu.Game.Rulesets.Objects.Types ;
2022-04-28 16:46:00 +08:00
using osu.Game.Utils ;
2018-04-13 17:19:50 +08:00
2017-09-15 19:54:34 +08:00
namespace osu.Game.Rulesets.Catch.Beatmaps
{
2018-04-19 21:04:12 +08:00
public class CatchBeatmapProcessor : BeatmapProcessor
2017-09-15 19:54:34 +08:00
{
2018-06-29 11:45:48 +08:00
public const int RNG_SEED = 1337 ;
2021-06-23 13:11:25 +08:00
public bool HardRockOffsets { get ; set ; }
2018-04-19 21:04:12 +08:00
public CatchBeatmapProcessor ( IBeatmap beatmap )
: base ( beatmap )
2017-09-15 19:54:34 +08:00
{
2018-04-19 21:04:12 +08:00
}
2023-11-23 12:41:01 +08: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 11:45:48 +08:00
public override void PostProcess ( )
{
2018-04-19 21:04:12 +08:00
base . PostProcess ( ) ;
2018-04-13 17:19:50 +08:00
2019-07-31 17:44:01 +08:00
ApplyPositionOffsets ( Beatmap ) ;
2018-06-29 11:45:48 +08:00
2018-03-20 14:45:40 +08:00
int index = 0 ;
2019-04-01 11:16:05 +08:00
2018-04-19 21:04:12 +08:00
foreach ( var obj in Beatmap . HitObjects . OfType < CatchHitObject > ( ) )
2018-06-26 19:13:55 +08:00
{
2020-02-19 14:37:12 +08:00
obj . IndexInBeatmap = index ;
foreach ( var nested in obj . NestedHitObjects . OfType < CatchHitObject > ( ) )
nested . IndexInBeatmap = index ;
2018-06-26 19:13:55 +08:00
if ( obj . LastInCombo & & obj . NestedHitObjects . LastOrDefault ( ) is IHasComboInformation lastNested )
lastNested . LastInCombo = true ;
2020-02-19 14:37:12 +08:00
index + + ;
2018-06-26 19:13:55 +08:00
}
2017-09-15 19:54:34 +08:00
}
2018-04-13 17:19:50 +08:00
2021-06-23 13:11:25 +08:00
public void ApplyPositionOffsets ( IBeatmap beatmap )
2018-05-25 18:11:29 +08:00
{
2022-04-28 16:46:00 +08:00
var rng = new LegacyRandom ( RNG_SEED ) ;
2018-05-25 18:11:29 +08:00
2019-07-31 17:44:01 +08:00
float? lastPosition = null ;
double lastStartTime = 0 ;
2019-08-01 12:33:00 +08:00
foreach ( var obj in beatmap . HitObjects . OfType < CatchHitObject > ( ) )
2018-05-25 18:11:29 +08:00
{
2019-08-01 12:33:00 +08:00
obj . XOffset = 0 ;
2018-05-25 18:11:29 +08:00
switch ( obj )
{
2019-07-31 17:44:01 +08:00
case Fruit fruit :
2021-06-23 13:11:25 +08:00
if ( HardRockOffsets )
2019-07-31 17:44:01 +08:00
applyHardRockOffset ( fruit , ref lastPosition , ref lastStartTime , rng ) ;
break ;
2018-05-25 18:11:29 +08:00
case BananaShower bananaShower :
2018-06-29 14:01:33 +08:00
foreach ( var banana in bananaShower . NestedHitObjects . OfType < Banana > ( ) )
2018-05-25 18:11:29 +08:00
{
2020-07-01 23:21:45 +08:00
banana . XOffset = ( float ) ( rng . NextDouble ( ) * CatchPlayfield . WIDTH ) ;
2018-06-13 17:39:26 +08:00
rng . Next ( ) ; // osu!stable retrieved a random banana type
rng . Next ( ) ; // osu!stable retrieved a random banana rotation
2018-06-13 20:10:54 +08:00
rng . Next ( ) ; // osu!stable retrieved a random banana colour
2018-05-25 18:11:29 +08:00
}
2019-02-28 12:31:40 +08:00
2018-05-25 18:11:29 +08:00
break ;
2019-04-01 11:16:05 +08:00
2018-05-25 18:11:29 +08:00
case JuiceStream juiceStream :
2020-03-11 17:37:58 +08: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-26 00:42:57 +08:00
lastPosition = juiceStream . OriginalX + juiceStream . Path . ControlPoints [ ^ 1 ] . Position . X ;
2020-03-11 17:37:58 +08: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 18:11:29 +08:00
foreach ( var nested in juiceStream . NestedHitObjects )
{
2019-08-01 13:57:17 +08:00
var catchObject = ( CatchHitObject ) nested ;
catchObject . XOffset = 0 ;
if ( catchObject is TinyDroplet )
2020-12-09 16:58:53 +08:00
catchObject . XOffset = Math . Clamp ( rng . Next ( - 20 , 20 ) , - catchObject . OriginalX , CatchPlayfield . WIDTH - catchObject . OriginalX ) ;
2019-08-01 13:57:17 +08:00
else if ( catchObject is Droplet )
2018-06-13 18:52:04 +08:00
rng . Next ( ) ; // osu!stable retrieved a random droplet rotation
2018-05-25 18:11:29 +08:00
}
2019-02-28 12:31:40 +08:00
2018-05-25 18:11:29 +08:00
break ;
}
}
2020-03-11 17:36:37 +08:00
initialiseHyperDash ( beatmap ) ;
2018-05-25 18:11:29 +08:00
}
2022-04-28 16:46:00 +08:00
private static void applyHardRockOffset ( CatchHitObject hitObject , ref float? lastPosition , ref double lastStartTime , LegacyRandom rng )
2019-07-31 17:44:01 +08:00
{
2020-12-09 16:58:53 +08:00
float offsetPosition = hitObject . OriginalX ;
2019-07-31 17:44:01 +08:00
double startTime = hitObject . StartTime ;
2024-03-02 05:19:45 +08: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 17:44:01 +08:00
{
2019-08-01 12:33:00 +08:00
lastPosition = offsetPosition ;
2019-07-31 17:44:01 +08:00
lastStartTime = startTime ;
return ;
}
2019-08-01 12:33:00 +08:00
float positionDiff = offsetPosition - lastPosition . Value ;
2020-03-11 17:43:08 +08: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 17:44:01 +08:00
if ( timeDiff > 1000 )
{
2019-08-01 12:33:00 +08:00
lastPosition = offsetPosition ;
2019-07-31 17:44:01 +08:00
lastStartTime = startTime ;
return ;
}
if ( positionDiff = = 0 )
{
2019-08-01 12:33:00 +08:00
applyRandomOffset ( ref offsetPosition , timeDiff / 4d , rng ) ;
2020-12-09 16:58:53 +08:00
hitObject . XOffset = offsetPosition - hitObject . OriginalX ;
2019-07-31 17:44:01 +08:00
return ;
}
2020-03-11 17:43:08 +08:00
// ReSharper disable once PossibleLossOfFraction
2020-07-01 23:21:45 +08:00
if ( Math . Abs ( positionDiff ) < timeDiff / 3 )
2019-08-01 12:33:00 +08:00
applyOffset ( ref offsetPosition , positionDiff ) ;
2019-07-31 17:44:01 +08:00
2020-12-09 16:58:53 +08:00
hitObject . XOffset = offsetPosition - hitObject . OriginalX ;
2019-07-31 17:44:01 +08:00
2019-08-01 12:33:00 +08:00
lastPosition = offsetPosition ;
2019-07-31 17:44:01 +08: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 16:46:00 +08:00
private static void applyRandomOffset ( ref float position , double maxOffset , LegacyRandom rng )
2019-07-31 17:44:01 +08:00
{
bool right = rng . NextBool ( ) ;
2020-07-01 23:21:45 +08:00
float rand = Math . Min ( 20 , ( float ) rng . Next ( 0 , Math . Max ( 0 , maxOffset ) ) ) ;
2019-07-31 17:44:01 +08:00
if ( right )
{
// Clamp to the right bound
2020-07-01 23:21:45 +08:00
if ( position + rand < = CatchPlayfield . WIDTH )
2019-07-31 17:44:01 +08: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 19:25:40 +08:00
if ( position + amount < CatchPlayfield . WIDTH )
2019-07-31 17:44:01 +08:00
position + = amount ;
}
else
{
// Clamp to the left bound
if ( position + amount > 0 )
position + = amount ;
}
}
2020-03-11 17:36:37 +08:00
private static void initialiseHyperDash ( IBeatmap beatmap )
2017-11-28 20:22:57 +08:00
{
2023-02-03 13:07:21 +08:00
var palpableObjects = CatchBeatmap . GetPalpableObjects ( beatmap . HitObjects )
. Where ( h = > h is Fruit | | ( h is Droplet & & h is not TinyDroplet ) )
. ToArray ( ) ;
2018-09-13 23:15:46 +08:00
2021-10-02 11:34:29 +08:00
double halfCatcherWidth = Catcher . CalculateCatchWidth ( beatmap . Difficulty ) / 2 ;
2020-08-21 01:21:16 +08: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-13 01:48:35 +08:00
int lastDirection = 0 ;
double lastExcess = halfCatcherWidth ;
2018-04-13 17:19:50 +08:00
2023-02-03 13:07:21 +08:00
for ( int i = 0 ; i < palpableObjects . Length - 1 ; i + + )
2018-09-13 01:48:35 +08:00
{
2020-11-24 18:57:37 +08:00
var currentObject = palpableObjects [ i ] ;
var nextObject = palpableObjects [ i + 1 ] ;
2018-04-13 17:19:50 +08:00
2020-03-11 17:36:37 +08:00
// Reset variables in-case values have changed (e.g. after applying HR)
currentObject . HyperDashTarget = null ;
currentObject . DistanceToHyperDash = 0 ;
2020-12-09 16:58:53 +08:00
int thisDirection = nextObject . EffectiveX > currentObject . EffectiveX ? 1 : - 1 ;
2023-12-06 13:50:03 +08:00
// Int truncation added to match osu!stable.
2023-12-04 13:32:14 +08: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 16:58:53 +08:00
double distanceToNext = Math . Abs ( nextObject . EffectiveX - currentObject . EffectiveX ) - ( lastDirection = = thisDirection ? lastExcess : halfCatcherWidth ) ;
2021-10-26 19:09:48 +08:00
float distanceToHyper = ( float ) ( timeToNext * Catcher . BASE_DASH_SPEED - distanceToNext ) ;
2019-04-01 11:16:05 +08:00
2018-09-13 01:48:35 +08:00
if ( distanceToHyper < 0 )
2017-12-01 14:08:12 +08:00
{
2017-12-01 18:24:48 +08:00
currentObject . HyperDashTarget = nextObject ;
2017-12-01 19:39:58 +08:00
lastExcess = halfCatcherWidth ;
2017-12-01 14:08:12 +08:00
}
else
{
2018-09-13 01:48:35 +08:00
currentObject . DistanceToHyperDash = distanceToHyper ;
2019-11-20 20:19:49 +08:00
lastExcess = Math . Clamp ( distanceToHyper , 0 , halfCatcherWidth ) ;
2017-12-01 14:08:12 +08:00
}
2018-04-13 17:19:50 +08:00
2017-12-01 14:08:12 +08:00
lastDirection = thisDirection ;
}
2017-11-28 20:22:57 +08:00
}
2017-09-15 19:54:34 +08:00
}
}