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