1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-27 11:03:22 +08:00

Merge branch 'master' into factor-out-hoc

This commit is contained in:
Dan Balasescu 2021-06-04 15:31:35 +09:00 committed by GitHub
commit b283c48abb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 1128 additions and 232 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 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 System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.EmptyFreeform.Objects;
using osu.Game.Rulesets.EmptyFreeform.Replays; using osu.Game.Rulesets.EmptyFreeform.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.EmptyFreeform.Mods 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 public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods 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 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 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 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.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.EmptyScrolling.Objects;
using osu.Game.Rulesets.EmptyScrolling.Replays; using osu.Game.Rulesets.EmptyScrolling.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
@ -11,7 +10,7 @@ using System.Collections.Generic;
namespace osu.Game.Rulesets.EmptyScrolling.Mods 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 public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -4,14 +4,13 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Pippidon.Objects;
using osu.Game.Rulesets.Pippidon.Replays; using osu.Game.Rulesets.Pippidon.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Pippidon.Mods 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 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 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; halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f;
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
return new Skill[] 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> /// </summary>
public readonly double StrainTime; public readonly double StrainTime;
public readonly double ClockRate;
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth) public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
: base(hitObject, lastObject, clockRate) : 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 // 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);
ClockRate = clockRate;
} }
} }
} }

View File

@ -28,10 +28,21 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
private float lastDistanceMoved; private float lastDistanceMoved;
private double lastStrainTime; 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) : base(mods)
{ {
HalfCatcherWidth = halfCatcherWidth; 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) protected override double StrainValueOf(DifficultyHitObject current)
@ -48,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
float distanceMoved = playerPosition - lastPlayerPosition.Value; 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 distanceAddition = (Math.Pow(Math.Abs(distanceMoved), 1.3) / 510);
double sqrtStrain = Math.Sqrt(weightedStrainTime); double sqrtStrain = Math.Sqrt(weightedStrainTime);
@ -81,7 +92,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills
playerPosition = catchCurrent.NormalizedPosition; 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; lastPlayerPosition = playerPosition;

View File

@ -3,7 +3,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Catch.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -11,7 +10,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Catch.Mods 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 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. // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input; 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) new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns)
}; };

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.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -12,7 +11,7 @@ using osu.Game.Users;
namespace osu.Game.Rulesets.Mania.Mods 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 public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{ {

View File

@ -1,6 +1,7 @@
// 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 osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Utils; using osu.Framework.Utils;
@ -17,8 +18,11 @@ namespace osu.Game.Rulesets.Mania.Mods
public void ApplyToBeatmap(IBeatmap beatmap) public void ApplyToBeatmap(IBeatmap beatmap)
{ {
Seed.Value ??= RNG.Next();
var rng = new Random((int)Seed.Value);
var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns; var availableColumns = ((ManiaBeatmap)beatmap).TotalColumns;
var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => RNG.Next()).ToList(); var shuffledColumns = Enumerable.Range(0, availableColumns).OrderBy(item => rng.Next()).ToList();
beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]); beatmap.HitObjects.OfType<ManiaHitObject>().ForEach(h => h.Column = shuffledColumns[h.Column]);
} }

View File

@ -0,0 +1,30 @@
// 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.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestSceneHitCircleKiai : TestSceneHitCircle
{
[SetUp]
public void SetUp() => Schedule(() =>
{
var controlPointInfo = new ControlPointInfo();
controlPointInfo.Add(0, new TimingControlPoint { BeatLength = 500 });
controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true });
Beatmap.Value = CreateWorkingBeatmap(new Beatmap
{
ControlPointInfo = controlPointInfo
});
// track needs to be playing for BeatSyncedContainer to work.
Beatmap.Value.Track.Start();
});
}
}

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 Aim(mods),
new Speed(mods) new Speed(mods)

View File

@ -6,14 +6,13 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Rulesets.Osu.Mods 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(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();

View File

@ -0,0 +1,280 @@
// 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.Graphics;
using osu.Framework.Utils;
using osu.Game.Beatmaps;
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;
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;
}
}
}
}

View File

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

View File

@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
Origin = Anchor.Centre, Origin = Anchor.Centre,
Texture = textures.Get(@"Gameplay/osu/disc"), Texture = textures.Get(@"Gameplay/osu/disc"),
}, },
new KiaiFlash
{
RelativeSizeAxes = Axes.Both,
},
triangles = new TrianglesPiece triangles = new TrianglesPiece
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,

View File

@ -0,0 +1,43 @@
// 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.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
public class KiaiFlash : BeatSyncedContainer
{
private const double fade_length = 80;
private const float flash_opacity = 0.25f;
public KiaiFlash()
{
EarlyActivationMilliseconds = 80;
Blending = BlendingParameters.Additive;
Child = new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.White,
Alpha = 0f,
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
Child
.FadeTo(flash_opacity, EarlyActivationMilliseconds, Easing.OutQuint)
.Then()
.FadeOut(timingPoint.BeatLength - fade_length, Easing.OutSine);
}
}
}

View File

@ -0,0 +1,61 @@
// 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.Track;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
{
internal class KiaiFlashingSprite : BeatSyncedContainer
{
private readonly Sprite mainSprite;
private readonly Sprite flashingSprite;
public Texture Texture
{
set
{
mainSprite.Texture = value;
flashingSprite.Texture = value;
}
}
private const float flash_opacity = 0.3f;
public KiaiFlashingSprite()
{
AutoSizeAxes = Axes.Both;
Children = new Drawable[]
{
mainSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
flashingSprite = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Alpha = 0,
Blending = BlendingParameters.Additive,
}
};
}
protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)
{
if (!effectPoint.KiaiMode)
return;
flashingSprite
.FadeTo(flash_opacity)
.Then()
.FadeOut(timingPoint.BeatLength * 0.75f);
}
}
}

View File

@ -5,7 +5,6 @@ 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.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -32,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
} }
private Container<Sprite> circleSprites; private Container circleSprites;
private Sprite hitCircleSprite; private Drawable hitCircleSprite;
private Sprite hitCircleOverlay; private Drawable hitCircleOverlay;
private SkinnableSpriteText hitCircleText; private SkinnableSpriteText hitCircleText;
@ -72,20 +71,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
circleSprites = new Container<Sprite> circleSprites = new Container
{ {
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Children = new[] Children = new[]
{ {
hitCircleSprite = new Sprite hitCircleSprite = new KiaiFlashingSprite
{ {
Texture = baseTexture, Texture = baseTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
hitCircleOverlay = new Sprite hitCircleOverlay = new KiaiFlashingSprite
{ {
Texture = overlayTexture, Texture = overlayTexture,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,

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 Colour(mods),
new Rhythm(mods), new Rhythm(mods),

View File

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

View File

@ -20,10 +20,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
{ {
var taikoBeatmap = (TaikoBeatmap)beatmap; var taikoBeatmap = (TaikoBeatmap)beatmap;
Seed.Value ??= RNG.Next();
var rng = new Random((int)Seed.Value);
foreach (var obj in taikoBeatmap.HitObjects) foreach (var obj in taikoBeatmap.HitObjects)
{ {
if (obj is Hit hit) if (obj is Hit hit)
hit.Type = RNG.Next(2) == 0 ? HitType.Centre : HitType.Rim; hit.Type = rng.Next(2) == 0 ? HitType.Centre : HitType.Rim;
} }
} }
} }

View File

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

View File

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

View File

@ -12,6 +12,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Mods; using osu.Game.Overlays.Mods;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Screens.Play; using osu.Game.Screens.Play;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -95,11 +96,12 @@ namespace osu.Game.Tests.Visual.Navigation
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); 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)); AddStep("press enter", () => InputManager.Key(Key.Enter));
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); 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); AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded);
AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action()); AddStep("attempt to retry", () => results.ChildrenOfType<HotkeyRetryOverlay>().First().Action());
AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player);

View File

@ -9,6 +9,8 @@ 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.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input;
using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -36,6 +38,10 @@ namespace osu.Game.Tests.Visual.Online
private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1); private Channel previousChannel => joinedChannels.ElementAt(joinedChannels.ToList().IndexOf(currentChannel) - 1);
private Channel channel1 => channels[0]; private Channel channel1 => channels[0];
private Channel channel2 => channels[1]; private Channel channel2 => channels[1];
private Channel channel3 => channels[2];
[Resolved]
private GameHost host { get; set; }
public TestSceneChatOverlay() public TestSceneChatOverlay()
{ {
@ -44,7 +50,8 @@ namespace osu.Game.Tests.Visual.Online
{ {
Name = $"Channel no. {index}", Name = $"Channel no. {index}",
Topic = index == 3 ? null : $"We talk about the number {index} here", Topic = index == 3 ? null : $"We talk about the number {index} here",
Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary Type = index % 2 == 0 ? ChannelType.PM : ChannelType.Temporary,
Id = index
}) })
.ToList(); .ToList();
} }
@ -228,6 +235,92 @@ namespace osu.Game.Tests.Visual.Online
AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any()); AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
} }
[Test]
public void TestCloseTabShortcut()
{
AddStep("Join 2 channels", () =>
{
channelManager.JoinChannel(channel1);
channelManager.JoinChannel(channel2);
});
// Want to close channel 2
AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
AddStep("Close tab via shortcut", pressCloseDocumentKeys);
// Channel 2 should be closed
AddAssert("Channel 1 open", () => channelManager.JoinedChannels.Contains(channel1));
AddAssert("Channel 2 closed", () => !channelManager.JoinedChannels.Contains(channel2));
// Want to close channel 1
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Close tab via shortcut", pressCloseDocumentKeys);
// Channel 1 and channel 2 should be closed
AddAssert("All channels closed", () => !channelManager.JoinedChannels.Any());
}
[Test]
public void TestNewTabShortcut()
{
AddStep("Join 2 channels", () =>
{
channelManager.JoinChannel(channel1);
channelManager.JoinChannel(channel2);
});
// Want to join another channel
AddStep("Press new tab shortcut", pressNewTabKeys);
// Selector should be visible
AddAssert("Selector is visible", () => chatOverlay.SelectionOverlayState == Visibility.Visible);
}
[Test]
public void TestRestoreTabShortcut()
{
AddStep("Join 3 channels", () =>
{
channelManager.JoinChannel(channel1);
channelManager.JoinChannel(channel2);
channelManager.JoinChannel(channel3);
});
// Should do nothing
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
AddAssert("All channels still open", () => channelManager.JoinedChannels.Count == 3);
// Close channel 1
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Click normal close button", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
AddAssert("Channel 1 closed", () => !channelManager.JoinedChannels.Contains(channel1));
AddAssert("Other channels still open", () => channelManager.JoinedChannels.Count == 2);
// Reopen channel 1
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
// Close two channels
AddStep("Select channel 1", () => clickDrawable(chatOverlay.TabMap[channel1]));
AddStep("Close channel 1", () => clickDrawable(((TestChannelTabItem)chatOverlay.TabMap[channel1]).CloseButton.Child));
AddStep("Select channel 2", () => clickDrawable(chatOverlay.TabMap[channel2]));
AddStep("Close channel 2", () => clickDrawable(((TestPrivateChannelTabItem)chatOverlay.TabMap[channel2]).CloseButton.Child));
AddAssert("Only one channel open", () => channelManager.JoinedChannels.Count == 1);
AddAssert("Current channel is channel 3", () => currentChannel == channel3);
// Should first re-open channel 2
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
AddAssert("Channel 1 still closed", () => !channelManager.JoinedChannels.Contains(channel1));
AddAssert("Channel 2 now open", () => channelManager.JoinedChannels.Contains(channel2));
AddAssert("Current channel is channel 2", () => currentChannel == channel2);
// Should then re-open channel 1
AddStep("Restore tab via shortcut", pressRestoreTabKeys);
AddAssert("All channels now open", () => channelManager.JoinedChannels.Count == 3);
AddAssert("Current channel is channel 1", () => currentChannel == channel1);
}
private void pressChannelHotkey(int number) private void pressChannelHotkey(int number)
{ {
var channelKey = Key.Number0 + number; var channelKey = Key.Number0 + number;
@ -236,6 +329,23 @@ namespace osu.Game.Tests.Visual.Online
InputManager.ReleaseKey(Key.AltLeft); InputManager.ReleaseKey(Key.AltLeft);
} }
private void pressCloseDocumentKeys() => pressKeysFor(PlatformActionType.DocumentClose);
private void pressNewTabKeys() => pressKeysFor(PlatformActionType.TabNew);
private void pressRestoreTabKeys() => pressKeysFor(PlatformActionType.TabRestore);
private void pressKeysFor(PlatformActionType type)
{
var binding = host.PlatformKeyBindings.First(b => ((PlatformAction)b.Action).ActionType == type);
foreach (var k in binding.KeyCombination.Keys)
InputManager.PressKey((Key)k);
foreach (var k in binding.KeyCombination.Keys)
InputManager.ReleaseKey((Key)k);
}
private void clickDrawable(Drawable d) private void clickDrawable(Drawable d)
{ {
InputManager.MoveMouseTo(d); InputManager.MoveMouseTo(d);

View File

@ -1,6 +1,7 @@
// 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.Net;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
@ -32,7 +33,14 @@ namespace osu.Game.Tests.Visual.Online
AddStep("Show Article Page", () => wiki.ShowPage("Interface")); AddStep("Show Article Page", () => wiki.ShowPage("Interface"));
} }
private void setUpWikiResponse(APIWikiPage r) [Test]
public void TestErrorPage()
{
setUpWikiResponse(null, true);
AddStep("Show Error Page", () => wiki.ShowPage("Error"));
}
private void setUpWikiResponse(APIWikiPage r, bool isFailed = false)
=> AddStep("set up response", () => => AddStep("set up response", () =>
{ {
dummyAPI.HandleRequest = request => dummyAPI.HandleRequest = request =>
@ -40,7 +48,11 @@ namespace osu.Game.Tests.Visual.Online
if (!(request is GetWikiRequest getWikiRequest)) if (!(request is GetWikiRequest getWikiRequest))
return false; return false;
getWikiRequest.TriggerSuccess(r); if (isFailed)
getWikiRequest.TriggerFailure(new WebException());
else
getWikiRequest.TriggerSuccess(r);
return true; return true;
}; };
}); });

View File

@ -8,6 +8,7 @@ using System.Threading.Tasks;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Game.Database;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Overlays.Chat.Tabs; using osu.Game.Overlays.Chat.Tabs;
@ -33,6 +34,16 @@ namespace osu.Game.Online.Chat
private readonly BindableList<Channel> availableChannels = new BindableList<Channel>(); private readonly BindableList<Channel> availableChannels = new BindableList<Channel>();
private readonly BindableList<Channel> joinedChannels = new BindableList<Channel>(); private readonly BindableList<Channel> joinedChannels = new BindableList<Channel>();
/// <summary>
/// Keeps a stack of recently closed channels
/// </summary>
private readonly List<ClosedChannel> closedChannels = new List<ClosedChannel>();
// For efficiency purposes, this constant bounds the number of closed channels we store.
// This number is somewhat arbitrary; future developers are free to modify it.
// Must be a positive number.
private const int closed_channels_max_size = 50;
/// <summary> /// <summary>
/// The currently opened channel /// The currently opened channel
/// </summary> /// </summary>
@ -51,6 +62,9 @@ namespace osu.Game.Online.Chat
[Resolved] [Resolved]
private IAPIProvider api { get; set; } private IAPIProvider api { get; set; }
[Resolved]
private UserLookupCache users { get; set; }
public readonly BindableBool HighPollRate = new BindableBool(); public readonly BindableBool HighPollRate = new BindableBool();
public ChannelManager() public ChannelManager()
@ -420,6 +434,18 @@ namespace osu.Game.Online.Chat
joinedChannels.Remove(channel); joinedChannels.Remove(channel);
// Prevent the closedChannel list from exceeding the max size
// by removing the oldest element
if (closedChannels.Count >= closed_channels_max_size)
{
closedChannels.RemoveAt(0);
}
// For PM channels, we store the user ID; else, we store the channel ID
closedChannels.Add(channel.Type == ChannelType.PM
? new ClosedChannel(ChannelType.PM, channel.Users.Single().Id)
: new ClosedChannel(channel.Type, channel.Id));
if (channel.Joined.Value) if (channel.Joined.Value)
{ {
api.Queue(new LeaveChannelRequest(channel)); api.Queue(new LeaveChannelRequest(channel));
@ -427,6 +453,46 @@ namespace osu.Game.Online.Chat
} }
}); });
/// <summary>
/// Opens the most recently closed channel that has not already been reopened,
/// Works similarly to reopening the last closed tab on a web browser.
/// </summary>
public void JoinLastClosedChannel()
{
// This loop could be eliminated if the join channel operation ensured that every channel joined
// is removed from the closedChannels list, but it'd require a linear scan of closed channels on every join.
// To keep the overhead of joining channels low, just lazily scan the list of closed channels locally.
while (closedChannels.Count > 0)
{
ClosedChannel lastClosedChannel = closedChannels.Last();
closedChannels.RemoveAt(closedChannels.Count - 1);
// If the user has already joined the channel, try the next one
if (joinedChannels.FirstOrDefault(lastClosedChannel.Matches) != null)
continue;
Channel lastChannel = AvailableChannels.FirstOrDefault(lastClosedChannel.Matches);
if (lastChannel != null)
{
// Channel exists as an available channel, directly join it
CurrentChannel.Value = JoinChannel(lastChannel);
}
else if (lastClosedChannel.Type == ChannelType.PM)
{
// Try to get user in order to open PM chat
users.GetUserAsync((int)lastClosedChannel.Id).ContinueWith(u =>
{
if (u.Result == null) return;
Schedule(() => CurrentChannel.Value = JoinChannel(new Channel(u.Result)));
});
}
return;
}
}
private long lastMessageId; private long lastMessageId;
private bool channelsInitialised; private bool channelsInitialised;
@ -511,4 +577,28 @@ namespace osu.Game.Online.Chat
{ {
} }
} }
/// <summary>
/// Stores information about a closed channel
/// </summary>
public class ClosedChannel
{
public readonly ChannelType Type;
public readonly long Id;
public ClosedChannel(ChannelType type, long id)
{
Type = type;
Id = id;
}
public bool Matches(Channel channel)
{
if (channel.Type != Type) return false;
return Type == ChannelType.PM
? channel.Users.Single().Id == Id
: channel.Id == Id;
}
}
} }

View File

@ -81,9 +81,11 @@ namespace osu.Game.Overlays.Chat.Tabs
RemoveItem(channel); RemoveItem(channel);
if (SelectedTab == null) if (SelectedTab == null)
SelectTab(selectorTab); SelectChannelSelectorTab();
} }
public void SelectChannelSelectorTab() => SelectTab(selectorTab);
protected override void SelectTab(TabItem<Channel> tab) protected override void SelectTab(TabItem<Channel> tab)
{ {
if (tab is ChannelSelectorTabItem) if (tab is ChannelSelectorTabItem)

View File

@ -24,13 +24,15 @@ using osu.Game.Overlays.Chat.Tabs;
using osuTK.Input; using osuTK.Input;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.Bindings;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Game.Localisation; using osu.Game.Localisation;
using osu.Game.Online; using osu.Game.Online;
namespace osu.Game.Overlays namespace osu.Game.Overlays
{ {
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler<PlatformAction>
{ {
public string IconTexture => "Icons/Hexacons/messaging"; public string IconTexture => "Icons/Hexacons/messaging";
public LocalisableString Title => ChatStrings.HeaderTitle; public LocalisableString Title => ChatStrings.HeaderTitle;
@ -370,6 +372,30 @@ namespace osu.Game.Overlays
return base.OnKeyDown(e); return base.OnKeyDown(e);
} }
public bool OnPressed(PlatformAction action)
{
switch (action.ActionType)
{
case PlatformActionType.TabNew:
ChannelTabControl.SelectChannelSelectorTab();
return true;
case PlatformActionType.TabRestore:
channelManager.JoinLastClosedChannel();
return true;
case PlatformActionType.DocumentClose:
channelManager.LeaveChannel(channelManager.CurrentChannel.Value);
return true;
}
return false;
}
public void OnReleased(PlatformAction action)
{
}
public override bool AcceptsFocus => true; public override bool AcceptsFocus => true;
protected override void OnFocus(FocusEvent e) protected override void OnFocus(FocusEvent e)

View File

@ -6,87 +6,36 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Framework.Graphics.Shapes;
using osuTK; using osuTK;
using System.Linq; using System.Linq;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays.News.Sidebar namespace osu.Game.Overlays.News.Sidebar
{ {
public class NewsSidebar : CompositeDrawable public class NewsSidebar : OverlaySidebar
{ {
[Cached] [Cached]
public readonly Bindable<APINewsSidebar> Metadata = new Bindable<APINewsSidebar>(); public readonly Bindable<APINewsSidebar> Metadata = new Bindable<APINewsSidebar>();
private FillFlowContainer<MonthSection> monthsFlow; private FillFlowContainer<MonthSection> monthsFlow;
[BackgroundDependencyLoader] protected override Drawable CreateContent() => new FillFlowContainer
private void load(OverlayColourProvider colourProvider)
{ {
RelativeSizeAxes = Axes.Y; Direction = FillDirection.Vertical,
Width = 250; RelativeSizeAxes = Axes.X,
InternalChildren = new Drawable[] AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{ {
new Box new YearsPanel(),
monthsFlow = new FillFlowContainer<MonthSection>
{ {
RelativeSizeAxes = Axes.Both, AutoSizeAxes = Axes.Y,
Colour = colourProvider.Background4 RelativeSizeAxes = Axes.X,
}, Direction = FillDirection.Vertical,
new Box Spacing = new Vector2(0, 10)
{
RelativeSizeAxes = Axes.Y,
Width = OsuScrollContainer.SCROLL_BAR_HEIGHT,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Colour = colourProvider.Background3,
Alpha = 0.5f
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 3 }, // Addeded 3px back
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Vertical = 20,
Left = 50,
Right = 30
},
Child = new FillFlowContainer
{
Direction = FillDirection.Vertical,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 20),
Children = new Drawable[]
{
new YearsPanel(),
monthsFlow = new FillFlowContainer<MonthSection>
{
AutoSizeAxes = Axes.Y,
RelativeSizeAxes = Axes.X,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10)
}
}
}
}
}
}
} }
}; }
} };
protected override void LoadComplete() protected override void LoadComplete()
{ {

View File

@ -0,0 +1,76 @@
// 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.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Containers;
namespace osu.Game.Overlays
{
public abstract class OverlaySidebar : CompositeDrawable
{
private readonly Box sidebarBackground;
private readonly Box scrollbarBackground;
protected OverlaySidebar()
{
RelativeSizeAxes = Axes.Y;
Width = 250;
InternalChildren = new Drawable[]
{
sidebarBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
scrollbarBackground = new Box
{
RelativeSizeAxes = Axes.Y,
Width = OsuScrollContainer.SCROLL_BAR_HEIGHT,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
Alpha = 0.5f
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = -3 }, // Compensate for scrollbar margin
Child = new OsuScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Right = 3 }, // Addeded 3px back
Child = new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Vertical = 20,
Left = 50,
Right = 30
},
Child = CreateContent()
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
sidebarBackground.Colour = colourProvider.Background4;
scrollbarBackground.Colour = colourProvider.Background3;
}
[NotNull]
protected virtual Drawable CreateContent() => Empty();
}
}

View File

@ -92,7 +92,7 @@ namespace osu.Game.Overlays
Loading.Show(); Loading.Show();
request.Success += response => Schedule(() => onSuccess(response)); request.Success += response => Schedule(() => onSuccess(response));
request.Failure += _ => Schedule(() => LoadDisplay(Empty())); request.Failure += _ => Schedule(onFail);
api.PerformAsync(request); api.PerformAsync(request);
} }
@ -132,6 +132,24 @@ namespace osu.Game.Overlays
} }
} }
private void onFail()
{
LoadDisplay(new WikiMarkdownContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
CurrentPath = $@"{api.WebsiteRootUrl}/wiki/",
Text = $"Something went wrong when trying to fetch page \"{path.Value}\".\n\n[Return to the main page](Main_Page).",
DocumentMargin = new MarginPadding(0),
DocumentPadding = new MarginPadding
{
Vertical = 20,
Left = 30,
Right = 50,
},
});
}
private void showParentPage() private void showParentPage()
{ {
var parentPath = string.Join("/", path.Value.Split('/').SkipLast(1)); var parentPath = string.Join("/", path.Value.Split('/').SkipLast(1));

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Difficulty
private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate)
{ {
var skills = CreateSkills(beatmap, mods); var skills = CreateSkills(beatmap, mods, clockRate);
if (!beatmap.HitObjects.Any()) if (!beatmap.HitObjects.Any())
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
@ -180,7 +180,8 @@ namespace osu.Game.Rulesets.Difficulty
/// </summary> /// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param> /// <param name="beatmap">The <see cref="IBeatmap"/> whose difficulty will be calculated.</param>
/// <param name="mods">Mods to calculate difficulty with.</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> /// <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. // 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.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Screens.Edit; using osu.Game.Screens.Edit;
@ -52,15 +54,21 @@ namespace osu.Game.Rulesets.Edit
if (changeHandler != null) if (changeHandler != null)
{ {
// for now only regenerate replay on a finalised state change, not HitObjectUpdated. // for now only regenerate replay on a finalised state change, not HitObjectUpdated.
changeHandler.OnStateChange += updateReplay; changeHandler.OnStateChange += () => Scheduler.AddOnce(regenerateAutoplay);
} }
else 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) 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.Beatmaps;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Replays; using osu.Game.Replays;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
public abstract class ModAutoplay<T> : ModAutoplay, IApplicableToDrawableRuleset<T> public abstract class ModAutoplay : Mod, IApplicableFailOverride, ICreateReplay
where T : HitObject
{
public virtual void ApplyToDrawableRuleset(DrawableRuleset<T> drawableRuleset)
{
drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods));
}
}
public abstract class ModAutoplay : Mod, IApplicableFailOverride
{ {
public override string Name => "Autoplay"; public override string Name => "Autoplay";
public override string Acronym => "AT"; public override string Acronym => "AT";

View File

@ -1,8 +1,15 @@
// 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 osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Configuration;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.UserInterface;
using osu.Game.Overlays.Settings;
namespace osu.Game.Rulesets.Mods namespace osu.Game.Rulesets.Mods
{ {
@ -13,5 +20,89 @@ namespace osu.Game.Rulesets.Mods
public override ModType Type => ModType.Conversion; public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => OsuIcon.Dice; public override IconUsage? Icon => OsuIcon.Dice;
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))]
public Bindable<int?> Seed { get; } = new Bindable<int?>
{
Default = null,
Value = null
};
private class ModRandomSettingsControl : 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

@ -182,18 +182,11 @@ namespace osu.Game.Rulesets.UI
.WithChild(ResumeOverlay))); .WithChild(ResumeOverlay)));
} }
RegenerateAutoplay(); applyRulesetMods(Mods, config);
loadObjects(cancellationToken ?? default); 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> /// <summary>
/// Creates and adds drawable representations of hit objects to the play field. /// Creates and adds drawable representations of hit objects to the play field.
/// </summary> /// </summary>
@ -274,6 +267,12 @@ namespace osu.Game.Rulesets.UI
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available");
if (score == null)
{
recordingInputManager.Recorder = null;
return;
}
var recorder = CreateReplayRecorder(score); var recorder = CreateReplayRecorder(score);
if (recorder == null) if (recorder == null)
@ -518,7 +517,7 @@ namespace osu.Game.Rulesets.UI
/// Sets a replay to be used to record gameplay. /// Sets a replay to be used to record gameplay.
/// </summary> /// </summary>
/// <param name="score">The target to be recorded to.</param> /// <param name="score">The target to be recorded to.</param>
public abstract void SetRecordTarget(Score score); public abstract void SetRecordTarget([CanBeNull] Score score);
/// <summary> /// <summary>
/// Invoked when the interactive user requests resuming from a paused state. /// Invoked when the interactive user requests resuming from a paused state.

View File

@ -30,12 +30,14 @@ namespace osu.Game.Rulesets.UI
{ {
set set
{ {
if (recorder != null) if (value != null && recorder != null)
throw new InvalidOperationException("Cannot attach more than one recorder"); throw new InvalidOperationException("Cannot attach more than one recorder");
recorder?.Expire();
recorder = value; recorder = value;
KeyBindingContainer.Add(recorder); if (recorder != null)
KeyBindingContainer.Add(recorder);
} }
} }

View File

@ -17,7 +17,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public class MultiSpectatorPlayer : SpectatorPlayer public class MultiSpectatorPlayer : SpectatorPlayer
{ {
private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true); private readonly Bindable<bool> waitingOnFrames = new Bindable<bool>(true);
private readonly Score score;
private readonly ISpectatorPlayerClock spectatorPlayerClock; private readonly ISpectatorPlayerClock spectatorPlayerClock;
/// <summary> /// <summary>
@ -28,7 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock) public MultiSpectatorPlayer([NotNull] Score score, [NotNull] ISpectatorPlayerClock spectatorPlayerClock)
: base(score) : base(score)
{ {
this.score = score;
this.spectatorPlayerClock = spectatorPlayerClock; this.spectatorPlayerClock = spectatorPlayerClock;
} }
@ -43,7 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Spectate
base.UpdateAfterChildren(); base.UpdateAfterChildren();
// This is required because the frame stable clock is set to WaitingOnFrames = false for one frame. // 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) 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); return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true);
} }
protected override Score CreateScore() protected override void PrepareScoreForResults()
{ {
var score = base.CreateScore(); base.PrepareScoreForResults();
score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
return score; Score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore());
} }
protected override void Dispose(bool isDisposing) protected override void Dispose(bool isDisposing)

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
@ -24,10 +23,8 @@ using osu.Game.IO.Archives;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.Spectator; using osu.Game.Online.Spectator;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Replays;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -137,6 +134,8 @@ namespace osu.Game.Screens.Play
public readonly PlayerConfiguration Configuration; public readonly PlayerConfiguration Configuration;
protected Score Score { get; private set; }
/// <summary> /// <summary>
/// Create a new player instance. /// Create a new player instance.
/// </summary> /// </summary>
@ -145,7 +144,7 @@ namespace osu.Game.Screens.Play
Configuration = configuration ?? new PlayerConfiguration(); Configuration = configuration ?? new PlayerConfiguration();
} }
private GameplayBeatmap gameplayBeatmap; protected GameplayBeatmap GameplayBeatmap { get; private set; }
private ScreenSuspensionHandler screenSuspension; private ScreenSuspensionHandler screenSuspension;
@ -161,24 +160,32 @@ namespace osu.Game.Screens.Play
if (!LoadedBeatmapSuccessfully) if (!LoadedBeatmapSuccessfully)
return; return;
// replays should never be recorded or played back when autoplay is enabled Score = CreateScore();
if (!Mods.Value.Any(m => m is ModAutoplay))
PrepareReplay(); // 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); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true);
} }
[CanBeNull]
private Score recordingScore;
/// <summary> /// <summary>
/// Run any recording / playback setup for replays. /// Run any recording / playback setup for replays.
/// </summary> /// </summary>
protected virtual void PrepareReplay() 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)] [BackgroundDependencyLoader(true)]
@ -223,10 +230,10 @@ namespace osu.Game.Screens.Play
InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime);
AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(GameplayBeatmap = new GameplayBeatmap(playableBeatmap));
AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer));
dependencies.CacheAs(gameplayBeatmap); dependencies.CacheAs(GameplayBeatmap);
var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin);
@ -284,7 +291,7 @@ namespace osu.Game.Screens.Play
{ {
HealthProcessor.ApplyResult(r); HealthProcessor.ApplyResult(r);
ScoreProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r);
gameplayBeatmap.ApplyResult(r); GameplayBeatmap.ApplyResult(r);
}; };
DrawableRuleset.RevertResult += r => DrawableRuleset.RevertResult += r =>
@ -629,15 +636,18 @@ namespace osu.Game.Screens.Play
ValidForResume = false; ValidForResume = false;
// ensure we are not writing to the replay any more, as we are about to consume and store the score.
DrawableRuleset.SetRecordTarget(null);
if (!Configuration.ShowResults) return; if (!Configuration.ShowResults) return;
prepareScoreForDisplayTask ??= Task.Run(async () => prepareScoreForDisplayTask ??= Task.Run(async () =>
{ {
var score = CreateScore(); PrepareScoreForResults();
try try
{ {
await PrepareScoreForResultsAsync(score).ConfigureAwait(false); await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -646,14 +656,14 @@ namespace osu.Game.Screens.Play
try try
{ {
await ImportScore(score).ConfigureAwait(false); await ImportScore(Score).ConfigureAwait(false);
} }
catch (Exception ex) catch (Exception ex)
{ {
Logger.Error(ex, "Score import failed!"); Logger.Error(ex, "Score import failed!");
} }
return score.ScoreInfo; return Score.ScoreInfo;
}); });
if (skipStoryboardOutro) if (skipStoryboardOutro)
@ -905,41 +915,19 @@ namespace osu.Game.Screens.Play
} }
/// <summary> /// <summary>
/// Creates the player's <see cref="Score"/>. /// Creates the player's <see cref="Scoring.Score"/>.
/// </summary> /// </summary>
/// <returns>The <see cref="Score"/>.</returns> /// <returns>The <see cref="Scoring.Score"/>.</returns>
protected virtual Score CreateScore() protected virtual Score CreateScore() =>
{ new Score
var score = new Score
{ {
ScoreInfo = new ScoreInfo ScoreInfo = new ScoreInfo { User = api.LocalUser.Value },
{
Beatmap = Beatmap.Value.BeatmapInfo,
Ruleset = rulesetInfo,
Mods = Mods.Value.ToArray(),
}
}; };
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> /// <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> /// </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> /// <returns>The imported score.</returns>
protected virtual async Task ImportScore(Score score) protected virtual async Task ImportScore(Score score)
{ {
@ -951,7 +939,7 @@ namespace osu.Game.Screens.Play
using (var stream = new MemoryStream()) 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"); replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr");
} }
@ -970,9 +958,9 @@ namespace osu.Game.Screens.Play
} }
/// <summary> /// <summary>
/// Prepare the <see cref="Score"/> for display at results. /// Prepare the <see cref="Scoring.Score"/> for display at results.
/// </summary> /// </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> /// <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; 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. // 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.Threading.Tasks; using System.Threading.Tasks;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Game.Beatmaps;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Rulesets.Mods;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -11,15 +15,20 @@ namespace osu.Game.Screens.Play
{ {
public class ReplayPlayer : Player, IKeyBindingHandler<GlobalAction> 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) // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108)
protected override bool CheckModsAllowFailure() => false; protected override bool CheckModsAllowFailure() => false;
public ReplayPlayer(Score score, PlayerConfiguration configuration = null) public ReplayPlayer(Score score, PlayerConfiguration configuration = null)
: this((_, __) => score, configuration)
{
}
public ReplayPlayer(Func<IBeatmap, IReadOnlyList<Mod>, Score> createScore, PlayerConfiguration configuration = null)
: base(configuration) : base(configuration)
{ {
Score = score; this.createScore = createScore;
} }
protected override void PrepareReplay() protected override void PrepareReplay()
@ -27,15 +36,7 @@ namespace osu.Game.Screens.Play
DrawableRuleset?.SetReplayScore(Score); DrawableRuleset?.SetReplayScore(Score);
} }
protected override Score CreateScore() protected override Score CreateScore() => createScore(GameplayBeatmap.PlayableBeatmap, Mods.Value);
{
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;
}
// Don't re-import replay scores as they're already present in the database. // Don't re-import replay scores as they're already present in the database.
protected override Task ImportScore(Score score) => Task.CompletedTask; protected override Task ImportScore(Score score) => Task.CompletedTask;

View File

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

View File

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

@ -4,6 +4,7 @@
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Framework.Testing.Input; using osu.Framework.Testing.Input;
using osu.Game.Graphics.Cursor; using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual
InputManager = new ManualInputManager InputManager = new ManualInputManager
{ {
UseParentInput = true, UseParentInput = true,
Child = mainContent Child = new PlatformActionContainer().WithChild(mainContent)
}, },
new Container new Container
{ {

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 System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
@ -48,6 +49,26 @@ namespace osu.Game.Tests.Visual
PauseOnFocusLost = pauseOnFocusLost; 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] [BackgroundDependencyLoader]
private void load() 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) private void load(LargeTextureStore textures)
{ {
if (user != null && user.Id > 1) 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"); Texture ??= textures.Get(@"Online/avatar-guest");
} }