mirror of
https://github.com/ppy/osu.git
synced 2025-01-12 17:43:05 +08:00
Merge branch 'master' into legacy-skin-default-fallback
This commit is contained in:
commit
06275a6a7d
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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)
|
||||
};
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Utils;
|
||||
@ -17,8 +18,11 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
Seed.Value ??= RNG.Next();
|
||||
var rng = new Random((int)Seed.Value);
|
||||
|
||||
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]);
|
||||
}
|
||||
|
30
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs
Normal file
30
osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleKiai.cs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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();
|
||||
|
||||
|
280
osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
Normal file
280
osu.Game.Rulesets.Osu/Mods/OsuModRandom.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -164,7 +164,8 @@ namespace osu.Game.Rulesets.Osu
|
||||
{
|
||||
new OsuModTarget(),
|
||||
new OsuModDifficultyAdjust(),
|
||||
new OsuModClassic()
|
||||
new OsuModClassic(),
|
||||
new OsuModRandom(),
|
||||
};
|
||||
|
||||
case ModType.Automation:
|
||||
|
@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
Origin = Anchor.Centre,
|
||||
Texture = textures.Get(@"Gameplay/osu/disc"),
|
||||
},
|
||||
new KiaiFlash
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
},
|
||||
triangles = new TrianglesPiece
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
|
43
osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs
Normal file
43
osu.Game.Rulesets.Osu/Skinning/Default/KiaiFlash.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
61
osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingSprite.cs
Normal file
61
osu.Game.Rulesets.Osu/Skinning/Legacy/KiaiFlashingSprite.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -32,9 +31,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
|
||||
}
|
||||
|
||||
private Container<Sprite> circleSprites;
|
||||
private Sprite hitCircleSprite;
|
||||
private Sprite hitCircleOverlay;
|
||||
private Container circleSprites;
|
||||
private Drawable hitCircleSprite;
|
||||
private Drawable hitCircleOverlay;
|
||||
|
||||
private SkinnableSpriteText hitCircleText;
|
||||
|
||||
@ -72,20 +71,20 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
circleSprites = new Container<Sprite>
|
||||
circleSprites = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new[]
|
||||
{
|
||||
hitCircleSprite = new Sprite
|
||||
hitCircleSprite = new KiaiFlashingSprite
|
||||
{
|
||||
Texture = baseTexture,
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
},
|
||||
hitCircleOverlay = new Sprite
|
||||
hitCircleOverlay = new KiaiFlashingSprite
|
||||
{
|
||||
Texture = overlayTexture,
|
||||
Anchor = Anchor.Centre,
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -20,10 +20,13 @@ namespace osu.Game.Rulesets.Taiko.Mods
|
||||
{
|
||||
var taikoBeatmap = (TaikoBeatmap)beatmap;
|
||||
|
||||
Seed.Value ??= RNG.Next();
|
||||
var rng = new Random((int)Seed.Value);
|
||||
|
||||
foreach (var obj in taikoBeatmap.HitObjects)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -9,6 +9,8 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
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 channel1 => channels[0];
|
||||
private Channel channel2 => channels[1];
|
||||
private Channel channel3 => channels[2];
|
||||
|
||||
[Resolved]
|
||||
private GameHost host { get; set; }
|
||||
|
||||
public TestSceneChatOverlay()
|
||||
{
|
||||
@ -44,7 +50,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
Name = $"Channel no. {index}",
|
||||
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();
|
||||
}
|
||||
@ -228,6 +235,92 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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)
|
||||
{
|
||||
var channelKey = Key.Number0 + number;
|
||||
@ -236,6 +329,23 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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)
|
||||
{
|
||||
InputManager.MoveMouseTo(d);
|
||||
|
@ -1,6 +1,7 @@
|
||||
// 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.Net;
|
||||
using NUnit.Framework;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
@ -32,7 +33,14 @@ namespace osu.Game.Tests.Visual.Online
|
||||
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", () =>
|
||||
{
|
||||
dummyAPI.HandleRequest = request =>
|
||||
@ -40,7 +48,11 @@ namespace osu.Game.Tests.Visual.Online
|
||||
if (!(request is GetWikiRequest getWikiRequest))
|
||||
return false;
|
||||
|
||||
getWikiRequest.TriggerSuccess(r);
|
||||
if (isFailed)
|
||||
getWikiRequest.TriggerFailure(new WebException());
|
||||
else
|
||||
getWikiRequest.TriggerSuccess(r);
|
||||
|
||||
return true;
|
||||
};
|
||||
});
|
||||
|
@ -8,6 +8,7 @@ using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
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> 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>
|
||||
/// The currently opened channel
|
||||
/// </summary>
|
||||
@ -51,6 +62,9 @@ namespace osu.Game.Online.Chat
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private UserLookupCache users { get; set; }
|
||||
|
||||
public readonly BindableBool HighPollRate = new BindableBool();
|
||||
|
||||
public ChannelManager()
|
||||
@ -420,6 +434,18 @@ namespace osu.Game.Online.Chat
|
||||
|
||||
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)
|
||||
{
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ namespace osu.Game
|
||||
/// <summary>
|
||||
/// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects.
|
||||
/// </summary>
|
||||
internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5;
|
||||
internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.8;
|
||||
|
||||
public bool UseDevelopmentServer { get; }
|
||||
|
||||
|
@ -81,9 +81,11 @@ namespace osu.Game.Overlays.Chat.Tabs
|
||||
RemoveItem(channel);
|
||||
|
||||
if (SelectedTab == null)
|
||||
SelectTab(selectorTab);
|
||||
SelectChannelSelectorTab();
|
||||
}
|
||||
|
||||
public void SelectChannelSelectorTab() => SelectTab(selectorTab);
|
||||
|
||||
protected override void SelectTab(TabItem<Channel> tab)
|
||||
{
|
||||
if (tab is ChannelSelectorTabItem)
|
||||
|
@ -24,13 +24,15 @@ using osu.Game.Overlays.Chat.Tabs;
|
||||
using osuTK.Input;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Localisation;
|
||||
using osu.Game.Online;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent
|
||||
public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent, IKeyBindingHandler<PlatformAction>
|
||||
{
|
||||
public string IconTexture => "Icons/Hexacons/messaging";
|
||||
public LocalisableString Title => ChatStrings.HeaderTitle;
|
||||
@ -370,6 +372,30 @@ namespace osu.Game.Overlays
|
||||
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;
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
|
@ -6,87 +6,36 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osuTK;
|
||||
using System.Linq;
|
||||
using osu.Game.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Overlays.News.Sidebar
|
||||
{
|
||||
public class NewsSidebar : CompositeDrawable
|
||||
public class NewsSidebar : OverlaySidebar
|
||||
{
|
||||
[Cached]
|
||||
public readonly Bindable<APINewsSidebar> Metadata = new Bindable<APINewsSidebar>();
|
||||
|
||||
private FillFlowContainer<MonthSection> monthsFlow;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OverlayColourProvider colourProvider)
|
||||
protected override Drawable CreateContent() => new FillFlowContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y;
|
||||
Width = 250;
|
||||
InternalChildren = new Drawable[]
|
||||
Direction = FillDirection.Vertical,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
Spacing = new Vector2(0, 20),
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Box
|
||||
new YearsPanel(),
|
||||
monthsFlow = new FillFlowContainer<MonthSection>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Colour = colourProvider.Background4
|
||||
},
|
||||
new Box
|
||||
{
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
AutoSizeAxes = Axes.Y,
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Vertical,
|
||||
Spacing = new Vector2(0, 10)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
|
76
osu.Game/Overlays/OverlaySidebar.cs
Normal file
76
osu.Game/Overlays/OverlaySidebar.cs
Normal 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();
|
||||
}
|
||||
}
|
@ -92,7 +92,7 @@ namespace osu.Game.Overlays
|
||||
Loading.Show();
|
||||
|
||||
request.Success += response => Schedule(() => onSuccess(response));
|
||||
request.Failure += _ => Schedule(() => LoadDisplay(Empty()));
|
||||
request.Failure += _ => Schedule(onFail);
|
||||
|
||||
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()
|
||||
{
|
||||
var parentPath = string.Join("/", path.Value.Split('/').SkipLast(1));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
14
osu.Game/Rulesets/Mods/ICreateReplay.cs
Normal file
14
osu.Game/Rulesets/Mods/ICreateReplay.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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";
|
||||
|
@ -1,8 +1,15 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -13,5 +20,89 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override ModType Type => ModType.Conversion;
|
||||
public override IconUsage? Icon => OsuIcon.Dice;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(ModRandomSettingsControl))]
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
@ -274,6 +267,12 @@ namespace osu.Game.Rulesets.UI
|
||||
if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager))
|
||||
throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available");
|
||||
|
||||
if (score == null)
|
||||
{
|
||||
recordingInputManager.Recorder = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var recorder = CreateReplayRecorder(score);
|
||||
|
||||
if (recorder == null)
|
||||
@ -518,7 +517,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// Sets a replay to be used to record gameplay.
|
||||
/// </summary>
|
||||
/// <param name="score">The target to be recorded to.</param>
|
||||
public abstract void SetRecordTarget(Score score);
|
||||
public abstract void SetRecordTarget([CanBeNull] Score score);
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the interactive user requests resuming from a paused state.
|
||||
|
@ -30,12 +30,14 @@ namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
set
|
||||
{
|
||||
if (recorder != null)
|
||||
if (value != null && recorder != null)
|
||||
throw new InvalidOperationException("Cannot attach more than one recorder");
|
||||
|
||||
recorder?.Expire();
|
||||
recorder = value;
|
||||
|
||||
KeyBindingContainer.Add(recorder);
|
||||
if (recorder != null)
|
||||
KeyBindingContainer.Add(recorder);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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 =>
|
||||
@ -629,15 +636,18 @@ namespace osu.Game.Screens.Play
|
||||
|
||||
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;
|
||||
|
||||
prepareScoreForDisplayTask ??= Task.Run(async () =>
|
||||
{
|
||||
var score = CreateScore();
|
||||
PrepareScoreForResults();
|
||||
|
||||
try
|
||||
{
|
||||
await PrepareScoreForResultsAsync(score).ConfigureAwait(false);
|
||||
await PrepareScoreForResultsAsync(Score).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -646,14 +656,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 +915,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 +939,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 +958,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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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()
|
||||
|
@ -186,11 +186,11 @@ namespace osu.Game.Screens.Select.Details
|
||||
set => name.Text = value;
|
||||
}
|
||||
|
||||
private (float baseValue, float? adjustedValue) value;
|
||||
private (float baseValue, float? adjustedValue)? value;
|
||||
|
||||
public (float baseValue, float? adjustedValue) Value
|
||||
{
|
||||
get => value;
|
||||
get => value ?? (0, null);
|
||||
set
|
||||
{
|
||||
if (value == this.value)
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing.Input;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -48,7 +49,7 @@ namespace osu.Game.Tests.Visual
|
||||
InputManager = new ManualInputManager
|
||||
{
|
||||
UseParentInput = true,
|
||||
Child = mainContent
|
||||
Child = new PlatformActionContainer().WithChild(mainContent)
|
||||
},
|
||||
new Container
|
||||
{
|
||||
|
@ -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()
|
||||
{
|
||||
|
65
osu.Game/Tests/Visual/TestReplayPlayer.cs
Normal file
65
osu.Game/Tests/Visual/TestReplayPlayer.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user