From fe099be443b7ce7f2552cfa9958752197a43ebea Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 19 May 2021 22:25:56 +0800 Subject: [PATCH 01/63] Implemented osu target mod --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 367 ++++++++++++++++++++- 1 file changed, 366 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 2464308347..667fc91142 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -1,13 +1,34 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModTarget : Mod + public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, + IApplicableToHealthProcessor, IApplicableToDifficulty { public override string Name => "Target"; public override string Acronym => "TP"; @@ -15,5 +36,349 @@ namespace osu.Game.Rulesets.Osu.Mods public override IconUsage? Icon => OsuIcon.ModTarget; public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + // Sudden death + healthProcessor.FailConditions += (_, result) + => result.Type.AffectsCombo() + && !result.IsHit; + } + + // Maximum distance to jump + public const float MAX_DISTANCE = 250f; + + public override void ApplyToBeatmap(IBeatmap beatmap) + { + base.ApplyToBeatmap(beatmap); + + var osuBeatmap = (OsuBeatmap)beatmap; + var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); + + var hitObjects = new List(); + + // Only place circles between startTime and endTime + var startTime = origHitObjects.First().StartTime; + double endTime; + var endObj = origHitObjects.Last(); + switch (endObj) + { + case Slider slider: + endTime = slider.EndTime; + break; + case Spinner spinner: + endTime = spinner.EndTime; + break; + default: + endTime = endObj.StartTime; + break; + } + + var comboStarts = origHitObjects.Where(x => x.NewCombo).Select(x => x.StartTime).ToList(); + + TimingControlPoint currentTimingPoint = osuBeatmap.ControlPointInfo.TimingPointAt(startTime); + var currentTime = currentTimingPoint.Time; + int currentCombo = -1; + IList lastSamples = null; + + float direction = MathHelper.TwoPi * RNG.NextSingle(); + float distance = 40f; + + while (!Precision.AlmostBigger(currentTime, endTime)) + { + // Place a circle + + // Don't place any circles before the start of the first hit object of the map + // Don't place any circles from the start of break time to the start of the first hit object after the break + if (!Precision.AlmostBigger(startTime, currentTime) + && !osuBeatmap.Breaks.Any(x => Precision.AlmostBigger(currentTime, x.StartTime) + && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, x.EndTime)).StartTime, currentTime))) + { + var newCircle = new HitCircle(); + newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.StartTime = currentTime; + + // Determine circle position + if (hitObjects.Count == 0) + { + newCircle.Position = new Vector2(RNG.NextSingle(OsuPlayfield.BASE_SIZE.X), RNG.NextSingle(OsuPlayfield.BASE_SIZE.Y)); + } + else + { + var relativePos = new Vector2( + distance * (float)Math.Cos(direction), + distance * (float)Math.Sin(direction) + ); + relativePos = getRotatedVector(hitObjects.Last().Position, relativePos); + direction = (float)Math.Atan2(relativePos.Y, relativePos.X); + + var newPosition = Vector2.Add(hitObjects.Last().Position, relativePos); + + if (newPosition.Y < 0) + newPosition.Y = 0; + else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) + newPosition.Y = OsuPlayfield.BASE_SIZE.Y; + if (newPosition.X < 0) + newPosition.X = 0; + else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) + newPosition.X = OsuPlayfield.BASE_SIZE.X; + + newCircle.Position = newPosition; + // Add a random nudge to the direction + direction += distance / MAX_DISTANCE * (RNG.NextSingle() * MathHelper.TwoPi - MathHelper.Pi); + } + + // Determine samples to use + var samples = getSamplesAtTime(origHitObjects, currentTime); + if (samples == null) + { + if (lastSamples != null) + newCircle.Samples = lastSamples; + } + else + { + newCircle.Samples = samples; + lastSamples = samples; + } + + // Determine combo + if (comboStarts.Count > currentCombo + 1 && !Precision.AlmostBigger(comboStarts[currentCombo + 1], currentTime)) + { + currentCombo++; + newCircle.NewCombo = true; + newCircle.IndexInCurrentCombo = 0; + if (hitObjects.Count > 0) + hitObjects.Last().LastInCombo = true; + + // Increase distance for every combo + distance = Math.Min(distance * 1.05f, MAX_DISTANCE); + // Randomize direction as well + direction = MathHelper.TwoPi * RNG.NextSingle(); + } + else + { + if (hitObjects.Count > 0) + newCircle.IndexInCurrentCombo = hitObjects.Last().IndexInCurrentCombo + 1; + } + newCircle.ComboIndex = currentCombo; + + // Remove the last circle if it is too close in time to this one + if (hitObjects.Count > 0 + && Precision.AlmostBigger(currentTimingPoint.BeatLength / 2, newCircle.StartTime - hitObjects.Last().StartTime)) + { + hitObjects.RemoveAt(hitObjects.Count - 1); + } + + hitObjects.Add(newCircle); + } + + // Advance to the next beat and check for timing point changes + + currentTime += currentTimingPoint.BeatLength; + + var newTimingPoint = osuBeatmap.ControlPointInfo.TimingPointAt(currentTime); + if (newTimingPoint != currentTimingPoint) + { + currentTimingPoint = newTimingPoint; + currentTime = currentTimingPoint.Time; + } + } + + osuBeatmap.HitObjects = hitObjects; + } + + /// + /// Get samples (if any) for a specific point in time. + /// + /// + /// Samples will be returned if a hit circle or a slider node exists at that point of time. + /// + /// The list of hit objects in a beatmap, ordered by StartTime + /// The point in time to get samples for + /// Hit samples + private IList getSamplesAtTime(List hitObjects, double time) + { + var sampleObj = hitObjects.FirstOrDefault(x => + { + if (Precision.AlmostEquals(time, x.StartTime)) return true; + if (x is Slider slider + && Precision.AlmostBigger(time, slider.StartTime) + && Precision.AlmostBigger(slider.EndTime, time)) + { + if (Precision.AlmostEquals((time - slider.StartTime) % slider.SpanDuration, 0)) + { + return true; + } + } + return false; + }); + if (sampleObj == null) return null; + + IList samples = null; + if (sampleObj is Slider slider) + { + samples = slider.NodeSamples[(int)Math.Round((time - slider.StartTime) % slider.SpanDuration)]; + } + else + { + samples = sampleObj.Samples; + } + return samples; + } + + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + } + + protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state) + { + if (drawable is DrawableSpinner) + return; + + var h = (OsuHitObject)drawable.HitObject; + + // apply grow and fade effect + if (drawable is DrawableHitCircle circle) + { + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + { + drawable.ScaleTo(0.4f) + .Then().ScaleTo(1.6f, h.TimePreempt * 2); + drawable.FadeTo(0.5f) + .Then().Delay(h.TimeFadeIn).FadeTo(1f); + + // remove approach circles + circle.ApproachCircle.Hide(); + } + } + } + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + // Decrease AR to increase preempt time + difficulty.ApproachRate *= 0.5f; + } + + // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. + // The closer the hit objects draw to the border, the sharper the turn + private const byte border_distance_x = 192; + private const byte border_distance_y = 144; + + /// + /// Determines the position of the current hit object relative to the previous one. + /// + /// The position of the current hit object relative to the previous one + private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) + { + var relativeRotationDistance = 0f; + var playfieldMiddle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); + + if (prevPosChanged.X < playfieldMiddle.X) + { + relativeRotationDistance = Math.Max( + (border_distance_x - prevPosChanged.X) / border_distance_x, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x, + relativeRotationDistance + ); + } + + if (prevPosChanged.Y < playfieldMiddle.Y) + { + relativeRotationDistance = Math.Max( + (border_distance_y - prevPosChanged.Y) / border_distance_y, + relativeRotationDistance + ); + } + else + { + relativeRotationDistance = Math.Max( + (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y, + relativeRotationDistance + ); + } + + return rotateVectorTowardsVector( + posRelativeToPrev, + Vector2.Subtract(playfieldMiddle, prevPosChanged), + relativeRotationDistance / 2 + ); + } + + /// + /// Rotates vector "initial" towards vector "destinantion" + /// + /// Vector to rotate to "destination" + /// Vector "initial" should be rotated to + /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors. + /// Resulting vector + private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) + { + var initialAngleRad = Math.Atan2(initial.Y, initial.X); + var destAngleRad = Math.Atan2(destination.Y, destination.X); + + var diff = destAngleRad - initialAngleRad; + + while (diff < -Math.PI) + { + diff += 2 * Math.PI; + } + + while (diff > Math.PI) + { + diff -= 2 * Math.PI; + } + + var finalAngleRad = initialAngleRad + relativeDistance * diff; + + return new Vector2( + initial.Length * (float)Math.Cos(finalAngleRad), + initial.Length * (float)Math.Sin(finalAngleRad) + ); + } + + // Background metronome + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Overlays.Add(new TargetBeatContainer()); + } + + public class TargetBeatContainer : BeatSyncedContainer + { + private PausableSkinnableSound sample; + + public TargetBeatContainer() + { + Divisor = 1; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")), // TODO: use another sample? + }; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!IsBeatSyncedWithTrack) return; + + sample?.Play(); + } + } } } From d20b5c2d5aa9865bb302fe17b4a541f237b436a1 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 20 May 2021 11:57:13 +0800 Subject: [PATCH 02/63] Refactored ApplyToBeatmap --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 202 +++++++++++---------- 1 file changed, 105 insertions(+), 97 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 667fc91142..412b7049ea 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -55,8 +55,6 @@ namespace osu.Game.Rulesets.Osu.Mods var osuBeatmap = (OsuBeatmap)beatmap; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); - var hitObjects = new List(); - // Only place circles between startTime and endTime var startTime = origHitObjects.First().StartTime; double endTime; @@ -74,113 +72,122 @@ namespace osu.Game.Rulesets.Osu.Mods break; } - var comboStarts = origHitObjects.Where(x => x.NewCombo).Select(x => x.StartTime).ToList(); - - TimingControlPoint currentTimingPoint = osuBeatmap.ControlPointInfo.TimingPointAt(startTime); - var currentTime = currentTimingPoint.Time; - int currentCombo = -1; - IList lastSamples = null; - - float direction = MathHelper.TwoPi * RNG.NextSingle(); - float distance = 40f; - - while (!Precision.AlmostBigger(currentTime, endTime)) - { - // Place a circle - - // Don't place any circles before the start of the first hit object of the map - // Don't place any circles from the start of break time to the start of the first hit object after the break - if (!Precision.AlmostBigger(startTime, currentTime) - && !osuBeatmap.Breaks.Any(x => Precision.AlmostBigger(currentTime, x.StartTime) - && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, x.EndTime)).StartTime, currentTime))) + // Generate the beats + var beats = osuBeatmap.ControlPointInfo.TimingPoints + .Where(x => Precision.AlmostBigger(endTime, x.Time)) + .SelectMany(tp => + { + var tpBeats = new List(); + var currentTime = tp.Time; + while (Precision.AlmostBigger(endTime, currentTime) && osuBeatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) + { + tpBeats.Add(currentTime); + currentTime += tp.BeatLength; + } + return tpBeats; + }) + // Remove beats that are before startTime + .Where(x => Precision.AlmostBigger(x, startTime)) + // Remove beats during breaks + .Where(x => !osuBeatmap.Breaks.Any(b => + Precision.AlmostBigger(x, b.StartTime) + && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) + )) + .ToList(); + // Generate a hit circle for each beat + var hitObjects = beats + // Remove beats that are too close to the next one (e.g. due to timing point changes) + .Where((x, idx) => + { + if (idx == beats.Count - 1) return true; + if (Precision.AlmostBigger(osuBeatmap.ControlPointInfo.TimingPointAt(x).BeatLength / 2, beats[idx + 1] - x)) + return false; + return true; + }) + .Select(x => { var newCircle = new HitCircle(); newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); - newCircle.StartTime = currentTime; + newCircle.StartTime = x; + return (OsuHitObject)newCircle; + }).ToList(); - // Determine circle position - if (hitObjects.Count == 0) - { - newCircle.Position = new Vector2(RNG.NextSingle(OsuPlayfield.BASE_SIZE.X), RNG.NextSingle(OsuPlayfield.BASE_SIZE.Y)); - } - else - { - var relativePos = new Vector2( + // Add hit samples to the circles + for (int i = 0; i < hitObjects.Count; i++) + { + var x = hitObjects[i]; + var samples = getSamplesAtTime(origHitObjects, x.StartTime); + if (samples == null) + { + if (i > 0) + x.Samples = hitObjects[i - 1].Samples; + } + else + { + x.Samples = samples; + } + } + + // Process combo numbers + // First follow the combo indices in the original beatmap + hitObjects.ForEach(x => + { + var origObj = origHitObjects.FindLast(y => Precision.AlmostBigger(x.StartTime, y.StartTime)); + if (origObj == null) x.ComboIndex = 0; + else x.ComboIndex = origObj.ComboIndex; + }); + // Then reprocess them to ensure continuity in the combo indices and add indices in current combo + var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList(); + for (int i = 0; i < combos.Count; i++) + { + var group = combos[i].ToList(); + group.First().NewCombo = true; + group.Last().LastInCombo = true; + + for (int j = 0; j < group.Count; j++) + { + var x = group[j]; + x.ComboIndex = i; + x.IndexInCurrentCombo = j; + } + } + + // Position all hit circles + var direction = MathHelper.TwoPi * RNG.NextSingle(); + for (int i = 0; i < hitObjects.Count; i++) + { + var x = hitObjects[i]; + if (i == 0) + { + x.Position = new Vector2(RNG.NextSingle(OsuPlayfield.BASE_SIZE.X), RNG.NextSingle(OsuPlayfield.BASE_SIZE.Y)); + } + else + { + var distance = Math.Min(MAX_DISTANCE, 40f * (float)Math.Pow(1.05, x.ComboIndex)); + var relativePos = new Vector2( distance * (float)Math.Cos(direction), distance * (float)Math.Sin(direction) ); - relativePos = getRotatedVector(hitObjects.Last().Position, relativePos); - direction = (float)Math.Atan2(relativePos.Y, relativePos.X); + relativePos = getRotatedVector(hitObjects[i - 1].Position, relativePos); + direction = (float)Math.Atan2(relativePos.Y, relativePos.X); - var newPosition = Vector2.Add(hitObjects.Last().Position, relativePos); + var newPosition = Vector2.Add(hitObjects[i - 1].Position, relativePos); - if (newPosition.Y < 0) - newPosition.Y = 0; - else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) - newPosition.Y = OsuPlayfield.BASE_SIZE.Y; - if (newPosition.X < 0) - newPosition.X = 0; - else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) - newPosition.X = OsuPlayfield.BASE_SIZE.X; + if (newPosition.Y < 0) + newPosition.Y = 0; + else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) + newPosition.Y = OsuPlayfield.BASE_SIZE.Y; + if (newPosition.X < 0) + newPosition.X = 0; + else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) + newPosition.X = OsuPlayfield.BASE_SIZE.X; - newCircle.Position = newPosition; - // Add a random nudge to the direction - direction += distance / MAX_DISTANCE * (RNG.NextSingle() * MathHelper.TwoPi - MathHelper.Pi); - } + x.Position = newPosition; - // Determine samples to use - var samples = getSamplesAtTime(origHitObjects, currentTime); - if (samples == null) - { - if (lastSamples != null) - newCircle.Samples = lastSamples; - } - else - { - newCircle.Samples = samples; - lastSamples = samples; - } - - // Determine combo - if (comboStarts.Count > currentCombo + 1 && !Precision.AlmostBigger(comboStarts[currentCombo + 1], currentTime)) - { - currentCombo++; - newCircle.NewCombo = true; - newCircle.IndexInCurrentCombo = 0; - if (hitObjects.Count > 0) - hitObjects.Last().LastInCombo = true; - - // Increase distance for every combo - distance = Math.Min(distance * 1.05f, MAX_DISTANCE); - // Randomize direction as well + if (x.LastInCombo) direction = MathHelper.TwoPi * RNG.NextSingle(); - } else - { - if (hitObjects.Count > 0) - newCircle.IndexInCurrentCombo = hitObjects.Last().IndexInCurrentCombo + 1; - } - newCircle.ComboIndex = currentCombo; - - // Remove the last circle if it is too close in time to this one - if (hitObjects.Count > 0 - && Precision.AlmostBigger(currentTimingPoint.BeatLength / 2, newCircle.StartTime - hitObjects.Last().StartTime)) - { - hitObjects.RemoveAt(hitObjects.Count - 1); - } - - hitObjects.Add(newCircle); - } - - // Advance to the next beat and check for timing point changes - - currentTime += currentTimingPoint.BeatLength; - - var newTimingPoint = osuBeatmap.ControlPointInfo.TimingPointAt(currentTime); - if (newTimingPoint != currentTimingPoint) - { - currentTimingPoint = newTimingPoint; - currentTime = currentTimingPoint.Time; + direction += distance / MAX_DISTANCE * (RNG.NextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } @@ -242,6 +249,7 @@ namespace osu.Game.Rulesets.Osu.Mods { using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) { + // todo: this doesn't feel quite right yet drawable.ScaleTo(0.4f) .Then().ScaleTo(1.6f, h.TimePreempt * 2); drawable.FadeTo(0.5f) @@ -367,7 +375,7 @@ namespace osu.Game.Rulesets.Osu.Mods { InternalChildren = new Drawable[] { - sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")), // TODO: use another sample? + sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")), // todo: use another sample? }; } From 15e7cce264ed3c31cf03a29a7cadd241abbf030f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 22 May 2021 14:14:41 +0800 Subject: [PATCH 03/63] Added seed setting, fixed mod ignoring IncreaseFirstObjectVisibility --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 116 +++++++++++++++++++-- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 412b7049ea..3fe0161742 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -6,15 +6,23 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -37,6 +45,13 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; + [SettingSource("Seed", "Seed for random circle placement", SettingControlType = typeof(OsuModTargetSettingsControl))] + public Bindable Seed { get; } = new Bindable + { + Default = null, + Value = null + }; + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { // Sudden death @@ -50,7 +65,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToBeatmap(IBeatmap beatmap) { - base.ApplyToBeatmap(beatmap); + Seed.Value ??= RNG.Next(); + + var rng = new Random(Seed.Value.GetValueOrDefault()); + + float nextSingle(float max = 1f) + { + return (float)(rng.NextDouble() * max); + } var osuBeatmap = (OsuBeatmap)beatmap; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); @@ -153,13 +175,13 @@ namespace osu.Game.Rulesets.Osu.Mods } // Position all hit circles - var direction = MathHelper.TwoPi * RNG.NextSingle(); + var direction = MathHelper.TwoPi * nextSingle(); for (int i = 0; i < hitObjects.Count; i++) { var x = hitObjects[i]; if (i == 0) { - x.Position = new Vector2(RNG.NextSingle(OsuPlayfield.BASE_SIZE.X), RNG.NextSingle(OsuPlayfield.BASE_SIZE.Y)); + x.Position = new Vector2(nextSingle(OsuPlayfield.BASE_SIZE.X), nextSingle(OsuPlayfield.BASE_SIZE.Y)); } else { @@ -185,13 +207,15 @@ namespace osu.Game.Rulesets.Osu.Mods x.Position = newPosition; if (x.LastInCombo) - direction = MathHelper.TwoPi * RNG.NextSingle(); + direction = MathHelper.TwoPi * nextSingle(); else - direction += distance / MAX_DISTANCE * (RNG.NextSingle() * MathHelper.TwoPi - MathHelper.Pi); + direction += distance / MAX_DISTANCE * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } osuBeatmap.HitObjects = hitObjects; + + base.ApplyToBeatmap(beatmap); } /// @@ -233,7 +257,7 @@ namespace osu.Game.Rulesets.Osu.Mods return samples; } - protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) + protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state) { } @@ -389,4 +413,84 @@ namespace osu.Game.Rulesets.Osu.Mods } } } + + public class OsuModTargetSettingsControl : SettingsItem + { + protected override Drawable CreateControl() => new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set + { + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 120) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true, + }, + new TriangleButton + { + Width = 120, + Text = "Randomise", + Action = () => current.Value = RNG.Next(), + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + }, + } + } + } + }; + + seedNumberBox.Current.BindValueChanged(e => + { + if (int.TryParse(e.NewValue, out var intVal)) + current.Value = intVal; + else + current.Value = null; + }); + current.BindValueChanged(e => + { + seedNumberBox.Text = e.NewValue.ToString(); + }); + } + } + } } From 7815b3c72bed5856e52803d646097fdffc56e668 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Jun 2021 10:58:42 +0800 Subject: [PATCH 04/63] Display results after fail --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 ++ osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 8 +++++++- osu.Game/Rulesets/Mods/IApplicableFailOverride.cs | 5 +++++ osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 ++ osu.Game/Rulesets/Mods/ModBlockFail.cs | 1 + osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs | 1 + osu.Game/Rulesets/Mods/ModFailCondition.cs | 1 + osu.Game/Screens/Play/Player.cs | 10 ++++++++-- 8 files changed, 27 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index aac830801b..2b92174b29 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Osu.Mods public bool RestartOnFail => false; + public bool DisplayResultsOnFail => false; + private OsuInputManager inputManager; private IFrameStableClock gameplayClock; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 3fe0161742..c3e9e60420 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -36,7 +36,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, - IApplicableToHealthProcessor, IApplicableToDifficulty + IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride { public override string Name => "Target"; public override string Acronym => "TP"; @@ -52,6 +52,12 @@ namespace osu.Game.Rulesets.Osu.Mods Value = null }; + public bool PerformFail() => true; + + public bool RestartOnFail => false; + + public bool DisplayResultsOnFail => true; + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { // Sudden death diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs index 8c99d739cb..b7d306e57b 100644 --- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -18,5 +18,10 @@ namespace osu.Game.Rulesets.Mods /// Whether we want to restart on fail. Only used if returns true. /// bool RestartOnFail { get; } + + /// + /// Whether to proceed to results screen on fail. Only used if returns true and is false. + /// + bool DisplayResultsOnFail { get; } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index d6e1d46b06..3fb92527f2 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -35,6 +35,8 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; + public bool DisplayResultsOnFail => false; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs index 1fde5abad4..973a192378 100644 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ b/osu.Game/Rulesets/Mods/ModBlockFail.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods public bool PerformFail() => false; public bool RestartOnFail => false; + public bool DisplayResultsOnFail => false; public void ReadFromConfig(OsuConfigManager config) { diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index 2ac0f59d84..91508b7e70 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Mods } public bool RestartOnFail => false; + public bool DisplayResultsOnFail => false; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index c0d7bae2b2..012a0f73ff 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mods public virtual bool PerformFail() => true; public virtual bool RestartOnFail => true; + public virtual bool DisplayResultsOnFail => false; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 890883f0d2..b72524ad99 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -604,7 +604,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - if (!ScoreProcessor.HasCompleted.Value) + if (!ScoreProcessor.HasCompleted.Value && !HealthProcessor.HasFailed) { completionProgressDelegate?.Cancel(); completionProgressDelegate = null; @@ -617,7 +617,7 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once"); // Only show the completion screen if the player hasn't failed - if (HealthProcessor.HasFailed) + if (HealthProcessor.HasFailed && !Mods.Value.OfType().Any(m => m.DisplayResultsOnFail)) return; ValidForResume = false; @@ -712,6 +712,12 @@ namespace osu.Game.Screens.Play // Called back when the transform finishes private void onFailComplete() { + if (Mods.Value.OfType().Any(m => m.DisplayResultsOnFail)) + { + updateCompletionState(true); + return; + } + GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; From 12a17d0983c6f3557ef7c8231fca247f8311181f Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 10 Jun 2021 15:31:20 +0800 Subject: [PATCH 05/63] Extract seed setting contorl to IHasSeed --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 90 +------------------- osu.Game/Rulesets/Mods/IHasSeed.cs | 95 ++++++++++++++++++++++ osu.Game/Rulesets/Mods/ModRandom.cs | 81 +----------------- 3 files changed, 99 insertions(+), 167 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IHasSeed.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index c3e9e60420..04a7365148 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -8,9 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -18,11 +16,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -36,7 +30,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, - IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride + IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed { public override string Name => "Target"; public override string Acronym => "TP"; @@ -45,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; - [SettingSource("Seed", "Seed for random circle placement", SettingControlType = typeof(OsuModTargetSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] public Bindable Seed { get; } = new Bindable { Default = null, @@ -419,84 +413,4 @@ namespace osu.Game.Rulesets.Osu.Mods } } } - - public class OsuModTargetSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current.Current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly OsuNumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 120) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new OsuNumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true, - }, - new TriangleButton - { - Width = 120, - Text = "Randomise", - Action = () => current.Value = RNG.Next(), - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - }, - } - } - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - if (int.TryParse(e.NewValue, out var intVal)) - current.Value = intVal; - else - current.Value = null; - }); - current.BindValueChanged(e => - { - seedNumberBox.Text = e.NewValue.ToString(); - }); - } - } - } } diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs new file mode 100644 index 0000000000..45a806e80c --- /dev/null +++ b/osu.Game/Rulesets/Mods/IHasSeed.cs @@ -0,0 +1,95 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Rulesets.Mods +{ + public interface IHasSeed + { + public Bindable Seed { get; } + } + + public class SeedSettingsControl : SettingsItem + { + protected override Drawable CreateControl() => new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + + private sealed class SeedControl : CompositeDrawable, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current; + set + { + current.Current = value; + seedNumberBox.Text = value.Value.ToString(); + } + } + + private readonly OsuNumberBox seedNumberBox; + + public SeedControl() + { + AutoSizeAxes = Axes.Y; + + InternalChildren = new[] + { + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + seedNumberBox = new OsuNumberBox + { + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true + } + } + } + } + }; + + seedNumberBox.Current.BindValueChanged(e => + { + int? value = null; + + if (int.TryParse(e.NewValue, out var intVal)) + value = intVal; + + current.Value = value; + }); + } + + protected override void Update() + { + if (current.Value == null) + seedNumberBox.Text = current.Current.Value.ToString(); + } + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index 3f14263420..eec66161ff 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -13,7 +13,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { - public abstract class ModRandom : Mod + public abstract class ModRandom : Mod, IHasSeed { public override string Name => "Random"; public override string Acronym => "RD"; @@ -21,88 +21,11 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.Dice; public override double ScoreMultiplier => 1; - [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] public Bindable Seed { get; } = new Bindable { Default = null, Value = null }; - - private class ModRandomSettingsControl : SettingsItem - { - protected override Drawable CreateControl() => new SeedControl - { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; - - private sealed class SeedControl : CompositeDrawable, IHasCurrentValue - { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current - { - get => current; - set - { - current.Current = value; - seedNumberBox.Text = value.Value.ToString(); - } - } - - private readonly OsuNumberBox seedNumberBox; - - public SeedControl() - { - AutoSizeAxes = Axes.Y; - - InternalChildren = new[] - { - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - seedNumberBox = new OsuNumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } - } - } - } - }; - - seedNumberBox.Current.BindValueChanged(e => - { - int? value = null; - - if (int.TryParse(e.NewValue, out var intVal)) - value = intVal; - - current.Value = value; - }); - } - - protected override void Update() - { - if (current.Value == null) - seedNumberBox.Text = current.Current.Value.ToString(); - } - } - } } } From 04c0db6dceb94b147c5dd8dad095ed0d97e39adc Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 14 Jun 2021 21:34:34 +0800 Subject: [PATCH 06/63] Code cleanup --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 161 ++++++++++----------- osu.Game/Rulesets/Mods/IHasSeed.cs | 54 +++---- osu.Game/Rulesets/Mods/ModRandom.cs | 5 - 3 files changed, 105 insertions(+), 115 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 04a7365148..0d123a4ce3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -30,7 +30,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, - IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed + IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed { public override string Name => "Target"; public override string Acronym => "TP"; @@ -57,11 +57,11 @@ namespace osu.Game.Rulesets.Osu.Mods // Sudden death healthProcessor.FailConditions += (_, result) => result.Type.AffectsCombo() - && !result.IsHit; + && !result.IsHit; } // Maximum distance to jump - public const float MAX_DISTANCE = 250f; + private const float max_distance = 250f; public override void ApplyToBeatmap(IBeatmap beatmap) { @@ -69,76 +69,68 @@ namespace osu.Game.Rulesets.Osu.Mods var rng = new Random(Seed.Value.GetValueOrDefault()); - float nextSingle(float max = 1f) - { - return (float)(rng.NextDouble() * max); - } + float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); var osuBeatmap = (OsuBeatmap)beatmap; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); // Only place circles between startTime and endTime var startTime = origHitObjects.First().StartTime; - double endTime; var endObj = origHitObjects.Last(); - switch (endObj) + var endTime = endObj switch { - case Slider slider: - endTime = slider.EndTime; - break; - case Spinner spinner: - endTime = spinner.EndTime; - break; - default: - endTime = endObj.StartTime; - break; - } + Slider slider => slider.EndTime, + Spinner spinner => spinner.EndTime, + _ => endObj.StartTime + }; // Generate the beats var beats = osuBeatmap.ControlPointInfo.TimingPoints - .Where(x => Precision.AlmostBigger(endTime, x.Time)) - .SelectMany(tp => - { - var tpBeats = new List(); - var currentTime = tp.Time; - while (Precision.AlmostBigger(endTime, currentTime) && osuBeatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) - { - tpBeats.Add(currentTime); - currentTime += tp.BeatLength; - } - return tpBeats; - }) - // Remove beats that are before startTime - .Where(x => Precision.AlmostBigger(x, startTime)) - // Remove beats during breaks - .Where(x => !osuBeatmap.Breaks.Any(b => - Precision.AlmostBigger(x, b.StartTime) - && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) - )) - .ToList(); + .Where(x => Precision.AlmostBigger(endTime, x.Time)) + .SelectMany(tp => + { + var tpBeats = new List(); + var currentTime = tp.Time; + + while (Precision.AlmostBigger(endTime, currentTime) && osuBeatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) + { + tpBeats.Add(currentTime); + currentTime += tp.BeatLength; + } + + return tpBeats; + }) + // Remove beats that are before startTime + .Where(x => Precision.AlmostBigger(x, startTime)) + // Remove beats during breaks + .Where(x => !osuBeatmap.Breaks.Any(b => + Precision.AlmostBigger(x, b.StartTime) + && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) + )) + .ToList(); // Generate a hit circle for each beat var hitObjects = beats - // Remove beats that are too close to the next one (e.g. due to timing point changes) - .Where((x, idx) => - { - if (idx == beats.Count - 1) return true; - if (Precision.AlmostBigger(osuBeatmap.ControlPointInfo.TimingPointAt(x).BeatLength / 2, beats[idx + 1] - x)) - return false; - return true; - }) - .Select(x => - { - var newCircle = new HitCircle(); - newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); - newCircle.StartTime = x; - return (OsuHitObject)newCircle; - }).ToList(); + // Remove beats that are too close to the next one (e.g. due to timing point changes) + .Where((x, idx) => + { + if (idx == beats.Count - 1) return true; + + return !Precision.AlmostBigger(osuBeatmap.ControlPointInfo.TimingPointAt(x).BeatLength / 2, beats[idx + 1] - x); + }) + .Select(x => + { + var newCircle = new HitCircle(); + newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.StartTime = x; + return (OsuHitObject)newCircle; + }).ToList(); // Add hit samples to the circles for (int i = 0; i < hitObjects.Count; i++) { var x = hitObjects[i]; var samples = getSamplesAtTime(origHitObjects, x.StartTime); + if (samples == null) { if (i > 0) @@ -155,11 +147,11 @@ namespace osu.Game.Rulesets.Osu.Mods hitObjects.ForEach(x => { var origObj = origHitObjects.FindLast(y => Precision.AlmostBigger(x.StartTime, y.StartTime)); - if (origObj == null) x.ComboIndex = 0; - else x.ComboIndex = origObj.ComboIndex; + x.ComboIndex = origObj?.ComboIndex ?? 0; }); // Then reprocess them to ensure continuity in the combo indices and add indices in current combo var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList(); + for (int i = 0; i < combos.Count; i++) { var group = combos[i].ToList(); @@ -176,20 +168,22 @@ namespace osu.Game.Rulesets.Osu.Mods // Position all hit circles var direction = MathHelper.TwoPi * nextSingle(); + for (int i = 0; i < hitObjects.Count; i++) { var x = hitObjects[i]; + if (i == 0) { x.Position = new Vector2(nextSingle(OsuPlayfield.BASE_SIZE.X), nextSingle(OsuPlayfield.BASE_SIZE.Y)); } else { - var distance = Math.Min(MAX_DISTANCE, 40f * (float)Math.Pow(1.05, x.ComboIndex)); + var distance = Math.Min(max_distance, 40f * (float)Math.Pow(1.05, x.ComboIndex)); var relativePos = new Vector2( - distance * (float)Math.Cos(direction), - distance * (float)Math.Sin(direction) - ); + distance * (float)Math.Cos(direction), + distance * (float)Math.Sin(direction) + ); relativePos = getRotatedVector(hitObjects[i - 1].Position, relativePos); direction = (float)Math.Atan2(relativePos.Y, relativePos.X); @@ -209,7 +203,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (x.LastInCombo) direction = MathHelper.TwoPi * nextSingle(); else - direction += distance / MAX_DISTANCE * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); + direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } @@ -227,25 +221,24 @@ namespace osu.Game.Rulesets.Osu.Mods /// The list of hit objects in a beatmap, ordered by StartTime /// The point in time to get samples for /// Hit samples - private IList getSamplesAtTime(List hitObjects, double time) + private IList getSamplesAtTime(IEnumerable hitObjects, double time) { var sampleObj = hitObjects.FirstOrDefault(x => { if (Precision.AlmostEquals(time, x.StartTime)) return true; - if (x is Slider slider - && Precision.AlmostBigger(time, slider.StartTime) - && Precision.AlmostBigger(slider.EndTime, time)) - { - if (Precision.AlmostEquals((time - slider.StartTime) % slider.SpanDuration, 0)) - { - return true; - } - } - return false; + + if (!(x is Slider s)) + return false; + if (!Precision.AlmostBigger(time, s.StartTime) + || !Precision.AlmostBigger(s.EndTime, time)) + return false; + + return Precision.AlmostEquals((time - s.StartTime) % s.SpanDuration, 0); }); if (sampleObj == null) return null; - IList samples = null; + IList samples; + if (sampleObj is Slider slider) { samples = slider.NodeSamples[(int)Math.Round((time - slider.StartTime) % slider.SpanDuration)]; @@ -254,6 +247,7 @@ namespace osu.Game.Rulesets.Osu.Mods { samples = sampleObj.Samples; } + return samples; } @@ -269,19 +263,18 @@ namespace osu.Game.Rulesets.Osu.Mods var h = (OsuHitObject)drawable.HitObject; // apply grow and fade effect - if (drawable is DrawableHitCircle circle) + if (!(drawable is DrawableHitCircle circle)) return; + + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) { - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) - { - // todo: this doesn't feel quite right yet - drawable.ScaleTo(0.4f) + // todo: this doesn't feel quite right yet + drawable.ScaleTo(0.4f) .Then().ScaleTo(1.6f, h.TimePreempt * 2); - drawable.FadeTo(0.5f) + drawable.FadeTo(0.5f) .Then().Delay(h.TimeFadeIn).FadeTo(1f); - // remove approach circles - circle.ApproachCircle.Hide(); - } + // remove approach circles + circle.ApproachCircle.Hide(); } } @@ -347,7 +340,7 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Rotates vector "initial" towards vector "destinantion" + /// Rotates vector "initial" towards vector "destination" /// /// Vector to rotate to "destination" /// Vector "initial" should be rotated to @@ -399,7 +392,7 @@ namespace osu.Game.Rulesets.Osu.Mods { InternalChildren = new Drawable[] { - sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")), // todo: use another sample? + sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")) // todo: use another sample? }; } diff --git a/osu.Game/Rulesets/Mods/IHasSeed.cs b/osu.Game/Rulesets/Mods/IHasSeed.cs index 45a806e80c..13d59a9855 100644 --- a/osu.Game/Rulesets/Mods/IHasSeed.cs +++ b/osu.Game/Rulesets/Mods/IHasSeed.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -18,11 +17,14 @@ namespace osu.Game.Rulesets.Mods public class SeedSettingsControl : SettingsItem { - protected override Drawable CreateControl() => new SeedControl + protected override Drawable CreateControl() { - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 5 } - }; + return new SeedControl + { + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 5 } + }; + } private sealed class SeedControl : CompositeDrawable, IHasCurrentValue { @@ -46,33 +48,33 @@ namespace osu.Game.Rulesets.Mods InternalChildren = new[] { - new GridContainer + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] + new Dimension(), + new Dimension(GridSizeMode.Absolute, 2), + new Dimension(GridSizeMode.Relative, 0.25f) + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { - new Dimension(), - new Dimension(GridSizeMode.Absolute, 2), - new Dimension(GridSizeMode.Relative, 0.25f) - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + seedNumberBox = new OsuNumberBox { - seedNumberBox = new OsuNumberBox - { - RelativeSizeAxes = Axes.X, - CommitOnFocusLost = true - } + RelativeSizeAxes = Axes.X, + CommitOnFocusLost = true } } } - }; + } + }; seedNumberBox.Current.BindValueChanged(e => { diff --git a/osu.Game/Rulesets/Mods/ModRandom.cs b/osu.Game/Rulesets/Mods/ModRandom.cs index eec66161ff..61297c162d 100644 --- a/osu.Game/Rulesets/Mods/ModRandom.cs +++ b/osu.Game/Rulesets/Mods/ModRandom.cs @@ -2,14 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.UserInterface; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.Settings; namespace osu.Game.Rulesets.Mods { From 4ffff06dcbc0ac52d49d70643c5bd00d881d8c1c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 15 Jun 2021 11:06:56 +0800 Subject: [PATCH 07/63] Break ApplyToBeatmap into subroutines --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 116 ++++++++++++--------- 1 file changed, 67 insertions(+), 49 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 0d123a4ce3..3bb4b0419f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -67,14 +67,31 @@ namespace osu.Game.Rulesets.Osu.Mods { Seed.Value ??= RNG.Next(); - var rng = new Random(Seed.Value.GetValueOrDefault()); - - float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); - var osuBeatmap = (OsuBeatmap)beatmap; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); - // Only place circles between startTime and endTime + var hitObjects = generateBeats(osuBeatmap, origHitObjects) + .Select(x => + { + var newCircle = new HitCircle(); + newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.StartTime = x; + return (OsuHitObject)newCircle; + }).ToList(); + + addHitSamples(hitObjects, origHitObjects); + + fixComboInfo(hitObjects, origHitObjects); + + randomizeCirclePos(hitObjects); + + osuBeatmap.HitObjects = hitObjects; + + base.ApplyToBeatmap(beatmap); + } + + private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection origHitObjects) + { var startTime = origHitObjects.First().StartTime; var endObj = origHitObjects.Last(); var endTime = endObj switch @@ -84,48 +101,45 @@ namespace osu.Game.Rulesets.Osu.Mods _ => endObj.StartTime }; - // Generate the beats - var beats = osuBeatmap.ControlPointInfo.TimingPoints - .Where(x => Precision.AlmostBigger(endTime, x.Time)) - .SelectMany(tp => - { - var tpBeats = new List(); - var currentTime = tp.Time; + var beats = beatmap.ControlPointInfo.TimingPoints + .Where(x => Precision.AlmostBigger(endTime, x.Time)) + .SelectMany(tp => + { + var tpBeats = new List(); + var currentTime = tp.Time; - while (Precision.AlmostBigger(endTime, currentTime) && osuBeatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) - { - tpBeats.Add(currentTime); - currentTime += tp.BeatLength; - } + while (Precision.AlmostBigger(endTime, currentTime) && beatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) + { + tpBeats.Add(currentTime); + currentTime += tp.BeatLength; + } - return tpBeats; - }) - // Remove beats that are before startTime - .Where(x => Precision.AlmostBigger(x, startTime)) - // Remove beats during breaks - .Where(x => !osuBeatmap.Breaks.Any(b => - Precision.AlmostBigger(x, b.StartTime) - && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) - )) - .ToList(); - // Generate a hit circle for each beat - var hitObjects = beats - // Remove beats that are too close to the next one (e.g. due to timing point changes) - .Where((x, idx) => - { - if (idx == beats.Count - 1) return true; + return tpBeats; + }) + .Where(x => Precision.AlmostBigger(x, startTime)) + // Remove beats during breaks + .Where(x => !beatmap.Breaks.Any(b => + Precision.AlmostBigger(x, b.StartTime) + && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) + )) + .ToList(); - return !Precision.AlmostBigger(osuBeatmap.ControlPointInfo.TimingPointAt(x).BeatLength / 2, beats[idx + 1] - x); - }) - .Select(x => - { - var newCircle = new HitCircle(); - newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); - newCircle.StartTime = x; - return (OsuHitObject)newCircle; - }).ToList(); + // Remove beats that are too close to the next one (e.g. due to timing point changes) + for (int i = beats.Count - 2; i >= 0; i--) + { + var beat = beats[i]; - // Add hit samples to the circles + if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) + { + beats.RemoveAt(i); + } + } + + return beats; + } + + private void addHitSamples(IReadOnlyList hitObjects, IReadOnlyCollection origHitObjects) + { for (int i = 0; i < hitObjects.Count; i++) { var x = hitObjects[i]; @@ -141,8 +155,10 @@ namespace osu.Game.Rulesets.Osu.Mods x.Samples = samples; } } + } - // Process combo numbers + private void fixComboInfo(List hitObjects, List origHitObjects) + { // First follow the combo indices in the original beatmap hitObjects.ForEach(x => { @@ -165,8 +181,14 @@ namespace osu.Game.Rulesets.Osu.Mods x.IndexInCurrentCombo = j; } } + } + + private void randomizeCirclePos(IReadOnlyList hitObjects) + { + var rng = new Random(Seed.Value.GetValueOrDefault()); + + float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); - // Position all hit circles var direction = MathHelper.TwoPi * nextSingle(); for (int i = 0; i < hitObjects.Count; i++) @@ -206,10 +228,6 @@ namespace osu.Game.Rulesets.Osu.Mods direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } - - osuBeatmap.HitObjects = hitObjects; - - base.ApplyToBeatmap(beatmap); } /// From 14622f473442cdf43a9539f669903fd4f9ba3f57 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 17 Jun 2021 10:20:50 +0800 Subject: [PATCH 08/63] Improved guesstimations; fixed hit samples --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 3bb4b0419f..b748cdf4a2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Mods while (Precision.AlmostBigger(endTime, currentTime) && beatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) { - tpBeats.Add(currentTime); + tpBeats.Add(Math.Floor(currentTime)); currentTime += tp.BeatLength; } @@ -148,7 +148,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (samples == null) { if (i > 0) - x.Samples = hitObjects[i - 1].Samples; + x.Samples = hitObjects[i - 1].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); } else { @@ -289,7 +289,7 @@ namespace osu.Game.Rulesets.Osu.Mods drawable.ScaleTo(0.4f) .Then().ScaleTo(1.6f, h.TimePreempt * 2); drawable.FadeTo(0.5f) - .Then().Delay(h.TimeFadeIn).FadeTo(1f); + .Then().Delay(h.TimePreempt * 2 / 3).FadeTo(1f); // remove approach circles circle.ApproachCircle.Hide(); @@ -304,6 +304,7 @@ namespace osu.Game.Rulesets.Osu.Mods { // Decrease AR to increase preempt time difficulty.ApproachRate *= 0.5f; + difficulty.CircleSize *= 0.75f; } // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. From f22beaeb5b4e63decc9fa0e0837920e6d496fbc2 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 17 Jun 2021 14:30:59 +0800 Subject: [PATCH 09/63] Increase distance between combos; pull circles closer to center --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b748cdf4a2..e91b2cd9d7 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -193,15 +193,17 @@ namespace osu.Game.Rulesets.Osu.Mods for (int i = 0; i < hitObjects.Count; i++) { - var x = hitObjects[i]; + var obj = hitObjects[i]; if (i == 0) { - x.Position = new Vector2(nextSingle(OsuPlayfield.BASE_SIZE.X), nextSingle(OsuPlayfield.BASE_SIZE.Y)); + obj.Position = new Vector2(nextSingle(OsuPlayfield.BASE_SIZE.X), nextSingle(OsuPlayfield.BASE_SIZE.Y)); } else { - var distance = Math.Min(max_distance, 40f * (float)Math.Pow(1.05, x.ComboIndex)); + var distance = 40f * (float)Math.Pow(1.05, obj.ComboIndex); + if (obj.NewCombo) distance *= 1.5f; + distance = Math.Min(max_distance, distance); var relativePos = new Vector2( distance * (float)Math.Cos(direction), distance * (float)Math.Sin(direction) @@ -220,9 +222,9 @@ namespace osu.Game.Rulesets.Osu.Mods else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) newPosition.X = OsuPlayfield.BASE_SIZE.X; - x.Position = newPosition; + obj.Position = newPosition; - if (x.LastInCombo) + if (obj.LastInCombo) direction = MathHelper.TwoPi * nextSingle(); else direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); @@ -354,7 +356,7 @@ namespace osu.Game.Rulesets.Osu.Mods return rotateVectorTowardsVector( posRelativeToPrev, Vector2.Subtract(playfieldMiddle, prevPosChanged), - relativeRotationDistance / 2 + relativeRotationDistance * 0.75f ); } From b7f43405fc3d9175db86cf165ced58a1a97ec057 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 17 Jun 2021 22:01:58 +0800 Subject: [PATCH 10/63] Dim circles instead of fade; improved hit samples; changed jump distance to be closer to cuttingedge --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 268 +++++++++++---------- 1 file changed, 138 insertions(+), 130 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index e91b2cd9d7..c9e43704e1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -26,6 +26,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -46,12 +47,23 @@ namespace osu.Game.Rulesets.Osu.Mods Value = null }; - public bool PerformFail() => true; - public bool RestartOnFail => false; public bool DisplayResultsOnFail => true; + // Maximum distance to jump + private const float max_distance = 250f; + + // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. + // The closer the hit objects draw to the border, the sharper the turn + private const byte border_distance_x = 192; + private const byte border_distance_y = 144; + + public bool PerformFail() + { + return true; + } + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { // Sudden death @@ -60,9 +72,6 @@ namespace osu.Game.Rulesets.Osu.Mods && !result.IsHit; } - // Maximum distance to jump - private const float max_distance = 250f; - public override void ApplyToBeatmap(IBeatmap beatmap) { Seed.Value ??= RNG.Next(); @@ -90,6 +99,55 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToBeatmap(beatmap); } + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + // Decrease AR to increase preempt time + difficulty.ApproachRate *= 0.5f; + difficulty.CircleSize *= 0.75f; + } + + // Background metronome + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Overlays.Add(new TargetBeatContainer()); + } + + protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state) + { + } + + protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state) + { + if (!(drawable is DrawableHitCircle circle)) return; + + var h = (OsuHitObject)drawable.HitObject; + + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + { + drawable.ScaleTo(0.5f) + .Then().ScaleTo(1f, h.TimePreempt); + + var colour = drawable.Colour; + + var avgColour = colour.AverageColour.Linear; + drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) + .Then().Delay(h.TimeFadeIn).FadeColour(colour); + + // remove approach circles + circle.ApproachCircle.Hide(); + } + } + + private static float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) + { + return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; + } + private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection origHitObjects) { var startTime = origHitObjects.First().StartTime; @@ -125,35 +183,43 @@ namespace osu.Game.Rulesets.Osu.Mods .ToList(); // Remove beats that are too close to the next one (e.g. due to timing point changes) - for (int i = beats.Count - 2; i >= 0; i--) + for (var i = beats.Count - 2; i >= 0; i--) { var beat = beats[i]; - if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) - { - beats.RemoveAt(i); - } + if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) beats.RemoveAt(i); } return beats; } - private void addHitSamples(IReadOnlyList hitObjects, IReadOnlyCollection origHitObjects) + private void addHitSamples(IEnumerable hitObjects, IReadOnlyList origHitObjects) { - for (int i = 0; i < hitObjects.Count; i++) + var lastSampleIdx = 0; + + foreach (var x in hitObjects) { - var x = hitObjects[i]; var samples = getSamplesAtTime(origHitObjects, x.StartTime); if (samples == null) { - if (i > 0) - x.Samples = hitObjects[i - 1].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); + while (lastSampleIdx < origHitObjects.Count && origHitObjects[lastSampleIdx].StartTime <= x.StartTime) + lastSampleIdx++; + lastSampleIdx--; + + if (lastSampleIdx < 0 && lastSampleIdx >= origHitObjects.Count) continue; + + if (lastSampleIdx < origHitObjects.Count - 1) + { + // get samples from the next hit object if it is closer in time + if (origHitObjects[lastSampleIdx + 1].StartTime - x.StartTime < x.StartTime - origHitObjects[lastSampleIdx].StartTime) + lastSampleIdx++; + } + + x.Samples = origHitObjects[lastSampleIdx].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); } else - { x.Samples = samples; - } } } @@ -168,13 +234,13 @@ namespace osu.Game.Rulesets.Osu.Mods // Then reprocess them to ensure continuity in the combo indices and add indices in current combo var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList(); - for (int i = 0; i < combos.Count; i++) + for (var i = 0; i < combos.Count; i++) { var group = combos[i].ToList(); group.First().NewCombo = true; group.Last().LastInCombo = true; - for (int j = 0; j < group.Count; j++) + for (var j = 0; j < group.Count; j++) { var x = group[j]; x.ComboIndex = i; @@ -190,53 +256,52 @@ namespace osu.Game.Rulesets.Osu.Mods float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); var direction = MathHelper.TwoPi * nextSingle(); + var maxComboIndex = hitObjects.Last().ComboIndex; - for (int i = 0; i < hitObjects.Count; i++) + for (var i = 0; i < hitObjects.Count; i++) { var obj = hitObjects[i]; + var lastPos = i == 0 + ? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2) + : hitObjects[i - 1].Position; - if (i == 0) - { - obj.Position = new Vector2(nextSingle(OsuPlayfield.BASE_SIZE.X), nextSingle(OsuPlayfield.BASE_SIZE.Y)); - } + var distance = map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, 333f); + if (obj.NewCombo) distance *= 1.5f; + if (obj.Kiai) distance *= 1.2f; + distance = Math.Min(max_distance, distance); + + var relativePos = new Vector2( + distance * (float)Math.Cos(direction), + distance * (float)Math.Sin(direction) + ); + relativePos = getRotatedVector(lastPos, relativePos); + direction = (float)Math.Atan2(relativePos.Y, relativePos.X); + + var newPosition = Vector2.Add(lastPos, relativePos); + + if (newPosition.Y < 0) + newPosition.Y = 0; + else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) + newPosition.Y = OsuPlayfield.BASE_SIZE.Y; + if (newPosition.X < 0) + newPosition.X = 0; + else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) + newPosition.X = OsuPlayfield.BASE_SIZE.X; + + obj.Position = newPosition; + + if (obj.LastInCombo) + direction = MathHelper.TwoPi * nextSingle(); else - { - var distance = 40f * (float)Math.Pow(1.05, obj.ComboIndex); - if (obj.NewCombo) distance *= 1.5f; - distance = Math.Min(max_distance, distance); - var relativePos = new Vector2( - distance * (float)Math.Cos(direction), - distance * (float)Math.Sin(direction) - ); - relativePos = getRotatedVector(hitObjects[i - 1].Position, relativePos); - direction = (float)Math.Atan2(relativePos.Y, relativePos.X); - - var newPosition = Vector2.Add(hitObjects[i - 1].Position, relativePos); - - if (newPosition.Y < 0) - newPosition.Y = 0; - else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) - newPosition.Y = OsuPlayfield.BASE_SIZE.Y; - if (newPosition.X < 0) - newPosition.X = 0; - else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) - newPosition.X = OsuPlayfield.BASE_SIZE.X; - - obj.Position = newPosition; - - if (obj.LastInCombo) - direction = MathHelper.TwoPi * nextSingle(); - else - direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); - } + direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } /// - /// Get samples (if any) for a specific point in time. + /// Get samples (if any) for a specific point in time. /// /// - /// Samples will be returned if a hit circle or a slider node exists at that point of time. + /// Samples will be returned if a hit circle or a slider node exists at that point of time. /// /// The list of hit objects in a beatmap, ordered by StartTime /// The point in time to get samples for @@ -260,62 +325,15 @@ namespace osu.Game.Rulesets.Osu.Mods IList samples; if (sampleObj is Slider slider) - { samples = slider.NodeSamples[(int)Math.Round((time - slider.StartTime) % slider.SpanDuration)]; - } else - { samples = sampleObj.Samples; - } return samples; } - protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state) - { - } - - protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state) - { - if (drawable is DrawableSpinner) - return; - - var h = (OsuHitObject)drawable.HitObject; - - // apply grow and fade effect - if (!(drawable is DrawableHitCircle circle)) return; - - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) - { - // todo: this doesn't feel quite right yet - drawable.ScaleTo(0.4f) - .Then().ScaleTo(1.6f, h.TimePreempt * 2); - drawable.FadeTo(0.5f) - .Then().Delay(h.TimePreempt * 2 / 3).FadeTo(1f); - - // remove approach circles - circle.ApproachCircle.Hide(); - } - } - - public void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - // Decrease AR to increase preempt time - difficulty.ApproachRate *= 0.5f; - difficulty.CircleSize *= 0.75f; - } - - // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. - // The closer the hit objects draw to the border, the sharper the turn - private const byte border_distance_x = 192; - private const byte border_distance_y = 144; - /// - /// Determines the position of the current hit object relative to the previous one. + /// Determines the position of the current hit object relative to the previous one. /// /// The position of the current hit object relative to the previous one private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) @@ -356,16 +374,19 @@ namespace osu.Game.Rulesets.Osu.Mods return rotateVectorTowardsVector( posRelativeToPrev, Vector2.Subtract(playfieldMiddle, prevPosChanged), - relativeRotationDistance * 0.75f + Math.Min(1, relativeRotationDistance * 0.75f) ); } /// - /// Rotates vector "initial" towards vector "destination" + /// Rotates vector "initial" towards vector "destination" /// /// Vector to rotate to "destination" /// Vector "initial" should be rotated to - /// The angle the vector should be rotated relative to the difference between the angles of the the two vectors. + /// + /// The angle the vector should be rotated relative to the difference between the angles of + /// the the two vectors. + /// /// Resulting vector private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) { @@ -374,15 +395,9 @@ namespace osu.Game.Rulesets.Osu.Mods var diff = destAngleRad - initialAngleRad; - while (diff < -Math.PI) - { - diff += 2 * Math.PI; - } + while (diff < -Math.PI) diff += 2 * Math.PI; - while (diff > Math.PI) - { - diff -= 2 * Math.PI; - } + while (diff > Math.PI) diff -= 2 * Math.PI; var finalAngleRad = initialAngleRad + relativeDistance * diff; @@ -392,13 +407,6 @@ namespace osu.Game.Rulesets.Osu.Mods ); } - // Background metronome - - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - drawableRuleset.Overlays.Add(new TargetBeatContainer()); - } - public class TargetBeatContainer : BeatSyncedContainer { private PausableSkinnableSound sample; @@ -408,15 +416,6 @@ namespace osu.Game.Rulesets.Osu.Mods Divisor = 1; } - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")) // todo: use another sample? - }; - } - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); @@ -425,6 +424,15 @@ namespace osu.Game.Rulesets.Osu.Mods sample?.Play(); } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")) // todo: use another sample + }; + } } } } From 639e8b62b94c37cadb3d9bf84da159efa20a9e36 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 18 Jun 2021 11:20:04 +0800 Subject: [PATCH 11/63] Make circles light up 1 beat length before start time --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index c9e43704e1..db6d483dee 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -59,6 +59,8 @@ namespace osu.Game.Rulesets.Osu.Mods private const byte border_distance_x = 192; private const byte border_distance_y = 144; + private ControlPointInfo controlPointInfo; + public bool PerformFail() { return true; @@ -77,13 +79,14 @@ namespace osu.Game.Rulesets.Osu.Mods Seed.Value ??= RNG.Next(); var osuBeatmap = (OsuBeatmap)beatmap; + controlPointInfo = osuBeatmap.ControlPointInfo; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); var hitObjects = generateBeats(osuBeatmap, origHitObjects) .Select(x => { var newCircle = new HitCircle(); - newCircle.ApplyDefaults(osuBeatmap.ControlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); + newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); newCircle.StartTime = x; return (OsuHitObject)newCircle; }).ToList(); @@ -136,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Mods var avgColour = colour.AverageColour.Linear; drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) - .Then().Delay(h.TimeFadeIn).FadeColour(colour); + .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength).FadeColour(colour); // remove approach circles circle.ApproachCircle.Hide(); From dca2d8af4f7c2027f4f874237cc6aadf809cc16c Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 18 Jun 2021 13:18:44 +0800 Subject: [PATCH 12/63] Animate circles undimming --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index db6d483dee..b7463103df 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Mods var avgColour = colour.AverageColour.Linear; drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) - .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength).FadeColour(colour); + .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - 96).FadeColour(colour, 96); // remove approach circles circle.ApproachCircle.Hide(); From f5134c7fc279d804067ecd8d30b291f654994a02 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 18 Jun 2021 14:39:46 +0800 Subject: [PATCH 13/63] Extract constants and add xmldoc --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 25 ++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b7463103df..63fda42393 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -51,14 +51,26 @@ namespace osu.Game.Rulesets.Osu.Mods public bool DisplayResultsOnFail => true; - // Maximum distance to jump - private const float max_distance = 250f; + /// + /// Jump distance for circles in the last combo + /// + private const float max_base_distance = 250f; + + /// + /// The maximum allowed jump distance after multipliers are applied + /// + private const float distance_cap = 350f; // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn private const byte border_distance_x = 192; private const byte border_distance_y = 144; + /// + /// Duration of the undimming animation + /// + private const double undim_duration = 96; + private ControlPointInfo controlPointInfo; public bool PerformFail() @@ -139,7 +151,8 @@ namespace osu.Game.Rulesets.Osu.Mods var avgColour = colour.AverageColour.Linear; drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) - .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - 96).FadeColour(colour, 96); + .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) + .FadeColour(colour, undim_duration); // remove approach circles circle.ApproachCircle.Hide(); @@ -268,10 +281,10 @@ namespace osu.Game.Rulesets.Osu.Mods ? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2) : hitObjects[i - 1].Position; - var distance = map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, 333f); + var distance = map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance); if (obj.NewCombo) distance *= 1.5f; if (obj.Kiai) distance *= 1.2f; - distance = Math.Min(max_distance, distance); + distance = Math.Min(distance_cap, distance); var relativePos = new Vector2( distance * (float)Math.Cos(direction), @@ -296,7 +309,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (obj.LastInCombo) direction = MathHelper.TwoPi * nextSingle(); else - direction += distance / max_distance * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); + direction += distance / distance_cap * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); } } From cca26d465110153254eeac9e2fbd078a4f771e5e Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Fri, 18 Jun 2021 16:05:09 +0800 Subject: [PATCH 14/63] Take circle radius into account when clamping hit objects to playfield --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 43 +++++++++++++--------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 63fda42393..de7309c27f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -52,12 +52,12 @@ namespace osu.Game.Rulesets.Osu.Mods public bool DisplayResultsOnFail => true; /// - /// Jump distance for circles in the last combo + /// Jump distance for circles in the last combo /// private const float max_base_distance = 250f; /// - /// The maximum allowed jump distance after multipliers are applied + /// The maximum allowed jump distance after multipliers are applied /// private const float distance_cap = 350f; @@ -67,7 +67,12 @@ namespace osu.Game.Rulesets.Osu.Mods private const byte border_distance_y = 144; /// - /// Duration of the undimming animation + /// The extent of rotation towards playfield centre when a circle is near the edge + /// + private const float edge_rotation_multiplier = 0.75f; + + /// + /// Duration of the undimming animation /// private const double undim_duration = 96; @@ -295,14 +300,16 @@ namespace osu.Game.Rulesets.Osu.Mods var newPosition = Vector2.Add(lastPos, relativePos); - if (newPosition.Y < 0) - newPosition.Y = 0; - else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y) - newPosition.Y = OsuPlayfield.BASE_SIZE.Y; - if (newPosition.X < 0) - newPosition.X = 0; - else if (newPosition.X > OsuPlayfield.BASE_SIZE.X) - newPosition.X = OsuPlayfield.BASE_SIZE.X; + var radius = (float)obj.Radius; + + if (newPosition.Y < radius) + newPosition.Y = radius; + else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y - radius) + newPosition.Y = OsuPlayfield.BASE_SIZE.Y - radius; + if (newPosition.X < radius) + newPosition.X = radius; + else if (newPosition.X > OsuPlayfield.BASE_SIZE.X - radius) + newPosition.X = OsuPlayfield.BASE_SIZE.X - radius; obj.Position = newPosition; @@ -314,10 +321,10 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Get samples (if any) for a specific point in time. + /// Get samples (if any) for a specific point in time. /// /// - /// Samples will be returned if a hit circle or a slider node exists at that point of time. + /// Samples will be returned if a hit circle or a slider node exists at that point of time. /// /// The list of hit objects in a beatmap, ordered by StartTime /// The point in time to get samples for @@ -349,7 +356,7 @@ namespace osu.Game.Rulesets.Osu.Mods } /// - /// Determines the position of the current hit object relative to the previous one. + /// Determines the position of the current hit object relative to the previous one. /// /// The position of the current hit object relative to the previous one private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) @@ -390,18 +397,18 @@ namespace osu.Game.Rulesets.Osu.Mods return rotateVectorTowardsVector( posRelativeToPrev, Vector2.Subtract(playfieldMiddle, prevPosChanged), - Math.Min(1, relativeRotationDistance * 0.75f) + Math.Min(1, relativeRotationDistance * edge_rotation_multiplier) ); } /// - /// Rotates vector "initial" towards vector "destination" + /// Rotates vector "initial" towards vector "destination" /// /// Vector to rotate to "destination" /// Vector "initial" should be rotated to /// - /// The angle the vector should be rotated relative to the difference between the angles of - /// the the two vectors. + /// The angle the vector should be rotated relative to the difference between the angles of + /// the the two vectors. /// /// Resulting vector private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) From ca8f08ca84894258478ff86a283aa8729986c433 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 19 Jun 2021 10:04:48 +0800 Subject: [PATCH 15/63] Avoid overlapping with recent circles --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 62 +++++++++++++++------- 1 file changed, 43 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index de7309c27f..4e4e798f47 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -54,12 +54,12 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Jump distance for circles in the last combo /// - private const float max_base_distance = 250f; + private const float max_base_distance = 333f; /// /// The maximum allowed jump distance after multipliers are applied /// - private const float distance_cap = 350f; + private const float distance_cap = 380f; // The distances from the hit objects to the borders of the playfield they start to "turn around" and curve towards the middle. // The closer the hit objects draw to the border, the sharper the turn @@ -71,6 +71,11 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const float edge_rotation_multiplier = 0.75f; + /// + /// Number of recent circles to check for overlap + /// + private const int overlap_check_count = 5; + /// /// Duration of the undimming animation /// @@ -291,27 +296,39 @@ namespace osu.Game.Rulesets.Osu.Mods if (obj.Kiai) distance *= 1.2f; distance = Math.Min(distance_cap, distance); - var relativePos = new Vector2( - distance * (float)Math.Cos(direction), - distance * (float)Math.Sin(direction) - ); - relativePos = getRotatedVector(lastPos, relativePos); - direction = (float)Math.Atan2(relativePos.Y, relativePos.X); + // Attempt to place the circle at a place that does not overlap with previous ones - var newPosition = Vector2.Add(lastPos, relativePos); + var tryCount = 0; - var radius = (float)obj.Radius; + do + { + if (tryCount > 0) direction = MathHelper.TwoPi * nextSingle(); - if (newPosition.Y < radius) - newPosition.Y = radius; - else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y - radius) - newPosition.Y = OsuPlayfield.BASE_SIZE.Y - radius; - if (newPosition.X < radius) - newPosition.X = radius; - else if (newPosition.X > OsuPlayfield.BASE_SIZE.X - radius) - newPosition.X = OsuPlayfield.BASE_SIZE.X - radius; + var relativePos = new Vector2( + distance * (float)Math.Cos(direction), + distance * (float)Math.Sin(direction) + ); + relativePos = getRotatedVector(lastPos, relativePos); + direction = (float)Math.Atan2(relativePos.Y, relativePos.X); - obj.Position = newPosition; + var newPosition = Vector2.Add(lastPos, relativePos); + + var radius = (float)obj.Radius; + + if (newPosition.Y < radius) + newPosition.Y = radius; + else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y - radius) + newPosition.Y = OsuPlayfield.BASE_SIZE.Y - radius; + if (newPosition.X < radius) + newPosition.X = radius; + else if (newPosition.X > OsuPlayfield.BASE_SIZE.X - radius) + newPosition.X = OsuPlayfield.BASE_SIZE.X - radius; + + obj.Position = newPosition; + + tryCount++; + if (tryCount % 10 == 0) distance *= 0.9f; + } while (distance >= obj.Radius * 2 && isOverlappingWithRecent(hitObjects, i)); if (obj.LastInCombo) direction = MathHelper.TwoPi * nextSingle(); @@ -355,6 +372,13 @@ namespace osu.Game.Rulesets.Osu.Mods return samples; } + private bool isOverlappingWithRecent(IReadOnlyList hitObjects, int idx) + { + var target = hitObjects[idx]; + return hitObjects.SkipLast(hitObjects.Count - idx).TakeLast(overlap_check_count) + .Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2); + } + /// /// Determines the position of the current hit object relative to the previous one. /// From 0cf3119006f8a0a9ec8d4513e646ca4705320bcf Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 19 Jun 2021 11:12:29 +0800 Subject: [PATCH 16/63] Guard against edge cases --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 4e4e798f47..0e7dc7a9d3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -101,6 +101,9 @@ namespace osu.Game.Rulesets.Osu.Mods Seed.Value ??= RNG.Next(); var osuBeatmap = (OsuBeatmap)beatmap; + + if (osuBeatmap.HitObjects.Count == 0) return; + controlPointInfo = osuBeatmap.ControlPointInfo; var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); @@ -277,6 +280,8 @@ namespace osu.Game.Rulesets.Osu.Mods private void randomizeCirclePos(IReadOnlyList hitObjects) { + if (hitObjects.Count == 0) return; + var rng = new Random(Seed.Value.GetValueOrDefault()); float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); @@ -291,7 +296,9 @@ namespace osu.Game.Rulesets.Osu.Mods ? Vector2.Divide(OsuPlayfield.BASE_SIZE, 2) : hitObjects[i - 1].Position; - var distance = map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance); + var distance = maxComboIndex == 0 + ? (float)obj.Radius + : map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance); if (obj.NewCombo) distance *= 1.5f; if (obj.Kiai) distance *= 1.2f; distance = Math.Min(distance_cap, distance); From b09165a074c11661f656836f946c6a5ddd1f13a0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 19 Jun 2021 11:13:19 +0800 Subject: [PATCH 17/63] Remove the circle size buff --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 0e7dc7a9d3..9688bc7154 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -135,7 +135,6 @@ namespace osu.Game.Rulesets.Osu.Mods { // Decrease AR to increase preempt time difficulty.ApproachRate *= 0.5f; - difficulty.CircleSize *= 0.75f; } // Background metronome From c86794058442186d219391fdcf05d396c8651775 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 19 Jun 2021 12:26:16 +0800 Subject: [PATCH 18/63] Marked target mod and traceable mod as incompatible; extracted playfield clamping logic Nothing is visible when target mod and traceable mod are enabled together. --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 33 ++++++++++++------- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 2 +- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 9688bc7154..a6476541ab 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; + public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable) }; + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] public Bindable Seed { get; } = new Bindable { @@ -319,19 +321,10 @@ namespace osu.Game.Rulesets.Osu.Mods var newPosition = Vector2.Add(lastPos, relativePos); - var radius = (float)obj.Radius; - - if (newPosition.Y < radius) - newPosition.Y = radius; - else if (newPosition.Y > OsuPlayfield.BASE_SIZE.Y - radius) - newPosition.Y = OsuPlayfield.BASE_SIZE.Y - radius; - if (newPosition.X < radius) - newPosition.X = radius; - else if (newPosition.X > OsuPlayfield.BASE_SIZE.X - radius) - newPosition.X = OsuPlayfield.BASE_SIZE.X - radius; - obj.Position = newPosition; + clampToPlayfield(obj); + tryCount++; if (tryCount % 10 == 0) distance *= 0.9f; } while (distance >= obj.Radius * 2 && isOverlappingWithRecent(hitObjects, i)); @@ -460,6 +453,24 @@ namespace osu.Game.Rulesets.Osu.Mods ); } + private void clampToPlayfield(OsuHitObject obj) + { + var position = obj.Position; + var radius = (float)obj.Radius; + + if (position.Y < radius) + position.Y = radius; + else if (position.Y > OsuPlayfield.BASE_SIZE.Y - radius) + position.Y = OsuPlayfield.BASE_SIZE.Y - radius; + + if (position.X < radius) + position.X = radius; + else if (position.X > OsuPlayfield.BASE_SIZE.X - radius) + position.X = OsuPlayfield.BASE_SIZE.X - radius; + + obj.Position = position; + } + public class TargetBeatContainer : BeatSyncedContainer { private PausableSkinnableSound sample; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 4b0939db16..c5b0a2da3e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Put your faith in the approach circles..."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween), typeof(OsuModTarget) }; protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { From e52a58c1bc5f17079ee3fe7fd85b10b2953ff3c0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 21 Jun 2021 16:24:37 +0800 Subject: [PATCH 19/63] Switched to a more reasonable sample sound for now --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index a6476541ab..75089fe237 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -494,7 +494,7 @@ namespace osu.Game.Rulesets.Osu.Mods { InternalChildren = new Drawable[] { - sample = new PausableSkinnableSound(new SampleInfo("spinnerbonus")) // todo: use another sample + sample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")) // todo: use another sample }; } } From 5a031eada861095340b1d9c7ae428b9318f240a7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 23 Jun 2021 16:22:10 +0800 Subject: [PATCH 20/63] Revert "Display results after fail" This commit reverts 7815b3c7 --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 -- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 2 -- osu.Game/Rulesets/Mods/IApplicableFailOverride.cs | 5 ----- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 -- osu.Game/Rulesets/Mods/ModBlockFail.cs | 1 - osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs | 1 - osu.Game/Rulesets/Mods/ModFailCondition.cs | 1 - osu.Game/Screens/Play/Player.cs | 10 ++-------- 8 files changed, 2 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 2b92174b29..aac830801b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -28,8 +28,6 @@ namespace osu.Game.Rulesets.Osu.Mods public bool RestartOnFail => false; - public bool DisplayResultsOnFail => false; - private OsuInputManager inputManager; private IFrameStableClock gameplayClock; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 75089fe237..bf31839991 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -51,8 +51,6 @@ namespace osu.Game.Rulesets.Osu.Mods public bool RestartOnFail => false; - public bool DisplayResultsOnFail => true; - /// /// Jump distance for circles in the last combo /// diff --git a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs index b7d306e57b..8c99d739cb 100644 --- a/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs +++ b/osu.Game/Rulesets/Mods/IApplicableFailOverride.cs @@ -18,10 +18,5 @@ namespace osu.Game.Rulesets.Mods /// Whether we want to restart on fail. Only used if returns true. /// bool RestartOnFail { get; } - - /// - /// Whether to proceed to results screen on fail. Only used if returns true and is false. - /// - bool DisplayResultsOnFail { get; } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index e5652a8609..b84b5671e1 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public bool DisplayResultsOnFail => false; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModBlockFail.cs b/osu.Game/Rulesets/Mods/ModBlockFail.cs index 973a192378..1fde5abad4 100644 --- a/osu.Game/Rulesets/Mods/ModBlockFail.cs +++ b/osu.Game/Rulesets/Mods/ModBlockFail.cs @@ -17,7 +17,6 @@ namespace osu.Game.Rulesets.Mods public bool PerformFail() => false; public bool RestartOnFail => false; - public bool DisplayResultsOnFail => false; public void ReadFromConfig(OsuConfigManager config) { diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs index 91508b7e70..2ac0f59d84 100644 --- a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs @@ -41,7 +41,6 @@ namespace osu.Game.Rulesets.Mods } public bool RestartOnFail => false; - public bool DisplayResultsOnFail => false; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 012a0f73ff..c0d7bae2b2 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -14,7 +14,6 @@ namespace osu.Game.Rulesets.Mods public virtual bool PerformFail() => true; public virtual bool RestartOnFail => true; - public virtual bool DisplayResultsOnFail => false; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4726d54ebc..f9036780aa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -640,7 +640,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; - if (!ScoreProcessor.HasCompleted.Value && !HealthProcessor.HasFailed) + if (!ScoreProcessor.HasCompleted.Value) { completionProgressDelegate?.Cancel(); completionProgressDelegate = null; @@ -653,7 +653,7 @@ namespace osu.Game.Screens.Play throw new InvalidOperationException($"{nameof(updateCompletionState)} was fired more than once"); // Only show the completion screen if the player hasn't failed - if (HealthProcessor.HasFailed && !Mods.Value.OfType().Any(m => m.DisplayResultsOnFail)) + if (HealthProcessor.HasFailed) return; ValidForResume = false; @@ -751,12 +751,6 @@ namespace osu.Game.Screens.Play // Called back when the transform finishes private void onFailComplete() { - if (Mods.Value.OfType().Any(m => m.DisplayResultsOnFail)) - { - updateCompletionState(true); - return; - } - GameplayClockContainer.Stop(); FailOverlay.Retries = RestartCount; From 6dc5f406b27818051aa6423b8f7e1187414408df Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 23 Jun 2021 16:29:36 +0800 Subject: [PATCH 21/63] Implement IMutateApproachCircles --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index bf31839991..559ad31751 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -31,7 +31,8 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, - IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, IHasSeed + IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, + IHasSeed, IMutateApproachCircles { public override string Name => "Target"; public override string Acronym => "TP"; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModTraceable) }; + public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] public Bindable Seed { get; } = new Bindable From b7dd26612d2ca78fe09a38980e3f1c0b9c472cd7 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Wed, 23 Jun 2021 16:50:05 +0800 Subject: [PATCH 22/63] Reordered things and added regions --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 185 ++++++++++++--------- 1 file changed, 106 insertions(+), 79 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 559ad31751..6057b134e3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods Value = null }; - public bool RestartOnFail => false; + #region Constants /// /// Jump distance for circles in the last combo @@ -82,12 +82,19 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const double undim_duration = 96; + #endregion + + #region Private Fields + private ControlPointInfo controlPointInfo; - public bool PerformFail() - { - return true; - } + #endregion + + #region Sudden Death (IApplicableFailOverride) + + public bool PerformFail() => true; + + public bool RestartOnFail => false; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { @@ -97,6 +104,55 @@ namespace osu.Game.Rulesets.Osu.Mods && !result.IsHit; } + #endregion + + #region Reduce AR (IApplicableToDifficulty) + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + // Decrease AR to increase preempt time + difficulty.ApproachRate *= 0.5f; + } + + #endregion + + #region Circle Transforms (ModWithVisibilityAdjustment) + + protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state) + { + } + + protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state) + { + if (!(drawable is DrawableHitCircle circle)) return; + + var h = (OsuHitObject)drawable.HitObject; + + using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + { + drawable.ScaleTo(0.5f) + .Then().ScaleTo(1f, h.TimePreempt); + + var colour = drawable.Colour; + + var avgColour = colour.AverageColour.Linear; + drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) + .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) + .FadeColour(colour, undim_duration); + + // remove approach circles + circle.ApproachCircle.Hide(); + } + } + + #endregion + + #region Beatmap Generation (IApplicableToBeatmap) + public override void ApplyToBeatmap(IBeatmap beatmap) { Seed.Value ??= RNG.Next(); @@ -128,55 +184,6 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToBeatmap(beatmap); } - public void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - // Decrease AR to increase preempt time - difficulty.ApproachRate *= 0.5f; - } - - // Background metronome - - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - drawableRuleset.Overlays.Add(new TargetBeatContainer()); - } - - protected override void ApplyIncreasedVisibilityState(DrawableHitObject drawable, ArmedState state) - { - } - - protected override void ApplyNormalVisibilityState(DrawableHitObject drawable, ArmedState state) - { - if (!(drawable is DrawableHitCircle circle)) return; - - var h = (OsuHitObject)drawable.HitObject; - - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) - { - drawable.ScaleTo(0.5f) - .Then().ScaleTo(1f, h.TimePreempt); - - var colour = drawable.Colour; - - var avgColour = colour.AverageColour.Linear; - drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) - .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) - .FadeColour(colour, undim_duration); - - // remove approach circles - circle.ApproachCircle.Hide(); - } - } - - private static float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) - { - return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; - } - private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection origHitObjects) { var startTime = origHitObjects.First().StartTime; @@ -335,6 +342,47 @@ namespace osu.Game.Rulesets.Osu.Mods } } + #endregion + + #region Metronome (IApplicableToDrawableRuleset) + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Overlays.Add(new TargetBeatContainer()); + } + + public class TargetBeatContainer : BeatSyncedContainer + { + private PausableSkinnableSound sample; + + public TargetBeatContainer() + { + Divisor = 1; + } + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + if (!IsBeatSyncedWithTrack) return; + + sample?.Play(); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + sample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")) // todo: use another sample + }; + } + } + + #endregion + + #region Helper Subroutines + /// /// Get samples (if any) for a specific point in time. /// @@ -470,32 +518,11 @@ namespace osu.Game.Rulesets.Osu.Mods obj.Position = position; } - public class TargetBeatContainer : BeatSyncedContainer + private static float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) { - private PausableSkinnableSound sample; - - public TargetBeatContainer() - { - Divisor = 1; - } - - protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) - { - base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); - - if (!IsBeatSyncedWithTrack) return; - - sample?.Play(); - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - sample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")) // todo: use another sample - }; - } + return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; } + + #endregion } } From a7ea7b8b0be060f50ba4e20044aa220e79229037 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 09:34:39 +0800 Subject: [PATCH 23/63] Use `GetEndTime()` instead of a switch expression --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 6057b134e3..834876b455 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -17,6 +17,7 @@ using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -188,12 +189,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var startTime = origHitObjects.First().StartTime; var endObj = origHitObjects.Last(); - var endTime = endObj switch - { - Slider slider => slider.EndTime, - Spinner spinner => spinner.EndTime, - _ => endObj.StartTime - }; + var endTime = endObj.GetEndTime(); var beats = beatmap.ControlPointInfo.TimingPoints .Where(x => Precision.AlmostBigger(endTime, x.Time)) From dae7b8025da3a867e449fe3025954dbacac449b3 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 09:51:45 +0800 Subject: [PATCH 24/63] Converted an inline lambda into a method (`getBeatsForTimingPoint`) --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 30 ++++++++++++---------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 834876b455..130489d4c8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -193,19 +193,7 @@ namespace osu.Game.Rulesets.Osu.Mods var beats = beatmap.ControlPointInfo.TimingPoints .Where(x => Precision.AlmostBigger(endTime, x.Time)) - .SelectMany(tp => - { - var tpBeats = new List(); - var currentTime = tp.Time; - - while (Precision.AlmostBigger(endTime, currentTime) && beatmap.ControlPointInfo.TimingPointAt(currentTime) == tp) - { - tpBeats.Add(Math.Floor(currentTime)); - currentTime += tp.BeatLength; - } - - return tpBeats; - }) + .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) .Where(x => Precision.AlmostBigger(x, startTime)) // Remove beats during breaks .Where(x => !beatmap.Breaks.Any(b => @@ -379,6 +367,22 @@ namespace osu.Game.Rulesets.Osu.Mods #region Helper Subroutines + private IEnumerable getBeatsForTimingPoint(TimingControlPoint timingPoint, double mapEndTime) + { + var beats = new List(); + int i = 0; + var currentTime = timingPoint.Time; + + while (Precision.AlmostBigger(mapEndTime, currentTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) + { + beats.Add(Math.Floor(currentTime)); + i++; + currentTime = timingPoint.Time + i * timingPoint.BeatLength; + } + + return beats; + } + /// /// Get samples (if any) for a specific point in time. /// From 3eab540bcca713c73d73f052a4e0f6656b78f1fb Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 10:07:29 +0800 Subject: [PATCH 25/63] Converted an inline lambda into a method (`isInsideBreakPeriod`); moved `origHitObjects` to be a private class field --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 47 +++++++++++++++------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 130489d4c8..7a194f4a98 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -13,6 +13,7 @@ using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -89,6 +90,8 @@ namespace osu.Game.Rulesets.Osu.Mods private ControlPointInfo controlPointInfo; + private List origHitObjects; + #endregion #region Sudden Death (IApplicableFailOverride) @@ -163,9 +166,9 @@ namespace osu.Game.Rulesets.Osu.Mods if (osuBeatmap.HitObjects.Count == 0) return; controlPointInfo = osuBeatmap.ControlPointInfo; - var origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); + origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); - var hitObjects = generateBeats(osuBeatmap, origHitObjects) + var hitObjects = generateBeats(osuBeatmap) .Select(x => { var newCircle = new HitCircle(); @@ -174,9 +177,9 @@ namespace osu.Game.Rulesets.Osu.Mods return (OsuHitObject)newCircle; }).ToList(); - addHitSamples(hitObjects, origHitObjects); + addHitSamples(hitObjects); - fixComboInfo(hitObjects, origHitObjects); + fixComboInfo(hitObjects); randomizeCirclePos(hitObjects); @@ -185,21 +188,17 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToBeatmap(beatmap); } - private IEnumerable generateBeats(IBeatmap beatmap, IReadOnlyCollection origHitObjects) + private IEnumerable generateBeats(IBeatmap beatmap) { var startTime = origHitObjects.First().StartTime; var endObj = origHitObjects.Last(); var endTime = endObj.GetEndTime(); var beats = beatmap.ControlPointInfo.TimingPoints - .Where(x => Precision.AlmostBigger(endTime, x.Time)) + .Where(timingPoint => Precision.AlmostBigger(endTime, timingPoint.Time)) .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) - .Where(x => Precision.AlmostBigger(x, startTime)) - // Remove beats during breaks - .Where(x => !beatmap.Breaks.Any(b => - Precision.AlmostBigger(x, b.StartTime) - && Precision.AlmostBigger(origHitObjects.First(y => Precision.AlmostBigger(y.StartTime, b.EndTime)).StartTime, x) - )) + .Where(beat => Precision.AlmostBigger(beat, startTime)) + .Where(beat => isInsideBreakPeriod(beatmap.Breaks, beat)) .ToList(); // Remove beats that are too close to the next one (e.g. due to timing point changes) @@ -213,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Mods return beats; } - private void addHitSamples(IEnumerable hitObjects, IReadOnlyList origHitObjects) + private void addHitSamples(IEnumerable hitObjects) { var lastSampleIdx = 0; @@ -243,7 +242,7 @@ namespace osu.Game.Rulesets.Osu.Mods } } - private void fixComboInfo(List hitObjects, List origHitObjects) + private void fixComboInfo(List hitObjects) { // First follow the combo indices in the original beatmap hitObjects.ForEach(x => @@ -367,6 +366,26 @@ namespace osu.Game.Rulesets.Osu.Mods #region Helper Subroutines + /// + /// Check if a given time is inside a . + /// + /// + /// The given time is also considered to be inside a break if it is earlier than the + /// start time of the first original hit object after the break. + /// + /// The breaks of the beatmap. + /// The time to be checked.= + private bool isInsideBreakPeriod(IEnumerable breaks, double time) + { + return !breaks.Any(breakPeriod => + { + var firstObjAfterBreak = origHitObjects.First(obj => Precision.AlmostBigger(obj.StartTime, breakPeriod.EndTime)); + + return Precision.AlmostBigger(time, breakPeriod.StartTime) + && Precision.AlmostBigger(firstObjAfterBreak.StartTime, time); + }); + } + private IEnumerable getBeatsForTimingPoint(TimingControlPoint timingPoint, double mapEndTime) { var beats = new List(); From 98003ec548f42a8a834ba42ae5c52d03abc24c35 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 10:33:54 +0800 Subject: [PATCH 26/63] Avoid modulo when finding slider node index --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 31 +++++++++++++++++----- 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 7a194f4a98..31fb70e9c0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -413,30 +414,48 @@ namespace osu.Game.Rulesets.Osu.Mods /// Hit samples private IList getSamplesAtTime(IEnumerable hitObjects, double time) { - var sampleObj = hitObjects.FirstOrDefault(x => + var sampleObj = hitObjects.FirstOrDefault(hitObject => { - if (Precision.AlmostEquals(time, x.StartTime)) return true; + if (Precision.AlmostEquals(time, hitObject.StartTime)) + return true; - if (!(x is Slider s)) + if (!(hitObject is IHasPathWithRepeats s)) return false; - if (!Precision.AlmostBigger(time, s.StartTime) + if (!Precision.AlmostBigger(time, hitObject.StartTime) || !Precision.AlmostBigger(s.EndTime, time)) return false; - return Precision.AlmostEquals((time - s.StartTime) % s.SpanDuration, 0); + return nodeIndexFromTime(s, time - hitObject.StartTime) != -1; }); if (sampleObj == null) return null; IList samples; if (sampleObj is Slider slider) - samples = slider.NodeSamples[(int)Math.Round((time - slider.StartTime) % slider.SpanDuration)]; + samples = slider.NodeSamples[nodeIndexFromTime(slider, time - slider.StartTime)]; else samples = sampleObj.Samples; return samples; } + /// + /// Get the repeat node at a point in time. + /// + /// The slider. + /// The time since the start time of the slider. + /// Index of the node. -1 if there isn't a node at the specific time. + private int nodeIndexFromTime(IHasPathWithRepeats curve, double timeSinceStart) + { + double spanDuration = curve.Duration / curve.SpanCount(); + double nodeIndex = timeSinceStart / spanDuration; + + if (Precision.AlmostEquals(nodeIndex - Math.Round(nodeIndex), 0)) + return (int)Math.Round(nodeIndex); + + return -1; + } + private bool isOverlappingWithRecent(IReadOnlyList hitObjects, int idx) { var target = hitObjects[idx]; From 58b439b728c962c858f1cacbd817d6b5cb4020b4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 10:38:21 +0800 Subject: [PATCH 27/63] Switch to `IHasPathWithRepeats` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 31fb70e9c0..54e6708bb8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -431,8 +431,8 @@ namespace osu.Game.Rulesets.Osu.Mods IList samples; - if (sampleObj is Slider slider) - samples = slider.NodeSamples[nodeIndexFromTime(slider, time - slider.StartTime)]; + if (sampleObj is IHasPathWithRepeats slider) + samples = slider.NodeSamples[nodeIndexFromTime(slider, time - sampleObj.StartTime)]; else samples = sampleObj.Samples; From 04510f1acee717847765b22edbba7dc2b63e416d Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 10:54:21 +0800 Subject: [PATCH 28/63] Removed odd-looking decrement and checks in `addHitSamples` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 54e6708bb8..f2b70b19a8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -217,29 +217,32 @@ namespace osu.Game.Rulesets.Osu.Mods { var lastSampleIdx = 0; - foreach (var x in hitObjects) + foreach (var obj in hitObjects) { - var samples = getSamplesAtTime(origHitObjects, x.StartTime); + var samples = getSamplesAtTime(origHitObjects, obj.StartTime); if (samples == null) { - while (lastSampleIdx < origHitObjects.Count && origHitObjects[lastSampleIdx].StartTime <= x.StartTime) + // If samples aren't available at the exact start time of the object, + // use samples (without additions) in the closest original hit object instead + + while (lastSampleIdx < origHitObjects.Count && origHitObjects[lastSampleIdx].StartTime <= obj.StartTime) lastSampleIdx++; - lastSampleIdx--; - if (lastSampleIdx < 0 && lastSampleIdx >= origHitObjects.Count) continue; + if (lastSampleIdx >= origHitObjects.Count) continue; - if (lastSampleIdx < origHitObjects.Count - 1) + if (lastSampleIdx > 0) { - // get samples from the next hit object if it is closer in time - if (origHitObjects[lastSampleIdx + 1].StartTime - x.StartTime < x.StartTime - origHitObjects[lastSampleIdx].StartTime) - lastSampleIdx++; + // get samples from the previous hit object if it is closer in time + if (obj.StartTime - origHitObjects[lastSampleIdx - 1].StartTime < origHitObjects[lastSampleIdx].StartTime - obj.StartTime) + lastSampleIdx--; } - x.Samples = origHitObjects[lastSampleIdx].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); + // Remove additions + obj.Samples = origHitObjects[lastSampleIdx].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); } else - x.Samples = samples; + obj.Samples = samples; } } From 6202eed5e24d903e18834efb2801aa4a5d2e42b9 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 10:56:14 +0800 Subject: [PATCH 29/63] Moved a misplaced `!` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index f2b70b19a8..796abf30f4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Osu.Mods .Where(timingPoint => Precision.AlmostBigger(endTime, timingPoint.Time)) .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) .Where(beat => Precision.AlmostBigger(beat, startTime)) - .Where(beat => isInsideBreakPeriod(beatmap.Breaks, beat)) + .Where(beat => !isInsideBreakPeriod(beatmap.Breaks, beat)) .ToList(); // Remove beats that are too close to the next one (e.g. due to timing point changes) @@ -207,7 +207,8 @@ namespace osu.Game.Rulesets.Osu.Mods { var beat = beats[i]; - if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) beats.RemoveAt(i); + if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) + beats.RemoveAt(i); } return beats; @@ -381,7 +382,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// The time to be checked.= private bool isInsideBreakPeriod(IEnumerable breaks, double time) { - return !breaks.Any(breakPeriod => + return breaks.Any(breakPeriod => { var firstObjAfterBreak = origHitObjects.First(obj => Precision.AlmostBigger(obj.StartTime, breakPeriod.EndTime)); From 6fca8ba5b07a55ebc97326812ee1548f3c0be9ab Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 11:21:43 +0800 Subject: [PATCH 30/63] Better explanation for `fixComboInfo` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 796abf30f4..0c6097bc7e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -249,13 +249,20 @@ namespace osu.Game.Rulesets.Osu.Mods private void fixComboInfo(List hitObjects) { - // First follow the combo indices in the original beatmap - hitObjects.ForEach(x => + // Copy combo indices from the closest preceding object + hitObjects.ForEach(obj => { - var origObj = origHitObjects.FindLast(y => Precision.AlmostBigger(x.StartTime, y.StartTime)); - x.ComboIndex = origObj?.ComboIndex ?? 0; + var closestOrigObj = origHitObjects.FindLast(y => Precision.AlmostBigger(obj.StartTime, y.StartTime)); + + // It shouldn't be possible for origObj to be null + // But if it is, obj should be in the first combo + obj.ComboIndex = closestOrigObj?.ComboIndex ?? 0; }); - // Then reprocess them to ensure continuity in the combo indices and add indices in current combo + + // The copied combo indices may not be continuous if the original map starts and ends a combo in between beats + // e.g. A stream with each object starting a new combo + // So combo indices need to be reprocessed to ensure continuity + // Other kinds of combo info are also added in the process var combos = hitObjects.GroupBy(x => x.ComboIndex).ToList(); for (var i = 0; i < combos.Count; i++) From f74275a3b5a676fe2f56f1d974ee41b12445d982 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 11:29:10 +0800 Subject: [PATCH 31/63] Moved RNG initialisation to a better place --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 0c6097bc7e..dfce423399 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -93,6 +93,8 @@ namespace osu.Game.Rulesets.Osu.Mods private List origHitObjects; + private Random rng; + #endregion #region Sudden Death (IApplicableFailOverride) @@ -161,6 +163,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToBeatmap(IBeatmap beatmap) { Seed.Value ??= RNG.Next(); + rng = new Random(Seed.Value.GetValueOrDefault()); var osuBeatmap = (OsuBeatmap)beatmap; @@ -284,8 +287,6 @@ namespace osu.Game.Rulesets.Osu.Mods { if (hitObjects.Count == 0) return; - var rng = new Random(Seed.Value.GetValueOrDefault()); - float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); var direction = MathHelper.TwoPi * nextSingle(); From 71b5ed16c0e5182f1555e3e6cfa739b4d74742d4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 11:37:00 +0800 Subject: [PATCH 32/63] Avoid using osuTK constants; Use `MathF` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index dfce423399..8bbc6f4703 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -289,7 +289,9 @@ namespace osu.Game.Rulesets.Osu.Mods float nextSingle(float max = 1f) => (float)(rng.NextDouble() * max); - var direction = MathHelper.TwoPi * nextSingle(); + const float two_pi = MathF.PI * 2; + + var direction = two_pi * nextSingle(); var maxComboIndex = hitObjects.Last().ComboIndex; for (var i = 0; i < hitObjects.Count; i++) @@ -312,14 +314,14 @@ namespace osu.Game.Rulesets.Osu.Mods do { - if (tryCount > 0) direction = MathHelper.TwoPi * nextSingle(); + if (tryCount > 0) direction = two_pi * nextSingle(); var relativePos = new Vector2( - distance * (float)Math.Cos(direction), - distance * (float)Math.Sin(direction) + distance * MathF.Cos(direction), + distance * MathF.Sin(direction) ); relativePos = getRotatedVector(lastPos, relativePos); - direction = (float)Math.Atan2(relativePos.Y, relativePos.X); + direction = MathF.Atan2(relativePos.Y, relativePos.X); var newPosition = Vector2.Add(lastPos, relativePos); @@ -332,9 +334,9 @@ namespace osu.Game.Rulesets.Osu.Mods } while (distance >= obj.Radius * 2 && isOverlappingWithRecent(hitObjects, i)); if (obj.LastInCombo) - direction = MathHelper.TwoPi * nextSingle(); + direction = two_pi * nextSingle(); else - direction += distance / distance_cap * (nextSingle() * MathHelper.TwoPi - MathHelper.Pi); + direction += distance / distance_cap * (nextSingle() * two_pi - MathF.PI); } } From f8fe4ab4828f72cc71bddebe58fe6890d683d906 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 12:02:48 +0800 Subject: [PATCH 33/63] Refactor and rename `isOverlappingWithRecent` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 8bbc6f4703..8a9f9bf224 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Mods tryCount++; if (tryCount % 10 == 0) distance *= 0.9f; - } while (distance >= obj.Radius * 2 && isOverlappingWithRecent(hitObjects, i)); + } while (distance >= obj.Radius * 2 && checkForOverlap(hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count), obj)); if (obj.LastInCombo) direction = two_pi * nextSingle(); @@ -470,11 +470,9 @@ namespace osu.Game.Rulesets.Osu.Mods return -1; } - private bool isOverlappingWithRecent(IReadOnlyList hitObjects, int idx) + private bool checkForOverlap(IEnumerable objectsToCheck, OsuHitObject target) { - var target = hitObjects[idx]; - return hitObjects.SkipLast(hitObjects.Count - idx).TakeLast(overlap_check_count) - .Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2); + return objectsToCheck.Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2); } /// From 877c775e35069ee02bba3a6120036ad602c576da Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 12:20:23 +0800 Subject: [PATCH 34/63] Added comments --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 8a9f9bf224..1ec08b2c40 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -173,11 +173,11 @@ namespace osu.Game.Rulesets.Osu.Mods origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); var hitObjects = generateBeats(osuBeatmap) - .Select(x => + .Select(beat => { var newCircle = new HitCircle(); newCircle.ApplyDefaults(controlPointInfo, osuBeatmap.BeatmapInfo.BaseDifficulty); - newCircle.StartTime = x; + newCircle.StartTime = beat; return (OsuHitObject)newCircle; }).ToList(); @@ -199,9 +199,13 @@ namespace osu.Game.Rulesets.Osu.Mods var endTime = endObj.GetEndTime(); var beats = beatmap.ControlPointInfo.TimingPoints + // Ignore timing points after endTime .Where(timingPoint => Precision.AlmostBigger(endTime, timingPoint.Time)) + // Generate the beats .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) + // Remove beats before startTime .Where(beat => Precision.AlmostBigger(beat, startTime)) + // Remove beats during breaks .Where(beat => !isInsideBreakPeriod(beatmap.Breaks, beat)) .ToList(); @@ -320,6 +324,7 @@ namespace osu.Game.Rulesets.Osu.Mods distance * MathF.Cos(direction), distance * MathF.Sin(direction) ); + // Rotate the new circle away from playfield border relativePos = getRotatedVector(lastPos, relativePos); direction = MathF.Atan2(relativePos.Y, relativePos.X); @@ -428,6 +433,9 @@ namespace osu.Game.Rulesets.Osu.Mods /// Hit samples private IList getSamplesAtTime(IEnumerable hitObjects, double time) { + // Get a hit object that + // either has StartTime equal to the target time + // or has a repeat node at the target time var sampleObj = hitObjects.FirstOrDefault(hitObject => { if (Precision.AlmostEquals(time, hitObject.StartTime)) @@ -550,6 +558,10 @@ namespace osu.Game.Rulesets.Osu.Mods ); } + /// + /// Move the hit object into playfield, taking its radius into account. + /// + /// The hit object to be clamped. private void clampToPlayfield(OsuHitObject obj) { var position = obj.Position; From 6629f8706a741b67224423f25b473bc067e11047 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 12:31:25 +0800 Subject: [PATCH 35/63] Directly fade to gray instead of computing the color values --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 1ec08b2c40..9734b17ce0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -29,7 +29,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -146,8 +145,7 @@ namespace osu.Game.Rulesets.Osu.Mods var colour = drawable.Colour; - var avgColour = colour.AverageColour.Linear; - drawable.FadeColour(new Color4(avgColour.R * 0.45f, avgColour.G * 0.45f, avgColour.B * 0.45f, avgColour.A)) + drawable.FadeColour(OsuColour.Gray(0.45f)) .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) .FadeColour(colour, undim_duration); From be55c7e075ad103bedd11dedf942ed193c731ea4 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 13:39:50 +0800 Subject: [PATCH 36/63] Minor fixes in comments --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 9734b17ce0..00caea0bfe 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.Mods .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) .FadeColour(colour, undim_duration); - // remove approach circles + // Remove approach circles circle.ApproachCircle.Hide(); } } @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (lastSampleIdx > 0) { - // get samples from the previous hit object if it is closer in time + // Get samples from the previous hit object if it is closer in time if (obj.StartTime - origHitObjects[lastSampleIdx - 1].StartTime < origHitObjects[lastSampleIdx].StartTime - obj.StartTime) lastSampleIdx--; } @@ -259,7 +259,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var closestOrigObj = origHitObjects.FindLast(y => Precision.AlmostBigger(obj.StartTime, y.StartTime)); - // It shouldn't be possible for origObj to be null + // It shouldn't be possible for closestOrigObj to be null // But if it is, obj should be in the first combo obj.ComboIndex = closestOrigObj?.ComboIndex ?? 0; }); From 1a47bc254d4e98407ab7799823fdba3b1992a444 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 24 Jun 2021 14:55:49 +0800 Subject: [PATCH 37/63] Increase acceptable difference for Precision calls --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 39 +++++++++++++++------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 00caea0bfe..bd3287634c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -84,6 +84,11 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const double undim_duration = 96; + /// + /// Acceptable difference for timing comparisons + /// + private const double timing_precision = 1; + #endregion #region Private Fields @@ -198,11 +203,11 @@ namespace osu.Game.Rulesets.Osu.Mods var beats = beatmap.ControlPointInfo.TimingPoints // Ignore timing points after endTime - .Where(timingPoint => Precision.AlmostBigger(endTime, timingPoint.Time)) + .Where(timingPoint => almostBigger(endTime, timingPoint.Time)) // Generate the beats .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) // Remove beats before startTime - .Where(beat => Precision.AlmostBigger(beat, startTime)) + .Where(beat => almostBigger(beat, startTime)) // Remove beats during breaks .Where(beat => !isInsideBreakPeriod(beatmap.Breaks, beat)) .ToList(); @@ -212,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var beat = beats[i]; - if (Precision.AlmostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) + if (almostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) beats.RemoveAt(i); } @@ -257,7 +262,7 @@ namespace osu.Game.Rulesets.Osu.Mods // Copy combo indices from the closest preceding object hitObjects.ForEach(obj => { - var closestOrigObj = origHitObjects.FindLast(y => Precision.AlmostBigger(obj.StartTime, y.StartTime)); + var closestOrigObj = origHitObjects.FindLast(y => almostBigger(obj.StartTime, y.StartTime)); // It shouldn't be possible for closestOrigObj to be null // But if it is, obj should be in the first combo @@ -397,10 +402,10 @@ namespace osu.Game.Rulesets.Osu.Mods { return breaks.Any(breakPeriod => { - var firstObjAfterBreak = origHitObjects.First(obj => Precision.AlmostBigger(obj.StartTime, breakPeriod.EndTime)); + var firstObjAfterBreak = origHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); - return Precision.AlmostBigger(time, breakPeriod.StartTime) - && Precision.AlmostBigger(firstObjAfterBreak.StartTime, time); + return almostBigger(time, breakPeriod.StartTime) + && almostBigger(firstObjAfterBreak.StartTime, time); }); } @@ -410,7 +415,7 @@ namespace osu.Game.Rulesets.Osu.Mods int i = 0; var currentTime = timingPoint.Time; - while (Precision.AlmostBigger(mapEndTime, currentTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) + while (almostBigger(mapEndTime, currentTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) { beats.Add(Math.Floor(currentTime)); i++; @@ -436,13 +441,13 @@ namespace osu.Game.Rulesets.Osu.Mods // or has a repeat node at the target time var sampleObj = hitObjects.FirstOrDefault(hitObject => { - if (Precision.AlmostEquals(time, hitObject.StartTime)) + if (almostEquals(time, hitObject.StartTime)) return true; if (!(hitObject is IHasPathWithRepeats s)) return false; - if (!Precision.AlmostBigger(time, hitObject.StartTime) - || !Precision.AlmostBigger(s.EndTime, time)) + if (!almostBigger(time, hitObject.StartTime) + || !almostBigger(s.EndTime, time)) return false; return nodeIndexFromTime(s, time - hitObject.StartTime) != -1; @@ -470,7 +475,7 @@ namespace osu.Game.Rulesets.Osu.Mods double spanDuration = curve.Duration / curve.SpanCount(); double nodeIndex = timeSinceStart / spanDuration; - if (Precision.AlmostEquals(nodeIndex - Math.Round(nodeIndex), 0)) + if (almostEquals(nodeIndex - Math.Round(nodeIndex), 0)) return (int)Math.Round(nodeIndex); return -1; @@ -583,6 +588,16 @@ namespace osu.Game.Rulesets.Osu.Mods return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; } + private static bool almostBigger(double value1, double value2) + { + return Precision.AlmostBigger(value1, value2, timing_precision); + } + + private static bool almostEquals(double value1, double value2) + { + return Precision.AlmostEquals(value1, value2, timing_precision); + } + #endregion } } From 3eaa04115f3dc6240e0b7231103db9c875168294 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 26 Jun 2021 11:34:10 +0800 Subject: [PATCH 38/63] Use `OsuHitObjectGenerationUtils` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 78 +--------------------- 1 file changed, 2 insertions(+), 76 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index bd3287634c..1a6de87745 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Osu.Utils; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; @@ -328,7 +329,7 @@ namespace osu.Game.Rulesets.Osu.Mods distance * MathF.Sin(direction) ); // Rotate the new circle away from playfield border - relativePos = getRotatedVector(lastPos, relativePos); + relativePos = OsuHitObjectGenerationUtils.RotateAwayFromEdge(lastPos, relativePos, edge_rotation_multiplier); direction = MathF.Atan2(relativePos.Y, relativePos.X); var newPosition = Vector2.Add(lastPos, relativePos); @@ -486,81 +487,6 @@ namespace osu.Game.Rulesets.Osu.Mods return objectsToCheck.Any(h => Vector2.Distance(h.Position, target.Position) < target.Radius * 2); } - /// - /// Determines the position of the current hit object relative to the previous one. - /// - /// The position of the current hit object relative to the previous one - private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev) - { - var relativeRotationDistance = 0f; - var playfieldMiddle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2); - - if (prevPosChanged.X < playfieldMiddle.X) - { - relativeRotationDistance = Math.Max( - (border_distance_x - prevPosChanged.X) / border_distance_x, - relativeRotationDistance - ); - } - else - { - relativeRotationDistance = Math.Max( - (prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x, - relativeRotationDistance - ); - } - - if (prevPosChanged.Y < playfieldMiddle.Y) - { - relativeRotationDistance = Math.Max( - (border_distance_y - prevPosChanged.Y) / border_distance_y, - relativeRotationDistance - ); - } - else - { - relativeRotationDistance = Math.Max( - (prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y, - relativeRotationDistance - ); - } - - return rotateVectorTowardsVector( - posRelativeToPrev, - Vector2.Subtract(playfieldMiddle, prevPosChanged), - Math.Min(1, relativeRotationDistance * edge_rotation_multiplier) - ); - } - - /// - /// Rotates vector "initial" towards vector "destination" - /// - /// Vector to rotate to "destination" - /// Vector "initial" should be rotated to - /// - /// The angle the vector should be rotated relative to the difference between the angles of - /// the the two vectors. - /// - /// Resulting vector - private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance) - { - var initialAngleRad = Math.Atan2(initial.Y, initial.X); - var destAngleRad = Math.Atan2(destination.Y, destination.X); - - var diff = destAngleRad - initialAngleRad; - - while (diff < -Math.PI) diff += 2 * Math.PI; - - while (diff > Math.PI) diff -= 2 * Math.PI; - - var finalAngleRad = initialAngleRad + relativeDistance * diff; - - return new Vector2( - initial.Length * (float)Math.Cos(finalAngleRad), - initial.Length * (float)Math.Sin(finalAngleRad) - ); - } - /// /// Move the hit object into playfield, taking its radius into account. /// From ea8993d6d6363654490d210fcac4ba369ac7d3d0 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Mon, 28 Jun 2021 11:33:19 +0800 Subject: [PATCH 39/63] Use `IHasRepeats` instead of `IHasPathWithRepeats` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 1a6de87745..b2513e4983 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -445,7 +445,7 @@ namespace osu.Game.Rulesets.Osu.Mods if (almostEquals(time, hitObject.StartTime)) return true; - if (!(hitObject is IHasPathWithRepeats s)) + if (!(hitObject is IHasRepeats s)) return false; if (!almostBigger(time, hitObject.StartTime) || !almostBigger(s.EndTime, time)) @@ -457,7 +457,7 @@ namespace osu.Game.Rulesets.Osu.Mods IList samples; - if (sampleObj is IHasPathWithRepeats slider) + if (sampleObj is IHasRepeats slider) samples = slider.NodeSamples[nodeIndexFromTime(slider, time - sampleObj.StartTime)]; else samples = sampleObj.Samples; @@ -471,7 +471,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// The slider. /// The time since the start time of the slider. /// Index of the node. -1 if there isn't a node at the specific time. - private int nodeIndexFromTime(IHasPathWithRepeats curve, double timeSinceStart) + private int nodeIndexFromTime(IHasRepeats curve, double timeSinceStart) { double spanDuration = curve.Duration / curve.SpanCount(); double nodeIndex = timeSinceStart / spanDuration; From d4ff4b26f55f7532fc2a67751a5f46e976f8cd15 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Tue, 29 Jun 2021 12:49:25 +0800 Subject: [PATCH 40/63] Split part of `addHitSamples` to a subroutine --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 39 +++++++++------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index b2513e4983..3ad432a40b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -227,34 +227,13 @@ namespace osu.Game.Rulesets.Osu.Mods private void addHitSamples(IEnumerable hitObjects) { - var lastSampleIdx = 0; - foreach (var obj in hitObjects) { var samples = getSamplesAtTime(origHitObjects, obj.StartTime); - if (samples == null) - { - // If samples aren't available at the exact start time of the object, - // use samples (without additions) in the closest original hit object instead - - while (lastSampleIdx < origHitObjects.Count && origHitObjects[lastSampleIdx].StartTime <= obj.StartTime) - lastSampleIdx++; - - if (lastSampleIdx >= origHitObjects.Count) continue; - - if (lastSampleIdx > 0) - { - // Get samples from the previous hit object if it is closer in time - if (obj.StartTime - origHitObjects[lastSampleIdx - 1].StartTime < origHitObjects[lastSampleIdx].StartTime - obj.StartTime) - lastSampleIdx--; - } - - // Remove additions - obj.Samples = origHitObjects[lastSampleIdx].Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); - } - else - obj.Samples = samples; + // If samples aren't available at the exact start time of the object, + // use samples (without additions) in the closest original hit object instead + obj.Samples = samples ?? getClosestHitObject(origHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); } } @@ -426,6 +405,18 @@ namespace osu.Game.Rulesets.Osu.Mods return beats; } + private OsuHitObject getClosestHitObject(List hitObjects, double time) + { + var closestIdx = hitObjects.FindLastIndex(h => h.StartTime < time); + + if (closestIdx == hitObjects.Count - 1) return hitObjects[closestIdx]; + + // return the closest preceding/succeeding hit object, whoever is closer in time + return hitObjects[closestIdx + 1].StartTime - time < time - hitObjects[closestIdx].StartTime + ? hitObjects[closestIdx + 1] + : hitObjects[closestIdx]; + } + /// /// Get samples (if any) for a specific point in time. /// From 913f7602e4829306aa6aa5e0faa5a97bd6522568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Jun 2021 20:41:08 +0200 Subject: [PATCH 41/63] Change seed control type in line with changes --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 3ad432a40b..12daa44477 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -17,6 +17,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; - [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))] + [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] public Bindable Seed { get; } = new Bindable { Default = null, From 58f80abe322559102ea1c47c5637057cf8aa8ab8 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 1 Jul 2021 11:57:57 +0800 Subject: [PATCH 42/63] Several requested changes - Rename `origHitObjects` to `originalHitObjects` - Use `Value` instead of `GetValueOrDefault()` - Remove `endObj` - Added comments - Rename `closestIdx` to `precedingIndex` - Changed an `almostEquals` call --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 47 +++++++++++++--------- 1 file changed, 28 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 12daa44477..60ca25a721 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Mods private ControlPointInfo controlPointInfo; - private List origHitObjects; + private List originalHitObjects; private Random rng; @@ -168,14 +168,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToBeatmap(IBeatmap beatmap) { Seed.Value ??= RNG.Next(); - rng = new Random(Seed.Value.GetValueOrDefault()); + rng = new Random(Seed.Value.Value); var osuBeatmap = (OsuBeatmap)beatmap; if (osuBeatmap.HitObjects.Count == 0) return; controlPointInfo = osuBeatmap.ControlPointInfo; - origHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); + originalHitObjects = osuBeatmap.HitObjects.OrderBy(x => x.StartTime).ToList(); var hitObjects = generateBeats(osuBeatmap) .Select(beat => @@ -199,9 +199,8 @@ namespace osu.Game.Rulesets.Osu.Mods private IEnumerable generateBeats(IBeatmap beatmap) { - var startTime = origHitObjects.First().StartTime; - var endObj = origHitObjects.Last(); - var endTime = endObj.GetEndTime(); + var startTime = originalHitObjects.First().StartTime; + var endTime = originalHitObjects.Last().GetEndTime(); var beats = beatmap.ControlPointInfo.TimingPoints // Ignore timing points after endTime @@ -230,24 +229,25 @@ namespace osu.Game.Rulesets.Osu.Mods { foreach (var obj in hitObjects) { - var samples = getSamplesAtTime(origHitObjects, obj.StartTime); + var samples = getSamplesAtTime(originalHitObjects, obj.StartTime); // If samples aren't available at the exact start time of the object, // use samples (without additions) in the closest original hit object instead - obj.Samples = samples ?? getClosestHitObject(origHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); + obj.Samples = samples ?? getClosestHitObject(originalHitObjects, obj.StartTime).Samples.Where(s => !HitSampleInfo.AllAdditions.Contains(s.Name)).ToList(); } } private void fixComboInfo(List hitObjects) { - // Copy combo indices from the closest preceding object - hitObjects.ForEach(obj => + // Copy combo indices from an original object at the same time or from the closest preceding object + // (Objects lying between two combos are assumed to belong to the preceding combo) + hitObjects.ForEach(newObj => { - var closestOrigObj = origHitObjects.FindLast(y => almostBigger(obj.StartTime, y.StartTime)); + var closestOrigObj = originalHitObjects.FindLast(y => almostBigger(newObj.StartTime, y.StartTime)); // It shouldn't be possible for closestOrigObj to be null // But if it is, obj should be in the first combo - obj.ComboIndex = closestOrigObj?.ComboIndex ?? 0; + newObj.ComboIndex = closestOrigObj?.ComboIndex ?? 0; }); // The copied combo indices may not be continuous if the original map starts and ends a combo in between beats @@ -383,7 +383,7 @@ namespace osu.Game.Rulesets.Osu.Mods { return breaks.Any(breakPeriod => { - var firstObjAfterBreak = origHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); + var firstObjAfterBreak = originalHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); return almostBigger(time, breakPeriod.StartTime) && almostBigger(firstObjAfterBreak.StartTime, time); @@ -408,14 +408,14 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuHitObject getClosestHitObject(List hitObjects, double time) { - var closestIdx = hitObjects.FindLastIndex(h => h.StartTime < time); + var precedingIndex = hitObjects.FindLastIndex(h => h.StartTime < time); - if (closestIdx == hitObjects.Count - 1) return hitObjects[closestIdx]; + if (precedingIndex == hitObjects.Count - 1) return hitObjects[precedingIndex]; // return the closest preceding/succeeding hit object, whoever is closer in time - return hitObjects[closestIdx + 1].StartTime - time < time - hitObjects[closestIdx].StartTime - ? hitObjects[closestIdx + 1] - : hitObjects[closestIdx]; + return hitObjects[precedingIndex + 1].StartTime - time < time - hitObjects[precedingIndex].StartTime + ? hitObjects[precedingIndex + 1] + : hitObjects[precedingIndex]; } /// @@ -468,7 +468,7 @@ namespace osu.Game.Rulesets.Osu.Mods double spanDuration = curve.Duration / curve.SpanCount(); double nodeIndex = timeSinceStart / spanDuration; - if (almostEquals(nodeIndex - Math.Round(nodeIndex), 0)) + if (almostEquals(nodeIndex, Math.Round(nodeIndex))) return (int)Math.Round(nodeIndex); return -1; @@ -501,6 +501,15 @@ namespace osu.Game.Rulesets.Osu.Mods obj.Position = position; } + /// + /// Re-maps a number from one range to another. + /// + /// The number to be re-mapped. + /// Beginning of the original range. + /// End of the original range. + /// Beginning of the new range. + /// End of the new range. + /// The re-mapped number. private static float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) { return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; From 34be437d7a6b9af4d71581e4b67481883e78fdcf Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 1 Jul 2021 12:21:41 +0800 Subject: [PATCH 43/63] Added `definitelyBigger` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 60ca25a721..a39dd7a90a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Mods var beats = beatmap.ControlPointInfo.TimingPoints // Ignore timing points after endTime - .Where(timingPoint => almostBigger(endTime, timingPoint.Time)) + .Where(timingPoint => !definitelyBigger(timingPoint.Time, endTime)) // Generate the beats .SelectMany(timingPoint => getBeatsForTimingPoint(timingPoint, endTime)) // Remove beats before startTime @@ -218,7 +218,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var beat = beats[i]; - if (almostBigger(beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2, beats[i + 1] - beat)) + if (!definitelyBigger(beats[i + 1] - beat, beatmap.ControlPointInfo.TimingPointAt(beat).BeatLength / 2)) beats.RemoveAt(i); } @@ -386,7 +386,7 @@ namespace osu.Game.Rulesets.Osu.Mods var firstObjAfterBreak = originalHitObjects.First(obj => almostBigger(obj.StartTime, breakPeriod.EndTime)); return almostBigger(time, breakPeriod.StartTime) - && almostBigger(firstObjAfterBreak.StartTime, time); + && definitelyBigger(firstObjAfterBreak.StartTime, time); }); } @@ -396,7 +396,7 @@ namespace osu.Game.Rulesets.Osu.Mods int i = 0; var currentTime = timingPoint.Time; - while (almostBigger(mapEndTime, currentTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) + while (!definitelyBigger(currentTime, mapEndTime) && controlPointInfo.TimingPointAt(currentTime) == timingPoint) { beats.Add(Math.Floor(currentTime)); i++; @@ -439,6 +439,8 @@ namespace osu.Game.Rulesets.Osu.Mods if (!(hitObject is IHasRepeats s)) return false; + // If time is outside the duration of the IHasRepeats, + // then this hitObject isn't the one we want if (!almostBigger(time, hitObject.StartTime) || !almostBigger(s.EndTime, time)) return false; @@ -520,6 +522,11 @@ namespace osu.Game.Rulesets.Osu.Mods return Precision.AlmostBigger(value1, value2, timing_precision); } + private static bool definitelyBigger(double value1, double value2) + { + return Precision.DefinitelyBigger(value1, value2, timing_precision); + } + private static bool almostEquals(double value1, double value2) { return Precision.AlmostEquals(value1, value2, timing_precision); From b0a619bb4cb0837265af38dcb86a597617fcb777 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Thu, 1 Jul 2021 12:49:34 +0800 Subject: [PATCH 44/63] Prevent multiple enumeration in `checkForOverlap` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index a39dd7a90a..cc3811c9b8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -300,6 +300,9 @@ namespace osu.Game.Rulesets.Osu.Mods var tryCount = 0; + // for checking overlap + var precedingObjects = hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count).ToList(); + do { if (tryCount > 0) direction = two_pi * nextSingle(); @@ -320,7 +323,7 @@ namespace osu.Game.Rulesets.Osu.Mods tryCount++; if (tryCount % 10 == 0) distance *= 0.9f; - } while (distance >= obj.Radius * 2 && checkForOverlap(hitObjects.SkipLast(hitObjects.Count - i).TakeLast(overlap_check_count), obj)); + } while (distance >= obj.Radius * 2 && checkForOverlap(precedingObjects, obj)); if (obj.LastInCombo) direction = two_pi * nextSingle(); From 1470bb15634c0946a08c91fd484b643a5e542659 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sat, 3 Jul 2021 13:02:39 +0800 Subject: [PATCH 45/63] Use `IHidesApproachCircles` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index cc3811c9b8..5ca9a15a0f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public class OsuModTarget : ModWithVisibilityAdjustment, IApplicableToDrawableRuleset, IApplicableToHealthProcessor, IApplicableToDifficulty, IApplicableFailOverride, - IHasSeed, IMutateApproachCircles + IHasSeed, IHidesApproachCircles { public override string Name => "Target"; public override string Acronym => "TP"; @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => @"Practice keeping up with the beat of the song."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(IMutateApproachCircles) }; + public override Type[] IncompatibleMods => new[] { typeof(IRequiresApproachCircles) }; [SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))] public Bindable Seed { get; } = new Bindable From d1862d8cff46945bfb7d90bfc984e1fb176f66af Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 4 Jul 2021 10:01:56 +0800 Subject: [PATCH 46/63] Rename `map` to `mapRange` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 5ca9a15a0f..27c352070a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -291,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.Mods var distance = maxComboIndex == 0 ? (float)obj.Radius - : map(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance); + : mapRange(obj.ComboIndex, 0, maxComboIndex, (float)obj.Radius, max_base_distance); if (obj.NewCombo) distance *= 1.5f; if (obj.Kiai) distance *= 1.2f; distance = Math.Min(distance_cap, distance); @@ -515,7 +515,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// Beginning of the new range. /// End of the new range. /// The re-mapped number. - private static float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) + private static float mapRange(float value, float fromLow, float fromHigh, float toLow, float toHigh) { return (value - fromLow) * (toHigh - toLow) / (fromHigh - fromLow) + toLow; } From e7b78b1ea5299dae24c07a16243f24ba4888d170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Jul 2021 20:26:05 +0900 Subject: [PATCH 47/63] Adjust transform logic to hopefully be a bit easier to parse --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 25 ++++++++++++---------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 27c352070a..8f9ff81808 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -31,6 +31,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Mods { @@ -143,22 +144,24 @@ namespace osu.Game.Rulesets.Osu.Mods { if (!(drawable is DrawableHitCircle circle)) return; - var h = (OsuHitObject)drawable.HitObject; + double startTime = circle.HitObject.StartTime; + double preempt = circle.HitObject.TimePreempt; - using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) + using (drawable.BeginAbsoluteSequence(startTime - preempt)) { + // initial state drawable.ScaleTo(0.5f) - .Then().ScaleTo(1f, h.TimePreempt); + .FadeColour(OsuColour.Gray(0.5f)); - var colour = drawable.Colour; - - drawable.FadeColour(OsuColour.Gray(0.45f)) - .Then().Delay(h.TimePreempt - controlPointInfo.TimingPointAt(h.StartTime).BeatLength - undim_duration) - .FadeColour(colour, undim_duration); - - // Remove approach circles - circle.ApproachCircle.Hide(); + // scale to final size + drawable.ScaleTo(1f, preempt); } + + using (drawable.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration)) + drawable.FadeColour(Color4.White, undim_duration); + + // Remove approach circles + circle.ApproachCircle.Hide(); } #endregion From c38590f1ff11469e024fa94b56837b8a3b92a0bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:42:07 +0900 Subject: [PATCH 48/63] Use a slightly more appropriate metronome sound --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 8f9ff81808..3e08cfcd7a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -353,23 +353,24 @@ namespace osu.Game.Rulesets.Osu.Mods Divisor = 1; } + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + sample = new PausableSkinnableSound(new SampleInfo("Gameplay/catch-banana")) + }; + } + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) { base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); if (!IsBeatSyncedWithTrack) return; + sample.Frequency.Value = beatIndex % (int)timingPoint.TimeSignature == 0 ? 1 : 0.5f; sample?.Play(); } - - [BackgroundDependencyLoader] - private void load() - { - InternalChildren = new Drawable[] - { - sample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")) // todo: use another sample - }; - } } #endregion From ea87869753a7b99a94f1b9fbb1e3a42a5bacc4a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Jul 2021 16:50:55 +0900 Subject: [PATCH 49/63] Fix metronome playing during intro time --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 3e08cfcd7a..5fa3bc4520 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -341,15 +341,18 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.Overlays.Add(new TargetBeatContainer()); + drawableRuleset.Overlays.Add(new TargetBeatContainer(drawableRuleset.Beatmap.HitObjects.First().StartTime)); } public class TargetBeatContainer : BeatSyncedContainer { + private readonly double firstHitTime; + private PausableSkinnableSound sample; - public TargetBeatContainer() + public TargetBeatContainer(double firstHitTime) { + this.firstHitTime = firstHitTime; Divisor = 1; } @@ -368,8 +371,15 @@ namespace osu.Game.Rulesets.Osu.Mods if (!IsBeatSyncedWithTrack) return; - sample.Frequency.Value = beatIndex % (int)timingPoint.TimeSignature == 0 ? 1 : 0.5f; - sample?.Play(); + int timeSignature = (int)timingPoint.TimeSignature; + + // play metronome from one measure before the first object. + // TODO: Use BeatSyncClock from https://github.com/ppy/osu/pull/13894. + if (Clock.CurrentTime < firstHitTime - timingPoint.BeatLength * timeSignature) + return; + + sample.Frequency.Value = beatIndex % timeSignature == 0 ? 1 : 0.5f; + sample.Play(); } } From 71f74f0e9860bcca7054df6bf40ec67cabdd3e5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jul 2021 23:34:28 +0900 Subject: [PATCH 50/63] Add warning message to screens which require a current match to be selected --- .../Screens/MapPool/MapPoolScreen.cs | 40 +++++++++---------- .../Screens/TournamentMatchScreen.cs | 34 ++++++++++++++++ 2 files changed, 54 insertions(+), 20 deletions(-) create mode 100644 osu.Game.Tournament/Screens/TournamentMatchScreen.cs diff --git a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs index 2c4fed8d86..d4292c5492 100644 --- a/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs +++ b/osu.Game.Tournament/Screens/MapPool/MapPoolScreen.cs @@ -21,12 +21,10 @@ using osuTK.Input; namespace osu.Game.Tournament.Screens.MapPool { - public class MapPoolScreen : TournamentScreen + public class MapPoolScreen : TournamentMatchScreen { private readonly FillFlowContainer> mapFlows; - private readonly Bindable currentMatch = new Bindable(); - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -96,7 +94,7 @@ namespace osu.Game.Tournament.Screens.MapPool Action = reset }, new ControlPanel.Spacer(), - } + }, } }; } @@ -104,15 +102,12 @@ namespace osu.Game.Tournament.Screens.MapPool [BackgroundDependencyLoader] private void load(MatchIPCInfo ipc) { - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(LadderInfo.CurrentMatch); - ipc.Beatmap.BindValueChanged(beatmapChanged); } private void beatmapChanged(ValueChangedEvent beatmap) { - if (currentMatch.Value == null || currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) + if (CurrentMatch.Value == null || CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) < 2) return; // if bans have already been placed, beatmap changes result in a selection being made autoamtically @@ -137,12 +132,12 @@ namespace osu.Game.Tournament.Screens.MapPool { const TeamColour roll_winner = TeamColour.Red; //todo: draw from match - var nextColour = (currentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red; + var nextColour = (CurrentMatch.Value.PicksBans.LastOrDefault()?.Team ?? roll_winner) == TeamColour.Red ? TeamColour.Blue : TeamColour.Red; - if (pickType == ChoiceType.Ban && currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2) + if (pickType == ChoiceType.Ban && CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2) setMode(pickColour, ChoiceType.Pick); else - setMode(nextColour, currentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2 ? ChoiceType.Pick : ChoiceType.Ban); + setMode(nextColour, CurrentMatch.Value.PicksBans.Count(p => p.Type == ChoiceType.Ban) >= 2 ? ChoiceType.Pick : ChoiceType.Ban); } protected override bool OnMouseDown(MouseDownEvent e) @@ -156,11 +151,11 @@ namespace osu.Game.Tournament.Screens.MapPool addForBeatmap(map.Beatmap.OnlineBeatmapID.Value); else { - var existing = currentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID); + var existing = CurrentMatch.Value.PicksBans.FirstOrDefault(p => p.BeatmapID == map.Beatmap.OnlineBeatmapID); if (existing != null) { - currentMatch.Value.PicksBans.Remove(existing); + CurrentMatch.Value.PicksBans.Remove(existing); setNextMode(); } } @@ -173,7 +168,7 @@ namespace osu.Game.Tournament.Screens.MapPool private void reset() { - currentMatch.Value.PicksBans.Clear(); + CurrentMatch.Value.PicksBans.Clear(); setNextMode(); } @@ -181,18 +176,18 @@ namespace osu.Game.Tournament.Screens.MapPool private void addForBeatmap(int beatmapId) { - if (currentMatch.Value == null) + if (CurrentMatch.Value == null) return; - if (currentMatch.Value.Round.Value.Beatmaps.All(b => b.BeatmapInfo.OnlineBeatmapID != beatmapId)) + if (CurrentMatch.Value.Round.Value.Beatmaps.All(b => b.BeatmapInfo.OnlineBeatmapID != beatmapId)) // don't attempt to add if the beatmap isn't in our pool return; - if (currentMatch.Value.PicksBans.Any(p => p.BeatmapID == beatmapId)) + if (CurrentMatch.Value.PicksBans.Any(p => p.BeatmapID == beatmapId)) // don't attempt to add if already exists. return; - currentMatch.Value.PicksBans.Add(new BeatmapChoice + CurrentMatch.Value.PicksBans.Add(new BeatmapChoice { Team = pickColour, Type = pickType, @@ -201,17 +196,22 @@ namespace osu.Game.Tournament.Screens.MapPool setNextMode(); - if (pickType == ChoiceType.Pick && currentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) + if (pickType == ChoiceType.Pick && CurrentMatch.Value.PicksBans.Any(i => i.Type == ChoiceType.Pick)) { scheduledChange?.Cancel(); scheduledChange = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(GameplayScreen)); }, 10000); } } - private void matchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { + base.CurrentMatchChanged(match); + mapFlows.Clear(); + if (match.NewValue == null) + return; + int totalRows = 0; if (match.NewValue.Round.Value != null) diff --git a/osu.Game.Tournament/Screens/TournamentMatchScreen.cs b/osu.Game.Tournament/Screens/TournamentMatchScreen.cs new file mode 100644 index 0000000000..5f00036653 --- /dev/null +++ b/osu.Game.Tournament/Screens/TournamentMatchScreen.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Game.Tournament.Models; + +namespace osu.Game.Tournament.Screens +{ + public abstract class TournamentMatchScreen : TournamentScreen + { + protected readonly Bindable CurrentMatch = new Bindable(); + private WarningBox noMatchWarning; + + protected override void LoadComplete() + { + base.LoadComplete(); + + CurrentMatch.BindTo(LadderInfo.CurrentMatch); + CurrentMatch.BindValueChanged(CurrentMatchChanged, true); + } + + protected virtual void CurrentMatchChanged(ValueChangedEvent match) + { + if (match.NewValue == null) + { + AddInternal(noMatchWarning = new WarningBox("Choose a match first from the brackets screen")); + return; + } + + noMatchWarning?.Expire(); + noMatchWarning = null; + } + } +} From 0a13e033eaa6907126fa852eb7dc71a8e587bc12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Jul 2021 23:59:16 +0900 Subject: [PATCH 51/63] Move height warning to bottom of screen to avoid overlap --- osu.Game.Tournament/TournamentGame.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 87e23e3404..cd0e601a2f 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -97,7 +97,12 @@ namespace osu.Game.Tournament }, } }, - heightWarning = new WarningBox("Please make the window wider"), + heightWarning = new WarningBox("Please make the window wider") + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Margin = new MarginPadding(20), + }, new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, From e8595871ded0ad53f3f3caf6f79f00a952523a5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 17 Jul 2021 00:14:48 +0900 Subject: [PATCH 52/63] Update remaining screens to also show the warning message --- .../Screens/BeatmapInfoScreen.cs | 2 +- .../Screens/Gameplay/GameplayScreen.cs | 29 ++++++++++--------- .../Screens/Showcase/ShowcaseScreen.cs | 8 +++++ .../Screens/TeamIntro/SeedingScreen.cs | 22 +++++++------- .../Screens/TeamIntro/TeamIntroScreen.cs | 16 ++++------ .../Screens/TeamWin/TeamWinScreen.cs | 19 ++++++------ 6 files changed, 52 insertions(+), 44 deletions(-) diff --git a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs index 0a3163ef43..50498304ca 100644 --- a/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs +++ b/osu.Game.Tournament/Screens/BeatmapInfoScreen.cs @@ -11,7 +11,7 @@ using osu.Game.Tournament.IPC; namespace osu.Game.Tournament.Screens { - public abstract class BeatmapInfoScreen : TournamentScreen + public abstract class BeatmapInfoScreen : TournamentMatchScreen { protected readonly SongBar SongBar; diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index 6e4c6784c8..f61506d7f2 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -24,8 +24,6 @@ namespace osu.Game.Tournament.Screens.Gameplay { private readonly BindableBool warmup = new BindableBool(); - private readonly Bindable currentMatch = new Bindable(); - public readonly Bindable State = new Bindable(); private OsuButton warmupButton; private MatchIPCInfo ipc; @@ -131,14 +129,6 @@ namespace osu.Game.Tournament.Screens.Gameplay ladder.ChromaKeyWidth.BindValueChanged(width => chroma.Width = width.NewValue, true); - currentMatch.BindValueChanged(m => - { - warmup.Value = m.NewValue.Team1Score.Value + m.NewValue.Team2Score.Value == 0; - scheduledOperation?.Cancel(); - }); - - currentMatch.BindTo(ladder.CurrentMatch); - warmup.BindValueChanged(w => { warmupButton.Alpha = !w.NewValue ? 0.5f : 1; @@ -146,6 +136,17 @@ namespace osu.Game.Tournament.Screens.Gameplay }, true); } + protected override void CurrentMatchChanged(ValueChangedEvent match) + { + base.CurrentMatchChanged(match); + + if (match.NewValue == null) + return; + + warmup.Value = match.NewValue.Team1Score.Value + match.NewValue.Team2Score.Value == 0; + scheduledOperation?.Cancel(); + } + private ScheduledDelegate scheduledOperation; private MatchScoreDisplay scoreDisplay; @@ -161,9 +162,9 @@ namespace osu.Game.Tournament.Screens.Gameplay if (warmup.Value) return; if (ipc.Score1.Value > ipc.Score2.Value) - currentMatch.Value.Team1Score.Value++; + CurrentMatch.Value.Team1Score.Value++; else - currentMatch.Value.Team2Score.Value++; + CurrentMatch.Value.Team2Score.Value++; } scheduledOperation?.Cancel(); @@ -198,9 +199,9 @@ namespace osu.Game.Tournament.Screens.Gameplay // we should automatically proceed after a short delay if (lastState == TourneyState.Ranking && !warmup.Value) { - if (currentMatch.Value?.Completed.Value == true) + if (CurrentMatch.Value?.Completed.Value == true) scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(TeamWinScreen)); }, delay_before_progression); - else if (currentMatch.Value?.Completed.Value == false) + else if (CurrentMatch.Value?.Completed.Value == false) scheduledOperation = Scheduler.AddDelayed(() => { sceneManager?.SetScreen(typeof(MapPoolScreen)); }, delay_before_progression); } diff --git a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs index 9785b7e647..32d458e191 100644 --- a/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs +++ b/osu.Game.Tournament/Screens/Showcase/ShowcaseScreen.cs @@ -2,10 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Tournament.Components; using osu.Framework.Graphics.Shapes; +using osu.Game.Tournament.Models; using osuTK.Graphics; namespace osu.Game.Tournament.Screens.Showcase @@ -39,5 +41,11 @@ namespace osu.Game.Tournament.Screens.Showcase } }); } + + protected override void CurrentMatchChanged(ValueChangedEvent match) + { + // showcase screen doesn't care about a match being selected. + // base call intentionally omitted to not show match warning. + } } } diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 4f66d89b7f..71aed69738 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -18,12 +18,10 @@ using osuTK; namespace osu.Game.Tournament.Screens.TeamIntro { - public class SeedingScreen : TournamentScreen, IProvideVideo + public class SeedingScreen : TournamentMatchScreen, IProvideVideo { private Container mainContainer; - private readonly Bindable currentMatch = new Bindable(); - private readonly Bindable currentTeam = new Bindable(); [BackgroundDependencyLoader] @@ -50,13 +48,13 @@ namespace osu.Game.Tournament.Screens.TeamIntro { RelativeSizeAxes = Axes.X, Text = "Show first team", - Action = () => currentTeam.Value = currentMatch.Value.Team1.Value, + Action = () => currentTeam.Value = CurrentMatch.Value.Team1.Value, }, new TourneyButton { RelativeSizeAxes = Axes.X, Text = "Show second team", - Action = () => currentTeam.Value = currentMatch.Value.Team2.Value, + Action = () => currentTeam.Value = CurrentMatch.Value.Team2.Value, }, new SettingsTeamDropdown(LadderInfo.Teams) { @@ -67,9 +65,6 @@ namespace osu.Game.Tournament.Screens.TeamIntro } }; - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(LadderInfo.CurrentMatch); - currentTeam.BindValueChanged(teamChanged, true); } @@ -84,8 +79,15 @@ namespace osu.Game.Tournament.Screens.TeamIntro showTeam(team.NewValue); } - private void matchChanged(ValueChangedEvent match) => - currentTeam.Value = currentMatch.Value.Team1.Value; + protected override void CurrentMatchChanged(ValueChangedEvent match) + { + base.CurrentMatchChanged(match); + + if (match.NewValue == null) + return; + + currentTeam.Value = match.NewValue.Team1.Value; + } private void showTeam(TournamentTeam team) { diff --git a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs index 6c2848897b..74957cbca5 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/TeamIntroScreen.cs @@ -12,12 +12,10 @@ using osuTK; namespace osu.Game.Tournament.Screens.TeamIntro { - public class TeamIntroScreen : TournamentScreen, IProvideVideo + public class TeamIntroScreen : TournamentMatchScreen, IProvideVideo { private Container mainContainer; - private readonly Bindable currentMatch = new Bindable(); - [BackgroundDependencyLoader] private void load(Storage storage) { @@ -35,18 +33,16 @@ namespace osu.Game.Tournament.Screens.TeamIntro RelativeSizeAxes = Axes.Both, } }; - - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(LadderInfo.CurrentMatch); } - private void matchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { + base.CurrentMatchChanged(match); + + mainContainer.Clear(); + if (match.NewValue == null) - { - mainContainer.Clear(); return; - } const float y_flag_offset = 292; diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 7ca262a2e8..ebe2908b74 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -13,11 +13,10 @@ using osuTK; namespace osu.Game.Tournament.Screens.TeamWin { - public class TeamWinScreen : TournamentScreen, IProvideVideo + public class TeamWinScreen : TournamentMatchScreen, IProvideVideo { private Container mainContainer; - private readonly Bindable currentMatch = new Bindable(); private readonly Bindable currentCompleted = new Bindable(); private TourneyVideo blueWinVideo; @@ -48,17 +47,19 @@ namespace osu.Game.Tournament.Screens.TeamWin } }; - currentMatch.BindValueChanged(matchChanged); - currentMatch.BindTo(ladder.CurrentMatch); - currentCompleted.BindValueChanged(_ => update()); } - private void matchChanged(ValueChangedEvent match) + protected override void CurrentMatchChanged(ValueChangedEvent match) { - currentCompleted.UnbindBindings(); - currentCompleted.BindTo(match.NewValue.Completed); + base.CurrentMatchChanged(match); + currentCompleted.UnbindBindings(); + + if (match.NewValue == null) + return; + + currentCompleted.BindTo(match.NewValue.Completed); update(); } @@ -66,7 +67,7 @@ namespace osu.Game.Tournament.Screens.TeamWin private void update() => Schedule(() => { - var match = currentMatch.Value; + var match = CurrentMatch.Value; if (match.Winner == null) { From 5b694ad28b1610b24a0d9303d3c39f18e0d14e4b Mon Sep 17 00:00:00 2001 From: huoyaoyuan Date: Sat, 17 Jul 2021 19:37:32 +0800 Subject: [PATCH 53/63] Replace string with LocalisableString --- osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs index 443b3dcf01..68faadeaf1 100644 --- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs @@ -4,15 +4,16 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { - public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader + public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader { - protected override OsuTabControl CreateTabControl() => new OverlayHeaderBreadcrumbControl(); + protected override OsuTabControl CreateTabControl() => new OverlayHeaderBreadcrumbControl(); - public class OverlayHeaderBreadcrumbControl : BreadcrumbControl + public class OverlayHeaderBreadcrumbControl : BreadcrumbControl { public OverlayHeaderBreadcrumbControl() { @@ -26,7 +27,7 @@ namespace osu.Game.Overlays AccentColour = colourProvider.Light2; } - protected override TabItem CreateTabItem(string value) => new ControlTabItem(value) + protected override TabItem CreateTabItem(LocalisableString value) => new ControlTabItem(value) { AccentColour = AccentColour, }; @@ -35,7 +36,7 @@ namespace osu.Game.Overlays { protected override float ChevronSize => 8; - public ControlTabItem(string value) + public ControlTabItem(LocalisableString value) : base(value) { RelativeSizeAxes = Axes.Y; From 22ff40fdd5f1362c511f3f2f31be0a1fe0e1c3b5 Mon Sep 17 00:00:00 2001 From: kj415j45 Date: Sat, 17 Jul 2021 19:40:25 +0800 Subject: [PATCH 54/63] Fix broken WikiHeader --- osu.Game/Overlays/Wiki/WikiHeader.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 6b8cba48b4..2e15fbb566 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Wiki @@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Wiki return; TabControl.Clear(); - Current.Value = null; + Current.Value = string.Empty; TabControl.AddItem(index_page_string); @@ -51,7 +52,7 @@ namespace osu.Game.Overlays.Wiki Current.Value = e.NewValue.Title; } - private void onCurrentChange(ValueChangedEvent e) + private void onCurrentChange(ValueChangedEvent e) { if (e.NewValue == TabControl.Items.LastOrDefault()) return; From e3c10e39945fe7612a467a41ebca735cbd7bbc7a Mon Sep 17 00:00:00 2001 From: huoyaoyuan Date: Sat, 17 Jul 2021 19:40:32 +0800 Subject: [PATCH 55/63] Add missing LocalisableString --- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index c447d7f609..f97880c7ca 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Framework.Utils; using osu.Game.Graphics.Sprites; @@ -160,7 +161,12 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Top = 5, Bottom = 5 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = (value as IHasDescription)?.Description ?? (value as Enum)?.GetLocalisableDescription() ?? value.ToString(), + Text = value switch{ + IHasDescription desc => desc?.Description, + Enum e => e.GetLocalisableDescription(), + LocalisableString l => l, + var other => other.ToString() + }, Font = OsuFont.GetFont(size: 14) }, Bar = new Box From 7859d02c5b4e0d65eef8b4ba9639b03c8a45af43 Mon Sep 17 00:00:00 2001 From: bdach Date: Sat, 17 Jul 2021 20:33:26 +0800 Subject: [PATCH 56/63] Allow null for breadcrumb control --- osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs | 10 +++++----- osu.Game/Overlays/Wiki/WikiHeader.cs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs index 68faadeaf1..0d383c374f 100644 --- a/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs +++ b/osu.Game/Overlays/BreadcrumbControlOverlayHeader.cs @@ -9,11 +9,11 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { - public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader + public abstract class BreadcrumbControlOverlayHeader : TabControlOverlayHeader { - protected override OsuTabControl CreateTabControl() => new OverlayHeaderBreadcrumbControl(); + protected override OsuTabControl CreateTabControl() => new OverlayHeaderBreadcrumbControl(); - public class OverlayHeaderBreadcrumbControl : BreadcrumbControl + public class OverlayHeaderBreadcrumbControl : BreadcrumbControl { public OverlayHeaderBreadcrumbControl() { @@ -27,7 +27,7 @@ namespace osu.Game.Overlays AccentColour = colourProvider.Light2; } - protected override TabItem CreateTabItem(LocalisableString value) => new ControlTabItem(value) + protected override TabItem CreateTabItem(LocalisableString? value) => new ControlTabItem(value) { AccentColour = AccentColour, }; @@ -36,7 +36,7 @@ namespace osu.Game.Overlays { protected override float ChevronSize => 8; - public ControlTabItem(LocalisableString value) + public ControlTabItem(LocalisableString? value) : base(value) { RelativeSizeAxes = Axes.Y; diff --git a/osu.Game/Overlays/Wiki/WikiHeader.cs b/osu.Game/Overlays/Wiki/WikiHeader.cs index 2e15fbb566..fb87486b4e 100644 --- a/osu.Game/Overlays/Wiki/WikiHeader.cs +++ b/osu.Game/Overlays/Wiki/WikiHeader.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Wiki return; TabControl.Clear(); - Current.Value = string.Empty; + Current.Value = null; TabControl.AddItem(index_page_string); @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Wiki Current.Value = e.NewValue.Title; } - private void onCurrentChange(ValueChangedEvent e) + private void onCurrentChange(ValueChangedEvent e) { if (e.NewValue == TabControl.Items.LastOrDefault()) return; From 5b4a1ef70a3294597979d5185919b51ac7a1f28d Mon Sep 17 00:00:00 2001 From: kj415j45 Date: Sat, 17 Jul 2021 20:40:59 +0800 Subject: [PATCH 57/63] Update test to match Breadcrumb change --- osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs index 863fa48ddf..e7e6030c66 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneWikiHeader.cs @@ -7,6 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Wiki; @@ -96,7 +97,7 @@ namespace osu.Game.Tests.Visual.Online private class TestHeader : WikiHeader { - public IReadOnlyList TabControlItems => TabControl.Items; + public IReadOnlyList TabControlItems => TabControl.Items; } } } From ca1080dfb5d3cfa28319b69785c55e8c2278781c Mon Sep 17 00:00:00 2001 From: kj415j45 Date: Sat, 17 Jul 2021 21:16:57 +0800 Subject: [PATCH 58/63] use switch statement Co-authored-by: bdach --- .../Graphics/UserInterface/OsuTabControl.cs | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index f97880c7ca..21e7a68e99 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -154,6 +154,26 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; + LocalisableString text; + switch (value) + { + case IHasDescription hasDescription: + text = hasDescription.GetDescription(); + break; + + case Enum e: + text = e.GetLocalisableDescription(); + break; + + case LocalisableString l: + text = l; + break; + + default: + text = value.ToString(); + break; + }; + Children = new Drawable[] { Text = new OsuSpriteText @@ -161,12 +181,7 @@ namespace osu.Game.Graphics.UserInterface Margin = new MarginPadding { Top = 5, Bottom = 5 }, Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, - Text = value switch{ - IHasDescription desc => desc?.Description, - Enum e => e.GetLocalisableDescription(), - LocalisableString l => l, - var other => other.ToString() - }, + Text = text, Font = OsuFont.GetFont(size: 14) }, Bar = new Box From f4eeb9139ed3784b9ada9f7b13cc307b839f84ab Mon Sep 17 00:00:00 2001 From: kj415j45 Date: Sat, 17 Jul 2021 21:37:58 +0800 Subject: [PATCH 59/63] Correct code style --- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 21e7a68e99..3572ea5c31 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -155,6 +155,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.Y; LocalisableString text; + switch (value) { case IHasDescription hasDescription: @@ -172,7 +173,7 @@ namespace osu.Game.Graphics.UserInterface default: text = value.ToString(); break; - }; + } Children = new Drawable[] { From 9a2fb8ca6c26daf8cb926f8744cc550496e7e5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Jul 2021 18:06:45 +0200 Subject: [PATCH 60/63] Add test coverage for null mod on seeding screen --- .../Screens/TestSceneSeedingScreen.cs | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs index d414d8e36e..a18e73e38f 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSeedingScreen.cs @@ -1,9 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Tournament.Models; +using osu.Game.Tournament.Screens.Ladder.Components; using osu.Game.Tournament.Screens.TeamIntro; namespace osu.Game.Tournament.Tests.Screens @@ -11,16 +15,41 @@ namespace osu.Game.Tournament.Tests.Screens public class TestSceneSeedingScreen : TournamentTestScene { [Cached] - private readonly LadderInfo ladder = new LadderInfo(); - - [BackgroundDependencyLoader] - private void load() + private readonly LadderInfo ladder = new LadderInfo { - Add(new SeedingScreen + Teams = + { + new TournamentTeam + { + FullName = { Value = @"Japan" }, + Acronym = { Value = "JPN" }, + SeedingResults = + { + new SeedingResult + { + // Mod intentionally left blank. + Seed = { Value = 4 } + }, + new SeedingResult + { + Mod = { Value = "DT" }, + Seed = { Value = 8 } + } + } + } + } + }; + + [Test] + public void TestBasic() + { + AddStep("create seeding screen", () => Add(new SeedingScreen { FillMode = FillMode.Fit, FillAspectRatio = 16 / 9f - }); + })); + + AddStep("set team to Japan", () => this.ChildrenOfType().Single().Current.Value = ladder.Teams.Single()); } } } From 714255e6d4059e72e46083328367e1d534726581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Jul 2021 17:53:58 +0200 Subject: [PATCH 61/63] Fix seeding screen crashing on seedings with null mod --- .../Screens/TeamIntro/SeedingScreen.cs | 64 ++++++++++--------- 1 file changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 4f66d89b7f..2f82578586 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -179,44 +179,48 @@ namespace osu.Game.Tournament.Screens.TeamIntro [BackgroundDependencyLoader] private void load(TextureStore textures) { + FillFlowContainer row; + InternalChildren = new Drawable[] { - new FillFlowContainer + row = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), - Children = new Drawable[] - { - new Sprite - { - Texture = textures.Get($"mods/{mods.ToLower()}"), - Scale = new Vector2(0.5f) - }, - new Container - { - Size = new Vector2(50, 16), - CornerRadius = 10, - Masking = true, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR, - }, - new TournamentSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = seeding.ToString("#,0"), - Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR - }, - } - }, - } }, }; + + if (!string.IsNullOrEmpty(mods)) + { + row.Add(new Sprite + { + Texture = textures.Get($"mods/{mods.ToLower()}"), + Scale = new Vector2(0.5f) + }); + } + + row.Add(new Container + { + Size = new Vector2(50, 16), + CornerRadius = 10, + Masking = true, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = TournamentGame.ELEMENT_BACKGROUND_COLOUR, + }, + new TournamentSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = seeding.ToString("#,0"), + Colour = TournamentGame.ELEMENT_FOREGROUND_COLOUR + }, + } + }); } } } From 49f0c707f6cb4e5676105f88e015966dbc41794c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 18 Jul 2021 22:34:28 +0900 Subject: [PATCH 62/63] Move approach circle hiding within `BeginAbsoluteSequence` --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 5fa3bc4520..471847dbd6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -147,21 +147,22 @@ namespace osu.Game.Rulesets.Osu.Mods double startTime = circle.HitObject.StartTime; double preempt = circle.HitObject.TimePreempt; - using (drawable.BeginAbsoluteSequence(startTime - preempt)) + using (circle.BeginAbsoluteSequence(startTime - preempt)) { // initial state - drawable.ScaleTo(0.5f) + circle.ScaleTo(0.5f) .FadeColour(OsuColour.Gray(0.5f)); // scale to final size - drawable.ScaleTo(1f, preempt); + circle.ScaleTo(1f, preempt); + + // Remove approach circles + circle.ApproachCircle.Hide(); } - using (drawable.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration)) - drawable.FadeColour(Color4.White, undim_duration); + using (circle.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration)) + circle.FadeColour(Color4.White, undim_duration); - // Remove approach circles - circle.ApproachCircle.Hide(); } #endregion From fdebe4b94ac30050334dc6330308c54506a50079 Mon Sep 17 00:00:00 2001 From: Henry Lin Date: Sun, 18 Jul 2021 22:01:26 +0800 Subject: [PATCH 63/63] Code formatting fixes --- osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs index 471847dbd6..918b9b1c94 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTarget.cs @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.Mods { // initial state circle.ScaleTo(0.5f) - .FadeColour(OsuColour.Gray(0.5f)); + .FadeColour(OsuColour.Gray(0.5f)); // scale to final size circle.ScaleTo(1f, preempt); @@ -162,7 +162,6 @@ namespace osu.Game.Rulesets.Osu.Mods using (circle.BeginAbsoluteSequence(startTime - controlPointInfo.TimingPointAt(startTime).BeatLength - undim_duration)) circle.FadeColour(Color4.White, undim_duration); - } #endregion