1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 13:22:55 +08:00

Merge branch 'master' into keyboard_shortcuts

This commit is contained in:
Bartłomiej Dach 2021-06-03 14:09:48 +02:00
commit acb4873157
37 changed files with 603 additions and 147 deletions

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyFreeform
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods
{
public class EmptyFreeformModAutoplay : ModAutoplay<EmptyFreeformHitObject>
public class EmptyFreeformModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject>
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.EmptyScrolling
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}

View File

@ -3,7 +3,6 @@
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods
{
public class EmptyScrollingModAutoplay : ModAutoplay<EmptyScrollingHitObject>
public class EmptyScrollingModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods
{
public class PippidonModAutoplay : ModAutoplay<PippidonHitObject>
public class PippidonModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -25,6 +25,6 @@ namespace osu.Game.Rulesets.Pippidon
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) => Enumerable.Empty<DifficultyHitObject>();
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[0];
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[0];
}
}

View File

@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
}
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[]
{
new Movement(mods, halfCatcherWidth),
new Movement(mods, halfCatcherWidth, clockRate),
};
}

View File

@ -24,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
/// </summary>
public readonly double StrainTime;
public readonly double ClockRate;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate)
{
@ -37,7 +35,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(40, DeltaTime);
ClockRate = clockRate;
}
}
}

View File

@ -28,10 +28,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved;
private double lastStrainTime;
public Movement(Mod[] mods, float halfCatcherWidth)
/// <summary>
/// The speed multiplier applied to the player's catcher.
/// </summary>
private readonly double catcherSpeedMultiplier;
public Movement(Mod[] mods, float halfCatcherWidth, double clockRate)
: base(mods)
{
HalfCatcherWidth = halfCatcherWidth;
// In catch, clockrate adjustments do not only affect the timings of hitobjects,
// but also the speed of the player's catcher, which has an impact on difficulty
// TODO: Support variable clockrates caused by mods such as ModTimeRamp
// (perhaps by using IApplicableToRate within the CatchDifficultyHitObject constructor to set a catcher speed for each object before processing)
catcherSpeedMultiplier = clockRate;
}
protected override double StrainValueOf(DifficultyHitObject current)
@ -48,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value;
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catchCurrent.ClockRate);
double weightedStrainTime = catchCurrent.StrainTime + 13 + (3 / catcherSpeedMultiplier);
double distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime);
@ -81,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition;
}
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
distanceAddition *= 1.0 + edgeDashBonus * ((20 - catchCurrent.LastObject.DistanceToHyperDash) / 20) * Math.Pow((Math.Min(catchCurrent.StrainTime * catcherSpeedMultiplier, 265) / 265), 1.5); // Edge Dashes are easier at lower ms values
}
lastPlayerPosition = playerPosition;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Catch.Mods
{
public class CatchModAutoplay : ModAutoplay<CatchHitObject>
public class CatchModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{
new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
};

View File

@ -4,7 +4,6 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
@ -12,7 +11,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Mania.Mods
{
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
public class ManiaModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
}
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{
new Aim(mods),
new Speed(mods)

View File

@ -6,14 +6,13 @@ using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay<OsuHitObject>
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();

View File

@ -0,0 +1,370 @@
// 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.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
/// <summary>
/// Mod that randomises the positions of the <see cref="HitObject"/>s
/// </summary>
public class OsuModRandom : ModRandom, IApplicableToBeatmap
{
public override string Description => "It never gets boring!";
public override bool Ranked => false;
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
// The closer the hit objects draw to the border, the sharper the turn
private const float playfield_edge_ratio = 0.375f;
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
private static readonly Vector2 playfield_middle = Vector2.Divide(OsuPlayfield.BASE_SIZE, 2);
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
private Random rng;
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(OsuModRandomSettingsControl))]
public Bindable<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
public void ApplyToBeatmap(IBeatmap beatmap)
{
if (!(beatmap is OsuBeatmap osuBeatmap))
return;
var hitObjects = osuBeatmap.HitObjects;
Seed.Value ??= RNG.Next();
rng = new Random((int)Seed.Value);
RandomObjectInfo previous = null;
float rateOfChangeMultiplier = 0;
for (int i = 0; i < hitObjects.Count; i++)
{
var hitObject = hitObjects[i];
var current = new RandomObjectInfo(hitObject);
// rateOfChangeMultiplier only changes every i iterations to prevent shaky-line-shaped streams
if (i % 3 == 0)
rateOfChangeMultiplier = (float)rng.NextDouble() * 2 - 1;
if (hitObject is Spinner)
{
previous = null;
continue;
}
applyRandomisation(rateOfChangeMultiplier, previous, current);
hitObject.Position = current.PositionRandomised;
// update end position as it may have changed as a result of the position update.
current.EndPositionRandomised = current.PositionRandomised;
switch (hitObject)
{
case Slider slider:
// Shift nested objects the same distance as the slider got shifted in the randomisation process
// so that moveSliderIntoPlayfield() can determine their relative distances to slider.Position and thus minMargin
shiftNestedObjects(slider, Vector2.Subtract(slider.Position, current.PositionOriginal));
var oldPos = new Vector2(slider.Position.X, slider.Position.Y);
moveSliderIntoPlayfield(slider, current);
// Shift them again to move them to their final position after the slider got moved into the playfield
shiftNestedObjects(slider, Vector2.Subtract(slider.Position, oldPos));
break;
}
previous = current;
}
}
/// <summary>
/// Returns the final position of the hit object
/// </summary>
/// <returns>Final position of the hit object</returns>
private void applyRandomisation(float rateOfChangeMultiplier, RandomObjectInfo previous, RandomObjectInfo current)
{
if (previous == null)
{
var playfieldSize = OsuPlayfield.BASE_SIZE;
current.AngleRad = (float)(rng.NextDouble() * 2 * Math.PI - Math.PI);
current.PositionRandomised = new Vector2((float)rng.NextDouble() * playfieldSize.X, (float)rng.NextDouble() * playfieldSize.Y);
return;
}
float distanceToPrev = Vector2.Distance(previous.EndPositionOriginal, current.PositionOriginal);
// The max. angle (relative to the angle of the vector pointing from the 2nd last to the last hit object)
// is proportional to the distance between the last and the current hit object
// to allow jumps and prevent too sharp turns during streams.
var randomAngleRad = rateOfChangeMultiplier * 2 * Math.PI * distanceToPrev / playfield_diagonal;
current.AngleRad = (float)randomAngleRad + previous.AngleRad;
if (current.AngleRad < 0)
current.AngleRad += 2 * (float)Math.PI;
var posRelativeToPrev = new Vector2(
distanceToPrev * (float)Math.Cos(current.AngleRad),
distanceToPrev * (float)Math.Sin(current.AngleRad)
);
posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
var position = Vector2.Add(previous.EndPositionRandomised, posRelativeToPrev);
// Move hit objects back into the playfield if they are outside of it,
// which would sometimes happen during big jumps otherwise.
position.X = MathHelper.Clamp(position.X, 0, OsuPlayfield.BASE_SIZE.X);
position.Y = MathHelper.Clamp(position.Y, 0, OsuPlayfield.BASE_SIZE.Y);
current.PositionRandomised = position;
}
/// <summary>
/// Moves the <see cref="Slider"/> and all necessary nested <see cref="OsuHitObject"/>s into the <see cref="OsuPlayfield"/> if they aren't already.
/// </summary>
private void moveSliderIntoPlayfield(Slider slider, RandomObjectInfo currentObjectInfo)
{
// Min. distances from the slider's position to the playfield border
var minMargin = new MarginPadding();
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderEndCircle))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
var relativePos = Vector2.Subtract(osuHitObject.Position, slider.Position);
minMargin.Left = Math.Max(minMargin.Left, -relativePos.X);
minMargin.Right = Math.Max(minMargin.Right, relativePos.X);
minMargin.Top = Math.Max(minMargin.Top, -relativePos.Y);
minMargin.Bottom = Math.Max(minMargin.Bottom, relativePos.Y);
}
if (slider.Position.X < minMargin.Left)
slider.Position = new Vector2(minMargin.Left, slider.Position.Y);
else if (slider.Position.X + minMargin.Right > OsuPlayfield.BASE_SIZE.X)
slider.Position = new Vector2(OsuPlayfield.BASE_SIZE.X - minMargin.Right, slider.Position.Y);
if (slider.Position.Y < minMargin.Top)
slider.Position = new Vector2(slider.Position.X, minMargin.Top);
else if (slider.Position.Y + minMargin.Bottom > OsuPlayfield.BASE_SIZE.Y)
slider.Position = new Vector2(slider.Position.X, OsuPlayfield.BASE_SIZE.Y - minMargin.Bottom);
currentObjectInfo.PositionRandomised = slider.Position;
currentObjectInfo.EndPositionRandomised = slider.EndPosition;
}
/// <summary>
/// Shifts all nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s by the specified shift.
/// </summary>
/// <param name="slider"><see cref="Slider"/> whose nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted</param>
/// <param name="shift">The <see cref="Vector2"/> the <see cref="Slider"/>'s nested <see cref="SliderTick"/>s and <see cref="SliderRepeat"/>s should be shifted by</param>
private void shiftNestedObjects(Slider slider, Vector2 shift)
{
foreach (var hitObject in slider.NestedHitObjects.Where(o => o is SliderTick || o is SliderRepeat))
{
if (!(hitObject is OsuHitObject osuHitObject))
continue;
osuHitObject.Position = Vector2.Add(osuHitObject.Position, shift);
}
}
/// <summary>
/// Determines the position of the current hit object relative to the previous one.
/// </summary>
/// <returns>The position of the current hit object relative to the previous one</returns>
private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
{
var relativeRotationDistance = 0f;
if (prevPosChanged.X < playfield_middle.X)
{
relativeRotationDistance = Math.Max(
(border_distance_x - prevPosChanged.X) / border_distance_x,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
relativeRotationDistance
);
}
if (prevPosChanged.Y < playfield_middle.Y)
{
relativeRotationDistance = Math.Max(
(border_distance_y - prevPosChanged.Y) / border_distance_y,
relativeRotationDistance
);
}
else
{
relativeRotationDistance = Math.Max(
(prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
relativeRotationDistance
);
}
return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
}
/// <summary>
/// Rotates vector "initial" towards vector "destinantion"
/// </summary>
/// <param name="initial">Vector to rotate to "destination"</param>
/// <param name="destination">Vector "initial" should be rotated to</param>
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
/// <returns>Resulting vector</returns>
private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
{
var initialAngleRad = Math.Atan2(initial.Y, initial.X);
var destAngleRad = Math.Atan2(destination.Y, destination.X);
var diff = destAngleRad - initialAngleRad;
while (diff < -Math.PI) diff += 2 * Math.PI;
while (diff > Math.PI) diff -= 2 * Math.PI;
var finalAngleRad = initialAngleRad + relativeDistance * diff;
return new Vector2(
initial.Length * (float)Math.Cos(finalAngleRad),
initial.Length * (float)Math.Sin(finalAngleRad)
);
}
private class RandomObjectInfo
{
public float AngleRad { get; set; }
public Vector2 PositionOriginal { get; }
public Vector2 PositionRandomised { get; set; }
public Vector2 EndPositionOriginal { get; }
public Vector2 EndPositionRandomised { get; set; }
public RandomObjectInfo(OsuHitObject hitObject)
{
PositionRandomised = PositionOriginal = hitObject.Position;
EndPositionRandomised = EndPositionOriginal = hitObject.EndPosition;
AngleRad = 0;
}
}
public class OsuModRandomSettingsControl : SettingsItem<int?>
{
protected override Drawable CreateControl() => new SeedControl
{
RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Top = 5 }
};
private sealed class SeedControl : CompositeDrawable, IHasCurrentValue<int?>
{
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
public Bindable<int?> Current
{
get => current;
set
{
current.Current = value;
seedNumberBox.Text = value.Value.ToString();
}
}
private readonly OsuNumberBox seedNumberBox;
public SeedControl()
{
AutoSizeAxes = Axes.Y;
InternalChildren = new[]
{
new GridContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 2),
new Dimension(GridSizeMode.Relative, 0.25f)
},
RowDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize)
},
Content = new[]
{
new Drawable[]
{
seedNumberBox = new OsuNumberBox
{
RelativeSizeAxes = Axes.X,
CommitOnFocusLost = true
}
}
}
}
};
seedNumberBox.Current.BindValueChanged(e =>
{
int? value = null;
if (int.TryParse(e.NewValue, out var intVal))
value = intVal;
current.Value = value;
});
}
protected override void Update()
{
if (current.Value == null)
seedNumberBox.Text = current.Current.Value.ToString();
}
}
}
}
}

View File

@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
{
new OsuModTarget(),
new OsuModDifficultyAdjust(),
new OsuModClassic()
new OsuModClassic(),
new OsuModRandom(),
};
case ModType.Automation:

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
{
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[]
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate) => new Skill[]
{
new Colour(mods),
new Rhythm(mods),

View File

@ -4,14 +4,13 @@
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Replays;
using osu.Game.Scoring;
using osu.Game.Users;
namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModAutoplay : ModAutoplay<TaikoHitObject>
public class TaikoModAutoplay : ModAutoplay
{
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -217,7 +217,7 @@ namespace osu.Game.Tests.NonVisual
throw new NotImplementedException();
}
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods)
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
{
throw new NotImplementedException();
}

View File

@ -18,12 +18,12 @@ namespace osu.Game.Tests.Visual.Gameplay
[Description("Player instantiated with an autoplay mod.")]
public class TestSceneAutoplay : TestSceneAllRulesetPlayers
{
protected new TestPlayer Player => (TestPlayer)base.Player;
protected new TestReplayPlayer Player => (TestReplayPlayer)base.Player;
protected override Player CreatePlayer(Ruleset ruleset)
{
SelectedMods.Value = new[] { ruleset.GetAutoplayMod() };
return new TestPlayer(false);
return new TestReplayPlayer(false);
}
protected override void AddCheckSteps()

View File

@ -12,6 +12,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays;
using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking;
@ -95,11 +96,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() });
AddStep("set mods", () => Game.SelectedMods.Value = new Mod[] { new OsuModNoFail(), new OsuModDoubleTime { SpeedChange = { Value = 2 } } });
AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
AddStep("seek to end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Track.Length));
AddUntilStep("wait for track playing", () => beatmap().Track.IsRunning);
AddStep("seek to near end", () => player.ChildrenOfType<GameplayClockContainer>().First().Seek(beatmap().Beatmap.HitObjects[^1].StartTime - 1000));
AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Difficulty
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{
var skills = CreateSkills(beatmap, mods);
var skills = CreateSkills(beatmap, mods, clockRate);
if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
@ -180,7 +180,8 @@ namespace osu.Game.Rulesets.Difficulty
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
/// <param name="mods">Mods to calculate difficulty with.</param>
/// <param name="clockRate">Clockrate to calculate difficulty with.</param>
/// <returns>The <see cref="Skill"/>s.</returns>
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods);
protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate);
}
}

View File

@ -1,9 +1,11 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit;
@ -52,15 +54,21 @@ namespace osu.Game.Rulesets.Edit
if (changeHandler != null)
{
// for now only regenerate replay on a finalised state change, not HitObjectUpdated.
changeHandler.OnStateChange += updateReplay;
changeHandler.OnStateChange += () => Scheduler.AddOnce(regenerateAutoplay);
}
else
{
beatmap.HitObjectUpdated += _ => updateReplay();
beatmap.HitObjectUpdated += _ => Scheduler.AddOnce(regenerateAutoplay);
}
Scheduler.AddOnce(regenerateAutoplay);
}
private void updateReplay() => Scheduler.AddOnce(drawableRuleset.RegenerateAutoplay);
private void regenerateAutoplay()
{
var autoplayMod = drawableRuleset.Mods.OfType<ModAutoplay>().Single();
drawableRuleset.SetReplayScore(autoplayMod.CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
private void addHitObject(HitObject hitObject)
{

View File

@ -0,0 +1,14 @@
// 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.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public interface ICreateReplay
{
public Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods);
}
}

View File

@ -7,22 +7,11 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Beatmaps;
using osu.Game.Graphics;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods
{
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T>
where T : HitObject
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
{
public override string Name => "Autoplay";
public override string Acronym => "AT";

View File

@ -182,18 +182,11 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay)));
}
RegenerateAutoplay();
applyRulesetMods(Mods, config);
loadObjects(cancellationToken ?? default);
}
public void RegenerateAutoplay()
{
// for now this is applying mods which aren't just autoplay.
// we'll need to reconsider this flow in the future.
applyRulesetMods(Mods, config);
}
/// <summary>
/// Creates and adds drawable representations of hit objects to the play field.
/// </summary>

View File

@ -17,7 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public class MultiSpectatorPlayer : SpectatorPlayer
{
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly Score score;
private readonly ISpectatorPlayerClock spectatorPlayerClock;
/// <summary>
@ -28,7 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
: base(score)
{
this.score = score;
this.spectatorPlayerClock = spectatorPlayerClock;
}
@ -43,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame.
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || score.Replay.Frames.Count == 0;
waitingOnFrames.Value = DrawableRuleset.FrameStableClock.WaitingOnFrames.Value || Score.Replay.Frames.Count == 0;
}
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart)

View File

@ -54,11 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.Playlists
return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
}
protected override Score CreateScore()
protected override void PrepareScoreForResults()
{
var score = base.CreateScore();
score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
return score;
base.PrepareScoreForResults();
Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
}
protected override void Dispose(bool isDisposing)

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
@ -24,10 +23,8 @@ using osu.Game.IO.Archives;
using osu.Game.Online.API;
using osu.Game.Online.Spectator;
using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
@ -137,6 +134,8 @@ namespace osu.Game.Screens.Play
public readonly PlayerConfiguration Configuration;
protected Score Score { get; private set; }
/// <summary>
/// Create a new player instance.
/// </summary>
@ -145,7 +144,7 @@ namespace osu.Game.Screens.Play
Configuration = configuration ?? new PlayerConfiguration();
}
private GameplayBeatmap gameplayBeatmap;
protected GameplayBeatmap GameplayBeatmap { get; private set; }
private ScreenSuspensionHandler screenSuspension;
@ -161,24 +160,32 @@ namespace osu.Game.Screens.Play
if (!LoadedBeatmapSuccessfully)
return;
// replays should never be recorded or played back when autoplay is enabled
if (!Mods.Value.Any(m => m is ModAutoplay))
PrepareReplay();
Score = CreateScore();
// ensure the score is in a consistent state with the current player.
Score.ScoreInfo.Beatmap = Beatmap.Value.BeatmapInfo;
Score.ScoreInfo.Ruleset = rulesetInfo;
Score.ScoreInfo.Mods = Mods.Value.ToArray();
PrepareReplay();
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(Score.ScoreInfo);
gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
}
[CanBeNull]
private Score recordingScore;
/// <summary>
/// Run any recording / playback setup for replays.
/// </summary>
protected virtual void PrepareReplay()
{
DrawableRuleset.SetRecordTarget(recordingScore = new Score());
DrawableRuleset.SetRecordTarget(Score);
}
ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo);
protected virtual void PrepareScoreForResults()
{
// perform one final population to ensure everything is up-to-date.
ScoreProcessor.PopulateScore(Score.ScoreInfo);
}
[BackgroundDependencyLoader(true)]
@ -223,10 +230,10 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap));
AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap));
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
dependencies.CacheAs(gameplayBeatmap);
dependencies.CacheAs(GameplayBeatmap);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
@ -284,7 +291,7 @@ namespace osu.Game.Screens.Play
{
HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r);
GameplayBeatmap.ApplyResult(r);
};
DrawableRuleset.RevertResult += r =>
@ -633,11 +640,11 @@ namespace osu.Game.Screens.Play
prepareScoreForDisplayTask ??= Task.Run(async () =>
{
var score = CreateScore();
PrepareScoreForResults();
try
{
await PrepareScoreForResultsAsync(score).ConfigureAwait(false);
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
@ -646,14 +653,14 @@ namespace osu.Game.Screens.Play
try
{
await ImportScore(score).ConfigureAwait(false);
await ImportScore(Score).ConfigureAwait(false);
}
catch (Exception ex)
{
Logger.Error(ex, "Score import failed!");
}
return score.ScoreInfo;
return Score.ScoreInfo;
});
if (skipStoryboardOutro)
@ -905,41 +912,19 @@ namespace osu.Game.Screens.Play
}
/// <summary>
/// Creates the player's <see cref="Score"/>.
/// Creates the player's <see cref="Scoring.Score"/>.
/// </summary>
/// <returns>The <see cref="Score"/>.</returns>
protected virtual Score CreateScore()
{
var score = new Score
/// <returns>The <see cref="Scoring.Score"/>.</returns>
protected virtual Score CreateScore() =>
new Score
{
ScoreInfo = new ScoreInfo
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
}
ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
};
if (DrawableRuleset.ReplayScore != null)
{
score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser();
score.Replay = DrawableRuleset.ReplayScore.Replay;
}
else
{
score.ScoreInfo.User = api.LocalUser.Value;
score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List<ReplayFrame>() };
}
ScoreProcessor.PopulateScore(score.ScoreInfo);
return score;
}
/// <summary>
/// Imports the player's <see cref="Score"/> to the local database.
/// Imports the player's <see cref="Scoring.Score"/> to the local database.
/// </summary>
/// <param name="score">The <see cref="Score"/> to import.</param>
/// <param name="score">The <see cref="Scoring.Score"/> to import.</param>
/// <returns>The imported score.</returns>
protected virtual async Task ImportScore(Score score)
{
@ -951,7 +936,7 @@ namespace osu.Game.Screens.Play
using (var stream = new MemoryStream())
{
new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream);
new LegacyScoreEncoder(score, GameplayBeatmap.PlayableBeatmap).Encode(stream);
replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
}
@ -970,9 +955,9 @@ namespace osu.Game.Screens.Play
}
/// <summary>
/// Prepare the <see cref="Score"/> for display at results.
/// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary>
/// <param name="score">The <see cref="Score"/> to prepare.</param>
/// <param name="score">The <see cref="Scoring.Score"/> to prepare.</param>
/// <returns>A task that prepares the provided score. On completion, the score is assumed to be ready for display.</returns>
protected virtual Task PrepareScoreForResultsAsync(Score score) => Task.CompletedTask;

View File

@ -1,9 +1,13 @@
// 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.Threading.Tasks;
using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring;
using osu.Game.Screens.Ranking;
@ -11,15 +15,20 @@ namespace osu.Game.Screens.Play
{
public class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction>
{
protected readonly Score Score;
private readonly Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore;
// Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure() => false;
public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
: this((_, __) => score, configuration)
{
}
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration configuration = null)
: base(configuration)
{
Score = score;
this.createScore = createScore;
}
protected override void PrepareReplay()
@ -27,15 +36,7 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(Score);
}
protected override Score CreateScore()
{
var baseScore = base.CreateScore();
// Since the replay score doesn't contain statistics, we'll pass them through here.
Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents;
return Score;
}
protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value);
// Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask;

View File

@ -18,6 +18,9 @@ namespace osu.Game.Screens.Play
{
private readonly Score score;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap
public SpectatorPlayer(Score score)
@ -25,13 +28,10 @@ namespace osu.Game.Screens.Play
this.score = score;
}
protected override ResultsScreen CreateResults(ScoreInfo score)
{
return new SpectatorResultsScreen(score);
}
protected override Score CreateScore() => score;
[Resolved]
private SpectatorClient spectatorClient { get; set; }
protected override ResultsScreen CreateResults(ScoreInfo score)
=> new SpectatorResultsScreen(score);
[BackgroundDependencyLoader]
private void load()

View File

@ -21,7 +21,7 @@ namespace osu.Game.Screens.Select
public class PlaySongSelect : SongSelect
{
private bool removeAutoModOnResume;
private OsuScreen player;
private OsuScreen playerLoader;
[Resolved(CanBeNull = true)]
private NotificationOverlay notifications { get; set; }
@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select
{
base.OnResuming(last);
player = null;
playerLoader = null;
if (removeAutoModOnResume)
{
@ -79,14 +79,14 @@ namespace osu.Game.Screens.Select
protected override bool OnStart()
{
if (player != null) return false;
if (playerLoader != null) return false;
// Ctrl+Enter should start map with autoplay enabled.
if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true)
{
var autoplayMod = getAutoplayMod();
var autoInstance = getAutoplayMod();
if (autoplayMod == null)
if (autoInstance == null)
{
notifications?.Post(new SimpleNotification
{
@ -97,18 +97,26 @@ namespace osu.Game.Screens.Select
var mods = Mods.Value;
if (mods.All(m => m.GetType() != autoplayMod.GetType()))
if (mods.All(m => m.GetType() != autoInstance.GetType()))
{
Mods.Value = mods.Append(autoplayMod).ToArray();
Mods.Value = mods.Append(autoInstance).ToArray();
removeAutoModOnResume = true;
}
}
SampleConfirm?.Play();
this.Push(player = new PlayerLoader(() => new SoloPlayer()));
this.Push(playerLoader = new PlayerLoader(createPlayer));
return true;
Player createPlayer()
{
var replayGeneratingMod = Mods.Value.OfType<ICreateReplay>().FirstOrDefault();
if (replayGeneratingMod != null)
return new ReplayPlayer((beatmap, mods) => replayGeneratingMod.CreateReplayScore(beatmap, mods));
return new SoloPlayer();
}
}
}
}

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
@ -48,6 +49,26 @@ namespace osu.Game.Tests.Visual
PauseOnFocusLost = pauseOnFocusLost;
}
protected override void PrepareReplay()
{
// Generally, replay generation is handled by whatever is constructing the player.
// This is implemented locally here to ease migration of test scenes that have some executions
// running with autoplay and some not, but are not written in a way that lends to instantiating
// different `Player` types.
//
// Eventually we will want to remove this and update all test usages which rely on autoplay to use
// a `TestReplayPlayer`.
var autoplayMod = Mods.Value.OfType<ModAutoplay>().FirstOrDefault();
if (autoplayMod != null)
{
DrawableRuleset?.SetReplayScore(autoplayMod.CreateReplayScore(GameplayBeatmap.PlayableBeatmap, Mods.Value));
return;
}
base.PrepareReplay();
}
[BackgroundDependencyLoader]
private void load()
{

View File

@ -0,0 +1,65 @@
// 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.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring;
using osu.Game.Screens.Play;
namespace osu.Game.Tests.Visual
{
/// <summary>
/// A player that exposes many components that would otherwise not be available, for testing purposes.
/// </summary>
public class TestReplayPlayer : ReplayPlayer
{
protected override bool PauseOnFocusLost { get; }
public new DrawableRuleset DrawableRuleset => base.DrawableRuleset;
/// <summary>
/// Mods from *player* (not OsuScreen).
/// </summary>
public new Bindable<IReadOnlyList<Mod>> Mods => base.Mods;
public new HUDOverlay HUDOverlay => base.HUDOverlay;
public new GameplayClockContainer GameplayClockContainer => base.GameplayClockContainer;
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public new HealthProcessor HealthProcessor => base.HealthProcessor;
public new bool PauseCooldownActive => base.PauseCooldownActive;
/// <summary>
/// Instantiate a replay player that renders an autoplay mod.
/// </summary>
public TestReplayPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base((beatmap, mods) => mods.OfType<ModAutoplay>().First().CreateReplayScore(beatmap, mods), new PlayerConfiguration
{
AllowPause = allowPause,
ShowResults = showResults
})
{
PauseOnFocusLost = pauseOnFocusLost;
}
/// <summary>
/// Instantiate a replay player that renders the provided replay.
/// </summary>
public TestReplayPlayer(Score score, bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false)
: base(score, new PlayerConfiguration
{
AllowPause = allowPause,
ShowResults = showResults
})
{
PauseOnFocusLost = pauseOnFocusLost;
}
}
}

View File

@ -31,7 +31,7 @@ namespace osu.Game.Users.Drawables
private void load(LargeTextureStore textures)
{
if (user != null && user.Id > 1)
Texture = textures.Get($@"https://a.ppy.sh/{user.Id}");
Texture = textures.Get(user.AvatarUrl);
Texture ??= textures.Get(@"Online/avatar-guest");
}