1010using osu . Game . Graphics ;
1111using osu . Game . Rulesets . Mods ;
1212using osu . Game . Rulesets . Osu . Objects ;
13+ using osu . Game . Rulesets . Osu . Objects . Drawables ;
1314using osu . Game . Rulesets . Osu . Replays ;
1415using osu . Game . Rulesets . Osu . UI ;
1516using osu . Game . Rulesets . UI ;
17+ using osuTK ;
1618
1719namespace osu . Game . Rulesets . Osu . Mods
1820{
@@ -43,12 +45,79 @@ public class OsuModAutopilot : Mod, IUpdatableByPlayfield, IApplicableToDrawable
4345
4446 public void Update ( Playfield playfield )
4547 {
46- if ( currentFrame == replayFrames . Count - 1 ) return ;
48+ if ( replayFrames . Count == 0 || currentFrame == replayFrames . Count - 1 ) return ;
4749
4850 double time = playfield . Clock . CurrentTime ;
4951
50- // Very naive implementation of autopilot based on proximity to replay frames.
52+ // Interpolate the cursor position using the replay frames.
5153 // Special case for the first frame is required to ensure the mouse is in a sane position until the actual time of the first frame is hit.
54+ while ( currentFrame > 0 && Math . Abs ( replayFrames [ currentFrame - 1 ] . Time - time ) <= Math . Abs ( replayFrames [ currentFrame ] . Time - time ) )
55+ {
56+ currentFrame -- ;
57+ }
58+
59+ while ( currentFrame < replayFrames . Count - 1 && Math . Abs ( replayFrames [ currentFrame + 1 ] . Time - time ) <= Math . Abs ( replayFrames [ currentFrame < 0 ? 0 : currentFrame ] . Time - time ) )
60+ {
61+ currentFrame ++ ;
62+ }
63+
64+ if ( currentFrame < 0 )
65+ return ;
66+
67+ Vector2 newPosition = playfield . ToScreenSpace ( replayFrames [ currentFrame ] . Position ) ;
68+
69+ var osuPlayfield = ( OsuPlayfield ) playfield ;
70+
71+ DrawableOsuHitObject ? nextObject = null ;
72+
73+ foreach ( var h in osuPlayfield . HitObjectContainer . AliveObjects )
74+ {
75+ if ( h is not DrawableOsuHitObject osuHitObject || osuHitObject . Result . HasResult )
76+ continue ;
77+
78+ if ( nextObject == null || osuHitObject . HitObject . StartTime < nextObject . HitObject . StartTime )
79+ {
80+ nextObject = osuHitObject ;
81+ }
82+ }
83+
84+ if ( nextObject != null )
85+ {
86+ // If we are currently "on" an object (in terms of time), we should check if it has been judged.
87+ // If it hasn't been judged, and we are still within the valid hit window, maybe we should clamp the cursor position to that object's position, effectively "pausing" the cursor movement along the path until the user clicks or it's too late.
88+
89+ bool shouldStay = false ;
90+ Vector2 stayPosition = Vector2 . Zero ;
91+
92+ if ( nextObject is DrawableSlider slider )
93+ {
94+ if ( ! slider . HeadCircle . Result . HasResult )
95+ {
96+ if ( time >= slider . HitObject . StartTime )
97+ {
98+ shouldStay = true ;
99+ stayPosition = slider . HitObject . StackedPosition ;
100+ }
101+ }
102+ }
103+ else if ( nextObject is DrawableHitCircle hitCircle )
104+ {
105+ if ( time >= hitCircle . HitObject . StartTime )
106+ {
107+ shouldStay = true ;
108+ stayPosition = hitCircle . HitObject . StackedPosition ;
109+ }
110+ }
111+
112+ if ( shouldStay )
113+ {
114+ newPosition = playfield . ToScreenSpace ( stayPosition ) ;
115+ }
116+ }
117+
118+ new MousePositionAbsoluteInput { Position = newPosition } . Apply ( inputManager . CurrentState , inputManager ) ;
119+
120+ // TODO: Implement the functionality to automatically spin spinners
52121 // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered).
53122 while ( currentFrame < replayFrames . Count - 1 )
54123 {
@@ -74,6 +143,7 @@ public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset
74143
75144 // Generate the replay frames the cursor should follow
76145 replayFrames = new OsuAutoGenerator ( drawableRuleset . Beatmap , drawableRuleset . Mods ) . Generate ( ) . Frames . Cast < OsuReplayFrame > ( ) . ToList ( ) ;
146+ currentFrame = - 1 ;
77147 }
78148 }
79149}
0 commit comments