Merge branch 'master' into match-subscreen-redesign

This commit is contained in:
Dean Herbert 2020-07-07 17:16:05 +09:00
commit 56a8b0d3f5
127 changed files with 3075 additions and 496 deletions

View File

@ -2,7 +2,6 @@
<project version="4"> <project version="4">
<component name="ProjectModuleManager"> <component name="ProjectModuleManager">
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/.idea.osu.Desktop.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/.idea.osu.Desktop.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" /> <module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" />
</modules> </modules>
</component> </component>

View File

@ -52,6 +52,6 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.622.1" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.622.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.623.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.704.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -9,7 +9,7 @@ using osu.Framework.Android;
namespace osu.Android namespace osu.Android
{ {
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = true)] [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullSensor, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity public class OsuGameActivity : AndroidGameActivity
{ {
protected override Framework.Game CreateGame() => new OsuGameAndroid(); protected override Framework.Game CreateGame() => new OsuGameAndroid();

View File

@ -8,7 +8,6 @@ using NUnit.Framework;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Catch.Mods;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
@ -83,7 +82,7 @@ namespace osu.Game.Rulesets.Catch.Tests
public float Position public float Position
{ {
get => HitObject?.X * CatchPlayfield.BASE_WIDTH ?? position; get => HitObject?.X ?? position;
set => position = value; set => position = value;
} }

View File

@ -27,15 +27,15 @@ namespace osu.Game.Rulesets.Catch.Tests
for (int i = 0; i < 100; i++) for (int i = 0; i < 100; i++)
{ {
float width = (i % 10 + 1) / 20f; float width = (i % 10 + 1) / 20f * CatchPlayfield.WIDTH;
beatmap.HitObjects.Add(new JuiceStream beatmap.HitObjects.Add(new JuiceStream
{ {
X = 0.5f - width / 2, X = CatchPlayfield.CENTER_X - width / 2,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0) new Vector2(width, 0)
}), }),
StartTime = i * 2000, StartTime = i * 2000,
NewCombo = i % 8 == 0 NewCombo = i % 8 == 0

View File

@ -4,6 +4,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
namespace osu.Game.Rulesets.Catch.Tests namespace osu.Game.Rulesets.Catch.Tests
{ {
@ -22,7 +23,14 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
for (int i = 0; i < 512; i++) for (int i = 0; i < 512; i++)
beatmap.HitObjects.Add(new Fruit { X = 0.5f + i / 2048f * (i % 10 - 5), StartTime = i * 100, NewCombo = i % 8 == 0 }); {
beatmap.HitObjects.Add(new Fruit
{
X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH,
StartTime = i * 100,
NewCombo = i % 8 == 0
});
}
return beatmap; return beatmap;
} }

View File

@ -76,8 +76,8 @@ namespace osu.Game.Rulesets.Catch.Tests
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size })
{ {
Anchor = Anchor.CentreLeft, Anchor = Anchor.Centre,
Origin = Anchor.TopLeft, Origin = Anchor.TopCentre,
CreateDrawableRepresentation = ((DrawableRuleset<CatchHitObject>)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation CreateDrawableRepresentation = ((DrawableRuleset<CatchHitObject>)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation
}, },
}); });

View File

@ -158,8 +158,8 @@ namespace osu.Game.Rulesets.Catch.Tests
private float getXCoords(bool hit) private float getXCoords(bool hit)
{ {
const float x_offset = 0.2f; const float x_offset = 0.2f * CatchPlayfield.WIDTH;
float xCoords = drawableRuleset.Playfield.Width / 2; float xCoords = CatchPlayfield.CENTER_X;
if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield) if (drawableRuleset.Playfield is CatchPlayfield catchPlayfield)
catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset; catchPlayfield.CatcherArea.MovableCatcher.X = xCoords - x_offset;

View File

@ -47,13 +47,13 @@ namespace osu.Game.Rulesets.Catch.Tests
}; };
// Should produce a hyper-dash (edge case test) // Should produce a hyper-dash (edge case test)
beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56 / 512f, NewCombo = true }); beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true });
beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308 / 512f, NewCombo = true }); beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true });
double startTime = 3000; double startTime = 3000;
const float left_x = 0.02f; const float left_x = 0.02f * CatchPlayfield.WIDTH;
const float right_x = 0.98f; const float right_x = 0.98f * CatchPlayfield.WIDTH;
createObjects(() => new Fruit { X = left_x }); createObjects(() => new Fruit { X = left_x });
createObjects(() => new TestJuiceStream(right_x), 1); createObjects(() => new TestJuiceStream(right_x), 1);

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
new JuiceStream new JuiceStream
{ {
X = 0.5f, X = CatchPlayfield.CENTER_X,
Path = new SliderPath(PathType.Linear, new[] Path = new SliderPath(PathType.Linear, new[]
{ {
Vector2.Zero, Vector2.Zero,
@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests
}, },
new Banana new Banana
{ {
X = 0.5f, X = CatchPlayfield.CENTER_X,
StartTime = 1000, StartTime = 1000,
NewCombo = true NewCombo = true
} }

View File

@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Path = curveData.Path, Path = curveData.Path,
NodeSamples = curveData.NodeSamples, NodeSamples = curveData.NodeSamples,
RepeatCount = curveData.RepeatCount, RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, X = positionData?.X ?? 0,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0
@ -59,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
Samples = obj.Samples, Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0, ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH X = positionData?.X ?? 0
}.Yield(); }.Yield();
} }
} }

View File

@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case BananaShower bananaShower: case BananaShower bananaShower:
foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>()) foreach (var banana in bananaShower.NestedHitObjects.OfType<Banana>())
{ {
banana.XOffset = (float)rng.NextDouble(); banana.XOffset = (float)(rng.NextDouble() * CatchPlayfield.WIDTH);
rng.Next(); // osu!stable retrieved a random banana type rng.Next(); // osu!stable retrieved a random banana type
rng.Next(); // osu!stable retrieved a random banana rotation rng.Next(); // osu!stable retrieved a random banana rotation
rng.Next(); // osu!stable retrieved a random banana colour rng.Next(); // osu!stable retrieved a random banana colour
@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
case JuiceStream juiceStream: case JuiceStream juiceStream:
// Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead. // Todo: BUG!! Stable used the last control point as the final position of the path, but it should use the computed path instead.
lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X / CatchPlayfield.BASE_WIDTH; lastPosition = juiceStream.X + juiceStream.Path.ControlPoints[^1].Position.Value.X;
// Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead. // Todo: BUG!! Stable attempted to use the end time of the stream, but referenced it too early in execution and used the start time instead.
lastStartTime = juiceStream.StartTime; lastStartTime = juiceStream.StartTime;
@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
catchObject.XOffset = 0; catchObject.XOffset = 0;
if (catchObject is TinyDroplet) if (catchObject is TinyDroplet)
catchObject.XOffset = Math.Clamp(rng.Next(-20, 20) / CatchPlayfield.BASE_WIDTH, -catchObject.X, 1 - catchObject.X); catchObject.XOffset = Math.Clamp(rng.Next(-20, 20), -catchObject.X, CatchPlayfield.WIDTH - catchObject.X);
else if (catchObject is Droplet) else if (catchObject is Droplet)
rng.Next(); // osu!stable retrieved a random droplet rotation rng.Next(); // osu!stable retrieved a random droplet rotation
} }
@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
} }
// ReSharper disable once PossibleLossOfFraction // ReSharper disable once PossibleLossOfFraction
if (Math.Abs(positionDiff * CatchPlayfield.BASE_WIDTH) < timeDiff / 3) if (Math.Abs(positionDiff) < timeDiff / 3)
applyOffset(ref offsetPosition, positionDiff); applyOffset(ref offsetPosition, positionDiff);
hitObject.XOffset = offsetPosition - hitObject.X; hitObject.XOffset = offsetPosition - hitObject.X;
@ -149,12 +149,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng) private static void applyRandomOffset(ref float position, double maxOffset, FastRandom rng)
{ {
bool right = rng.NextBool(); bool right = rng.NextBool();
float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset))) / CatchPlayfield.BASE_WIDTH; float rand = Math.Min(20, (float)rng.Next(0, Math.Max(0, maxOffset)));
if (right) if (right)
{ {
// Clamp to the right bound // Clamp to the right bound
if (position + rand <= 1) if (position + rand <= CatchPlayfield.WIDTH)
position += rand; position += rand;
else else
position -= rand; position -= rand;
@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = CatcherArea.GetCatcherSize(beatmap.BeatmapInfo.BaseDifficulty) / 2; double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0; int lastDirection = 0;
double lastExcess = halfCatcherWidth; double lastExcess = halfCatcherWidth;

View File

@ -3,7 +3,6 @@
using System; using System;
using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -33,8 +32,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps. // We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
var scalingFactor = normalized_hitobject_radius / halfCatcherWidth; var scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
NormalizedPosition = BaseObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; NormalizedPosition = BaseObject.X * scalingFactor;
LastNormalizedPosition = LastObject.X * CatchPlayfield.BASE_WIDTH * scalingFactor; LastNormalizedPosition = LastObject.X * scalingFactor;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure // Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime); StrainTime = Math.Max(40, DeltaTime);

View File

@ -3,7 +3,6 @@
using System; using System;
using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
@ -68,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
} }
// Bonus for edge dashes. // Bonus for edge dashes.
if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f / CatchPlayfield.BASE_WIDTH) if (catchCurrent.LastObject.DistanceToHyperDash <= 20.0f)
{ {
if (!catchCurrent.LastObject.HyperDash) if (!catchCurrent.LastObject.HyperDash)
edgeDashBonus += 5.7; edgeDashBonus += 5.7;
@ -78,7 +77,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition; playerPosition = catchCurrent.NormalizedPosition;
} }
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash * CatchPlayfield.BASE_WIDTH) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catchCurrent.ClockRate, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
} }
lastPlayerPosition = playerPosition; lastPlayerPosition = playerPosition;

View File

@ -5,6 +5,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -17,6 +18,9 @@ namespace osu.Game.Rulesets.Catch.Objects
private float x; private float x;
/// <summary>
/// The horizontal position of the fruit between 0 and <see cref="CatchPlayfield.WIDTH"/>.
/// </summary>
public float X public float X
{ {
get => x + XOffset; get => x + XOffset;

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Catch.UI;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -70,12 +71,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables
public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale;
protected override float SamplePlaybackPosition => HitObject.X; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH;
protected DrawableCatchHitObject(CatchHitObject hitObject) protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
RelativePositionAxes = Axes.X;
X = hitObject.X; X = hitObject.X;
} }

View File

@ -7,7 +7,6 @@ using System.Threading;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
@ -80,7 +79,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
StartTime = t + lastEvent.Value.Time, StartTime = t + lastEvent.Value.Time,
X = X + Path.PositionAt( X = X + Path.PositionAt(
lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X / CatchPlayfield.BASE_WIDTH, lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X,
}); });
} }
} }
@ -97,7 +96,7 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
Samples = dropletSamples, Samples = dropletSamples,
StartTime = e.Time, StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, X = X + Path.PositionAt(e.PathProgress).X,
}); });
break; break;
@ -108,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Objects
{ {
Samples = Samples, Samples = Samples,
StartTime = e.Time, StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X / CatchPlayfield.BASE_WIDTH, X = X + Path.PositionAt(e.PathProgress).X,
}); });
break; break;
} }
} }
} }
public float EndX => X + this.CurvePositionAt(1).X / CatchPlayfield.BASE_WIDTH; public float EndX => X + this.CurvePositionAt(1).X;
public double Duration public double Duration
{ {

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Replays
// todo: add support for HT DT // todo: add support for HT DT
const double dash_speed = Catcher.BASE_SPEED; const double dash_speed = Catcher.BASE_SPEED;
const double movement_speed = dash_speed / 2; const double movement_speed = dash_speed / 2;
float lastPosition = 0.5f; float lastPosition = CatchPlayfield.CENTER_X;
double lastTime = 0; double lastTime = 0;
void moveToNext(CatchHitObject h) void moveToNext(CatchHitObject h)
@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Replays
bool impossibleJump = speedRequired > movement_speed * 2; bool impossibleJump = speedRequired > movement_speed * 2;
// todo: get correct catcher size, based on difficulty CS. // todo: get correct catcher size, based on difficulty CS.
const float catcher_width_half = CatcherArea.CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * 0.3f * 0.5f; const float catcher_width_half = CatcherArea.CATCHER_SIZE * 0.3f * 0.5f;
if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X) if (lastPosition - catcher_width_half < h.X && lastPosition + catcher_width_half > h.X)
{ {

View File

@ -4,7 +4,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays.Legacy; using osu.Game.Replays.Legacy;
using osu.Game.Rulesets.Catch.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.Replays.Types;
@ -41,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays
public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null)
{ {
Position = currentFrame.Position.X / CatchPlayfield.BASE_WIDTH; Position = currentFrame.Position.X;
Dashing = currentFrame.ButtonState == ReplayButtonState.Left1; Dashing = currentFrame.ButtonState == ReplayButtonState.Left1;
if (Dashing) if (Dashing)
@ -63,7 +62,7 @@ namespace osu.Game.Rulesets.Catch.Replays
if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1; if (Actions.Contains(CatchAction.Dash)) state |= ReplayButtonState.Left1;
return new LegacyReplayFrame(Time, Position * CatchPlayfield.BASE_WIDTH, null, state); return new LegacyReplayFrame(Time, Position, null, state);
} }
} }
} }

View File

@ -16,7 +16,16 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
public class CatchPlayfield : ScrollingPlayfield public class CatchPlayfield : ScrollingPlayfield
{ {
public const float BASE_WIDTH = 512; /// <summary>
/// The width of the playfield.
/// The horizontal movement of the catcher is confined in the area of this width.
/// </summary>
public const float WIDTH = 512;
/// <summary>
/// The center position of the playfield.
/// </summary>
public const float CENTER_X = WIDTH / 2;
internal readonly CatcherArea CatcherArea; internal readonly CatcherArea CatcherArea;

View File

@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
base.Update(); base.Update();
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH); Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH);
Size = Vector2.Divide(Vector2.One, Scale); Size = Vector2.Divide(Vector2.One, Scale);
} }
} }

View File

@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// <summary> /// <summary>
/// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable. /// The relative space to cover in 1 millisecond. based on 1 game pixel per millisecond as in osu-stable.
/// </summary> /// </summary>
public const double BASE_SPEED = 1.0 / 512; public const double BASE_SPEED = 1.0;
public Container ExplodingFruitTarget; public Container ExplodingFruitTarget;
@ -104,9 +104,6 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
this.trailsTarget = trailsTarget; this.trailsTarget = trailsTarget;
RelativePositionAxes = Axes.X;
X = 0.5f;
Origin = Anchor.TopCentre; Origin = Anchor.TopCentre;
Size = new Vector2(CatcherArea.CATCHER_SIZE); Size = new Vector2(CatcherArea.CATCHER_SIZE);
@ -209,8 +206,8 @@ namespace osu.Game.Rulesets.Catch.UI
var halfCatchWidth = catchWidth * 0.5f; var halfCatchWidth = catchWidth * 0.5f;
// this stuff wil disappear once we move fruit to non-relative coordinate space in the future. // this stuff wil disappear once we move fruit to non-relative coordinate space in the future.
var catchObjectPosition = fruit.X * CatchPlayfield.BASE_WIDTH; var catchObjectPosition = fruit.X;
var catcherPosition = Position.X * CatchPlayfield.BASE_WIDTH; var catcherPosition = Position.X;
var validCatch = var validCatch =
catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition >= catcherPosition - halfCatchWidth &&
@ -224,7 +221,7 @@ namespace osu.Game.Rulesets.Catch.UI
{ {
var target = fruit.HyperDashTarget; var target = fruit.HyperDashTarget;
var timeDifference = target.StartTime - fruit.StartTime; var timeDifference = target.StartTime - fruit.StartTime;
double positionDifference = target.X * CatchPlayfield.BASE_WIDTH - catcherPosition; double positionDifference = target.X - catcherPosition;
var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0);
SetHyperDashState(Math.Abs(velocity), target.X); SetHyperDashState(Math.Abs(velocity), target.X);
@ -331,7 +328,7 @@ namespace osu.Game.Rulesets.Catch.UI
public void UpdatePosition(float position) public void UpdatePosition(float position)
{ {
position = Math.Clamp(position, 0, 1); position = Math.Clamp(position, 0, CatchPlayfield.WIDTH);
if (position == X) if (position == X)
return; return;

View File

@ -31,14 +31,8 @@ namespace osu.Game.Rulesets.Catch.UI
public CatcherArea(BeatmapDifficulty difficulty = null) public CatcherArea(BeatmapDifficulty difficulty = null)
{ {
RelativeSizeAxes = Axes.X; Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
Height = CATCHER_SIZE; Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X };
Child = MovableCatcher = new Catcher(this, difficulty);
}
public static float GetCatcherSize(BeatmapDifficulty difficulty)
{
return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
} }
public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) public void OnResult(DrawableCatchHitObject fruit, JudgementResult result)

View File

@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Mania.Tests
[TestCase("convert-samples")] [TestCase("convert-samples")]
[TestCase("mania-samples")] [TestCase("mania-samples")]
[TestCase("slider-convert-samples")]
public void Test(string name) => base.Test(name); public void Test(string name) => base.Test(name);
protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject) protected override IEnumerable<SampleConvertValue> CreateConvertValue(HitObject hitObject)
@ -29,13 +30,16 @@ namespace osu.Game.Rulesets.Mania.Tests
StartTime = hitObject.StartTime, StartTime = hitObject.StartTime,
EndTime = hitObject.GetEndTime(), EndTime = hitObject.GetEndTime(),
Column = ((ManiaHitObject)hitObject).Column, Column = ((ManiaHitObject)hitObject).Column,
NodeSamples = getSampleNames((hitObject as HoldNote)?.NodeSamples) Samples = getSampleNames(hitObject.Samples),
NodeSamples = getNodeSampleNames((hitObject as HoldNote)?.NodeSamples)
}; };
} }
private IList<IList<string>> getSampleNames(List<IList<HitSampleInfo>> hitSampleInfo) private IList<string> getSampleNames(IList<HitSampleInfo> hitSampleInfo)
=> hitSampleInfo?.Select(samples => => hitSampleInfo.Select(sample => sample.LookupNames.First()).ToList();
(IList<string>)samples.Select(sample => sample.LookupNames.First()).ToList())
private IList<IList<string>> getNodeSampleNames(List<IList<HitSampleInfo>> hitSampleInfo)
=> hitSampleInfo?.Select(getSampleNames)
.ToList(); .ToList();
protected override Ruleset CreateRuleset() => new ManiaRuleset(); protected override Ruleset CreateRuleset() => new ManiaRuleset();
@ -51,14 +55,19 @@ namespace osu.Game.Rulesets.Mania.Tests
public double StartTime; public double StartTime;
public double EndTime; public double EndTime;
public int Column; public int Column;
public IList<string> Samples;
public IList<IList<string>> NodeSamples; public IList<IList<string>> NodeSamples;
public bool Equals(SampleConvertValue other) public bool Equals(SampleConvertValue other)
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& samplesEqual(NodeSamples, other.NodeSamples); && samplesEqual(Samples, other.Samples)
&& nodeSamplesEqual(NodeSamples, other.NodeSamples);
private static bool samplesEqual(ICollection<IList<string>> firstSampleList, ICollection<IList<string>> secondSampleList) private static bool samplesEqual(ICollection<string> firstSampleList, ICollection<string> secondSampleList)
=> firstSampleList.SequenceEqual(secondSampleList);
private static bool nodeSamplesEqual(ICollection<IList<string>> firstSampleList, ICollection<IList<string>> secondSampleList)
{ {
if (firstSampleList == null && secondSampleList == null) if (firstSampleList == null && secondSampleList == null)
return true; return true;

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 3
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:

View File

@ -0,0 +1,10 @@
osu file format v14
[General]
Mode: 3
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,1,0:0:0:0:

View File

@ -9,4 +9,6 @@ Hit50: mania/hit50
Hit100: mania/hit100 Hit100: mania/hit100
Hit200: mania/hit200 Hit200: mania/hit200
Hit300: mania/hit300 Hit300: mania/hit300
Hit300g: mania/hit300g Hit300g: mania/hit300g
StageLeft: mania/stage-left
StageRight: mania/stage-right

View File

@ -0,0 +1,49 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Reflection;
using NUnit.Framework;
using osu.Framework.IO.Stores;
using osu.Game.Tests.Beatmaps;
namespace osu.Game.Rulesets.Mania.Tests
{
public class TestSceneManiaHitObjectSamples : HitObjectSampleTest
{
protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset();
protected override IResourceStore<byte[]> Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneManiaHitObjectSamples)));
/// <summary>
/// Tests that when a normal sample bank is used, the normal hitsound will be looked up.
/// </summary>
[Test]
public void TestManiaHitObjectNormalSampleBank()
{
const string expected_sample = "normal-hitnormal2";
SetupSkins(expected_sample, expected_sample);
CreateTestWithBeatmap("mania-hitobject-beatmap-normal-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
}
/// <summary>
/// Tests that when a custom sample bank is used, layered hitsounds are not played
/// (only the sample from the custom bank is looked up).
/// </summary>
[Test]
public void TestManiaHitObjectCustomSampleBank()
{
const string expected_sample = "normal-hitwhistle2";
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(expected_sample, unwanted_sample);
CreateTestWithBeatmap("mania-hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
}
}

View File

@ -483,9 +483,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (!(HitObject is IHasPathWithRepeats curveData)) if (!(HitObject is IHasPathWithRepeats curveData))
return null; return null;
double segmentTime = (EndTime - HitObject.StartTime) / spanCount; // mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
int index = (int)(segmentTime == 0 ? 0 : (time - HitObject.StartTime) / segmentTime);
// avoid slicing the list & creating copies, if at all possible. // avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();

View File

@ -30,6 +30,7 @@ using osu.Game.Rulesets.Mania.Skinning;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Mania namespace osu.Game.Rulesets.Mania
{ {
@ -309,6 +310,21 @@ namespace osu.Game.Rulesets.Mania
{ {
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v); return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
} }
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
}
};
} }
public enum PlayfieldType public enum PlayfieldType

View File

@ -9,7 +9,8 @@
["normal-hitnormal"], ["normal-hitnormal"],
["soft-hitnormal"], ["soft-hitnormal"],
["drum-hitnormal"] ["drum-hitnormal"]
] ],
"Samples": ["drum-hitnormal"]
}, { }, {
"StartTime": 1875.0, "StartTime": 1875.0,
"EndTime": 2750.0, "EndTime": 2750.0,
@ -17,14 +18,16 @@
"NodeSamples": [ "NodeSamples": [
["soft-hitnormal"], ["soft-hitnormal"],
["drum-hitnormal"] ["drum-hitnormal"]
] ],
"Samples": ["drum-hitnormal"]
}] }]
}, { }, {
"StartTime": 3750.0, "StartTime": 3750.0,
"Objects": [{ "Objects": [{
"StartTime": 3750.0, "StartTime": 3750.0,
"EndTime": 3750.0, "EndTime": 3750.0,
"Column": 3 "Column": 3,
"Samples": ["normal-hitnormal"]
}] }]
}] }]
} }

View File

@ -13,4 +13,4 @@ SliderTickRate:1
[HitObjects] [HitObjects]
88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0: 88,99,1000,6,0,L|306:259,2,245,0|0|0,1:0|2:0|3:0,0:0:0:0:
259,118,3750,1,0,0:0:0:0: 259,118,3750,1,0,1:0:0:0:

View File

@ -8,7 +8,8 @@
"NodeSamples": [ "NodeSamples": [
["normal-hitnormal"], ["normal-hitnormal"],
[] []
] ],
"Samples": ["normal-hitnormal"]
}] }]
}, { }, {
"StartTime": 2000.0, "StartTime": 2000.0,
@ -19,7 +20,8 @@
"NodeSamples": [ "NodeSamples": [
["drum-hitnormal"], ["drum-hitnormal"],
[] []
] ],
"Samples": ["drum-hitnormal"]
}] }]
}] }]
} }

View File

@ -0,0 +1,21 @@
{
"Mappings": [{
"StartTime": 8470.0,
"Objects": [{
"StartTime": 8470.0,
"EndTime": 8470.0,
"Column": 0,
"Samples": ["normal-hitnormal", "normal-hitclap"]
}, {
"StartTime": 8626.470587768974,
"EndTime": 8626.470587768974,
"Column": 1,
"Samples": ["normal-hitnormal"]
}, {
"StartTime": 8782.941175537948,
"EndTime": 8782.941175537948,
"Column": 2,
"Samples": ["normal-hitnormal", "normal-hitclap"]
}]
}]
}

View File

@ -0,0 +1,15 @@
osu file format v14
[Difficulty]
HPDrainRate:6
CircleSize:4
OverallDifficulty:8
ApproachRate:9.5
SliderMultiplier:2.00000000596047
SliderTickRate:1
[TimingPoints]
0,312.941176470588,4,1,0,100,1,0
[HitObjects]
82,216,8470,6,0,P|52:161|99:113,2,100,8|0|8,1:0|1:0|1:0,0:0:0:0:

View File

@ -9,6 +9,9 @@ using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
using System.Collections.Generic; using System.Collections.Generic;
using osu.Framework.Audio.Sample;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Legacy;
namespace osu.Game.Rulesets.Mania.Skinning namespace osu.Game.Rulesets.Mania.Skinning
{ {
@ -129,6 +132,15 @@ namespace osu.Game.Rulesets.Mania.Skinning
return this.GetAnimation(filename, true, true); return this.GetAnimation(filename, true, true);
} }
public override SampleChannel GetSample(ISampleInfo sampleInfo)
{
// layered hit sounds never play in mania
if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample && legacySample.IsLayered)
return new SampleChannelVirtual();
return Source.GetSample(sampleInfo);
}
public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) public override IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup)
{ {
if (lookup is ManiaSkinConfigurationLookup maniaLookup) if (lookup is ManiaSkinConfigurationLookup maniaLookup)

View File

@ -0,0 +1,132 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading;
using osu.Framework.Utils;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Scoring;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Visual;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestSceneAccuracyHeatmap : OsuManualInputManagerTestScene
{
private Box background;
private Drawable object1;
private Drawable object2;
private TestAccuracyHeatmap accuracyHeatmap;
private ScheduledDelegate automaticAdditionDelegate;
[SetUp]
public void Setup() => Schedule(() =>
{
automaticAdditionDelegate?.Cancel();
automaticAdditionDelegate = null;
Children = new[]
{
background = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333"),
},
object1 = new BorderCircle
{
Position = new Vector2(256, 192),
Colour = Color4.Yellow,
},
object2 = new BorderCircle
{
Position = new Vector2(100, 300),
},
accuracyHeatmap = new TestAccuracyHeatmap(new ScoreInfo { Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo })
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(130)
}
};
});
[Test]
public void TestManyHitPointsAutomatic()
{
AddStep("add scheduled delegate", () =>
{
automaticAdditionDelegate = Scheduler.AddDelayed(() =>
{
var randomPos = new Vector2(
RNG.NextSingle(object1.DrawPosition.X - object1.DrawSize.X / 2, object1.DrawPosition.X + object1.DrawSize.X / 2),
RNG.NextSingle(object1.DrawPosition.Y - object1.DrawSize.Y / 2, object1.DrawPosition.Y + object1.DrawSize.Y / 2));
// The background is used for ToLocalSpace() since we need to go _inside_ the DrawSizePreservingContainer (Content of TestScene).
accuracyHeatmap.AddPoint(object2.Position, object1.Position, randomPos, RNG.NextSingle(10, 500));
InputManager.MoveMouseTo(background.ToScreenSpace(randomPos));
}, 1, true);
});
AddWaitStep("wait for some hit points", 10);
}
[Test]
public void TestManualPlacement()
{
AddStep("return user input", () => InputManager.UseParentInput = true);
}
protected override bool OnMouseDown(MouseDownEvent e)
{
accuracyHeatmap.AddPoint(object2.Position, object1.Position, background.ToLocalSpace(e.ScreenSpaceMouseDownPosition), 50);
return true;
}
private class TestAccuracyHeatmap : AccuracyHeatmap
{
public TestAccuracyHeatmap(ScoreInfo score)
: base(score, new TestBeatmap(new OsuRuleset().RulesetInfo))
{
}
public new void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
=> base.AddPoint(start, end, hitPoint, radius);
}
private class BorderCircle : CircularContainer
{
public BorderCircle()
{
Origin = Anchor.Centre;
Size = new Vector2(100);
Masking = true;
BorderThickness = 2;
BorderColour = Color4.White;
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
},
new Circle
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(4),
}
};
}
}
}
}

View File

@ -0,0 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects;
using osuTK;
namespace osu.Game.Rulesets.Osu.Judgements
{
public class OsuHitCircleJudgementResult : OsuJudgementResult
{
/// <summary>
/// The <see cref="HitCircle"/>.
/// </summary>
public HitCircle HitCircle => (HitCircle)HitObject;
/// <summary>
/// The position of the player's cursor when <see cref="HitCircle"/> was hit.
/// </summary>
public Vector2? CursorPositionAtHit;
public OsuHitCircleJudgementResult(HitObject hitObject, Judgement judgement)
: base(hitObject, judgement)
{
}
}
}

View File

@ -7,8 +7,11 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Input;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK; using osuTK;
@ -32,6 +35,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle;
private InputManager inputManager;
public DrawableHitCircle(HitCircle h) public DrawableHitCircle(HitCircle h)
: base(h) : base(h)
{ {
@ -86,6 +91,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true);
} }
protected override void LoadComplete()
{
base.LoadComplete();
inputManager = GetContainingInputManager();
}
public override double LifetimeStart public override double LifetimeStart
{ {
get => base.LifetimeStart; get => base.LifetimeStart;
@ -126,7 +138,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return; return;
} }
ApplyResult(r => r.Type = result); ApplyResult(r =>
{
var circleResult = (OsuHitCircleJudgementResult)r;
// Todo: This should also consider misses, but they're a little more interesting to handle, since we don't necessarily know the position at the time of a miss.
if (result != HitResult.Miss)
{
var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position);
circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2);
}
circleResult.Type = result;
});
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
@ -172,6 +196,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => ApproachCircle; public Drawable ProxiedLayer => ApproachCircle;
protected override JudgementResult CreateResult(Judgement judgement) => new OsuHitCircleJudgementResult(HitObject, judgement);
public class HitReceptor : CompositeDrawable, IKeyBindingHandler<OsuAction> public class HitReceptor : CompositeDrawable, IKeyBindingHandler<OsuAction>
{ {
// IsHovered is used // IsHovered is used

View File

@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -193,9 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
SpmCounter.SetRotation(Disc.RotationAbsolute); SpmCounter.SetRotation(Disc.RotationAbsolute);
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
symbol.RotateTo(Disc.Rotation / 2, 500, Easing.OutQuint); symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.RotationAbsolute / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
} }
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()

View File

@ -29,6 +29,10 @@ using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using System; using System;
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Statistics;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
{ {
@ -186,5 +190,31 @@ namespace osu.Game.Rulesets.Osu
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame();
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList())
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
},
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap)
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
}
};
} }
} }

View File

@ -4,13 +4,27 @@
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Scoring namespace osu.Game.Rulesets.Osu.Scoring
{ {
public class OsuScoreProcessor : ScoreProcessor public class OsuScoreProcessor : ScoreProcessor
{ {
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement) => new OsuJudgementResult(hitObject, judgement); protected override HitEvent CreateHitEvent(JudgementResult result)
=> base.CreateHitEvent(result).With((result as OsuHitCircleJudgementResult)?.CursorPositionAtHit);
protected override JudgementResult CreateResult(HitObject hitObject, Judgement judgement)
{
switch (hitObject)
{
case HitCircle _:
return new OsuHitCircleJudgementResult(hitObject, judgement);
default:
return new OsuJudgementResult(hitObject, judgement);
}
}
public override HitWindows CreateHitWindows() => new OsuHitWindows(); public override HitWindows CreateHitWindows() => new OsuHitWindows();
} }

View File

@ -0,0 +1,297 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Scoring;
using osuTK;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Statistics
{
public class AccuracyHeatmap : CompositeDrawable
{
/// <summary>
/// Size of the inner circle containing the "hit" points, relative to the size of this <see cref="AccuracyHeatmap"/>.
/// All other points outside of the inner circle are "miss" points.
/// </summary>
private const float inner_portion = 0.8f;
/// <summary>
/// Number of rows/columns of points.
/// ~4px per point @ 128x128 size (the contents of the <see cref="AccuracyHeatmap"/> are always square). 1089 total points.
/// </summary>
private const int points_per_dimension = 33;
private const float rotation = 45;
private BufferedContainer bufferedGrid;
private GridContainer pointGrid;
private readonly ScoreInfo score;
private readonly IBeatmap playableBeatmap;
private const float line_thickness = 2;
/// <summary>
/// The highest count of any point currently being displayed.
/// </summary>
protected float PeakValue { get; private set; }
public AccuracyHeatmap(ScoreInfo score, IBeatmap playableBeatmap)
{
this.score = score;
this.playableBeatmap = playableBeatmap;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Children = new Drawable[]
{
new CircularContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(inner_portion),
Masking = true,
BorderThickness = line_thickness,
BorderColour = Color4.White,
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#202624")
}
},
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding(1),
Child = new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Children = new Drawable[]
{
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = line_thickness / 2,
Rotation = -rotation,
Alpha = 0.3f,
},
new Box
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
EdgeSmoothness = new Vector2(1),
RelativeSizeAxes = Axes.Y,
Height = 2, // We're rotating along a diagonal - we don't really care how big this is.
Width = line_thickness / 2, // adjust for edgesmoothness
Rotation = rotation
},
}
},
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Width = 10,
EdgeSmoothness = new Vector2(1),
Height = line_thickness / 2, // adjust for edgesmoothness
},
new Box
{
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
EdgeSmoothness = new Vector2(1),
Width = line_thickness / 2, // adjust for edgesmoothness
Height = 10,
}
}
},
bufferedGrid = new BufferedContainer
{
RelativeSizeAxes = Axes.Both,
CacheDrawnFrameBuffer = true,
BackgroundColour = Color4Extensions.FromHex("#202624").Opacity(0),
Child = pointGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both
}
},
}
};
Vector2 centre = new Vector2(points_per_dimension) / 2;
float innerRadius = centre.X * inner_portion;
Drawable[][] points = new Drawable[points_per_dimension][];
for (int r = 0; r < points_per_dimension; r++)
{
points[r] = new Drawable[points_per_dimension];
for (int c = 0; c < points_per_dimension; c++)
{
HitPointType pointType = Vector2.Distance(new Vector2(c, r), centre) <= innerRadius
? HitPointType.Hit
: HitPointType.Miss;
var point = new HitPoint(pointType, this)
{
Colour = pointType == HitPointType.Hit ? new Color4(102, 255, 204, 255) : new Color4(255, 102, 102, 255)
};
points[r][c] = point;
}
}
pointGrid.Content = points;
if (score.HitEvents == null || score.HitEvents.Count == 0)
return;
// Todo: This should probably not be done like this.
float radius = OsuHitObject.OBJECT_RADIUS * (1.0f - 0.7f * (playableBeatmap.BeatmapInfo.BaseDifficulty.CircleSize - 5) / 5) / 2;
foreach (var e in score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)))
{
if (e.LastHitObject == null || e.Position == null)
continue;
AddPoint(((OsuHitObject)e.LastHitObject).StackedEndPosition, ((OsuHitObject)e.HitObject).StackedEndPosition, e.Position.Value, radius);
}
}
protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius)
{
if (pointGrid.Content.Length == 0)
return;
double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point.
double angle2 = Math.Atan2(end.Y - start.Y, start.X - end.X); // Angle between the end point and the start point.
double finalAngle = angle2 - angle1; // Angle between start, end, and hit points.
float normalisedDistance = Vector2.Distance(hitPoint, end) / radius;
// Consider two objects placed horizontally, with the start on the left and the end on the right.
// The above calculated the angle between {end, start}, and the angle between {end, hitPoint}, in the form:
// +pi | 0
// O --------- O -----> Note: Math.Atan2 has a range (-pi <= theta <= +pi)
// -pi | 0
// E.g. If the hit point was directly above end, it would have an angle pi/2.
//
// It also calculated the angle separating hitPoint from the line joining {start, end}, that is anti-clockwise in the form:
// 0 | pi
// O --------- O ----->
// 2pi | pi
//
// However keep in mind that cos(0)=1 and cos(2pi)=1, whereas we actually want these values to appear on the left, so the x-coordinate needs to be inverted.
// Likewise sin(pi/2)=1 and sin(3pi/2)=-1, whereas we actually want these values to appear on the bottom/top respectively, so the y-coordinate also needs to be inverted.
//
// We also need to apply the anti-clockwise rotation.
var rotatedAngle = finalAngle - MathUtils.DegreesToRadians(rotation);
var rotatedCoordinate = -1 * new Vector2((float)Math.Cos(rotatedAngle), (float)Math.Sin(rotatedAngle));
Vector2 localCentre = new Vector2(points_per_dimension - 1) / 2;
float localRadius = localCentre.X * inner_portion * normalisedDistance; // The radius inside the inner portion which of the heatmap which the closest point lies.
Vector2 localPoint = localCentre + localRadius * rotatedCoordinate;
// Find the most relevant hit point.
int r = Math.Clamp((int)Math.Round(localPoint.Y), 0, points_per_dimension - 1);
int c = Math.Clamp((int)Math.Round(localPoint.X), 0, points_per_dimension - 1);
PeakValue = Math.Max(PeakValue, ((HitPoint)pointGrid.Content[r][c]).Increment());
bufferedGrid.ForceRedraw();
}
private class HitPoint : Circle
{
private readonly HitPointType pointType;
private readonly AccuracyHeatmap heatmap;
public override bool IsPresent => count > 0;
public HitPoint(HitPointType pointType, AccuracyHeatmap heatmap)
{
this.pointType = pointType;
this.heatmap = heatmap;
RelativeSizeAxes = Axes.Both;
Alpha = 1;
}
private int count;
/// <summary>
/// Increment the value of this point by one.
/// </summary>
/// <returns>The value after incrementing.</returns>
public int Increment()
{
return ++count;
}
protected override void Update()
{
base.Update();
// the point at which alpha is saturated and we begin to adjust colour lightness.
const float lighten_cutoff = 0.95f;
// the amount of lightness to attribute regardless of relative value to peak point.
const float non_relative_portion = 0.2f;
float amount = 0;
// give some amount of alpha regardless of relative count
amount += non_relative_portion * Math.Min(1, count / 10f);
// add relative portion
amount += (1 - non_relative_portion) * (count / heatmap.PeakValue);
// apply easing
amount = (float)Interpolation.ApplyEasing(Easing.OutQuint, Math.Min(1, amount));
Debug.Assert(amount <= 1);
Alpha = Math.Min(amount / lighten_cutoff, 1);
if (pointType == HitPointType.Hit)
Colour = ((Color4)Colour).Lighten(Math.Max(0, amount - lighten_cutoff));
}
}
private enum HitPointType
{
Hit,
Miss
}
}
}

View File

@ -21,9 +21,12 @@ using osu.Game.Rulesets.Taiko.Difficulty;
using osu.Game.Rulesets.Taiko.Scoring; using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using System; using System;
using System.Linq;
using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Edit;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Skinning; using osu.Game.Rulesets.Taiko.Skinning;
using osu.Game.Screens.Ranking.Statistics;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko namespace osu.Game.Rulesets.Taiko
@ -155,5 +158,20 @@ namespace osu.Game.Rulesets.Taiko
public int LegacyID => 1; public int LegacyID => 1;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow
{
Columns = new[]
{
new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList())
{
RelativeSizeAxes = Axes.X,
Height = 250
}),
}
}
};
} }
} }

View File

@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Scale = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE),
Masking = true, Masking = true,
BorderColour = Color4.White, BorderColour = Color4.White,
BorderThickness = border_thickness, BorderThickness = border_thickness,
@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Scale = new Vector2(TaikoHitObject.DEFAULT_SIZE), Size = new Vector2(TaikoHitObject.DEFAULT_SIZE),
Masking = true, Masking = true,
BorderColour = Color4.White, BorderColour = Color4.White,
BorderThickness = border_thickness, BorderThickness = border_thickness,

View File

@ -6,6 +6,7 @@ using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Skinning;
using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources; using osu.Game.Tests.Resources;
@ -167,5 +168,64 @@ namespace osu.Game.Tests.Gameplay
AssertBeatmapLookup(expected_sample); AssertBeatmapLookup(expected_sample);
} }
/// <summary>
/// Tests that when a custom sample bank is used, both the normal and additional sounds will be looked up.
/// </summary>
[Test]
public void TestHitObjectCustomSampleBank()
{
string[] expectedSamples =
{
"normal-hitnormal2",
"normal-hitwhistle2"
};
SetupSkins(expectedSamples[0], expectedSamples[1]);
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expectedSamples[0]);
AssertUserLookup(expectedSamples[1]);
}
/// <summary>
/// Tests that when a custom sample bank is used, but <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// only the additional sound will be looked up.
/// </summary>
[Test]
public void TestHitObjectCustomSampleBankWithoutLayered()
{
const string expected_sample = "normal-hitwhistle2";
const string unwanted_sample = "normal-hitnormal2";
SetupSkins(expected_sample, unwanted_sample);
disableLayeredHitSounds();
CreateTestWithBeatmap("hitobject-beatmap-custom-sample-bank.osu");
AssertBeatmapLookup(expected_sample);
AssertNoLookup(unwanted_sample);
}
/// <summary>
/// Tests that when a normal sample bank is used and <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> is disabled,
/// the normal sound will be looked up anyway.
/// </summary>
[Test]
public void TestHitObjectNormalSampleBankWithoutLayered()
{
const string expected_sample = "normal-hitnormal";
SetupSkins(expected_sample, expected_sample);
disableLayeredHitSounds();
CreateTestWithBeatmap("hitobject-beatmap-sample.osu");
AssertBeatmapLookup(expected_sample);
}
private void disableLayeredHitSounds()
=> AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0");
} }
} }

View File

@ -11,8 +11,10 @@ using osu.Framework.Audio.Sample;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Audio; using osu.Game.Audio;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Storyboards; using osu.Game.Storyboards;
@ -70,6 +72,50 @@ namespace osu.Game.Tests.Gameplay
AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue); AddUntilStep("sample playback succeeded", () => sample.LifetimeEnd < double.MaxValue);
} }
[TestCase(typeof(OsuModDoubleTime), 1.5)]
[TestCase(typeof(OsuModHalfTime), 0.75)]
[TestCase(typeof(ModWindUp), 1.5)]
[TestCase(typeof(ModWindDown), 0.75)]
[TestCase(typeof(OsuModDoubleTime), 2)]
[TestCase(typeof(OsuModHalfTime), 0.5)]
[TestCase(typeof(ModWindUp), 2)]
[TestCase(typeof(ModWindDown), 0.5)]
public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate)
{
GameplayClockContainer gameplayContainer = null;
TestDrawableStoryboardSample sample = null;
Mod testedMod = Activator.CreateInstance(expectedMod) as Mod;
switch (testedMod)
{
case ModRateAdjust m:
m.SpeedChange.Value = expectedRate;
break;
case ModTimeRamp m:
m.InitialRate.Value = m.FinalRate.Value = expectedRate;
break;
}
AddStep("setup storyboard sample", () =>
{
Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio);
SelectedMods.Value = new[] { testedMod };
Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0));
gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1))
{
Clock = gameplayContainer.GameplayClock
});
});
AddStep("start", () => gameplayContainer.Start());
AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate);
}
private class TestSkin : LegacySkin private class TestSkin : LegacySkin
{ {
public TestSkin(string resourceName, AudioManager audioManager) public TestSkin(string resourceName, AudioManager audioManager)
@ -99,5 +145,28 @@ namespace osu.Game.Tests.Gameplay
{ {
} }
} }
private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap
{
private readonly AudioManager audio;
public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio)
: base(ruleset, null, audio)
{
this.audio = audio;
}
protected override ISkin GetSkin() => new TestSkin("test-sample", audio);
}
private class TestDrawableStoryboardSample : DrawableStoryboardSample
{
public TestDrawableStoryboardSample(StoryboardSampleInfo sampleInfo)
: base(sampleInfo)
{
}
public new SampleChannel Channel => base.Channel;
}
} }
} }

View File

@ -127,6 +127,9 @@ namespace osu.Game.Tests.NonVisual
var osu = loadOsu(host); var osu = loadOsu(host);
var storage = osu.Dependencies.Get<Storage>(); var storage = osu.Dependencies.Get<Storage>();
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath(".");
// ensure we perform a save // ensure we perform a save
host.Dependencies.Get<FrameworkConfigManager>().Save(); host.Dependencies.Get<FrameworkConfigManager>().Save();
@ -145,25 +148,25 @@ namespace osu.Game.Tests.NonVisual
Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath)); Assert.That(storage.GetFullPath("."), Is.EqualTo(customPath));
// ensure cache was not moved // ensure cache was not moved
Assert.That(host.Storage.ExistsDirectory("cache")); Assert.That(Directory.Exists(Path.Combine(originalDirectory, "cache")));
// ensure nested cache was moved // ensure nested cache was moved
Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES) foreach (var file in OsuStorage.IGNORE_FILES)
{ {
Assert.That(host.Storage.Exists(file), Is.True); Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False); Assert.That(storage.Exists(file), Is.False);
} }
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES) foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
{ {
Assert.That(host.Storage.ExistsDirectory(dir), Is.True); Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False); Assert.That(storage.ExistsDirectory(dir), Is.False);
} }
Assert.That(new StreamReader(host.Storage.GetStream("storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}")); Assert.That(new StreamReader(Path.Combine(originalDirectory, "storage.ini")).ReadToEnd().Contains($"FullPath = {customPath}"));
} }
finally finally
{ {

View File

@ -49,9 +49,32 @@ namespace osu.Game.Tests.Online
Assert.That(converted.TestSetting.Value, Is.EqualTo(2)); Assert.That(converted.TestSetting.Value, Is.EqualTo(2));
} }
[Test]
public void TestDeserialiseTimeRampMod()
{
// Create the mod with values different from default.
var apiMod = new APIMod(new TestModTimeRamp
{
AdjustPitch = { Value = false },
InitialRate = { Value = 1.25 },
FinalRate = { Value = 0.25 }
});
var deserialised = JsonConvert.DeserializeObject<APIMod>(JsonConvert.SerializeObject(apiMod));
var converted = (TestModTimeRamp)deserialised.ToMod(new TestRuleset());
Assert.That(converted.AdjustPitch.Value, Is.EqualTo(false));
Assert.That(converted.InitialRate.Value, Is.EqualTo(1.25));
Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25));
}
private class TestRuleset : Ruleset private class TestRuleset : Ruleset
{ {
public override IEnumerable<Mod> GetModsFor(ModType type) => new[] { new TestMod() }; public override IEnumerable<Mod> GetModsFor(ModType type) => new Mod[]
{
new TestMod(),
new TestModTimeRamp(),
};
public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList<Mod> mods = null) => throw new System.NotImplementedException();
@ -78,5 +101,39 @@ namespace osu.Game.Tests.Online
Precision = 0.01, Precision = 0.01,
}; };
} }
private class TestModTimeRamp : ModTimeRamp
{
public override string Name => "Test Mod";
public override string Acronym => "TMTR";
public override double ScoreMultiplier => 1;
[SettingSource("Initial rate", "The starting speed of the track")]
public override BindableNumber<double> InitialRate { get; } = new BindableDouble
{
MinValue = 1,
MaxValue = 2,
Default = 1.5,
Value = 1.5,
Precision = 0.01,
};
[SettingSource("Final rate", "The speed increase to ramp towards")]
public override BindableNumber<double> FinalRate { get; } = new BindableDouble
{
MinValue = 0,
MaxValue = 1,
Default = 0.5,
Value = 0.5,
Precision = 0.01,
};
[SettingSource("Adjust pitch", "Should pitch be adjusted with speed")]
public override BindableBool AdjustPitch { get; } = new BindableBool
{
Default = true,
Value = true
};
}
} }
} }

View File

@ -0,0 +1,7 @@
osu file format v14
[TimingPoints]
0,300,4,0,2,100,1,0
[HitObjects]
444,320,1000,5,2,0:0:0:0:

View File

@ -0,0 +1,5 @@
[General]
Version: latest
[Colours]
Combo1: 255,255,255,0

View File

@ -108,5 +108,15 @@ namespace osu.Game.Tests.Skins
using (var stream = new LineBufferedReader(resStream)) using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m)); Assert.That(decoder.Decode(stream).LegacyVersion, Is.EqualTo(1.0m));
} }
[Test]
public void TestDecodeColourWithZeroAlpha()
{
var decoder = new LegacySkinDecoder();
using (var resStream = TestResources.OpenResource("skin-zero-alpha-colour.ini"))
using (var stream = new LineBufferedReader(resStream))
Assert.That(decoder.Decode(stream).ComboColours[0].A, Is.EqualTo(1.0f));
}
} }
} }

View File

@ -3,6 +3,7 @@
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -14,6 +15,8 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
private FailingLayer layer; private FailingLayer layer;
private readonly Bindable<bool> showHealth = new Bindable<bool>();
[Resolved] [Resolved]
private OsuConfigManager config { get; set; } private OsuConfigManager config { get; set; }
@ -24,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
Child = layer = new FailingLayer(); Child = layer = new FailingLayer();
layer.BindHealthProcessor(new DrainingHealthProcessor(1)); layer.BindHealthProcessor(new DrainingHealthProcessor(1));
layer.ShowHealth.BindTo(showHealth);
}); });
AddStep("show health", () => showHealth.Value = true);
AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true)); AddStep("enable layer", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer is visible", () => layer.IsPresent); AddUntilStep("layer is visible", () => layer.IsPresent);
} }
@ -69,5 +74,27 @@ namespace osu.Game.Tests.Visual.Gameplay
AddWaitStep("wait for potential fade", 10); AddWaitStep("wait for potential fade", 10);
AddAssert("layer is still visible", () => layer.IsPresent); AddAssert("layer is still visible", () => layer.IsPresent);
} }
[Test]
public void TestLayerVisibilityWithDifferentOptions()
{
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
AddStep("don't show health", () => showHealth.Value = false);
AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
AddStep("don't show health", () => showHealth.Value = false);
AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
AddStep("show health", () => showHealth.Value = true);
AddStep("disable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, false));
AddUntilStep("layer fade is invisible", () => !layer.IsPresent);
AddStep("show health", () => showHealth.Value = true);
AddStep("enable FadePlayfieldWhenHealthLow", () => config.Set(OsuSetting.FadePlayfieldWhenHealthLow, true));
AddUntilStep("layer fade is visible", () => layer.IsPresent);
}
} }
} }

View File

@ -19,10 +19,10 @@ namespace osu.Game.Tests.Visual.Menus
[Cached] [Cached]
private OsuLogo logo; private OsuLogo logo;
protected OsuScreenStack IntroStack;
protected IntroTestScene() protected IntroTestScene()
{ {
OsuScreenStack introStack = null;
Children = new Drawable[] Children = new Drawable[]
{ {
new Box new Box
@ -45,17 +45,17 @@ namespace osu.Game.Tests.Visual.Menus
logo.FinishTransforms(); logo.FinishTransforms();
logo.IsTracking = false; logo.IsTracking = false;
introStack?.Expire(); IntroStack?.Expire();
Add(introStack = new OsuScreenStack Add(IntroStack = new OsuScreenStack
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}); });
introStack.Push(CreateScreen()); IntroStack.Push(CreateScreen());
}); });
AddUntilStep("wait for menu", () => introStack.CurrentScreen is MainMenu); AddUntilStep("wait for menu", () => IntroStack.CurrentScreen is MainMenu);
} }
protected abstract IScreen CreateScreen(); protected abstract IScreen CreateScreen();

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Audio.Track;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
@ -11,5 +12,14 @@ namespace osu.Game.Tests.Visual.Menus
public class TestSceneIntroWelcome : IntroTestScene public class TestSceneIntroWelcome : IntroTestScene
{ {
protected override IScreen CreateScreen() => new IntroWelcome(); protected override IScreen CreateScreen() => new IntroWelcome();
public TestSceneIntroWelcome()
{
AddUntilStep("wait for load", () => getTrack() != null);
AddAssert("check if menu music loops", () => getTrack().Looping);
}
private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track;
} }
} }

View File

@ -0,0 +1,71 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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 NUnit.Framework;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneHitEventTimingDistributionGraph : OsuTestScene
{
[Test]
public void TestManyDistributedEvents()
{
createTest(CreateDistributedHitEvents());
}
[Test]
public void TestZeroTimeOffset()
{
createTest(Enumerable.Range(0, 100).Select(_ => new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
}
[Test]
public void TestNoEvents()
{
createTest(new List<HitEvent>());
}
private void createTest(List<HitEvent> events) => AddStep("create test", () =>
{
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4Extensions.FromHex("#333")
},
new HitEventTimingDistributionGraph(events)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = new Vector2(600, 130)
}
};
});
public static List<HitEvent> CreateDistributedHitEvents()
{
var hitEvents = new List<HitEvent>();
for (int i = 0; i < 50; i++)
{
int count = (int)(Math.Pow(25 - Math.Abs(i - 25), 2));
for (int j = 0; j < count; j++)
hitEvents.Add(new HitEvent(i - 25, HitResult.Perfect, new HitCircle(), new HitCircle(), null));
}
return hitEvents;
}
}
}

View File

@ -1,23 +1,32 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens; using osu.Game.Screens;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
using osu.Game.Screens.Ranking.Statistics;
using osuTK;
using osuTK.Input;
namespace osu.Game.Tests.Visual.Ranking namespace osu.Game.Tests.Visual.Ranking
{ {
[TestFixture] [TestFixture]
public class TestSceneResultsScreen : ScreenTestScene public class TestSceneResultsScreen : OsuManualInputManagerTestScene
{ {
private BeatmapManager beatmaps; private BeatmapManager beatmaps;
@ -41,7 +50,7 @@ namespace osu.Game.Tests.Visual.Ranking
private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo)); private UnrankedSoloResultsScreen createUnrankedSoloResultsScreen() => new UnrankedSoloResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo));
[Test] [Test]
public void ResultsWithoutPlayer() public void TestResultsWithoutPlayer()
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
OsuScreenStack stack; OsuScreenStack stack;
@ -60,7 +69,7 @@ namespace osu.Game.Tests.Visual.Ranking
} }
[Test] [Test]
public void ResultsWithPlayer() public void TestResultsWithPlayer()
{ {
TestResultsScreen screen = null; TestResultsScreen screen = null;
@ -70,7 +79,7 @@ namespace osu.Game.Tests.Visual.Ranking
} }
[Test] [Test]
public void ResultsForUnranked() public void TestResultsForUnranked()
{ {
UnrankedSoloResultsScreen screen = null; UnrankedSoloResultsScreen screen = null;
@ -79,6 +88,130 @@ namespace osu.Game.Tests.Visual.Ranking
AddAssert("retry overlay present", () => screen.RetryOverlay != null); AddAssert("retry overlay present", () => screen.RetryOverlay != null);
} }
[Test]
public void TestShowHideStatisticsViaOutsideClick()
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
});
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
AddUntilStep("expanded panel at the left of the screen", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
});
AddStep("click to right of panel", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel.ScreenSpaceDrawQuad.TopRight + new Vector2(100, 0));
InputManager.Click(MouseButton.Left);
});
AddAssert("statistics hidden", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Hidden);
AddUntilStep("expanded panel in centre of screen", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
});
}
[Test]
public void TestShowHideStatistics()
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
});
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
AddUntilStep("expanded panel at the left of the screen", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return expandedPanel.ScreenSpaceDrawQuad.TopLeft.X - screen.ScreenSpaceDrawQuad.TopLeft.X < 150;
});
AddStep("click expanded panel", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
});
AddAssert("statistics hidden", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Hidden);
AddUntilStep("expanded panel in centre of screen", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
return Precision.AlmostEquals(expandedPanel.ScreenSpaceDrawQuad.Centre.X, screen.ScreenSpaceDrawQuad.Centre.X, 1);
});
}
[Test]
public void TestShowStatisticsAndClickOtherPanel()
{
TestResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen()));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
ScorePanel expandedPanel = null;
ScorePanel contractedPanel = null;
AddStep("click expanded panel then contracted panel", () =>
{
expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
contractedPanel = this.ChildrenOfType<ScorePanel>().First(p => p.State == PanelState.Contracted && p.ScreenSpaceDrawQuad.TopLeft.X > screen.ScreenSpaceDrawQuad.TopLeft.X);
InputManager.MoveMouseTo(contractedPanel);
InputManager.Click(MouseButton.Left);
});
AddAssert("statistics shown", () => this.ChildrenOfType<StatisticsPanel>().Single().State.Value == Visibility.Visible);
AddAssert("contracted panel still contracted", () => contractedPanel.State == PanelState.Contracted);
AddAssert("expanded panel still expanded", () => expandedPanel.State == PanelState.Expanded);
}
[Test]
public void TestFetchScoresAfterShowingStatistics()
{
DelayedFetchResultsScreen screen = null;
AddStep("load results", () => Child = new TestResultsContainer(screen = new DelayedFetchResultsScreen(new TestScoreInfo(new OsuRuleset().RulesetInfo), 3000)));
AddUntilStep("wait for loaded", () => screen.IsLoaded);
AddStep("click expanded panel", () =>
{
var expandedPanel = this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded);
InputManager.MoveMouseTo(expandedPanel);
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for fetch", () => screen.FetchCompleted);
AddAssert("expanded panel still on screen", () => this.ChildrenOfType<ScorePanel>().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0);
}
private class TestResultsContainer : Container private class TestResultsContainer : Container
{ {
[Cached(typeof(Player))] [Cached(typeof(Player))]
@ -113,6 +246,58 @@ namespace osu.Game.Tests.Visual.Ranking
RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault(); RetryOverlay = InternalChildren.OfType<HotkeyRetryOverlay>().SingleOrDefault();
} }
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
var scores = new List<ScoreInfo>();
for (int i = 0; i < 20; i++)
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
score.TotalScore += 10 - i;
scores.Add(score);
}
scoresCallback?.Invoke(scores);
return null;
}
}
private class DelayedFetchResultsScreen : TestResultsScreen
{
public bool FetchCompleted { get; private set; }
private readonly double delay;
public DelayedFetchResultsScreen(ScoreInfo score, double delay)
: base(score)
{
this.delay = delay;
}
protected override APIRequest FetchScores(Action<IEnumerable<ScoreInfo>> scoresCallback)
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromMilliseconds(delay));
var scores = new List<ScoreInfo>();
for (int i = 0; i < 20; i++)
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo);
score.TotalScore += 10 - i;
scores.Add(score);
}
scoresCallback?.Invoke(scores);
Schedule(() => FetchCompleted = true);
});
return null;
}
} }
private class UnrankedSoloResultsScreen : SoloResultsScreen private class UnrankedSoloResultsScreen : SoloResultsScreen

View File

@ -0,0 +1,48 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Osu;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Tests.Visual.Ranking
{
public class TestSceneStatisticsPanel : OsuTestScene
{
[Test]
public void TestScoreWithStatistics()
{
var score = new TestScoreInfo(new OsuRuleset().RulesetInfo)
{
HitEvents = TestSceneHitEventTimingDistributionGraph.CreateDistributedHitEvents()
};
loadPanel(score);
}
[Test]
public void TestScoreWithoutStatistics()
{
loadPanel(new TestScoreInfo(new OsuRuleset().RulesetInfo));
}
[Test]
public void TestNullScore()
{
loadPanel(null);
}
private void loadPanel(ScoreInfo score) => AddStep("load panel", () =>
{
Child = new StatisticsPanel
{
RelativeSizeAxes = Axes.Both,
State = { Value = Visibility.Visible },
Score = { Value = score }
};
});
}
}

View File

@ -17,11 +17,12 @@ using osu.Game.Rulesets;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Screens.Select.Carousel; using osu.Game.Screens.Select.Carousel;
using osu.Game.Screens.Select.Filter; using osu.Game.Screens.Select.Filter;
using osuTK.Input;
namespace osu.Game.Tests.Visual.SongSelect namespace osu.Game.Tests.Visual.SongSelect
{ {
[TestFixture] [TestFixture]
public class TestSceneBeatmapCarousel : OsuTestScene public class TestSceneBeatmapCarousel : OsuManualInputManagerTestScene
{ {
private TestBeatmapCarousel carousel; private TestBeatmapCarousel carousel;
private RulesetStore rulesets; private RulesetStore rulesets;
@ -39,6 +40,43 @@ namespace osu.Game.Tests.Visual.SongSelect
this.rulesets = rulesets; this.rulesets = rulesets;
} }
[Test]
public void TestKeyRepeat()
{
loadBeatmaps();
advanceSelection(false);
AddStep("press down arrow", () => InputManager.PressKey(Key.Down));
BeatmapInfo selection = null;
checkSelectionIterating(true);
AddStep("press up arrow", () => InputManager.PressKey(Key.Up));
checkSelectionIterating(true);
AddStep("release down arrow", () => InputManager.ReleaseKey(Key.Down));
checkSelectionIterating(true);
AddStep("release up arrow", () => InputManager.ReleaseKey(Key.Up));
checkSelectionIterating(false);
void checkSelectionIterating(bool isIterating)
{
for (int i = 0; i < 3; i++)
{
AddStep("store selection", () => selection = carousel.SelectedBeatmap);
if (isIterating)
AddUntilStep("selection changed", () => carousel.SelectedBeatmap != selection);
else
AddUntilStep("selection not changed", () => carousel.SelectedBeatmap == selection);
}
}
}
[Test] [Test]
public void TestRecommendedSelection() public void TestRecommendedSelection()
{ {

View File

@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.Gameplay.Components;
namespace osu.Game.Tournament.Tests.Components namespace osu.Game.Tournament.Tests.Components
{ {
public class TestSceneMatchScoreDisplay : LadderTestScene public class TestSceneMatchScoreDisplay : TournamentTestScene
{ {
[Cached(Type = typeof(MatchIPCInfo))] [Cached(Type = typeof(MatchIPCInfo))]
private MatchIPCInfo matchInfo = new MatchIPCInfo(); private MatchIPCInfo matchInfo = new MatchIPCInfo();

View File

@ -8,12 +8,11 @@ using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Tests.Visual;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
namespace osu.Game.Tournament.Tests.Components namespace osu.Game.Tournament.Tests.Components
{ {
public class TestSceneTournamentBeatmapPanel : OsuTestScene public class TestSceneTournamentBeatmapPanel : TournamentTestScene
{ {
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }

View File

@ -1,146 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. 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.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Tournament.Models;
using osu.Game.Users;
namespace osu.Game.Tournament.Tests
{
[TestFixture]
public abstract class LadderTestScene : TournamentTestScene
{
[Cached]
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
[Resolved]
private RulesetStore rulesetStore { get; set; }
[BackgroundDependencyLoader]
private void load()
{
Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First();
Ruleset.BindTo(Ladder.Ruleset);
}
protected override void LoadComplete()
{
base.LoadComplete();
TournamentMatch match = CreateSampleMatch();
Ladder.Rounds.Add(match.Round.Value);
Ladder.Matches.Add(match);
Ladder.Teams.Add(match.Team1.Value);
Ladder.Teams.Add(match.Team2.Value);
Ladder.CurrentMatch.Value = match;
}
public static TournamentMatch CreateSampleMatch() => new TournamentMatch
{
Team1 =
{
Value = new TournamentTeam
{
FlagName = { Value = "JP" },
FullName = { Value = "Japan" },
LastYearPlacing = { Value = 10 },
Seed = { Value = "Low" },
SeedingResults =
{
new SeedingResult
{
Mod = { Value = "NM" },
Seed = { Value = 10 },
Beatmaps =
{
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 12345672,
Seed = { Value = 24 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 1234567,
Seed = { Value = 12 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 1234567,
Seed = { Value = 16 },
}
}
},
new SeedingResult
{
Mod = { Value = "DT" },
Seed = { Value = 5 },
Beatmaps =
{
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 3 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 6 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 12 },
}
}
}
},
Players =
{
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } },
}
}
},
Team2 =
{
Value = new TournamentTeam
{
FlagName = { Value = "US" },
FullName = { Value = "United States" },
Players =
{
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
}
}
},
Round =
{
Value = new TournamentRound { Name = { Value = "Quarterfinals" } }
}
};
public static BeatmapInfo CreateSampleBeatmapInfo() =>
new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } };
}
}

View File

@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneLadderEditorScreen : LadderTestScene public class TestSceneLadderEditorScreen : TournamentTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.Ladder;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneLadderScreen : LadderTestScene public class TestSceneLadderScreen : TournamentTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()

View File

@ -12,7 +12,7 @@ using osu.Game.Tournament.Screens.MapPool;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneMapPoolScreen : LadderTestScene public class TestSceneMapPoolScreen : TournamentTestScene
{ {
private MapPoolScreen screen; private MapPoolScreen screen;

View File

@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneRoundEditorScreen : LadderTestScene public class TestSceneRoundEditorScreen : TournamentTestScene
{ {
public TestSceneRoundEditorScreen() public TestSceneRoundEditorScreen()
{ {

View File

@ -7,7 +7,7 @@ using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneSeedingEditorScreen : LadderTestScene public class TestSceneSeedingEditorScreen : TournamentTestScene
{ {
[Cached] [Cached]
private readonly LadderInfo ladder = new LadderInfo(); private readonly LadderInfo ladder = new LadderInfo();

View File

@ -8,7 +8,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneSeedingScreen : LadderTestScene public class TestSceneSeedingScreen : TournamentTestScene
{ {
[Cached] [Cached]
private readonly LadderInfo ladder = new LadderInfo(); private readonly LadderInfo ladder = new LadderInfo();

View File

@ -5,7 +5,7 @@ using osu.Game.Tournament.Screens.Editors;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneTeamEditorScreen : LadderTestScene public class TestSceneTeamEditorScreen : TournamentTestScene
{ {
public TestSceneTeamEditorScreen() public TestSceneTeamEditorScreen()
{ {

View File

@ -9,7 +9,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneTeamIntroScreen : LadderTestScene public class TestSceneTeamIntroScreen : TournamentTestScene
{ {
[Cached] [Cached]
private readonly LadderInfo ladder = new LadderInfo(); private readonly LadderInfo ladder = new LadderInfo();

View File

@ -4,25 +4,19 @@
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Tournament.Models;
using osu.Game.Tournament.Screens.TeamWin; using osu.Game.Tournament.Screens.TeamWin;
namespace osu.Game.Tournament.Tests.Screens namespace osu.Game.Tournament.Tests.Screens
{ {
public class TestSceneTeamWinScreen : LadderTestScene public class TestSceneTeamWinScreen : TournamentTestScene
{ {
[Cached]
private readonly LadderInfo ladder = new LadderInfo();
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
var match = new TournamentMatch(); var match = Ladder.CurrentMatch.Value;
match.Team1.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "USA");
match.Team2.Value = Ladder.Teams.FirstOrDefault(t => t.Acronym.Value == "JPN");
match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals"); match.Round.Value = Ladder.Rounds.FirstOrDefault(g => g.Name.Value == "Finals");
match.Completed.Value = true; match.Completed.Value = true;
ladder.CurrentMatch.Value = match;
Add(new TeamWinScreen Add(new TeamWinScreen
{ {

View File

@ -1,13 +1,151 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Tests.Visual; using osu.Game.Tests.Visual;
using osu.Game.Tournament.IPC;
using osu.Game.Tournament.Models;
using osu.Game.Users;
namespace osu.Game.Tournament.Tests namespace osu.Game.Tournament.Tests
{ {
public abstract class TournamentTestScene : OsuTestScene public abstract class TournamentTestScene : OsuTestScene
{ {
[Cached]
protected LadderInfo Ladder { get; private set; } = new LadderInfo();
[Resolved]
private RulesetStore rulesetStore { get; set; }
[Cached]
protected MatchIPCInfo IPCInfo { get; private set; } = new MatchIPCInfo();
[BackgroundDependencyLoader]
private void load(Storage storage)
{
Ladder.Ruleset.Value ??= rulesetStore.AvailableRulesets.First();
TournamentMatch match = CreateSampleMatch();
Ladder.Rounds.Add(match.Round.Value);
Ladder.Matches.Add(match);
Ladder.Teams.Add(match.Team1.Value);
Ladder.Teams.Add(match.Team2.Value);
Ladder.CurrentMatch.Value = match;
Ruleset.BindTo(Ladder.Ruleset);
Dependencies.CacheAs(new StableInfo(storage));
}
public static TournamentMatch CreateSampleMatch() => new TournamentMatch
{
Team1 =
{
Value = new TournamentTeam
{
Acronym = { Value = "JPN" },
FlagName = { Value = "JP" },
FullName = { Value = "Japan" },
LastYearPlacing = { Value = 10 },
Seed = { Value = "Low" },
SeedingResults =
{
new SeedingResult
{
Mod = { Value = "NM" },
Seed = { Value = 10 },
Beatmaps =
{
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 12345672,
Seed = { Value = 24 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 1234567,
Seed = { Value = 12 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 1234567,
Seed = { Value = 16 },
}
}
},
new SeedingResult
{
Mod = { Value = "DT" },
Seed = { Value = 5 },
Beatmaps =
{
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 3 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 6 },
},
new SeedingBeatmap
{
BeatmapInfo = CreateSampleBeatmapInfo(),
Score = 234567,
Seed = { Value = 12 },
}
}
}
},
Players =
{
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 12 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 16 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 20 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 24 } } },
new User { Username = "Hello", Statistics = new UserStatistics { Ranks = new UserStatistics.UserRanks { Global = 30 } } },
}
}
},
Team2 =
{
Value = new TournamentTeam
{
Acronym = { Value = "USA" },
FlagName = { Value = "US" },
FullName = { Value = "United States" },
Players =
{
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
new User { Username = "Hello" },
}
}
},
Round =
{
Value = new TournamentRound { Name = { Value = "Quarterfinals" } }
}
};
public static BeatmapInfo CreateSampleBeatmapInfo() =>
new BeatmapInfo { Metadata = new BeatmapMetadata { Title = "Test Title", Artist = "Test Artist", ID = RNG.Next(0, 1000000) } };
protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner(); protected override ITestSceneTestRunner CreateRunner() => new TournamentTestSceneTestRunner();
public class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner public class TournamentTestSceneTestRunner : TournamentGameBase, ITestSceneTestRunner

View File

@ -6,6 +6,7 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -66,6 +67,9 @@ namespace osu.Game.Tournament.Components
} }
} }
// Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
@ -77,8 +81,6 @@ namespace osu.Game.Tournament.Components
flow = new FillFlowContainer flow = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
// Todo: This is a hack for https://github.com/ppy/osu-framework/issues/3617 since this container is at the very edge of the screen and potentially initially masked away.
Height = 1,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
LayoutDuration = 500, LayoutDuration = 500,
LayoutEasing = Easing.OutQuint, LayoutEasing = Easing.OutQuint,

View File

@ -37,7 +37,7 @@ namespace osu.Game.Tournament
public const float STREAM_AREA_WIDTH = 1366; public const float STREAM_AREA_WIDTH = 1366;
public const double REQUIRED_WIDTH = TournamentSceneManager.CONTROL_AREA_WIDTH * 2 + TournamentSceneManager.STREAM_AREA_WIDTH; public const double REQUIRED_WIDTH = CONTROL_AREA_WIDTH * 2 + STREAM_AREA_WIDTH;
[Cached] [Cached]
private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay(); private TournamentMatchChatDisplay chat = new TournamentMatchChatDisplay();

View File

@ -240,6 +240,9 @@ namespace osu.Game.Beatmaps
beatmapInfo = QueryBeatmap(b => b.ID == info.ID); beatmapInfo = QueryBeatmap(b => b.ID == info.ID);
} }
if (beatmapInfo == null)
return DefaultBeatmap;
lock (workingCache) lock (workingCache)
{ {
var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID);

View File

@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps.Formats
break; break;
case 2: case 2:
position.X = ((IHasXPosition)hitObject).X * 512; position.X = ((IHasXPosition)hitObject).X;
break; break;
case 3: case 3:

View File

@ -103,7 +103,12 @@ namespace osu.Game.Beatmaps.Formats
try try
{ {
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), split.Length == 4 ? byte.Parse(split[3]) : (byte)255); byte alpha = split.Length == 4 ? byte.Parse(split[3]) : (byte)255;
if (alpha == 0)
alpha = 255;
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), alpha);
} }
catch catch
{ {

View File

@ -11,7 +11,7 @@ namespace osu.Game.Beatmaps.Formats
/// </summary> /// </summary>
public static class Parsing public static class Parsing
{ {
public const int MAX_COORDINATE_VALUE = 65536; public const int MAX_COORDINATE_VALUE = 131072;
public const double MAX_PARSE_VALUE = int.MaxValue; public const double MAX_PARSE_VALUE = int.MaxValue;

View File

@ -2,9 +2,11 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using JetBrains.Annotations;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -13,28 +15,86 @@ namespace osu.Game.IO
{ {
public class OsuStorage : WrappedStorage public class OsuStorage : WrappedStorage
{ {
/// <summary>
/// Indicates the error (if any) that occurred when initialising the custom storage during initial startup.
/// </summary>
public readonly OsuStorageError Error;
/// <summary>
/// The custom storage path as selected by the user.
/// </summary>
[CanBeNull]
public string CustomStoragePath => storageConfig.Get<string>(StorageConfig.FullPath);
/// <summary>
/// The default storage path to be used if a custom storage path hasn't been selected or is not accessible.
/// </summary>
[NotNull]
public string DefaultStoragePath => defaultStorage.GetFullPath(".");
private readonly GameHost host; private readonly GameHost host;
private readonly StorageConfigManager storageConfig; private readonly StorageConfigManager storageConfig;
private readonly Storage defaultStorage;
internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; public static readonly string[] IGNORE_DIRECTORIES = { "cache" };
internal static readonly string[] IGNORE_FILES = public static readonly string[] IGNORE_FILES =
{ {
"framework.ini", "framework.ini",
"storage.ini" "storage.ini"
}; };
public OsuStorage(GameHost host) public OsuStorage(GameHost host, Storage defaultStorage)
: base(host.Storage, string.Empty) : base(defaultStorage, string.Empty)
{ {
this.host = host; this.host = host;
this.defaultStorage = defaultStorage;
storageConfig = new StorageConfigManager(host.Storage); storageConfig = new StorageConfigManager(defaultStorage);
var customStoragePath = storageConfig.Get<string>(StorageConfig.FullPath); if (!string.IsNullOrEmpty(CustomStoragePath))
TryChangeToCustomStorage(out Error);
}
if (!string.IsNullOrEmpty(customStoragePath)) /// <summary>
ChangeTargetStorage(host.GetStorage(customStoragePath)); /// Resets the custom storage path, changing the target storage to the default location.
/// </summary>
public void ResetCustomStoragePath()
{
storageConfig.Set(StorageConfig.FullPath, string.Empty);
storageConfig.Save();
ChangeTargetStorage(defaultStorage);
}
/// <summary>
/// Attempts to change to the user's custom storage path.
/// </summary>
/// <param name="error">The error that occurred.</param>
/// <returns>Whether the custom storage path was used successfully. If not, <paramref name="error"/> will be populated with the reason.</returns>
public bool TryChangeToCustomStorage(out OsuStorageError error)
{
Debug.Assert(!string.IsNullOrEmpty(CustomStoragePath));
error = OsuStorageError.None;
Storage lastStorage = UnderlyingStorage;
try
{
Storage userStorage = host.GetStorage(CustomStoragePath);
if (!userStorage.ExistsDirectory(".") || !userStorage.GetFiles(".").Any())
error = OsuStorageError.AccessibleButEmpty;
ChangeTargetStorage(userStorage);
}
catch
{
error = OsuStorageError.NotAccessible;
ChangeTargetStorage(lastStorage);
}
return error == OsuStorageError.None;
} }
protected override void ChangeTargetStorage(Storage newStorage) protected override void ChangeTargetStorage(Storage newStorage)
@ -145,4 +205,23 @@ namespace osu.Game.IO
} }
} }
} }
public enum OsuStorageError
{
/// <summary>
/// No error.
/// </summary>
None,
/// <summary>
/// Occurs when the target storage directory is accessible but does not already contain game files.
/// Only happens when the user changes the storage directory and then moves the files manually or mounts a different device to the same path.
/// </summary>
AccessibleButEmpty,
/// <summary>
/// Occurs when the target storage directory cannot be accessed at all.
/// </summary>
NotAccessible,
}
} }

View File

@ -767,7 +767,7 @@ namespace osu.Game
Text = "Subsequent messages have been logged. Click to view log files.", Text = "Subsequent messages have been logged. Click to view log files.",
Activated = () => Activated = () =>
{ {
Host.Storage.GetStorageForDirectory("logs").OpenInNativeExplorer(); Storage.GetStorageForDirectory("logs").OpenInNativeExplorer();
return true; return true;
} }
})); }));

View File

@ -312,11 +312,13 @@ namespace osu.Game
base.SetHost(host); base.SetHost(host);
// may be non-null for certain tests // may be non-null for certain tests
Storage ??= new OsuStorage(host); Storage ??= host.Storage;
LocalConfig ??= new OsuConfigManager(Storage); LocalConfig ??= new OsuConfigManager(Storage);
} }
protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage);
private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>(); private readonly List<ICanAcceptFiles> fileImporters = new List<ICanAcceptFiles>();
public async Task Import(params string[] paths) public async Task Import(params string[] paths)

View File

@ -42,25 +42,34 @@ namespace osu.Game.Overlays.Dialog
set => icon.Icon = value; set => icon.Icon = value;
} }
private string text; private string headerText;
public string HeaderText public string HeaderText
{ {
get => text; get => headerText;
set set
{ {
if (text == value) if (headerText == value)
return; return;
text = value; headerText = value;
header.Text = value; header.Text = value;
} }
} }
private string bodyText;
public string BodyText public string BodyText
{ {
set => body.Text = value; get => bodyText;
set
{
if (bodyText == value)
return;
bodyText = value;
body.Text = value;
}
} }
public IEnumerable<PopupDialogButton> Buttons public IEnumerable<PopupDialogButton> Buttons

View File

@ -0,0 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
namespace osu.Game.Rulesets.Mods
{
public interface IApplicableToAudio : IApplicableToTrack, IApplicableToSample
{
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio.Sample;
namespace osu.Game.Rulesets.Mods
{
/// <summary>
/// An interface for mods that make adjustments to a sample.
/// </summary>
public interface IApplicableToSample : IApplicableMod
{
void ApplyToSample(SampleChannel sample);
}
}

View File

@ -47,9 +47,25 @@ namespace osu.Game.Rulesets.Mods
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
{ {
Combo.BindTo(scoreProcessor.Combo); Combo.BindTo(scoreProcessor.Combo);
// Default value of ScoreProcessor's Rank in Flashlight Mod should be SS+
scoreProcessor.Rank.Value = ScoreRank.XH;
} }
public ScoreRank AdjustRank(ScoreRank rank, double accuracy) => rank; public ScoreRank AdjustRank(ScoreRank rank, double accuracy)
{
switch (rank)
{
case ScoreRank.X:
return ScoreRank.XH;
case ScoreRank.S:
return ScoreRank.SH;
default:
return rank;
}
}
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset) public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{ {

View File

@ -2,12 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Audio.Track; using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModRateAdjust : Mod, IApplicableToTrack public abstract class ModRateAdjust : Mod, IApplicableToAudio
{ {
public abstract BindableNumber<double> SpeedChange { get; } public abstract BindableNumber<double> SpeedChange { get; }
@ -16,6 +17,11 @@ namespace osu.Game.Rulesets.Mods
track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange);
} }
public virtual void ApplyToSample(SampleChannel sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x"; public override string SettingDescription => SpeedChange.IsDefault ? string.Empty : $"{SpeedChange.Value:N2}x";
} }
} }

View File

@ -10,10 +10,11 @@ using osu.Game.Beatmaps;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Framework.Audio.Sample;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToTrack public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio
{ {
/// <summary> /// <summary>
/// The point in the beatmap at which the final ramping rate should be reached. /// The point in the beatmap at which the final ramping rate should be reached.
@ -58,6 +59,11 @@ namespace osu.Game.Rulesets.Mods
AdjustPitch.TriggerChange(); AdjustPitch.TriggerChange();
} }
public void ApplyToSample(SampleChannel sample)
{
sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange);
}
public virtual void ApplyToBeatmap(IBeatmap beatmap) public virtual void ApplyToBeatmap(IBeatmap beatmap)
{ {
HitObject lastObject = beatmap.HitObjects.LastOrDefault(); HitObject lastObject = beatmap.HitObjects.LastOrDefault();
@ -83,9 +89,9 @@ namespace osu.Game.Rulesets.Mods
private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting) private void applyPitchAdjustment(ValueChangedEvent<bool> adjustPitchSetting)
{ {
// remove existing old adjustment // remove existing old adjustment
track.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange);
track.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange);
} }
private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue)

View File

@ -12,6 +12,7 @@ using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Legacy;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
{ {
@ -356,7 +357,10 @@ namespace osu.Game.Rulesets.Objects.Legacy
Bank = bankInfo.Normal, Bank = bankInfo.Normal,
Name = HitSampleInfo.HIT_NORMAL, Name = HitSampleInfo.HIT_NORMAL,
Volume = bankInfo.Volume, Volume = bankInfo.Volume,
CustomSampleBank = bankInfo.CustomSampleBank CustomSampleBank = bankInfo.CustomSampleBank,
// if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample.
// None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds
IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)
} }
}; };
@ -409,7 +413,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone();
} }
internal class LegacyHitSampleInfo : HitSampleInfo public class LegacyHitSampleInfo : HitSampleInfo
{ {
private int customSampleBank; private int customSampleBank;
@ -424,6 +428,15 @@ namespace osu.Game.Rulesets.Objects.Legacy
Suffix = value.ToString(); Suffix = value.ToString();
} }
} }
/// <summary>
/// Whether this hit sample is layered.
/// </summary>
/// <remarks>
/// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled
/// using the <see cref="GlobalSkinConfiguration.LayeredHitSounds"/> skin config option.
/// </remarks>
public bool IsLayered { get; set; }
} }
private class FileHitSampleInfo : LegacyHitSampleInfo private class FileHitSampleInfo : LegacyHitSampleInfo

View File

@ -23,6 +23,7 @@ using osu.Game.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using osu.Game.Users; using osu.Game.Users;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Game.Screens.Ranking.Statistics;
namespace osu.Game.Rulesets namespace osu.Game.Rulesets
{ {
@ -208,5 +209,14 @@ namespace osu.Game.Rulesets
/// </summary> /// </summary>
/// <returns>An empty frame for the current ruleset, or null if unsupported.</returns> /// <returns>An empty frame for the current ruleset, or null if unsupported.</returns>
public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null; public virtual IConvertibleReplayFrame CreateConvertibleReplayFrame() => null;
/// <summary>
/// Creates the statistics for a <see cref="ScoreInfo"/> to be displayed in the results screen.
/// </summary>
/// <param name="score">The <see cref="ScoreInfo"/> to create the statistics for. The score is guaranteed to have <see cref="ScoreInfo.HitEvents"/> populated.</param>
/// <param name="playableBeatmap">The <see cref="IBeatmap"/>, converted for this <see cref="Ruleset"/> with all relevant <see cref="Mod"/>s applied.</param>
/// <returns>The <see cref="StatisticRow"/>s to display. Each <see cref="StatisticRow"/> may contain 0 or more <see cref="StatisticItem"/>.</returns>
[NotNull]
public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty<StatisticRow>();
} }
} }

View File

@ -0,0 +1,66 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using JetBrains.Annotations;
using osu.Game.Rulesets.Objects;
using osuTK;
namespace osu.Game.Rulesets.Scoring
{
/// <summary>
/// A <see cref="HitEvent"/> generated by the <see cref="ScoreProcessor"/> containing extra statistics around a <see cref="HitResult"/>.
/// </summary>
public readonly struct HitEvent
{
/// <summary>
/// The time offset from the end of <see cref="HitObject"/> at which the event occurred.
/// </summary>
public readonly double TimeOffset;
/// <summary>
/// The hit result.
/// </summary>
public readonly HitResult Result;
/// <summary>
/// The <see cref="HitObject"/> on which the result occurred.
/// </summary>
public readonly HitObject HitObject;
/// <summary>
/// The <see cref="HitObject"/> occurring prior to <see cref="HitObject"/>.
/// </summary>
[CanBeNull]
public readonly HitObject LastHitObject;
/// <summary>
/// A position, if available, at the time of the event.
/// </summary>
[CanBeNull]
public readonly Vector2? Position;
/// <summary>
/// Creates a new <see cref="HitEvent"/>.
/// </summary>
/// <param name="timeOffset">The time offset from the end of <paramref name="hitObject"/> at which the event occurs.</param>
/// <param name="result">The <see cref="HitResult"/>.</param>
/// <param name="hitObject">The <see cref="HitObject"/> that triggered the event.</param>
/// <param name="lastHitObject">The previous <see cref="HitObject"/>.</param>
/// <param name="position">A position corresponding to the event.</param>
public HitEvent(double timeOffset, HitResult result, HitObject hitObject, [CanBeNull] HitObject lastHitObject, [CanBeNull] Vector2? position)
{
TimeOffset = timeOffset;
Result = result;
HitObject = hitObject;
LastHitObject = lastHitObject;
Position = position;
}
/// <summary>
/// Creates a new <see cref="HitEvent"/> with an optional positional offset.
/// </summary>
/// <param name="positionOffset">The positional offset.</param>
/// <returns>The new <see cref="HitEvent"/>.</returns>
public HitEvent With(Vector2? positionOffset) => new HitEvent(TimeOffset, Result, HitObject, LastHitObject, positionOffset);
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Bindables;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Rulesets.Scoring namespace osu.Game.Rulesets.Scoring
@ -61,6 +62,9 @@ namespace osu.Game.Rulesets.Scoring
private double baseScore; private double baseScore;
private double bonusScore; private double bonusScore;
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
private HitObject lastHitObject;
private double scoreMultiplier = 1; private double scoreMultiplier = 1;
public ScoreProcessor() public ScoreProcessor()
@ -128,9 +132,20 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore += result.Judgement.MaxNumericResult; rollingMaxBaseScore += result.Judgement.MaxNumericResult;
} }
hitEvents.Add(CreateHitEvent(result));
lastHitObject = result.HitObject;
updateScore(); updateScore();
} }
/// <summary>
/// Creates the <see cref="HitEvent"/> that describes a <see cref="JudgementResult"/>.
/// </summary>
/// <param name="result">The <see cref="JudgementResult"/> to describe.</param>
/// <returns>The <see cref="HitEvent"/>.</returns>
protected virtual HitEvent CreateHitEvent(JudgementResult result)
=> new HitEvent(result.TimeOffset, result.Type, result.HitObject, lastHitObject, null);
protected sealed override void RevertResultInternal(JudgementResult result) protected sealed override void RevertResultInternal(JudgementResult result)
{ {
Combo.Value = result.ComboAtJudgement; Combo.Value = result.ComboAtJudgement;
@ -153,6 +168,10 @@ namespace osu.Game.Rulesets.Scoring
rollingMaxBaseScore -= result.Judgement.MaxNumericResult; rollingMaxBaseScore -= result.Judgement.MaxNumericResult;
} }
Debug.Assert(hitEvents.Count > 0);
lastHitObject = hitEvents[^1].LastHitObject;
hitEvents.RemoveAt(hitEvents.Count - 1);
updateScore(); updateScore();
} }
@ -207,6 +226,8 @@ namespace osu.Game.Rulesets.Scoring
base.Reset(storeResults); base.Reset(storeResults);
scoreResultCounts.Clear(); scoreResultCounts.Clear();
hitEvents.Clear();
lastHitObject = null;
if (storeResults) if (storeResults)
{ {
@ -247,6 +268,8 @@ namespace osu.Game.Rulesets.Scoring
foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) foreach (var result in Enum.GetValues(typeof(HitResult)).OfType<HitResult>().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r)))
score.Statistics[result] = GetStatistic(result); score.Statistics[result] = GetStatistic(result);
score.HitEvents = new List<HitEvent>(hitEvents);
} }
/// <summary> /// <summary>

View File

@ -18,9 +18,6 @@ using osu.Game.Input.Handlers;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osuTK.Input; using osuTK.Input;
using static osu.Game.Input.Handlers.ReplayInputHandler; using static osu.Game.Input.Handlers.ReplayInputHandler;
using JoystickState = osu.Framework.Input.States.JoystickState;
using KeyboardState = osu.Framework.Input.States.KeyboardState;
using MouseState = osu.Framework.Input.States.MouseState;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -42,11 +39,7 @@ namespace osu.Game.Rulesets.UI
} }
} }
protected override InputState CreateInitialState() protected override InputState CreateInitialState() => new RulesetInputManagerInputState<T>(base.CreateInitialState());
{
var state = base.CreateInitialState();
return new RulesetInputManagerInputState<T>(state.Mouse, state.Keyboard, state.Joystick);
}
protected readonly KeyBindingContainer<T> KeyBindingContainer; protected readonly KeyBindingContainer<T> KeyBindingContainer;
@ -203,8 +196,8 @@ namespace osu.Game.Rulesets.UI
{ {
public ReplayState<T> LastReplayState; public ReplayState<T> LastReplayState;
public RulesetInputManagerInputState(MouseState mouse = null, KeyboardState keyboard = null, JoystickState joystick = null) public RulesetInputManagerInputState(InputState state = null)
: base(mouse, keyboard, joystick) : base(state)
{ {
} }
} }

View File

@ -3,21 +3,26 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.Timing;
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{ {
public class SequentialScrollAlgorithm : IScrollAlgorithm public class SequentialScrollAlgorithm : IScrollAlgorithm
{ {
private readonly Dictionary<double, double> positionCache; private static readonly IComparer<PositionMapping> by_position_comparer = Comparer<PositionMapping>.Create((c1, c2) => c1.Position.CompareTo(c2.Position));
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints; private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
/// <summary>
/// Stores a mapping of time -> position for each control point.
/// </summary>
private readonly List<PositionMapping> positionMappings = new List<PositionMapping>();
public SequentialScrollAlgorithm(IReadOnlyList<MultiplierControlPoint> controlPoints) public SequentialScrollAlgorithm(IReadOnlyList<MultiplierControlPoint> controlPoints)
{ {
this.controlPoints = controlPoints; this.controlPoints = controlPoints;
positionCache = new Dictionary<double, double>();
} }
public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength) public double GetDisplayStartTime(double originTime, float offset, double timeRange, float scrollLength)
@ -27,55 +32,31 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
public float GetLength(double startTime, double endTime, double timeRange, float scrollLength) public float GetLength(double startTime, double endTime, double timeRange, float scrollLength)
{ {
var objectLength = relativePositionAtCached(endTime, timeRange) - relativePositionAtCached(startTime, timeRange); var objectLength = relativePositionAt(endTime, timeRange) - relativePositionAt(startTime, timeRange);
return (float)(objectLength * scrollLength); return (float)(objectLength * scrollLength);
} }
public float PositionAt(double time, double currentTime, double timeRange, float scrollLength) public float PositionAt(double time, double currentTime, double timeRange, float scrollLength)
{ {
// Caching is not used here as currentTime is unlikely to have been previously cached double timelineLength = relativePositionAt(time, timeRange) - relativePositionAt(currentTime, timeRange);
double timelinePosition = relativePositionAt(currentTime, timeRange); return (float)(timelineLength * scrollLength);
return (float)((relativePositionAtCached(time, timeRange) - timelinePosition) * scrollLength);
} }
public double TimeAt(float position, double currentTime, double timeRange, float scrollLength) public double TimeAt(float position, double currentTime, double timeRange, float scrollLength)
{ {
// Convert the position to a length relative to time = 0 if (controlPoints.Count == 0)
double length = position / scrollLength + relativePositionAt(currentTime, timeRange); return position * timeRange;
// We need to consider all timing points until the specified time and not just the currently-active one, // Find the position at the current time, and the given length.
// since each timing point individually affects the positions of _all_ hitobjects after its start time double relativePosition = relativePositionAt(currentTime, timeRange) + position / scrollLength;
for (int i = 0; i < controlPoints.Count; i++)
{
var current = controlPoints[i];
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
// Duration of the current control point var positionMapping = findControlPointMapping(timeRange, new PositionMapping(0, null, relativePosition), by_position_comparer);
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
// Figure out the length of control point // Begin at the control point's time and add the remaining time to reach the given position.
var currentLength = currentDuration / timeRange * current.Multiplier; return positionMapping.Time + (relativePosition - positionMapping.Position) * timeRange / positionMapping.ControlPoint.Multiplier;
if (currentLength > length)
{
// The point is within this control point
return current.StartTime + length * timeRange / current.Multiplier;
}
length -= currentLength;
}
return 0; // Should never occur
} }
private double relativePositionAtCached(double time, double timeRange) public void Reset() => positionMappings.Clear();
{
if (!positionCache.TryGetValue(time, out double existing))
positionCache[time] = existing = relativePositionAt(time, timeRange);
return existing;
}
public void Reset() => positionCache.Clear();
/// <summary> /// <summary>
/// Finds the position which corresponds to a point in time. /// Finds the position which corresponds to a point in time.
@ -84,37 +65,100 @@ namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
/// <param name="time">The time to find the position at.</param> /// <param name="time">The time to find the position at.</param>
/// <param name="timeRange">The amount of time visualised by the scrolling area.</param> /// <param name="timeRange">The amount of time visualised by the scrolling area.</param>
/// <returns>A positive value indicating the position at <paramref name="time"/>.</returns> /// <returns>A positive value indicating the position at <paramref name="time"/>.</returns>
private double relativePositionAt(double time, double timeRange) private double relativePositionAt(in double time, in double timeRange)
{ {
if (controlPoints.Count == 0) if (controlPoints.Count == 0)
return time / timeRange; return time / timeRange;
double length = 0; var mapping = findControlPointMapping(timeRange, new PositionMapping(time));
// We need to consider all timing points until the specified time and not just the currently-active one, // Begin at the control point's position and add the remaining distance to reach the given time.
// since each timing point individually affects the positions of _all_ hitobjects after its start time return mapping.Position + (time - mapping.Time) / timeRange * mapping.ControlPoint.Multiplier;
for (int i = 0; i < controlPoints.Count; i++) }
/// <summary>
/// Finds a <see cref="MultiplierControlPoint"/>'s <see cref="PositionMapping"/> that is relevant to a given <see cref="PositionMapping"/>.
/// </summary>
/// <remarks>
/// This is used to find the last <see cref="MultiplierControlPoint"/> occuring prior to a time value, or prior to a position value (if <see cref="by_position_comparer"/> is used).
/// </remarks>
/// <param name="timeRange">The time range.</param>
/// <param name="search">The <see cref="PositionMapping"/> to find the closest <see cref="PositionMapping"/> to.</param>
/// <param name="comparer">The comparison. If null, the default comparer is used (by time).</param>
/// <returns>The <see cref="MultiplierControlPoint"/>'s <see cref="PositionMapping"/> that is relevant for <paramref name="search"/>.</returns>
private PositionMapping findControlPointMapping(in double timeRange, in PositionMapping search, IComparer<PositionMapping> comparer = null)
{
generatePositionMappings(timeRange);
var mappingIndex = positionMappings.BinarySearch(search, comparer ?? Comparer<PositionMapping>.Default);
if (mappingIndex < 0)
{ {
var current = controlPoints[i]; // If the search value isn't found, the _next_ control point is returned, but we actually want the _previous_ control point.
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null; // In doing so, we must make sure to not underflow the position mapping list (i.e. always use the 0th control point for time < first_control_point_time).
mappingIndex = Math.Max(0, ~mappingIndex - 1);
// We don't need to consider any control points beyond the current time, since it will not yet Debug.Assert(mappingIndex < positionMappings.Count);
// affect any hitobjects
if (i > 0 && current.StartTime > time)
continue;
// Duration of the current control point
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
// We want to consider the minimal amount of time that this control point has affected,
// which may be either its duration, or the amount of time that has passed within it
var durationInCurrent = Math.Min(currentDuration, time - current.StartTime);
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
length += durationInCurrent / timeRange * current.Multiplier;
} }
return length; var mapping = positionMappings[mappingIndex];
Debug.Assert(mapping.ControlPoint != null);
return mapping;
}
/// <summary>
/// Generates the mapping of <see cref="MultiplierControlPoint"/> (and their respective start times) to their relative position from 0.
/// </summary>
/// <param name="timeRange">The time range.</param>
private void generatePositionMappings(in double timeRange)
{
if (positionMappings.Count > 0)
return;
if (controlPoints.Count == 0)
return;
positionMappings.Add(new PositionMapping(controlPoints[0].StartTime, controlPoints[0]));
for (int i = 0; i < controlPoints.Count - 1; i++)
{
var current = controlPoints[i];
var next = controlPoints[i + 1];
// Figure out how much of the time range the duration represents, and adjust it by the speed multiplier
float length = (float)((next.StartTime - current.StartTime) / timeRange * current.Multiplier);
positionMappings.Add(new PositionMapping(next.StartTime, next, positionMappings[^1].Position + length));
}
}
private readonly struct PositionMapping : IComparable<PositionMapping>
{
/// <summary>
/// The time corresponding to this position.
/// </summary>
public readonly double Time;
/// <summary>
/// The <see cref="MultiplierControlPoint"/> at <see cref="Time"/>.
/// </summary>
[CanBeNull]
public readonly MultiplierControlPoint ControlPoint;
/// <summary>
/// The relative position from 0 of <see cref="ControlPoint"/>.
/// </summary>
public readonly double Position;
public PositionMapping(double time, MultiplierControlPoint controlPoint = null, double position = default)
{
Time = time;
ControlPoint = controlPoint;
Position = position;
}
public int CompareTo(PositionMapping other) => Time.CompareTo(other.Time);
} }
} }
} }

View File

@ -166,6 +166,10 @@ namespace osu.Game.Scoring
} }
} }
[NotMapped]
[JsonIgnore]
public List<HitEvent> HitEvents { get; set; }
[JsonIgnore] [JsonIgnore]
public List<ScoreFileInfo> Files { get; set; } public List<ScoreFileInfo> Files { get; set; }

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Graphics.Sprites;
using osu.Game.Overlays.Dialog;
namespace osu.Game.Screens.Menu
{
public class ConfirmExitDialog : PopupDialog
{
public ConfirmExitDialog(Action confirm, Action cancel)
{
HeaderText = "Are you sure you want to exit?";
BodyText = "Last chance to back out.";
Icon = FontAwesome.Solid.ExclamationTriangle;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Goodbye",
Action = confirm
},
new PopupDialogCancelButton
{
Text = @"Just a little more",
Action = cancel
},
};
}
}
}

View File

@ -73,7 +73,6 @@ namespace osu.Game.Screens.Menu
MenuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice); MenuVoice = config.GetBindable<bool>(OsuSetting.MenuVoice);
MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic); MenuMusic = config.GetBindable<bool>(OsuSetting.MenuMusic);
seeya = audio.Samples.Get(SeeyaSampleName); seeya = audio.Samples.Get(SeeyaSampleName);
BeatmapSetInfo setInfo = null; BeatmapSetInfo setInfo = null;

View File

@ -39,6 +39,8 @@ namespace osu.Game.Screens.Menu
welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); welcome = audio.Samples.Get(@"Intro/Welcome/welcome");
pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano");
Track.Looping = true;
} }
protected override void LogoArriving(OsuLogo logo, bool resuming) protected override void LogoArriving(OsuLogo logo, bool resuming)

View File

@ -1,22 +1,21 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq; using System.Linq;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio.Track;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.IO;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Dialog;
using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Backgrounds;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
using osu.Game.Screens.Multi; using osu.Game.Screens.Multi;
@ -63,6 +62,8 @@ namespace osu.Game.Screens.Menu
protected override BackgroundScreen CreateBackground() => background; protected override BackgroundScreen CreateBackground() => background;
internal Track Track { get; private set; }
private Bindable<float> holdDelay; private Bindable<float> holdDelay;
private Bindable<bool> loginDisplayed; private Bindable<bool> loginDisplayed;
@ -168,22 +169,28 @@ namespace osu.Game.Screens.Menu
return s; return s;
} }
[Resolved]
private Storage storage { get; set; }
public override void OnEntering(IScreen last) public override void OnEntering(IScreen last)
{ {
base.OnEntering(last); base.OnEntering(last);
buttons.FadeInFromZero(500); buttons.FadeInFromZero(500);
var track = Beatmap.Value.Track; Track = Beatmap.Value.Track;
var metadata = Beatmap.Value.Metadata; var metadata = Beatmap.Value.Metadata;
if (last is IntroScreen && track != null) if (last is IntroScreen && Track != null)
{ {
if (!track.IsRunning) if (!Track.IsRunning)
{ {
track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * track.Length); Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length);
track.Start(); Track.Start();
} }
} }
if (storage is OsuStorage osuStorage && osuStorage.Error != OsuStorageError.None)
dialogOverlay?.Push(new StorageErrorDialog(osuStorage, osuStorage.Error));
} }
private bool exitConfirmed; private bool exitConfirmed;
@ -280,30 +287,5 @@ namespace osu.Game.Screens.Menu
this.FadeOut(3000); this.FadeOut(3000);
return base.OnExiting(next); return base.OnExiting(next);
} }
private class ConfirmExitDialog : PopupDialog
{
public ConfirmExitDialog(Action confirm, Action cancel)
{
HeaderText = "Are you sure you want to exit?";
BodyText = "Last chance to back out.";
Icon = FontAwesome.Solid.ExclamationTriangle;
Buttons = new PopupDialogButton[]
{
new PopupDialogOkButton
{
Text = @"Goodbye",
Action = confirm
},
new PopupDialogCancelButton
{
Text = @"Just a little more",
Action = cancel
},
};
}
}
} }
} }

Some files were not shown because too many files have changed in this diff Show More