mirror of
https://github.com/SK-la/Ez2Lazer.git
synced 2026-03-13 11:20:28 +00:00
Effectively closing https://github.com/ppy/osu/issues/21471 as a wontfix. The issue is demonstrated by the following test case: <details> <summary>patch</summary> ```diff diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs index 31498295da..36b4fe5122 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModFreezeFrame.cs @@ -55,5 +55,49 @@ public void TestSkipToFirstSpinnerNotSuppressed() PassCondition = () => Player.GameplayClockContainer.GameplayStartTime > 0 }); } + + [Test] + public void TestFreezeFrameAppliedBeforeHidden() + { + CreateModTest(new ModTestData + { + Mods = + [ + new OsuModFreezeFrame(), + new OsuModHidden(), + ], + CreateBeatmap = () => new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 3000, Position = OsuPlayfield.BASE_SIZE / 2, NewCombo = true }, + new HitCircle { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }, + } + }, + PassCondition = () => ((HitCircle)Player.GameplayState.Beatmap.HitObjects[1]).TimeFadeIn == 480 + }); + } + + [Test] + public void TestFreezeFrameAppliedAfterHidden() + { + CreateModTest(new ModTestData + { + Mods = + [ + new OsuModHidden(), + new OsuModFreezeFrame(), + ], + CreateBeatmap = () => new OsuBeatmap + { + HitObjects = + { + new HitCircle { StartTime = 3000, Position = OsuPlayfield.BASE_SIZE / 2, NewCombo = true }, + new HitCircle { StartTime = 5000, Position = OsuPlayfield.BASE_SIZE / 2 }, + } + }, + PassCondition = () => ((HitCircle)Player.GameplayState.Beatmap.HitObjects[1]).TimeFadeIn == 480 + }); + } } } ``` </details> The reason that this is happening is a data dependency. Freeze Frame modifies `TimePreempt` of hitobjects:54c0b2c20c/osu.Game.Rulesets.Osu/Mods/OsuModFreezeFrame.cs (L53)while Hidden uses `TimePreempt` to set `TimeFadeIn`:54c0b2c20c/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs (L45)Therefore the final value of `TimeFadeIn` with these two mods active depends on the order of application. The reason why I'm bothering to do this is that I was pinged from the PP development server about this again in the context of some ongoing 'rework' and I wish to get ahead of this before it becomes a bigger problem than it already is. The current order of application of these mods as done in `Player` is constant, but essentially undefined. I'm not even sure what even enforces the current order. It's currently Hidden, then Freeze Frame. If I were to guess, the thing "enforcing" (insert 400 tonne solid lead air quotes) it is probably the mod select overlay with its "I need to own all of the mod instances in the global bindable" substitution logic. I'm already getting pushback for this from the PP server crowd who are attempting to justify the current "behaviour" by saying that the player base wants this or by saying that it's already broken so it should remain that way forever. I am however not willing to accept [stable-taiko-tier stupidity again](https://github.com/ppy/osu/pull/27136#issuecomment-1957055402) and am not willing to accept the complexity this would invite everywhere else. I do not see any other easy way of fixing this problem at this point in time. I had hoped that I could inline the `TimePreempt` read in `OsuModHidden`, but it's not that easy because it's set in multiple places. Existing HDFF scores would probably need to be delisted from the leaderboards. I'm not even sure I can get myself to pretend to care.
102 lines
3.8 KiB
C#
102 lines
3.8 KiB
C#
// 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.
|
|
|
|
using System;
|
|
using System.Linq;
|
|
using osu.Framework.Graphics;
|
|
using osu.Framework.Graphics.Sprites;
|
|
using osu.Framework.Localisation;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Graphics;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects.Drawables;
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
using osu.Game.Rulesets.Osu.Objects.Drawables;
|
|
|
|
namespace osu.Game.Rulesets.Osu.Mods
|
|
{
|
|
public class OsuModFreezeFrame : Mod, IApplicableToDrawableHitObject, IApplicableToBeatmap
|
|
{
|
|
public override string Name => "Freeze Frame";
|
|
|
|
public override string Acronym => "FR";
|
|
|
|
public override IconUsage? Icon => OsuIcon.ModFreezeFrame;
|
|
|
|
public override double ScoreMultiplier => 1;
|
|
|
|
public override LocalisableString Description => "Burn the notes into your memory.";
|
|
|
|
/// <remarks>
|
|
/// Incompatible with all mods that directly modify or indirectly depend on <see cref="OsuHitObject.TimePreempt"/>, or alter the behaviour of approach circles.
|
|
/// </remarks>
|
|
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModApproachDifferent), typeof(OsuModTransform), typeof(OsuModDepth), typeof(OsuModHidden) }).ToArray();
|
|
|
|
public override ModType Type => ModType.Fun;
|
|
|
|
//mod breaks normal approach circle preempt
|
|
private double originalPreempt;
|
|
|
|
public void ApplyToBeatmap(IBeatmap beatmap)
|
|
{
|
|
var firstHitObject = beatmap.HitObjects.OfType<OsuHitObject>().FirstOrDefault();
|
|
if (firstHitObject == null)
|
|
return;
|
|
|
|
double lastNewComboTime = 0;
|
|
|
|
originalPreempt = firstHitObject.TimePreempt;
|
|
|
|
foreach (var obj in beatmap.HitObjects.OfType<OsuHitObject>())
|
|
{
|
|
if (obj.NewCombo)
|
|
{
|
|
lastNewComboTime = obj.StartTime;
|
|
}
|
|
|
|
applyFadeInAdjustment(obj);
|
|
}
|
|
|
|
void applyFadeInAdjustment(OsuHitObject osuObject)
|
|
{
|
|
if (osuObject is not Spinner)
|
|
osuObject.TimePreempt += osuObject.StartTime - lastNewComboTime;
|
|
|
|
foreach (var nested in osuObject.NestedHitObjects.OfType<OsuHitObject>())
|
|
{
|
|
switch (nested)
|
|
{
|
|
//Freezing the SliderTicks doesnt play well with snaking sliders
|
|
case SliderTick:
|
|
//SliderRepeat wont layer correctly if preempt is changed.
|
|
case SliderRepeat:
|
|
break;
|
|
|
|
default:
|
|
applyFadeInAdjustment(nested);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void ApplyToDrawableHitObject(DrawableHitObject drawableObject)
|
|
{
|
|
drawableObject.ApplyCustomUpdateState += (drawableHitObject, _) =>
|
|
{
|
|
if (drawableHitObject is not DrawableHitCircle drawableHitCircle) return;
|
|
|
|
var hitCircle = drawableHitCircle.HitObject;
|
|
var approachCircle = drawableHitCircle.ApproachCircle;
|
|
|
|
// Reapply scale, ensuring the AR isn't changed due to the new preempt.
|
|
approachCircle.ClearTransforms(targetMember: nameof(approachCircle.Scale));
|
|
approachCircle.ScaleTo(4 * (float)(hitCircle.TimePreempt / originalPreempt));
|
|
|
|
using (drawableHitCircle.ApproachCircle.BeginAbsoluteSequence(hitCircle.StartTime - hitCircle.TimePreempt))
|
|
approachCircle.ScaleTo(1, hitCircle.TimePreempt).Then().Expire();
|
|
};
|
|
}
|
|
}
|
|
}
|