private void moveToHitObject(OsuHitObject h, Vector2 targetPos, Easing easing) { OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[Frames.Count - 1]; // Wait until Auto could "see and react" to the next note. double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); if (waitTime > lastFrame.Time) { lastFrame = new OsuReplayFrame(waitTime, lastFrame.Position) { Actions = lastFrame.Actions }; AddFrameToReplay(lastFrame); } Vector2 lastPosition = lastFrame.Position; double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time); // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (timeDifference > 0 && // Sanity checks ((lastPosition - targetPos).Length > h.Radius * (1.5 + 100.0 / timeDifference) || // Either the distance is big enough timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. { // Perform eased movement for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); } buttonIndex = 0; } else { buttonIndex++; } }
// Add frames to click the hitobject private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) { // Time to insert the first frame which clicks the object // Here we mainly need to determine which button to use var action = buttonIndex % 2 == 0 ? OsuAction.LeftButton : OsuAction.RightButton; var startFrame = new OsuReplayFrame(h.StartTime, new Vector2(startPosition.X, startPosition.Y), action); // TODO: Why do we delay 1 ms if the object is a spinner? There already is KEY_UP_DELAY from hEndTime. double hEndTime = h.GetEndTime() + KEY_UP_DELAY; int endDelay = h is Spinner ? 1 : 0; var endFrame = new OsuReplayFrame(hEndTime + endDelay, new Vector2(h.StackedEndPosition.X, h.StackedEndPosition.Y)); // Decrement because we want the previous frame, not the next one int index = FindInsertionIndex(startFrame) - 1; // If the previous frame has a button pressed, force alternation. // If there are frames ahead, modify those to use the new button press. // Do we have a previous frame? No need to check for < replay.Count since we decremented! if (index >= 0) { var previousFrame = (OsuReplayFrame)Frames[index]; var previousActions = previousFrame.Actions; // If a button is already held, then we simply alternate if (previousActions.Any()) { // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. if (previousActions.Contains(action)) { action = action == OsuAction.LeftButton ? OsuAction.RightButton : OsuAction.LeftButton; startFrame.Actions.Clear(); startFrame.Actions.Add(action); } // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. int endIndex = FindInsertionIndex(endFrame); if (index < Frames.Count - 1) { Frames.RemoveRange(index + 1, Math.Max(0, endIndex - (index + 1))); } // After alternating we need to keep holding the other button in the future rather than the previous one. for (int j = index + 1; j < Frames.Count; ++j) { var frame = (OsuReplayFrame)Frames[j]; // Don't affect frames which stop pressing a button! if (j < Frames.Count - 1 || frame.Actions.SequenceEqual(previousActions)) { frame.Actions.Clear(); frame.Actions.Add(action); } } } } AddFrameToReplay(startFrame); switch (h) { // We add intermediate frames for spinning / following a slider here. case Spinner spinner: Vector2 difference = startPosition - SPINNER_CENTRE; float radius = difference.Length; float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); double t; for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay) { t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); } t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection; Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); endFrame.Position = endPosition; break; case Slider slider: for (double j = FrameDelay; j < slider.Duration; j += FrameDelay) { Vector2 pos = slider.StackedPositionAt(j / slider.Duration); AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); } AddFrameToReplay(new OsuReplayFrame(slider.EndTime, new Vector2(slider.StackedEndPosition.X, slider.StackedEndPosition.Y), action)); break; } // We only want to let go of our button if we are at the end of the current replay. Otherwise something is still going on after us so we need to keep the button pressed! if (Frames[Frames.Count - 1].Time <= endFrame.Time) { AddFrameToReplay(endFrame); } }