internal override void UpdateFlashlight() { if (s_Flashlight != null) { Vector2 destination = new Vector2( Math.Max(0, Math.Min(640, (InputManager.s_Cursor.Position.X - GameBase.WindowManager.NonWidescreenOffsetX) / GameBase.WindowManager.Ratio)) + GameBase.WindowManager.OffsetXScaled, Math.Max(0, Math.Min(480, InputManager.s_Cursor.Position.Y / GameBase.WindowManager.Ratio))); if (s_Flashlight != null) { s_Flashlight.Position = OsuMathHelper.TweenValues(s_Flashlight.Position, destination, (float)GameBase.ElapsedMilliseconds, 0, 120, EasingTypes.Out); } } base.UpdateFlashlight(); }
internal override void CreateAutoplayReplay() { int buttonIndex = 0; bool delayedMovements = ModManager.CheckActive(Mods.Relax2); EasingTypes preferredEasing = delayedMovements ? EasingTypes.InOutCubic : EasingTypes.Out; InputManager.ReplayScore.Replay = new List <bReplayFrame>(); List <bReplayFrame> replay = InputManager.ReplayScore.Replay; AddFrameToReplay(replay, new bReplayFrame(-100000, 256, 500, pButtonState.None)); AddFrameToReplay(replay, new bReplayFrame(hitObjectManager.hitObjects[0].StartTime - 1500, 256, 500, pButtonState.None)); AddFrameToReplay(replay, new bReplayFrame(hitObjectManager.hitObjects[0].StartTime - 1000, 256, 192, pButtonState.None)); // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. float frameDelay = (float)HitObjectManager.ApplyModsToRate(1000.0 / 60.0); Vector2 spinnerCentre = new Vector2(256, 192); const float spinnerRadius = 50; // Already superhuman, but still somewhat realistic int reactionTime = (int)HitObjectManager.ApplyModsToRate(100); for (int i = 0; i < hitObjectManager.hitObjectsCount; i++) { HitObject h = hitObjectManager.hitObjects[i]; if (h.EndTime < InputManager.ReplayStartTime) { h.IsHit = true; continue; } int endDelay = h is SpinnerOsu ? 1 : 0; if (delayedMovements && i > 0) { HitObject last = hitObjectManager.hitObjects[i - 1]; //Make the cursor stay at a hitObject as long as possible (mainly for autopilot). if (h.StartTime - HitObjectManager.HITTABLE_RANGE > last.EndTime + hitObjectManager.HitWindow50 + 50) { if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) { AddFrameToReplay(replay, new bReplayFrame(last.EndTime + hitObjectManager.HitWindow50, last.EndPosition.X, last.EndPosition.Y, pButtonState.None)); } if (!(h is Spinner)) { AddFrameToReplay(replay, new bReplayFrame(h.StartTime - HitObjectManager.HITTABLE_RANGE, h.Position.X, h.Position.Y, pButtonState.None)); } } else if (h.StartTime - hitObjectManager.HitWindow50 > last.EndTime + hitObjectManager.HitWindow50 + 50) { if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) { AddFrameToReplay(replay, new bReplayFrame(last.EndTime + hitObjectManager.HitWindow50, last.EndPosition.X, last.EndPosition.Y, pButtonState.None)); } if (!(h is Spinner)) { AddFrameToReplay(replay, new bReplayFrame(h.StartTime - hitObjectManager.HitWindow50, h.Position.X, h.Position.Y, pButtonState.None)); } } else if (h.StartTime - hitObjectManager.HitWindow100 > last.EndTime + hitObjectManager.HitWindow100 + 50) { if (!(last is Spinner) && h.StartTime - last.EndTime < 1000) { AddFrameToReplay(replay, new bReplayFrame(last.EndTime + hitObjectManager.HitWindow100, last.EndPosition.X, last.EndPosition.Y, pButtonState.None)); } if (!(h is Spinner)) { AddFrameToReplay(replay, new bReplayFrame(h.StartTime - hitObjectManager.HitWindow100, h.Position.X, h.Position.Y, pButtonState.None)); } } } Vector2 targetPosition = h.Position; EasingTypes easing = preferredEasing; float spinnerDirection = -1; if (h is Spinner) { targetPosition.X = replay[replay.Count - 1].mouseX; targetPosition.Y = replay[replay.Count - 1].mouseY; Vector2 difference = spinnerCentre - targetPosition; float differenceLength = difference.Length(); float newLength = (float)Math.Sqrt(differenceLength * differenceLength - spinnerRadius * spinnerRadius); if (differenceLength > spinnerRadius) { float angle = (float)Math.Asin(spinnerRadius / differenceLength); if (angle > 0) { spinnerDirection = -1; } else { spinnerDirection = 1; } difference.X = difference.X * (float)Math.Cos(angle) - difference.Y * (float)Math.Sin(angle); difference.Y = difference.X * (float)Math.Sin(angle) + difference.Y * (float)Math.Cos(angle); difference.Normalize(); difference *= newLength; targetPosition += difference; easing = EasingTypes.In; } else if (difference.Length() > 0) { targetPosition = spinnerCentre - difference * (spinnerRadius / difference.Length()); } else { targetPosition = spinnerCentre + new Vector2(0, -spinnerRadius); } } // Do some nice easing for cursor movements if (replay.Count > 0) { bReplayFrame lastFrame = replay[replay.Count - 1]; // Wait until Auto could "see and react" to the next note. int waitTime = h.StartTime - (int)Math.Max(0.0, hitObjectManager.PreEmpt - reactionTime); if (waitTime > lastFrame.time) { lastFrame = new bReplayFrame(waitTime, lastFrame.mouseX, lastFrame.mouseY, lastFrame.buttonState); AddFrameToReplay(replay, lastFrame); } Vector2 lastPosition = new Vector2(lastFrame.mouseX, lastFrame.mouseY); HitObjectManagerOsu hom = hitObjectManager as HitObjectManagerOsu; double timeDifference = HitObjectManager.ApplyModsToTime(h.StartTime - lastFrame.time, ModManager.ModStatus); // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (hom != null && timeDifference > 0 && // Sanity checks ((lastPosition - targetPosition).Length() > hom.HitObjectRadius * (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 (float time = lastFrame.time + frameDelay; time < h.StartTime; time += frameDelay) { Vector2 currentPosition = OsuMathHelper.TweenValues(lastPosition, targetPosition, time, lastFrame.time, h.StartTime, easing); AddFrameToReplay(replay, new bReplayFrame((int)time, currentPosition.X, currentPosition.Y, lastFrame.buttonState)); } buttonIndex = 0; } else { buttonIndex++; } } pButtonState button = buttonIndex % 2 == 0 ? pButtonState.Left1 : pButtonState.Right1; pButtonState previousButton = pButtonState.None; bReplayFrame newFrame = new bReplayFrame(h.StartTime, targetPosition.X, targetPosition.Y, button); bReplayFrame endFrame = new bReplayFrame(h.EndTime + endDelay, h.EndPosition.X, h.EndPosition.Y, pButtonState.None); // Decrement because we want the previous frame, not the next one int index = FindInsertionIndex(replay, newFrame) - 1; // Do we have a previous frame? No need to check for < replay.Count since we decremented! if (index >= 0) { bReplayFrame previousFrame = replay[index]; previousButton = previousFrame.buttonState; // If a button is already held, then we simply alternate if (previousButton != pButtonState.None) { Debug.Assert(previousButton != (pButtonState.Left1 | pButtonState.Right1)); // Force alternation if we have the same button. Otherwise we can just keep the naturally to us assigned button. if (previousButton == button) { button = (pButtonState.Left1 | pButtonState.Right1) & ~button; newFrame.SetButtonStates(button); } // We always follow the most recent slider / spinner, so remove any other frames that occur while it exists. int endIndex = FindInsertionIndex(replay, endFrame); if (index < replay.Count - 1) { replay.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 < replay.Count; ++j) { // Don't affect frames which stop pressing a button! if (j < replay.Count - 1 || replay[j].buttonState == previousButton) { replay[j].SetButtonStates(button); } } } } AddFrameToReplay(replay, newFrame); // We add intermediate frames for spinning / following a slider here. if (h is SpinnerOsu) { Vector2 difference = targetPosition - spinnerCentre; float radius = difference.Length(); float angle = radius == 0 ? 0 : (float)Math.Atan2(difference.Y, difference.X); float t; for (float j = h.StartTime + frameDelay; j < h.EndTime; j += frameDelay) { t = (float)HitObjectManager.ApplyModsToTime(j - h.StartTime) * spinnerDirection; Vector2 pos = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); AddFrameToReplay(replay, new bReplayFrame((int)j, pos.X, pos.Y, button)); } t = (float)HitObjectManager.ApplyModsToTime(h.EndTime - h.StartTime) * spinnerDirection; Vector2 endPosition = spinnerCentre + CirclePosition(t / 20 + angle, spinnerRadius); AddFrameToReplay(replay, new bReplayFrame(h.EndTime, endPosition.X, endPosition.Y, button)); endFrame.mouseX = endPosition.X; endFrame.mouseY = endPosition.Y; } else if (h is SliderOsu) { SliderOsu s = h as SliderOsu; int lastTime = 0; foreach ( Transformation t in s.sliderFollower.Transformations.FindAll( tr => tr.Type == TransformationType.Movement)) { if (lastTime != 0 && t.Time1 - lastTime < frameDelay) { continue; } AddFrameToReplay(replay, new bReplayFrame(t.Time1, t.StartVector.X, t.StartVector.Y, button)); lastTime = t.Time1; } AddFrameToReplay(replay, new bReplayFrame(h.EndTime, h.EndPosition.X, h.EndPosition.Y, button)); } // 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 (replay[replay.Count - 1].time <= endFrame.time) { AddFrameToReplay(replay, endFrame); } } Player.currentScore.Replay = InputManager.ReplayScore.Replay; Player.currentScore.PlayerName = "osu!"; }