/// <summary> /// When we have latency involved, we need to look at changing the autoplay replay /// to act as if a player was hitting notes. So we should check for any non-300s /// and apply then to the replay by warping some frames. /// </summary> private void AlterReplay(IncreaseScoreType type) { List <bReplayFrame> replay = autoScore.Replay; int currentFrame = InputManager.ReplayFrame + 1; while (currentFrame < replay.Count - 2) { bReplayFrame prev = replay[currentFrame - 1]; bReplayFrame curr = replay[currentFrame]; bReplayFrame next = replay[currentFrame + 1]; if (curr.mouseLeft) { if (prev.mouseLeft || next.mouseLeft || isLocalUserActiveAt(curr.time)) { currentFrame++; continue; //slider or spinner. } switch (type) { case IncreaseScoreType.Miss: curr.SetButtonStates(pButtonState.None); return; case IncreaseScoreType.Hit50: { int aim = (int)(player.hitObjectManager.HitWindow100 * 1.1f); if (curr.time - prev.time > aim) { curr.time -= aim; return; } } break; case IncreaseScoreType.Hit100: { int aim = (int)(player.hitObjectManager.HitWindow300 * 1.1f); if (curr.time - prev.time > aim) { curr.time -= aim; return; } } break; } } currentFrame++; } }
/// <summary> /// When we have latency involved, we need to look at changing the autoplay replay /// to act as if a player was hitting notes. So we should check for any non-300s /// and apply then to the replay by warping some frames. /// </summary> private void AlterReplaySlider(IncreaseScoreType type) { //First check if we just want to increase the tick count... List <bReplayFrame> replay = autoScore.Replay; int currentFrame = InputManager.ReplayFrame + 1; int replayCount = replay.Count; if (currentFrame > replayCount - 1) { return; } int sliderCheckFrame = Math.Max(safeNoClickFrame, currentFrame); //Even if we already are in the middle of a slider, we should //rewind back to the start and utilise the rest of it. while (sliderCheckFrame < replayCount - 2 && replay[sliderCheckFrame + 1].mouseLeft) { sliderCheckFrame++; } safeNoClickFrame = sliderCheckFrame; while (currentFrame < replayCount - 2) { bReplayFrame prev = replay[currentFrame - 1]; bReplayFrame curr = replay[currentFrame]; bReplayFrame next = replay[currentFrame + 1]; if (curr.mouseLeft && next.mouseLeft && !prev.mouseLeft && !isLocalUserActiveAt(curr.time)) { //Found the start of a slider or spinner. //We need to confirm this is actually a slider though. //bool firstMatchRewindFound = sliderCheckFrame != currentFrame && first; //Is the first check in this loop and rewinding has been utilised. //if (!firstMatchRewindFound) //Rewinding current slider not used. Use currentFrame. // sliderCheckFrame = currentFrame; sliderCheckFrame = currentFrame; int searchTime = replay[sliderCheckFrame].time; SliderOsu found = player.hitObjectManager.hitObjects.Find(ho => ho.StartTime == searchTime) as SliderOsu; if (found == null) { currentFrame++; continue; //No slider or spinner. } //At this point we know we have a slider. //Now we need to choose how to change the replay depending on the type of //penalty we are planning to deal. switch (type) { case IncreaseScoreType.Miss: //The slider should be TOTALLY missed. //We can't do this if we have already started on a slider that //has already had a non-miss... /*if (firstMatchRewindFound && found.sliderTicksMissed == 0) * { * currentFrame++; * continue; * }*/ while (curr.mouseLeft) { //No mouse control over the whole slider is required. curr.SetButtonStates(pButtonState.None); if (currentFrame == replayCount - 1) { return; } curr = replay[++currentFrame]; } return; case IncreaseScoreType.Hit50: case IncreaseScoreType.Hit100: case IncreaseScoreType.Hit300: { int possibleCount; if (type == IncreaseScoreType.Hit50) { //In case of a 50, we can miss a minimum of 50%. possibleCount = (found.sliderScoreTimingPoints.Count + 1) / 2 == (found.sliderScoreTimingPoints.Count + 1) / 2f ? (found.sliderScoreTimingPoints.Count + 1) / 2 : (found.sliderScoreTimingPoints.Count + 1) / 2 + 1; //possibleCount = found.sliderScoreTimingPoints.Count; } else if (type == IncreaseScoreType.Hit100) { //In case of a 100, we can miss a maximum of 50%. possibleCount = 1; //TOO LENIANT } else { return; } //If the slider is already partway through, we should remove any ticks which have already been consumed. possibleCount -= found.sliderTicksHit; if (possibleCount <= 0) { currentFrame++; continue; } bool firstTick = true; int firstTickEndTime = curr.time + Math.Min(found.sliderScoreTimingPoints[0], player.hitObjectManager.HitWindow50); while (curr.time <= found.EndTime && possibleCount > 0) { if (firstTick) { if (curr.time < firstTickEndTime) { curr.SetButtonStates(pButtonState.None); } else { firstTick = false; possibleCount--; } } if (!firstTick) { bool containsTimingPoint = found.sliderScoreTimingPoints.FindIndex( tp => curr.time <= tp && next.time > tp) >= 0; if (containsTimingPoint) { if (possibleCount > 0) { possibleCount--; curr.SetButtonStates(pButtonState.None); next.SetButtonStates(pButtonState.None); Debug.Print(" Consumed 1 tick (remaining to consume " + possibleCount + ")"); } else { return; } } } if (currentFrame == replayCount - 1) { break; } curr = replay[++currentFrame]; next = replay[currentFrame + 1]; } } Debug.Print(" Slider exhausted..."); return; } } currentFrame++; } }
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!"; }