mirror of
https://github.com/ppy/osu.git
synced 2025-01-14 03:15:45 +08:00
Merge branch 'master' into better-skin-hashing
This commit is contained in:
commit
53f9381ad9
@ -52,6 +52,6 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.910.0" />
|
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.911.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
|
|||||||
{
|
{
|
||||||
base.InitialiseDefaults();
|
base.InitialiseDefaults();
|
||||||
|
|
||||||
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1);
|
Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5);
|
||||||
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
new SettingsSlider<double, TimeSlider>
|
new SettingsSlider<double, TimeSlider>
|
||||||
{
|
{
|
||||||
LabelText = "Scroll speed",
|
LabelText = "Scroll speed",
|
||||||
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime)
|
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
|
||||||
|
KeyboardStep = 5
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests
|
|||||||
{
|
{
|
||||||
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
|
||||||
|
|
||||||
[TestCase(2.9811338051242915d, "diffcalc-test")]
|
[TestCase(2.2867022617692685d, "diffcalc-test")]
|
||||||
[TestCase(2.9811338051242915d, "diffcalc-test-strong")]
|
[TestCase(2.2867022617692685d, "diffcalc-test-strong")]
|
||||||
public void Test(double expected, string name)
|
public void Test(double expected, string name)
|
||||||
=> base.Test(expected, name);
|
=> base.Test(expected, name);
|
||||||
|
|
||||||
|
@ -0,0 +1,140 @@
|
|||||||
|
// 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.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Detects special hit object patterns which are easier to hit using special techniques
|
||||||
|
/// than normally assumed in the fully-alternating play style.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This component detects two basic types of patterns, leveraged by the following techniques:
|
||||||
|
/// <list>
|
||||||
|
/// <item>Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand.</item>
|
||||||
|
/// <item>TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between.</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
public class StaminaCheeseDetector
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll.
|
||||||
|
/// </summary>
|
||||||
|
private const int roll_min_repetitions = 12;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap.
|
||||||
|
/// </summary>
|
||||||
|
private const int tl_min_repetitions = 16;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of all <see cref="TaikoDifficultyHitObject"/>s in the map.
|
||||||
|
/// </summary>
|
||||||
|
private readonly List<TaikoDifficultyHitObject> hitObjects;
|
||||||
|
|
||||||
|
public StaminaCheeseDetector(List<TaikoDifficultyHitObject> hitObjects)
|
||||||
|
{
|
||||||
|
this.hitObjects = hitObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all objects in <see cref="hitObjects"/> that special difficulty-reducing techiques apply to
|
||||||
|
/// with the <see cref="TaikoDifficultyHitObject.StaminaCheese"/> flag.
|
||||||
|
/// </summary>
|
||||||
|
public void FindCheese()
|
||||||
|
{
|
||||||
|
findRolls(3);
|
||||||
|
findRolls(4);
|
||||||
|
|
||||||
|
findTlTap(0, HitType.Rim);
|
||||||
|
findTlTap(1, HitType.Rim);
|
||||||
|
findTlTap(0, HitType.Centre);
|
||||||
|
findTlTap(1, HitType.Centre);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all sequences hittable using a roll.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="patternLength">The length of a single repeating pattern to consider (triplets/quadruplets).</param>
|
||||||
|
private void findRolls(int patternLength)
|
||||||
|
{
|
||||||
|
var history = new LimitedCapacityQueue<TaikoDifficultyHitObject>(2 * patternLength);
|
||||||
|
|
||||||
|
// for convenience, we're tracking the index of the item *before* our suspected repeat's start,
|
||||||
|
// as that index can be simply subtracted from the current index to get the number of elements in between
|
||||||
|
// without off-by-one errors
|
||||||
|
int indexBeforeLastRepeat = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < hitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
history.Enqueue(hitObjects[i]);
|
||||||
|
if (!history.Full)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!containsPatternRepeat(history, patternLength))
|
||||||
|
{
|
||||||
|
// we're setting this up for the next iteration, hence the +1.
|
||||||
|
// right here this index will point at the queue's front (oldest item),
|
||||||
|
// but that item is about to be popped next loop with an enqueue.
|
||||||
|
indexBeforeLastRepeat = i - history.Count + 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int repeatedLength = i - indexBeforeLastRepeat;
|
||||||
|
if (repeatedLength < roll_min_repetitions)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
markObjectsAsCheese(i, repeatedLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the objects stored in <paramref name="history"/> contain a repetition of a pattern of length <paramref name="patternLength"/>.
|
||||||
|
/// </summary>
|
||||||
|
private static bool containsPatternRepeat(LimitedCapacityQueue<TaikoDifficultyHitObject> history, int patternLength)
|
||||||
|
{
|
||||||
|
for (int j = 0; j < patternLength; j++)
|
||||||
|
{
|
||||||
|
if (history[j].HitType != history[j + patternLength].HitType)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds and marks all sequences hittable using a TL tap.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="parity">Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked.</param>
|
||||||
|
/// <param name="type">The type of hit to check for TL taps.</param>
|
||||||
|
private void findTlTap(int parity, HitType type)
|
||||||
|
{
|
||||||
|
int tlLength = -2;
|
||||||
|
|
||||||
|
for (int i = parity; i < hitObjects.Count; i += 2)
|
||||||
|
{
|
||||||
|
if (hitObjects[i].HitType == type)
|
||||||
|
tlLength += 2;
|
||||||
|
else
|
||||||
|
tlLength = -2;
|
||||||
|
|
||||||
|
if (tlLength < tl_min_repetitions)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
markObjectsAsCheese(i, tlLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Marks <paramref name="count"/> elements counting backwards from <paramref name="end"/> as <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||||
|
/// </summary>
|
||||||
|
private void markObjectsAsCheese(int end, int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
hitObjects[end - i].StaminaCheese = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,20 +1,94 @@
|
|||||||
// 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 osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single hit object in taiko difficulty calculation.
|
||||||
|
/// </summary>
|
||||||
public class TaikoDifficultyHitObject : DifficultyHitObject
|
public class TaikoDifficultyHitObject : DifficultyHitObject
|
||||||
{
|
{
|
||||||
public readonly bool HasTypeChange;
|
/// <summary>
|
||||||
|
/// The rhythm required to hit this hit object.
|
||||||
|
/// </summary>
|
||||||
|
public readonly TaikoDifficultyHitObjectRhythm Rhythm;
|
||||||
|
|
||||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
/// <summary>
|
||||||
|
/// The hit type of this hit object.
|
||||||
|
/// </summary>
|
||||||
|
public readonly HitType? HitType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the object in the beatmap.
|
||||||
|
/// </summary>
|
||||||
|
public readonly int ObjectIndex;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the object should carry a penalty due to being hittable using special techniques
|
||||||
|
/// making it easier to do so.
|
||||||
|
/// </summary>
|
||||||
|
public bool StaminaCheese;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new difficulty hit object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The gameplay <see cref="HitObject"/> associated with this difficulty object.</param>
|
||||||
|
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</param>
|
||||||
|
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||||
|
/// <param name="clockRate">The rate of the gameplay clock. Modified by speed-changing mods.</param>
|
||||||
|
/// <param name="objectIndex">The index of the object in the beatmap.</param>
|
||||||
|
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
|
||||||
: base(hitObject, lastObject, clockRate)
|
: base(hitObject, lastObject, clockRate)
|
||||||
{
|
{
|
||||||
HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type;
|
var currentHit = hitObject as Hit;
|
||||||
|
|
||||||
|
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||||
|
HitType = currentHit?.Type;
|
||||||
|
|
||||||
|
ObjectIndex = objectIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of most common rhythm changes in taiko maps.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The general guidelines for the values are:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>rhythm changes with ratio closer to 1 (that are <i>not</i> 1) are harder to play,</item>
|
||||||
|
/// <item>speeding up is <i>generally</i> harder than slowing down (with exceptions of rhythm changes requiring a hand switch).</item>
|
||||||
|
/// </list>
|
||||||
|
/// </remarks>
|
||||||
|
private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms =
|
||||||
|
{
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 1, 0.0),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(2, 1, 0.3),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 2, 0.5),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(3, 1, 0.3),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(1, 3, 0.35),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style)
|
||||||
|
new TaikoDifficultyHitObjectRhythm(2, 3, 0.4),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(5, 4, 0.5),
|
||||||
|
new TaikoDifficultyHitObjectRhythm(4, 5, 0.7)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the closest rhythm change from <see cref="common_rhythms"/> required to hit this object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding this one.</param>
|
||||||
|
/// <param name="lastLastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="lastObject"/>.</param>
|
||||||
|
/// <param name="clockRate">The rate of the gameplay clock.</param>
|
||||||
|
private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate)
|
||||||
|
{
|
||||||
|
double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate;
|
||||||
|
double ratio = DeltaTime / prevLength;
|
||||||
|
|
||||||
|
return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a rhythm change in a taiko map.
|
||||||
|
/// </summary>
|
||||||
|
public class TaikoDifficultyHitObjectRhythm
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The difficulty multiplier associated with this rhythm change.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double Difficulty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ratio of current <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/>
|
||||||
|
/// to previous <see cref="osu.Game.Rulesets.Difficulty.Preprocessing.DifficultyHitObject.DeltaTime"/> for the rhythm change.
|
||||||
|
/// A <see cref="Ratio"/> above 1 indicates a slow-down; a <see cref="Ratio"/> below 1 indicates a speed-up.
|
||||||
|
/// </summary>
|
||||||
|
public readonly double Ratio;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates an object representing a rhythm change.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="numerator">The numerator for <see cref="Ratio"/>.</param>
|
||||||
|
/// <param name="denominator">The denominator for <see cref="Ratio"/></param>
|
||||||
|
/// <param name="difficulty">The difficulty multiplier associated with this rhythm change.</param>
|
||||||
|
public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty)
|
||||||
|
{
|
||||||
|
Ratio = numerator / (double)denominator;
|
||||||
|
Difficulty = difficulty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
135
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
135
osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the colour coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
public class Colour : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries to keep in <see cref="monoHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int mono_history_max_length = 5;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue with the lengths of the last <see cref="mono_history_max_length"/> most recent mono (single-colour) patterns,
|
||||||
|
/// with the most recent value at the end of the queue.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<int> monoHistory = new LimitedCapacityQueue<int>(mono_history_max_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="HitType"/> of the last object hit before the one being considered.
|
||||||
|
/// </summary>
|
||||||
|
private HitType? previousHitType;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Length of the current mono pattern.
|
||||||
|
/// </summary>
|
||||||
|
private int currentMonoLength;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// changing from/to a drum roll or a swell does not constitute a colour change.
|
||||||
|
// hits spaced more than a second apart are also exempt from colour strain.
|
||||||
|
if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000))
|
||||||
|
{
|
||||||
|
monoHistory.Clear();
|
||||||
|
|
||||||
|
var currentHit = current.BaseObject as Hit;
|
||||||
|
currentMonoLength = currentHit != null ? 1 : 0;
|
||||||
|
previousHitType = currentHit?.Type;
|
||||||
|
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var taikoCurrent = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
double objectStrain = 0.0;
|
||||||
|
|
||||||
|
if (previousHitType != null && taikoCurrent.HitType != previousHitType)
|
||||||
|
{
|
||||||
|
// The colour has changed.
|
||||||
|
objectStrain = 1.0;
|
||||||
|
|
||||||
|
if (monoHistory.Count < 2)
|
||||||
|
{
|
||||||
|
// There needs to be at least two streaks to determine a strain.
|
||||||
|
objectStrain = 0.0;
|
||||||
|
}
|
||||||
|
else if ((monoHistory[^1] + currentMonoLength) % 2 == 0)
|
||||||
|
{
|
||||||
|
// The last streak in the history is guaranteed to be a different type to the current streak.
|
||||||
|
// If the total number of notes in the two streaks is even, nullify this object's strain.
|
||||||
|
objectStrain = 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
objectStrain *= repetitionPenalties();
|
||||||
|
currentMonoLength = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
currentMonoLength += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
previousHitType = taikoCurrent.HitType;
|
||||||
|
return objectStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The penalty to apply due to the length of repetition in colour streaks.
|
||||||
|
/// </summary>
|
||||||
|
private double repetitionPenalties()
|
||||||
|
{
|
||||||
|
const int most_recent_patterns_to_compare = 2;
|
||||||
|
double penalty = 1.0;
|
||||||
|
|
||||||
|
monoHistory.Enqueue(currentMonoLength);
|
||||||
|
|
||||||
|
for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--)
|
||||||
|
{
|
||||||
|
if (!isSamePattern(start, most_recent_patterns_to_compare))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int notesSince = 0;
|
||||||
|
for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i];
|
||||||
|
penalty *= repetitionPenalty(notesSince);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the last <paramref name="mostRecentPatternsToCompare"/> patterns have repeated in the history
|
||||||
|
/// of single-colour note sequences, starting from <paramref name="start"/>.
|
||||||
|
/// </summary>
|
||||||
|
private bool isSamePattern(int start, int mostRecentPatternsToCompare)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||||
|
{
|
||||||
|
if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i])
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the strain penalty for a colour pattern repetition.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notesSince">The number of notes since the last repetition of the pattern.</param>
|
||||||
|
private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||||
|
}
|
||||||
|
}
|
167
osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
Normal file
167
osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the rhythm coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
public class Rhythm : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 10;
|
||||||
|
protected override double StrainDecayBase => 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The note-based decay for rhythm strain.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <see cref="StrainDecayBase"/> is not used here, as it's time- and not note-based.
|
||||||
|
/// </remarks>
|
||||||
|
private const double strain_decay = 0.96;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries in <see cref="rhythmHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int rhythm_history_max_length = 8;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the last <see cref="rhythm_history_max_length"/> changes in note sequence rhythms.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<TaikoDifficultyHitObject> rhythmHistory = new LimitedCapacityQueue<TaikoDifficultyHitObject>(rhythm_history_max_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Contains the rolling rhythm strain.
|
||||||
|
/// Used to apply per-note decay.
|
||||||
|
/// </summary>
|
||||||
|
private double currentStrain;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of notes since the last rhythm change has taken place.
|
||||||
|
/// </summary>
|
||||||
|
private int notesSinceRhythmChange;
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
// drum rolls and swells are exempt.
|
||||||
|
if (!(current.BaseObject is Hit))
|
||||||
|
{
|
||||||
|
resetRhythmAndStrain();
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentStrain *= strain_decay;
|
||||||
|
|
||||||
|
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||||
|
notesSinceRhythmChange += 1;
|
||||||
|
|
||||||
|
// rhythm difficulty zero (due to rhythm not changing) => no rhythm strain.
|
||||||
|
if (hitObject.Rhythm.Difficulty == 0.0)
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
double objectStrain = hitObject.Rhythm.Difficulty;
|
||||||
|
|
||||||
|
objectStrain *= repetitionPenalties(hitObject);
|
||||||
|
objectStrain *= patternLengthPenalty(notesSinceRhythmChange);
|
||||||
|
objectStrain *= speedPenalty(hitObject.DeltaTime);
|
||||||
|
|
||||||
|
// careful - needs to be done here since calls above read this value
|
||||||
|
notesSinceRhythmChange = 0;
|
||||||
|
|
||||||
|
currentStrain += objectStrain;
|
||||||
|
return currentStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a penalty to apply to the current hit object caused by repeating rhythm changes.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Repetitions of more recent patterns are associated with a higher penalty.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="hitObject">The current hit object being considered.</param>
|
||||||
|
private double repetitionPenalties(TaikoDifficultyHitObject hitObject)
|
||||||
|
{
|
||||||
|
double penalty = 1;
|
||||||
|
|
||||||
|
rhythmHistory.Enqueue(hitObject);
|
||||||
|
|
||||||
|
for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++)
|
||||||
|
{
|
||||||
|
for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--)
|
||||||
|
{
|
||||||
|
if (!samePattern(start, mostRecentPatternsToCompare))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
|
||||||
|
penalty *= repetitionPenalty(notesSince);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return penalty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the rhythm change pattern starting at <paramref name="start"/> is a repeat of any of the
|
||||||
|
/// <paramref name="mostRecentPatternsToCompare"/>.
|
||||||
|
/// </summary>
|
||||||
|
private bool samePattern(int start, int mostRecentPatternsToCompare)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < mostRecentPatternsToCompare; i++)
|
||||||
|
{
|
||||||
|
if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a single rhythm repetition penalty.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notesSince">Number of notes since the last repetition of a rhythm change.</param>
|
||||||
|
private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a penalty based on the number of notes since the last rhythm change.
|
||||||
|
/// Both rare and frequent rhythm changes are penalised.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="patternLength">Number of notes since the last rhythm change.</param>
|
||||||
|
private static double patternLengthPenalty(int patternLength)
|
||||||
|
{
|
||||||
|
double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0);
|
||||||
|
double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0);
|
||||||
|
return Math.Min(shortPatternPenalty, longPatternPenalty);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a penalty for objects that do not require alternating hands.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deltaTime">Time (in milliseconds) since the last hit object.</param>
|
||||||
|
private double speedPenalty(double deltaTime)
|
||||||
|
{
|
||||||
|
if (deltaTime < 80) return 1;
|
||||||
|
if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime);
|
||||||
|
|
||||||
|
resetRhythmAndStrain();
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resets the rolling strain value and <see cref="notesSinceRhythmChange"/> counter.
|
||||||
|
/// </summary>
|
||||||
|
private void resetRhythmAndStrain()
|
||||||
|
{
|
||||||
|
currentStrain = 0.0;
|
||||||
|
notesSinceRhythmChange = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
113
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
113
osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 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.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Skills;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the stamina coefficient of taiko difficulty.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit).
|
||||||
|
/// </remarks>
|
||||||
|
public class Stamina : Skill
|
||||||
|
{
|
||||||
|
protected override double SkillMultiplier => 1;
|
||||||
|
protected override double StrainDecayBase => 0.4;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of entries to keep in <see cref="notePairDurationHistory"/>.
|
||||||
|
/// </summary>
|
||||||
|
private const int max_history_length = 2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of the hand this <see cref="Stamina"/> instance is associated with.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed).
|
||||||
|
/// This naturally translates onto index offsets of the objects in the map.
|
||||||
|
/// </remarks>
|
||||||
|
private readonly int hand;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the last <see cref="max_history_length"/> durations between notes hit with the hand indicated by <see cref="hand"/>.
|
||||||
|
/// </summary>
|
||||||
|
private readonly LimitedCapacityQueue<double> notePairDurationHistory = new LimitedCapacityQueue<double>(max_history_length);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the <see cref="DifficultyHitObject.DeltaTime"/> of the last object that was hit by the <i>other</i> hand.
|
||||||
|
/// </summary>
|
||||||
|
private double offhandObjectDuration = double.MaxValue;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a <see cref="Stamina"/> skill.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="rightHand">Whether this instance is performing calculations for the right hand.</param>
|
||||||
|
public Stamina(bool rightHand)
|
||||||
|
{
|
||||||
|
hand = rightHand ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override double StrainValueOf(DifficultyHitObject current)
|
||||||
|
{
|
||||||
|
if (!(current.BaseObject is Hit))
|
||||||
|
{
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current;
|
||||||
|
|
||||||
|
if (hitObject.ObjectIndex % 2 == hand)
|
||||||
|
{
|
||||||
|
double objectStrain = 1;
|
||||||
|
|
||||||
|
if (hitObject.ObjectIndex == 1)
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration);
|
||||||
|
|
||||||
|
double shortestRecentNote = notePairDurationHistory.Min();
|
||||||
|
objectStrain += speedBonus(shortestRecentNote);
|
||||||
|
|
||||||
|
if (hitObject.StaminaCheese)
|
||||||
|
objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration);
|
||||||
|
|
||||||
|
return objectStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
offhandObjectDuration = hitObject.DeltaTime;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a penalty for hit objects marked with <see cref="TaikoDifficultyHitObject.StaminaCheese"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||||
|
private double cheesePenalty(double notePairDuration)
|
||||||
|
{
|
||||||
|
if (notePairDuration > 125) return 1;
|
||||||
|
if (notePairDuration < 100) return 0.6;
|
||||||
|
|
||||||
|
return 0.6 + (notePairDuration - 100) * 0.016;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a speed bonus dependent on the time since the last hit performed using this hand.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notePairDuration">The duration between the current and previous note hit using the hand indicated by <see cref="hand"/>.</param>
|
||||||
|
private double speedBonus(double notePairDuration)
|
||||||
|
{
|
||||||
|
if (notePairDuration >= 200) return 0;
|
||||||
|
|
||||||
|
double bonus = 200 - notePairDuration;
|
||||||
|
bonus *= bonus;
|
||||||
|
return bonus / 100000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,95 +0,0 @@
|
|||||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
||||||
// See the LICENCE file in the repository root for full licence text.
|
|
||||||
|
|
||||||
using System;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Difficulty.Skills;
|
|
||||||
using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|
||||||
{
|
|
||||||
public class Strain : Skill
|
|
||||||
{
|
|
||||||
private const double rhythm_change_base_threshold = 0.2;
|
|
||||||
private const double rhythm_change_base = 2.0;
|
|
||||||
|
|
||||||
protected override double SkillMultiplier => 1;
|
|
||||||
protected override double StrainDecayBase => 0.3;
|
|
||||||
|
|
||||||
private ColourSwitch lastColourSwitch = ColourSwitch.None;
|
|
||||||
|
|
||||||
private int sameColourCount = 1;
|
|
||||||
|
|
||||||
protected override double StrainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
double addition = 1;
|
|
||||||
|
|
||||||
// We get an extra addition if we are not a slider or spinner
|
|
||||||
if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000)
|
|
||||||
{
|
|
||||||
if (hasColourChange(current))
|
|
||||||
addition += 0.75;
|
|
||||||
|
|
||||||
if (hasRhythmChange(current))
|
|
||||||
addition += 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
lastColourSwitch = ColourSwitch.None;
|
|
||||||
sameColourCount = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
double additionFactor = 1;
|
|
||||||
|
|
||||||
// Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50
|
|
||||||
if (current.DeltaTime < 50)
|
|
||||||
additionFactor = 0.4 + 0.6 * current.DeltaTime / 50;
|
|
||||||
|
|
||||||
return additionFactor * addition;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool hasRhythmChange(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
// We don't want a division by zero if some random mapper decides to put two HitObjects at the same time.
|
|
||||||
if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime);
|
|
||||||
|
|
||||||
if (timeElapsedRatio >= 8)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0;
|
|
||||||
|
|
||||||
return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool hasColourChange(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
var taikoCurrent = (TaikoDifficultyHitObject)current;
|
|
||||||
|
|
||||||
if (!taikoCurrent.HasTypeChange)
|
|
||||||
{
|
|
||||||
sameColourCount++;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var oldColourSwitch = lastColourSwitch;
|
|
||||||
var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd;
|
|
||||||
|
|
||||||
lastColourSwitch = newColourSwitch;
|
|
||||||
sameColourCount = 1;
|
|
||||||
|
|
||||||
// We only want a bonus if the parity of the color switch changes
|
|
||||||
return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch;
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum ColourSwitch
|
|
||||||
{
|
|
||||||
None,
|
|
||||||
Even,
|
|
||||||
Odd
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public class TaikoDifficultyAttributes : DifficultyAttributes
|
public class TaikoDifficultyAttributes : DifficultyAttributes
|
||||||
{
|
{
|
||||||
|
public double StaminaStrain;
|
||||||
|
public double RhythmStrain;
|
||||||
|
public double ColourStrain;
|
||||||
|
public double ApproachRate;
|
||||||
public double GreatHitWindow;
|
public double GreatHitWindow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
@ -19,39 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
{
|
{
|
||||||
public class TaikoDifficultyCalculator : DifficultyCalculator
|
public class TaikoDifficultyCalculator : DifficultyCalculator
|
||||||
{
|
{
|
||||||
private const double star_scaling_factor = 0.04125;
|
private const double rhythm_skill_multiplier = 0.014;
|
||||||
|
private const double colour_skill_multiplier = 0.01;
|
||||||
|
private const double stamina_skill_multiplier = 0.02;
|
||||||
|
|
||||||
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
|
||||||
: base(ruleset, beatmap)
|
: base(ruleset, beatmap)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
|
||||||
{
|
{
|
||||||
if (beatmap.HitObjects.Count == 0)
|
new Colour(),
|
||||||
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
|
new Rhythm(),
|
||||||
|
new Stamina(true),
|
||||||
HitWindows hitWindows = new TaikoHitWindows();
|
new Stamina(false),
|
||||||
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
};
|
||||||
|
|
||||||
return new TaikoDifficultyAttributes
|
|
||||||
{
|
|
||||||
StarRating = skills.Single().DifficultyValue() * star_scaling_factor,
|
|
||||||
Mods = mods,
|
|
||||||
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
|
||||||
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
|
|
||||||
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
|
||||||
Skills = skills
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
|
||||||
{
|
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
|
||||||
yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() };
|
|
||||||
|
|
||||||
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
|
||||||
{
|
{
|
||||||
@ -60,5 +44,124 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
new TaikoModEasy(),
|
new TaikoModEasy(),
|
||||||
new TaikoModHardRock(),
|
new TaikoModHardRock(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
|
{
|
||||||
|
List<TaikoDifficultyHitObject> taikoDifficultyHitObjects = new List<TaikoDifficultyHitObject>();
|
||||||
|
|
||||||
|
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||||
|
{
|
||||||
|
taikoDifficultyHitObjects.Add(
|
||||||
|
new TaikoDifficultyHitObject(
|
||||||
|
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese();
|
||||||
|
return taikoDifficultyHitObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
|
||||||
|
{
|
||||||
|
if (beatmap.HitObjects.Count == 0)
|
||||||
|
return new TaikoDifficultyAttributes { Mods = mods, Skills = skills };
|
||||||
|
|
||||||
|
var colour = (Colour)skills[0];
|
||||||
|
var rhythm = (Rhythm)skills[1];
|
||||||
|
var staminaRight = (Stamina)skills[2];
|
||||||
|
var staminaLeft = (Stamina)skills[3];
|
||||||
|
|
||||||
|
double colourRating = colour.DifficultyValue() * colour_skill_multiplier;
|
||||||
|
double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier;
|
||||||
|
double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier;
|
||||||
|
|
||||||
|
double staminaPenalty = simpleColourPenalty(staminaRating, colourRating);
|
||||||
|
staminaRating *= staminaPenalty;
|
||||||
|
|
||||||
|
double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty);
|
||||||
|
double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating);
|
||||||
|
double starRating = 1.4 * separatedRating + 0.5 * combinedRating;
|
||||||
|
starRating = rescale(starRating);
|
||||||
|
|
||||||
|
HitWindows hitWindows = new TaikoHitWindows();
|
||||||
|
hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty);
|
||||||
|
|
||||||
|
return new TaikoDifficultyAttributes
|
||||||
|
{
|
||||||
|
StarRating = starRating,
|
||||||
|
Mods = mods,
|
||||||
|
StaminaStrain = staminaRating,
|
||||||
|
RhythmStrain = rhythmRating,
|
||||||
|
ColourStrain = colourRating,
|
||||||
|
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
|
||||||
|
GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate,
|
||||||
|
MaxCombo = beatmap.HitObjects.Count(h => h is Hit),
|
||||||
|
Skills = skills
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the penalty for the stamina skill for maps with low colour difficulty.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Some maps (especially converts) can be easy to read despite a high note density.
|
||||||
|
/// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill.
|
||||||
|
/// </remarks>
|
||||||
|
private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty)
|
||||||
|
{
|
||||||
|
if (colorDifficulty <= 0) return 0.79 - 0.25;
|
||||||
|
|
||||||
|
return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the <i>p</i>-norm of an <i>n</i>-dimensional vector.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="p">The value of <i>p</i> to calculate the norm for.</param>
|
||||||
|
/// <param name="values">The coefficients of the vector.</param>
|
||||||
|
private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// For each section, the peak strains of all separate skills are combined into a single peak strain for the section.
|
||||||
|
/// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more).
|
||||||
|
/// </remarks>
|
||||||
|
private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty)
|
||||||
|
{
|
||||||
|
List<double> peaks = new List<double>();
|
||||||
|
|
||||||
|
for (int i = 0; i < colour.StrainPeaks.Count; i++)
|
||||||
|
{
|
||||||
|
double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier;
|
||||||
|
double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier;
|
||||||
|
double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty;
|
||||||
|
peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak));
|
||||||
|
}
|
||||||
|
|
||||||
|
double difficulty = 0;
|
||||||
|
double weight = 1;
|
||||||
|
|
||||||
|
foreach (double strain in peaks.OrderByDescending(d => d))
|
||||||
|
{
|
||||||
|
difficulty += strain * weight;
|
||||||
|
weight *= 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
return difficulty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sr">The raw star rating value before re-scaling.</param>
|
||||||
|
private double rescale(double sr)
|
||||||
|
{
|
||||||
|
if (sr < 0) return sr;
|
||||||
|
|
||||||
|
return 10.43 * Math.Log(sr / 8 + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||||
strainValue *= Math.Pow(0.985, countMiss);
|
strainValue *= Math.Pow(0.985, countMiss);
|
||||||
|
|
||||||
// Combo scaling
|
|
||||||
if (Attributes.MaxCombo > 0)
|
|
||||||
strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0);
|
|
||||||
|
|
||||||
if (mods.Any(m => m is ModHidden))
|
if (mods.Any(m => m is ModHidden))
|
||||||
strainValue *= 1.025;
|
strainValue *= 1.025;
|
||||||
|
|
||||||
|
@ -23,15 +23,19 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestHitObjectAddEvent()
|
public void TestHitObjectAddEvent()
|
||||||
{
|
{
|
||||||
var editorBeatmap = new EditorBeatmap(new OsuBeatmap());
|
|
||||||
|
|
||||||
HitObject addedObject = null;
|
|
||||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
|
||||||
|
|
||||||
var hitCircle = new HitCircle();
|
var hitCircle = new HitCircle();
|
||||||
|
|
||||||
editorBeatmap.Add(hitCircle);
|
HitObject addedObject = null;
|
||||||
Assert.That(addedObject, Is.EqualTo(hitCircle));
|
EditorBeatmap editorBeatmap = null;
|
||||||
|
|
||||||
|
AddStep("add beatmap", () =>
|
||||||
|
{
|
||||||
|
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap());
|
||||||
|
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||||
|
});
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => editorBeatmap.Add(hitCircle));
|
||||||
|
AddAssert("received add event", () => addedObject == hitCircle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -41,13 +45,15 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
public void HitObjectRemoveEvent()
|
public void HitObjectRemoveEvent()
|
||||||
{
|
{
|
||||||
var hitCircle = new HitCircle();
|
var hitCircle = new HitCircle();
|
||||||
var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
|
|
||||||
|
|
||||||
HitObject removedObject = null;
|
HitObject removedObject = null;
|
||||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
EditorBeatmap editorBeatmap = null;
|
||||||
|
AddStep("add beatmap", () =>
|
||||||
editorBeatmap.Remove(hitCircle);
|
{
|
||||||
Assert.That(removedObject, Is.EqualTo(hitCircle));
|
Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } });
|
||||||
|
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||||
|
});
|
||||||
|
AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First()));
|
||||||
|
AddAssert("received remove event", () => removedObject == hitCircle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -147,6 +153,7 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
public void TestResortWhenStartTimeChanged()
|
public void TestResortWhenStartTimeChanged()
|
||||||
{
|
{
|
||||||
var hitCircle = new HitCircle { StartTime = 1000 };
|
var hitCircle = new HitCircle { StartTime = 1000 };
|
||||||
|
|
||||||
var editorBeatmap = new EditorBeatmap(new OsuBeatmap
|
var editorBeatmap = new EditorBeatmap(new OsuBeatmap
|
||||||
{
|
{
|
||||||
HitObjects =
|
HitObjects =
|
||||||
|
119
osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
Normal file
119
osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// 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 NUnit.Framework;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Utils;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.NonVisual
|
||||||
|
{
|
||||||
|
[TestFixture]
|
||||||
|
public class LimitedCapacityQueueTest
|
||||||
|
{
|
||||||
|
private const int capacity = 3;
|
||||||
|
|
||||||
|
private LimitedCapacityQueue<int> queue;
|
||||||
|
|
||||||
|
[SetUp]
|
||||||
|
public void SetUp()
|
||||||
|
{
|
||||||
|
queue = new LimitedCapacityQueue<int>(capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestEmptyQueue()
|
||||||
|
{
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => _ = queue.Dequeue());
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
foreach (var _ in queue)
|
||||||
|
count++;
|
||||||
|
|
||||||
|
Assert.AreEqual(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1)]
|
||||||
|
[TestCase(2)]
|
||||||
|
[TestCase(3)]
|
||||||
|
public void TestBelowCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(count, queue.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
Assert.AreEqual(i, queue[i]);
|
||||||
|
|
||||||
|
int j = 0;
|
||||||
|
foreach (var item in queue)
|
||||||
|
Assert.AreEqual(j++, item);
|
||||||
|
|
||||||
|
for (int i = queue.Count; i < queue.Count + capacity; i++)
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestEnqueueAtFullCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
Assert.AreEqual(capacity, queue.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < queue.Count; ++i)
|
||||||
|
Assert.AreEqual(count - capacity + i, queue[i]);
|
||||||
|
|
||||||
|
int j = count - capacity;
|
||||||
|
foreach (var item in queue)
|
||||||
|
Assert.AreEqual(j++, item);
|
||||||
|
|
||||||
|
for (int i = queue.Count; i < queue.Count + capacity; i++)
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(4)]
|
||||||
|
[TestCase(5)]
|
||||||
|
[TestCase(6)]
|
||||||
|
public void TestDequeueAtFullCapacity(int count)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < count; ++i)
|
||||||
|
queue.Enqueue(i);
|
||||||
|
|
||||||
|
for (int i = 0; i < capacity; ++i)
|
||||||
|
{
|
||||||
|
Assert.AreEqual(count - capacity + i, queue.Dequeue());
|
||||||
|
Assert.AreEqual(2 - i, queue.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestClearQueue()
|
||||||
|
{
|
||||||
|
queue.Enqueue(3);
|
||||||
|
queue.Enqueue(5);
|
||||||
|
Assert.AreEqual(2, queue.Count);
|
||||||
|
|
||||||
|
queue.Clear();
|
||||||
|
Assert.AreEqual(0, queue.Count);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[0]);
|
||||||
|
|
||||||
|
queue.Enqueue(7);
|
||||||
|
Assert.AreEqual(1, queue.Count);
|
||||||
|
Assert.AreEqual(7, queue[0]);
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => _ = queue[1]);
|
||||||
|
|
||||||
|
queue.Enqueue(9);
|
||||||
|
Assert.AreEqual(2, queue.Count);
|
||||||
|
Assert.AreEqual(9, queue[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -22,27 +22,16 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
{
|
{
|
||||||
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
|
public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
|
|
||||||
private readonly Container content;
|
private DialogOverlay dialogOverlay;
|
||||||
private readonly DialogOverlay dialogOverlay;
|
private CollectionManager manager;
|
||||||
private readonly CollectionManager manager;
|
|
||||||
|
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets;
|
||||||
private BeatmapManager beatmapManager;
|
private BeatmapManager beatmapManager;
|
||||||
|
|
||||||
private ManageCollectionsDialog dialog;
|
private ManageCollectionsDialog dialog;
|
||||||
|
|
||||||
public TestSceneManageCollectionsDialog()
|
|
||||||
{
|
|
||||||
base.Content.AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
manager = new CollectionManager(LocalStorage),
|
|
||||||
content = new Container { RelativeSizeAxes = Axes.Both },
|
|
||||||
dialogOverlay = new DialogOverlay()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
{
|
{
|
||||||
@ -50,14 +39,16 @@ namespace osu.Game.Tests.Visual.Collections
|
|||||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
||||||
|
|
||||||
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
||||||
}
|
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
manager = new CollectionManager(LocalStorage),
|
||||||
dependencies.Cache(manager);
|
Content,
|
||||||
dependencies.Cache(dialogOverlay);
|
dialogOverlay = new DialogOverlay()
|
||||||
return dependencies;
|
});
|
||||||
|
|
||||||
|
Dependencies.Cache(manager);
|
||||||
|
Dependencies.Cache(dialogOverlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
@ -1,41 +1,27 @@
|
|||||||
// 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 NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Testing;
|
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
using osu.Game.Rulesets.Osu;
|
using osu.Game.Rulesets.Osu;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
using osu.Game.Screens.Edit;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Editing
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
{
|
{
|
||||||
public class TestSceneEditorChangeStates : EditorTestScene
|
public class TestSceneEditorChangeStates : EditorTestScene
|
||||||
{
|
{
|
||||||
private EditorBeatmap editorBeatmap;
|
|
||||||
|
|
||||||
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
protected new TestEditor Editor => (TestEditor)base.Editor;
|
|
||||||
|
|
||||||
public override void SetUpSteps()
|
|
||||||
{
|
|
||||||
base.SetUpSteps();
|
|
||||||
|
|
||||||
AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestSelectedObjects()
|
public void TestSelectedObjects()
|
||||||
{
|
{
|
||||||
HitCircle obj = null;
|
HitCircle obj = null;
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(obj = new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(obj = new HitCircle { StartTime = 1000 }));
|
||||||
AddStep("select hitobject", () => editorBeatmap.SelectedHitObjects.Add(obj));
|
AddStep("select hitobject", () => EditorBeatmap.SelectedHitObjects.Add(obj));
|
||||||
AddAssert("confirm 1 selected", () => editorBeatmap.SelectedHitObjects.Count == 1);
|
AddAssert("confirm 1 selected", () => EditorBeatmap.SelectedHitObjects.Count == 1);
|
||||||
AddStep("deselect hitobject", () => editorBeatmap.SelectedHitObjects.Remove(obj));
|
AddStep("deselect hitobject", () => EditorBeatmap.SelectedHitObjects.Remove(obj));
|
||||||
AddAssert("confirm 0 selected", () => editorBeatmap.SelectedHitObjects.Count == 0);
|
AddAssert("confirm 0 selected", () => EditorBeatmap.SelectedHitObjects.Count == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
@ -43,11 +29,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
int hitObjectCount = 0;
|
int hitObjectCount = 0;
|
||||||
|
|
||||||
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
|
AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
|
||||||
|
|
||||||
addUndoSteps();
|
addUndoSteps();
|
||||||
|
|
||||||
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
|
AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
|
||||||
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
|
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,11 +42,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
{
|
{
|
||||||
int hitObjectCount = 0;
|
int hitObjectCount = 0;
|
||||||
|
|
||||||
AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count);
|
AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count);
|
||||||
|
|
||||||
addRedoSteps();
|
addRedoSteps();
|
||||||
|
|
||||||
AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count);
|
AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count);
|
||||||
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
|
AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,11 +59,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("bind removal", () =>
|
AddStep("bind removal", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
EditorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||||
AddAssert("hitobject added", () => addedObject == expectedObject);
|
AddAssert("hitobject added", () => addedObject == expectedObject);
|
||||||
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
||||||
|
|
||||||
@ -95,11 +81,11 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("bind removal", () =>
|
AddStep("bind removal", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
EditorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||||
addUndoSteps();
|
addUndoSteps();
|
||||||
|
|
||||||
AddStep("reset variables", () =>
|
AddStep("reset variables", () =>
|
||||||
@ -117,7 +103,7 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
[Test]
|
[Test]
|
||||||
public void TestAddObjectThenSaveHasNoUnsavedChanges()
|
public void TestAddObjectThenSaveHasNoUnsavedChanges()
|
||||||
{
|
{
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(new HitCircle { StartTime = 1000 }));
|
||||||
|
|
||||||
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
AddAssert("unsaved changes", () => Editor.HasUnsavedChanges);
|
||||||
AddStep("save changes", () => Editor.Save());
|
AddStep("save changes", () => Editor.Save());
|
||||||
@ -133,12 +119,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("bind removal", () =>
|
AddStep("bind removal", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
EditorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||||
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
|
AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
|
||||||
AddStep("reset variables", () =>
|
AddStep("reset variables", () =>
|
||||||
{
|
{
|
||||||
addedObject = null;
|
addedObject = null;
|
||||||
@ -160,12 +146,12 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
|
|
||||||
AddStep("bind removal", () =>
|
AddStep("bind removal", () =>
|
||||||
{
|
{
|
||||||
editorBeatmap.HitObjectAdded += h => addedObject = h;
|
EditorBeatmap.HitObjectAdded += h => addedObject = h;
|
||||||
editorBeatmap.HitObjectRemoved += h => removedObject = h;
|
EditorBeatmap.HitObjectRemoved += h => removedObject = h;
|
||||||
});
|
});
|
||||||
|
|
||||||
AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 }));
|
||||||
AddStep("remove object", () => editorBeatmap.Remove(expectedObject));
|
AddStep("remove object", () => EditorBeatmap.Remove(expectedObject));
|
||||||
addUndoSteps();
|
addUndoSteps();
|
||||||
|
|
||||||
AddStep("reset variables", () =>
|
AddStep("reset variables", () =>
|
||||||
@ -183,18 +169,5 @@ namespace osu.Game.Tests.Visual.Editing
|
|||||||
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
|
private void addUndoSteps() => AddStep("undo", () => Editor.Undo());
|
||||||
|
|
||||||
private void addRedoSteps() => AddStep("redo", () => Editor.Redo());
|
private void addRedoSteps() => AddStep("redo", () => Editor.Redo());
|
||||||
|
|
||||||
protected override Editor CreateEditor() => new TestEditor();
|
|
||||||
|
|
||||||
protected class TestEditor : Editor
|
|
||||||
{
|
|
||||||
public new void Undo() => base.Undo();
|
|
||||||
|
|
||||||
public new void Redo() => base.Redo();
|
|
||||||
|
|
||||||
public new void Save() => base.Save();
|
|
||||||
|
|
||||||
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
154
osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
Normal file
154
osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Objects.Types;
|
||||||
|
using osu.Game.Rulesets.Osu;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Tests.Beatmaps;
|
||||||
|
using osuTK;
|
||||||
|
|
||||||
|
namespace osu.Game.Tests.Visual.Editing
|
||||||
|
{
|
||||||
|
public class TestSceneEditorClipboard : EditorTestScene
|
||||||
|
{
|
||||||
|
protected override Ruleset CreateEditorRuleset() => new OsuRuleset();
|
||||||
|
|
||||||
|
protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false);
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCutRemovesObjects()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 1000 };
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("cut hitobject", () => Editor.Cut());
|
||||||
|
|
||||||
|
AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(1000)]
|
||||||
|
[TestCase(2000)]
|
||||||
|
public void TestCutPaste(double newTime)
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 1000 };
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("cut hitobject", () => Editor.Cut());
|
||||||
|
|
||||||
|
AddStep("move forward in time", () => EditorClock.Seek(newTime));
|
||||||
|
|
||||||
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
|
|
||||||
|
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
|
||||||
|
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCutPasteSlider()
|
||||||
|
{
|
||||||
|
var addedObject = new Slider
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
Path = new SliderPath
|
||||||
|
{
|
||||||
|
ControlPoints =
|
||||||
|
{
|
||||||
|
new PathControlPoint(),
|
||||||
|
new PathControlPoint(new Vector2(100, 0), PathType.Bezier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("cut hitobject", () => Editor.Cut());
|
||||||
|
|
||||||
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
|
|
||||||
|
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
|
||||||
|
AddAssert("path matches", () =>
|
||||||
|
{
|
||||||
|
var path = ((Slider)EditorBeatmap.HitObjects.Single()).Path;
|
||||||
|
return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCutPasteSpinner()
|
||||||
|
{
|
||||||
|
var addedObject = new Spinner
|
||||||
|
{
|
||||||
|
StartTime = 1000,
|
||||||
|
Duration = 5000
|
||||||
|
};
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("cut hitobject", () => Editor.Cut());
|
||||||
|
|
||||||
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
|
|
||||||
|
AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1);
|
||||||
|
|
||||||
|
AddAssert("duration matches", () => ((Spinner)EditorBeatmap.HitObjects.Single()).Duration == 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCopyPaste()
|
||||||
|
{
|
||||||
|
var addedObject = new HitCircle { StartTime = 1000 };
|
||||||
|
|
||||||
|
AddStep("add hitobject", () => EditorBeatmap.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject));
|
||||||
|
|
||||||
|
AddStep("copy hitobject", () => Editor.Copy());
|
||||||
|
|
||||||
|
AddStep("move forward in time", () => EditorClock.Seek(2000));
|
||||||
|
|
||||||
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
|
|
||||||
|
AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2);
|
||||||
|
|
||||||
|
AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCutNothing()
|
||||||
|
{
|
||||||
|
AddStep("cut hitobject", () => Editor.Cut());
|
||||||
|
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestCopyNothing()
|
||||||
|
{
|
||||||
|
AddStep("copy hitobject", () => Editor.Copy());
|
||||||
|
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestPasteNothing()
|
||||||
|
{
|
||||||
|
AddStep("paste hitobject", () => Editor.Paste());
|
||||||
|
AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -54,7 +54,6 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
|||||||
{
|
{
|
||||||
Ruleset = new OsuRuleset().RulesetInfo,
|
Ruleset = new OsuRuleset().RulesetInfo,
|
||||||
OnlineBeatmapID = beatmapId,
|
OnlineBeatmapID = beatmapId,
|
||||||
Path = "normal.osu",
|
|
||||||
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||||
Length = length,
|
Length = length,
|
||||||
BPM = bpm,
|
BPM = bpm,
|
||||||
|
@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
|
|
||||||
protected TestOsuGame Game;
|
protected TestOsuGame Game;
|
||||||
|
|
||||||
|
protected override bool UseFreshStoragePerRun => true;
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
{
|
{
|
||||||
|
@ -133,6 +133,12 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
return () => imported;
|
return () => imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time.
|
||||||
|
/// There's a case where they may succeed incorrectly if we don't compare against the previous instance.
|
||||||
|
/// </summary>
|
||||||
|
private IScreen lastWaitedScreen;
|
||||||
|
|
||||||
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
private void presentAndConfirm(Func<ScoreInfo> getImport, ScorePresentType type)
|
||||||
{
|
{
|
||||||
AddStep("present score", () => Game.PresentScore(getImport(), type));
|
AddStep("present score", () => Game.PresentScore(getImport(), type));
|
||||||
@ -140,13 +146,15 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ScorePresentType.Results:
|
case ScorePresentType.Results:
|
||||||
AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen);
|
AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen);
|
||||||
|
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
||||||
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
AddUntilStep("correct score displayed", () => ((ResultsScreen)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
||||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ScorePresentType.Gameplay:
|
case ScorePresentType.Gameplay:
|
||||||
AddUntilStep("wait for player loader", () => Game.ScreenStack.CurrentScreen is ReplayPlayerLoader);
|
AddUntilStep("wait for player loader", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ReplayPlayerLoader);
|
||||||
|
AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen);
|
||||||
AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID);
|
||||||
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID);
|
||||||
break;
|
break;
|
||||||
|
@ -13,6 +13,7 @@ using osu.Game.Overlays.Mods;
|
|||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
using osu.Game.Screens.Play;
|
using osu.Game.Screens.Play;
|
||||||
using osu.Game.Screens.Select;
|
using osu.Game.Screens.Select;
|
||||||
|
using osu.Game.Screens.Select.Options;
|
||||||
using osu.Game.Tests.Beatmaps.IO;
|
using osu.Game.Tests.Beatmaps.IO;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
@ -168,6 +169,29 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestBeatmapOptionsInput()
|
||||||
|
{
|
||||||
|
TestSongSelect songSelect = null;
|
||||||
|
|
||||||
|
PushAndConfirm(() => songSelect = new TestSongSelect());
|
||||||
|
|
||||||
|
AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show());
|
||||||
|
|
||||||
|
AddStep("Change ruleset to osu!taiko", () =>
|
||||||
|
{
|
||||||
|
InputManager.PressKey(Key.ControlLeft);
|
||||||
|
InputManager.PressKey(Key.Number2);
|
||||||
|
|
||||||
|
InputManager.ReleaseKey(Key.ControlLeft);
|
||||||
|
InputManager.ReleaseKey(Key.Number2);
|
||||||
|
});
|
||||||
|
|
||||||
|
AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType<ToolbarRulesetSelector>().Single().Current.Value.ID == 1);
|
||||||
|
|
||||||
|
AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible);
|
||||||
|
}
|
||||||
|
|
||||||
private void pushEscape() =>
|
private void pushEscape() =>
|
||||||
AddStep("Press escape", () => pressAndRelease(Key.Escape));
|
AddStep("Press escape", () => pressAndRelease(Key.Escape));
|
||||||
|
|
||||||
@ -193,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation
|
|||||||
private class TestSongSelect : PlaySongSelect
|
private class TestSongSelect : PlaySongSelect
|
||||||
{
|
{
|
||||||
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
public ModSelectOverlay ModSelectOverlay => ModSelect;
|
||||||
|
|
||||||
|
public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -839,7 +839,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
new BeatmapInfo
|
new BeatmapInfo
|
||||||
{
|
{
|
||||||
OnlineBeatmapID = id * 10,
|
OnlineBeatmapID = id * 10,
|
||||||
Path = "normal.osu",
|
|
||||||
Version = "Normal",
|
Version = "Normal",
|
||||||
StarDifficulty = 2,
|
StarDifficulty = 2,
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
@ -850,7 +849,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
new BeatmapInfo
|
new BeatmapInfo
|
||||||
{
|
{
|
||||||
OnlineBeatmapID = id * 10 + 1,
|
OnlineBeatmapID = id * 10 + 1,
|
||||||
Path = "hard.osu",
|
|
||||||
Version = "Hard",
|
Version = "Hard",
|
||||||
StarDifficulty = 5,
|
StarDifficulty = 5,
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
@ -861,7 +859,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
new BeatmapInfo
|
new BeatmapInfo
|
||||||
{
|
{
|
||||||
OnlineBeatmapID = id * 10 + 2,
|
OnlineBeatmapID = id * 10 + 2,
|
||||||
Path = "insane.osu",
|
|
||||||
Version = "Insane",
|
Version = "Insane",
|
||||||
StarDifficulty = 6,
|
StarDifficulty = 6,
|
||||||
BaseDifficulty = new BeatmapDifficulty
|
BaseDifficulty = new BeatmapDifficulty
|
||||||
|
@ -3,9 +3,8 @@
|
|||||||
|
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using osu.Framework.Graphics.Sprites;
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
using osu.Game.Graphics;
|
||||||
using osu.Game.Screens.Select.Options;
|
using osu.Game.Screens.Select.Options;
|
||||||
using osuTK.Graphics;
|
|
||||||
using osuTK.Input;
|
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.SongSelect
|
namespace osu.Game.Tests.Visual.SongSelect
|
||||||
{
|
{
|
||||||
@ -16,10 +15,13 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
var overlay = new BeatmapOptionsOverlay();
|
var overlay = new BeatmapOptionsOverlay();
|
||||||
|
|
||||||
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1);
|
var colours = new OsuColour();
|
||||||
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2);
|
|
||||||
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3);
|
overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null);
|
||||||
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4);
|
overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null);
|
||||||
|
overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
|
||||||
|
overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null);
|
||||||
|
overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null);
|
||||||
|
|
||||||
Add(overlay);
|
Add(overlay);
|
||||||
|
|
||||||
|
@ -23,25 +23,15 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
public class TestSceneFilterControl : OsuManualInputManagerTestScene
|
public class TestSceneFilterControl : OsuManualInputManagerTestScene
|
||||||
{
|
{
|
||||||
protected override Container<Drawable> Content => content;
|
protected override Container<Drawable> Content { get; } = new Container { RelativeSizeAxes = Axes.Both };
|
||||||
private readonly Container content;
|
|
||||||
|
|
||||||
private readonly CollectionManager collectionManager;
|
private CollectionManager collectionManager;
|
||||||
|
|
||||||
private RulesetStore rulesets;
|
private RulesetStore rulesets;
|
||||||
private BeatmapManager beatmapManager;
|
private BeatmapManager beatmapManager;
|
||||||
|
|
||||||
private FilterControl control;
|
private FilterControl control;
|
||||||
|
|
||||||
public TestSceneFilterControl()
|
|
||||||
{
|
|
||||||
base.Content.AddRange(new Drawable[]
|
|
||||||
{
|
|
||||||
collectionManager = new CollectionManager(LocalStorage),
|
|
||||||
content = new Container { RelativeSizeAxes = Axes.Both }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(GameHost host)
|
private void load(GameHost host)
|
||||||
{
|
{
|
||||||
@ -49,13 +39,14 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default));
|
||||||
|
|
||||||
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait();
|
||||||
}
|
|
||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
base.Content.AddRange(new Drawable[]
|
||||||
{
|
{
|
||||||
var dependencies = new DependencyContainer(base.CreateChildDependencies(parent));
|
collectionManager = new CollectionManager(LocalStorage),
|
||||||
dependencies.Cache(collectionManager);
|
Content
|
||||||
return dependencies;
|
});
|
||||||
|
|
||||||
|
Dependencies.Cache(collectionManager);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SetUp]
|
[SetUp]
|
||||||
|
@ -879,7 +879,6 @@ namespace osu.Game.Tests.Visual.SongSelect
|
|||||||
{
|
{
|
||||||
Ruleset = getRuleset(),
|
Ruleset = getRuleset(),
|
||||||
OnlineBeatmapID = beatmapId,
|
OnlineBeatmapID = beatmapId,
|
||||||
Path = "normal.osu",
|
|
||||||
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})",
|
||||||
Length = length,
|
Length = length,
|
||||||
BPM = bpm,
|
BPM = bpm,
|
||||||
|
123
osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
Normal file
123
osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Difficulty.Utils
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An indexed queue with limited capacity.
|
||||||
|
/// Respects first-in-first-out insertion order.
|
||||||
|
/// </summary>
|
||||||
|
public class LimitedCapacityQueue<T> : IEnumerable<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The number of elements in the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int Count { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the queue is full (adding any new items will cause removing existing ones).
|
||||||
|
/// </summary>
|
||||||
|
public bool Full => Count == capacity;
|
||||||
|
|
||||||
|
private readonly T[] array;
|
||||||
|
private readonly int capacity;
|
||||||
|
|
||||||
|
// Markers tracking the queue's first and last element.
|
||||||
|
private int start, end;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constructs a new <see cref="LimitedCapacityQueue{T}"/>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="capacity">The number of items the queue can hold.</param>
|
||||||
|
public LimitedCapacityQueue(int capacity)
|
||||||
|
{
|
||||||
|
if (capacity < 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(capacity));
|
||||||
|
|
||||||
|
this.capacity = capacity;
|
||||||
|
array = new T[capacity];
|
||||||
|
Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all elements from the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
start = 0;
|
||||||
|
end = -1;
|
||||||
|
Count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes an item from the front of the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The item removed from the front of the queue.</returns>
|
||||||
|
public T Dequeue()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
throw new InvalidOperationException("Queue is empty.");
|
||||||
|
|
||||||
|
var result = array[start];
|
||||||
|
start = (start + 1) % capacity;
|
||||||
|
Count--;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an item to the back of the <see cref="LimitedCapacityQueue{T}"/>.
|
||||||
|
/// If the queue is holding <see cref="Count"/> elements at the point of addition,
|
||||||
|
/// the item at the front of the queue will be removed.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="item">The item to be added to the back of the queue.</param>
|
||||||
|
public void Enqueue(T item)
|
||||||
|
{
|
||||||
|
end = (end + 1) % capacity;
|
||||||
|
if (Count == capacity)
|
||||||
|
start = (start + 1) % capacity;
|
||||||
|
else
|
||||||
|
Count++;
|
||||||
|
array[end] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the item at the given index in the queue.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="index">
|
||||||
|
/// The index of the item to retrieve.
|
||||||
|
/// The item with index 0 is at the front of the queue
|
||||||
|
/// (it was added the earliest).
|
||||||
|
/// </param>
|
||||||
|
public T this[int index]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= Count)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
return array[(start + index) % capacity];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates the queue from its start to its end.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerator<T> GetEnumerator()
|
||||||
|
{
|
||||||
|
if (Count == 0)
|
||||||
|
yield break;
|
||||||
|
|
||||||
|
for (int i = 0; i < Count; i++)
|
||||||
|
yield return array[(start + i) % capacity];
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
27
osu.Game/Screens/Edit/ClipboardContent.cs
Normal file
27
osu.Game/Screens/Edit/ClipboardContent.cs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
// 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 Newtonsoft.Json;
|
||||||
|
using osu.Game.IO.Serialization;
|
||||||
|
using osu.Game.IO.Serialization.Converters;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Edit
|
||||||
|
{
|
||||||
|
public class ClipboardContent : IJsonSerializable
|
||||||
|
{
|
||||||
|
[JsonConverter(typeof(TypedListConverter<HitObject>))]
|
||||||
|
public IList<HitObject> HitObjects;
|
||||||
|
|
||||||
|
public ClipboardContent()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public ClipboardContent(EditorBeatmap editorBeatmap)
|
||||||
|
{
|
||||||
|
HitObjects = editorBeatmap.SelectedHitObjects.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -271,6 +271,9 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
|||||||
blueprint.Selected += onBlueprintSelected;
|
blueprint.Selected += onBlueprintSelected;
|
||||||
blueprint.Deselected += onBlueprintDeselected;
|
blueprint.Deselected += onBlueprintDeselected;
|
||||||
|
|
||||||
|
if (beatmap.SelectedHitObjects.Contains(hitObject))
|
||||||
|
blueprint.Select();
|
||||||
|
|
||||||
SelectionBlueprints.Add(blueprint);
|
SelectionBlueprints.Add(blueprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework;
|
using osu.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
@ -22,6 +24,7 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Cursor;
|
using osu.Game.Graphics.Cursor;
|
||||||
using osu.Game.Graphics.UserInterface;
|
using osu.Game.Graphics.UserInterface;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
using osu.Game.IO.Serialization;
|
||||||
using osu.Game.Online.API;
|
using osu.Game.Online.API;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
@ -131,9 +134,14 @@ namespace osu.Game.Screens.Edit
|
|||||||
updateLastSavedHash();
|
updateLastSavedHash();
|
||||||
|
|
||||||
EditorMenuBar menuBar;
|
EditorMenuBar menuBar;
|
||||||
|
|
||||||
OsuMenuItem undoMenuItem;
|
OsuMenuItem undoMenuItem;
|
||||||
OsuMenuItem redoMenuItem;
|
OsuMenuItem redoMenuItem;
|
||||||
|
|
||||||
|
EditorMenuItem cutMenuItem;
|
||||||
|
EditorMenuItem copyMenuItem;
|
||||||
|
EditorMenuItem pasteMenuItem;
|
||||||
|
|
||||||
var fileMenuItems = new List<MenuItem>
|
var fileMenuItems = new List<MenuItem>
|
||||||
{
|
{
|
||||||
new EditorMenuItem("Save", MenuItemType.Standard, Save)
|
new EditorMenuItem("Save", MenuItemType.Standard, Save)
|
||||||
@ -183,7 +191,11 @@ namespace osu.Game.Screens.Edit
|
|||||||
Items = new[]
|
Items = new[]
|
||||||
{
|
{
|
||||||
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
|
undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo),
|
||||||
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo)
|
redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo),
|
||||||
|
new EditorMenuItemSpacer(),
|
||||||
|
cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut),
|
||||||
|
copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy),
|
||||||
|
pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,6 +256,16 @@ namespace osu.Game.Screens.Edit
|
|||||||
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
changeHandler.CanUndo.BindValueChanged(v => undoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||||
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true);
|
||||||
|
|
||||||
|
editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) =>
|
||||||
|
{
|
||||||
|
var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0;
|
||||||
|
|
||||||
|
cutMenuItem.Action.Disabled = !hasObjects;
|
||||||
|
copyMenuItem.Action.Disabled = !hasObjects;
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue));
|
||||||
|
|
||||||
menuBar.Mode.ValueChanged += onModeChanged;
|
menuBar.Mode.ValueChanged += onModeChanged;
|
||||||
|
|
||||||
bottomBackground.Colour = colours.Gray2;
|
bottomBackground.Colour = colours.Gray2;
|
||||||
@ -270,6 +292,18 @@ namespace osu.Game.Screens.Edit
|
|||||||
{
|
{
|
||||||
switch (action.ActionType)
|
switch (action.ActionType)
|
||||||
{
|
{
|
||||||
|
case PlatformActionType.Cut:
|
||||||
|
Cut();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformActionType.Copy:
|
||||||
|
Copy();
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case PlatformActionType.Paste:
|
||||||
|
Paste();
|
||||||
|
return true;
|
||||||
|
|
||||||
case PlatformActionType.Undo:
|
case PlatformActionType.Undo:
|
||||||
Undo();
|
Undo();
|
||||||
return true;
|
return true;
|
||||||
@ -394,6 +428,47 @@ namespace osu.Game.Screens.Edit
|
|||||||
this.Exit();
|
this.Exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private readonly Bindable<string> clipboard = new Bindable<string>();
|
||||||
|
|
||||||
|
protected void Cut()
|
||||||
|
{
|
||||||
|
Copy();
|
||||||
|
foreach (var h in editorBeatmap.SelectedHitObjects.ToArray())
|
||||||
|
editorBeatmap.Remove(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Copy()
|
||||||
|
{
|
||||||
|
if (editorBeatmap.SelectedHitObjects.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
clipboard.Value = new ClipboardContent(editorBeatmap).Serialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void Paste()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(clipboard.Value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var objects = clipboard.Value.Deserialize<ClipboardContent>().HitObjects;
|
||||||
|
|
||||||
|
Debug.Assert(objects.Any());
|
||||||
|
|
||||||
|
double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime);
|
||||||
|
|
||||||
|
foreach (var h in objects)
|
||||||
|
h.StartTime += timeOffset;
|
||||||
|
|
||||||
|
changeHandler.BeginChange();
|
||||||
|
|
||||||
|
editorBeatmap.SelectedHitObjects.Clear();
|
||||||
|
|
||||||
|
editorBeatmap.AddRange(objects);
|
||||||
|
editorBeatmap.SelectedHitObjects.AddRange(objects);
|
||||||
|
|
||||||
|
changeHandler.EndChange();
|
||||||
|
}
|
||||||
|
|
||||||
protected void Undo() => changeHandler.RestoreState(-1);
|
protected void Undo() => changeHandler.RestoreState(-1);
|
||||||
|
|
||||||
protected void Redo() => changeHandler.RestoreState(1);
|
protected void Redo() => changeHandler.RestoreState(1);
|
||||||
|
@ -9,7 +9,6 @@ using JetBrains.Annotations;
|
|||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Framework.Threading;
|
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Beatmaps.ControlPoints;
|
using osu.Game.Beatmaps.ControlPoints;
|
||||||
using osu.Game.Beatmaps.Timing;
|
using osu.Game.Beatmaps.Timing;
|
||||||
@ -68,41 +67,6 @@ namespace osu.Game.Screens.Edit
|
|||||||
trackStartTime(obj);
|
trackStartTime(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
|
|
||||||
private ScheduledDelegate scheduledUpdate;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
|
|
||||||
public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false);
|
|
||||||
|
|
||||||
private void updateHitObject([CanBeNull] HitObject hitObject, bool silent)
|
|
||||||
{
|
|
||||||
scheduledUpdate?.Cancel();
|
|
||||||
|
|
||||||
if (hitObject != null)
|
|
||||||
pendingUpdates.Add(hitObject);
|
|
||||||
|
|
||||||
scheduledUpdate = Schedule(() =>
|
|
||||||
{
|
|
||||||
beatmapProcessor?.PreProcess();
|
|
||||||
|
|
||||||
foreach (var obj in pendingUpdates)
|
|
||||||
obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
|
|
||||||
|
|
||||||
beatmapProcessor?.PostProcess();
|
|
||||||
|
|
||||||
if (!silent)
|
|
||||||
{
|
|
||||||
foreach (var obj in pendingUpdates)
|
|
||||||
HitObjectUpdated?.Invoke(obj);
|
|
||||||
}
|
|
||||||
|
|
||||||
pendingUpdates.Clear();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapInfo BeatmapInfo
|
public BeatmapInfo BeatmapInfo
|
||||||
{
|
{
|
||||||
get => PlayableBeatmap.BeatmapInfo;
|
get => PlayableBeatmap.BeatmapInfo;
|
||||||
@ -125,6 +89,8 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
|
private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects;
|
||||||
|
|
||||||
|
private readonly HashSet<HitObject> pendingUpdates = new HashSet<HitObject>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
|
/// Adds a collection of <see cref="HitObject"/>s to this <see cref="EditorBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -160,14 +126,27 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
mutableHitObjects.Insert(index, hitObject);
|
mutableHitObjects.Insert(index, hitObject);
|
||||||
|
|
||||||
|
// must be run after any change to hitobject ordering
|
||||||
|
beatmapProcessor?.PreProcess();
|
||||||
|
processHitObject(hitObject);
|
||||||
|
beatmapProcessor?.PostProcess();
|
||||||
|
|
||||||
HitObjectAdded?.Invoke(hitObject);
|
HitObjectAdded?.Invoke(hitObject);
|
||||||
updateHitObject(hitObject, true);
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Updates a <see cref="HitObject"/>, invoking <see cref="HitObject.ApplyDefaults"/> and re-processing the beatmap.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="hitObject">The <see cref="HitObject"/> to update.</param>
|
||||||
|
public void UpdateHitObject([NotNull] HitObject hitObject)
|
||||||
|
{
|
||||||
|
pendingUpdates.Add(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Removes a <see cref="HitObject"/> from this <see cref="EditorBeatmap"/>.
|
/// Removes a <see cref="HitObject"/> from this <see cref="EditorBeatmap"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="hitObject">The <see cref="HitObject"/> to add.</param>
|
/// <param name="hitObject">The <see cref="HitObject"/> to remove.</param>
|
||||||
/// <returns>True if the <see cref="HitObject"/> has been removed, false otherwise.</returns>
|
/// <returns>True if the <see cref="HitObject"/> has been removed, false otherwise.</returns>
|
||||||
public bool Remove(HitObject hitObject)
|
public bool Remove(HitObject hitObject)
|
||||||
{
|
{
|
||||||
@ -199,11 +178,14 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
var bindable = startTimeBindables[hitObject];
|
var bindable = startTimeBindables[hitObject];
|
||||||
bindable.UnbindAll();
|
bindable.UnbindAll();
|
||||||
|
|
||||||
startTimeBindables.Remove(hitObject);
|
startTimeBindables.Remove(hitObject);
|
||||||
HitObjectRemoved?.Invoke(hitObject);
|
|
||||||
|
|
||||||
updateHitObject(null, true);
|
// must be run after any change to hitobject ordering
|
||||||
|
beatmapProcessor?.PreProcess();
|
||||||
|
processHitObject(hitObject);
|
||||||
|
beatmapProcessor?.PostProcess();
|
||||||
|
|
||||||
|
HitObjectRemoved?.Invoke(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -211,20 +193,33 @@ namespace osu.Game.Screens.Edit
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
var removed = HitObjects.ToList();
|
foreach (var h in HitObjects.ToArray())
|
||||||
|
Remove(h);
|
||||||
mutableHitObjects.Clear();
|
|
||||||
|
|
||||||
foreach (var b in startTimeBindables)
|
|
||||||
b.Value.UnbindAll();
|
|
||||||
startTimeBindables.Clear();
|
|
||||||
|
|
||||||
foreach (var h in removed)
|
|
||||||
HitObjectRemoved?.Invoke(h);
|
|
||||||
|
|
||||||
updateHitObject(null, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Update()
|
||||||
|
{
|
||||||
|
base.Update();
|
||||||
|
|
||||||
|
// debounce updates as they are common and may come from input events, which can run needlessly many times per update frame.
|
||||||
|
if (pendingUpdates.Count > 0)
|
||||||
|
{
|
||||||
|
beatmapProcessor?.PreProcess();
|
||||||
|
|
||||||
|
foreach (var hitObject in pendingUpdates)
|
||||||
|
{
|
||||||
|
processHitObject(hitObject);
|
||||||
|
HitObjectUpdated?.Invoke(hitObject);
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingUpdates.Clear();
|
||||||
|
|
||||||
|
beatmapProcessor?.PostProcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty);
|
||||||
|
|
||||||
private void trackStartTime(HitObject hitObject)
|
private void trackStartTime(HitObject hitObject)
|
||||||
{
|
{
|
||||||
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
|
startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy();
|
||||||
|
@ -66,24 +66,9 @@ namespace osu.Game.Screens.Select
|
|||||||
[BackgroundDependencyLoader(permitNulls: true)]
|
[BackgroundDependencyLoader(permitNulls: true)]
|
||||||
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
|
private void load(OsuColour colours, IBindable<RulesetInfo> parentRuleset, OsuConfigManager config)
|
||||||
{
|
{
|
||||||
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
|
|
||||||
showConverted.ValueChanged += _ => updateCriteria();
|
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
|
|
||||||
minimumStars.ValueChanged += _ => updateCriteria();
|
|
||||||
|
|
||||||
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
|
|
||||||
maximumStars.ValueChanged += _ => updateCriteria();
|
|
||||||
|
|
||||||
ruleset.BindTo(parentRuleset);
|
|
||||||
ruleset.BindValueChanged(_ => updateCriteria());
|
|
||||||
|
|
||||||
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
|
sortMode = config.GetBindable<SortMode>(OsuSetting.SongSelectSortingMode);
|
||||||
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
|
groupMode = config.GetBindable<GroupMode>(OsuSetting.SongSelectGroupingMode);
|
||||||
|
|
||||||
groupMode.BindValueChanged(_ => updateCriteria());
|
|
||||||
sortMode.BindValueChanged(_ => updateCriteria());
|
|
||||||
|
|
||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new Box
|
new Box
|
||||||
@ -182,6 +167,21 @@ namespace osu.Game.Screens.Select
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.ShowConvertedBeatmaps, showConverted);
|
||||||
|
showConverted.ValueChanged += _ => updateCriteria();
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.DisplayStarsMinimum, minimumStars);
|
||||||
|
minimumStars.ValueChanged += _ => updateCriteria();
|
||||||
|
|
||||||
|
config.BindWith(OsuSetting.DisplayStarsMaximum, maximumStars);
|
||||||
|
maximumStars.ValueChanged += _ => updateCriteria();
|
||||||
|
|
||||||
|
ruleset.BindTo(parentRuleset);
|
||||||
|
ruleset.BindValueChanged(_ => updateCriteria());
|
||||||
|
|
||||||
|
groupMode.BindValueChanged(_ => updateCriteria());
|
||||||
|
sortMode.BindValueChanged(_ => updateCriteria());
|
||||||
|
|
||||||
collectionDropdown.Current.ValueChanged += val =>
|
collectionDropdown.Current.ValueChanged += val =>
|
||||||
{
|
{
|
||||||
if (val.NewValue == null)
|
if (val.NewValue == null)
|
||||||
|
@ -12,7 +12,6 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Graphics.Sprites;
|
using osu.Game.Graphics.Sprites;
|
||||||
using osuTK;
|
using osuTK;
|
||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Options
|
namespace osu.Game.Screens.Select.Options
|
||||||
@ -52,8 +51,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
set => secondLine.Text = value;
|
set => secondLine.Text = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Key? HotKey;
|
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
flash.FadeTo(0.1f, 1000, Easing.OutQuint);
|
||||||
@ -75,17 +72,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
return base.OnClick(e);
|
return base.OnClick(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
|
||||||
{
|
|
||||||
if (!e.Repeat && e.Key == HotKey)
|
|
||||||
{
|
|
||||||
Click();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos);
|
||||||
|
|
||||||
public BeatmapOptionsButton()
|
public BeatmapOptionsButton()
|
||||||
|
@ -11,6 +11,8 @@ using osuTK;
|
|||||||
using osuTK.Graphics;
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
using osu.Game.Graphics.Containers;
|
using osu.Game.Graphics.Containers;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace osu.Game.Screens.Select.Options
|
namespace osu.Game.Screens.Select.Options
|
||||||
{
|
{
|
||||||
@ -27,33 +29,6 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
|
|
||||||
public override bool BlockScreenWideMouse => false;
|
public override bool BlockScreenWideMouse => false;
|
||||||
|
|
||||||
protected override void PopIn()
|
|
||||||
{
|
|
||||||
base.PopIn();
|
|
||||||
|
|
||||||
this.FadeIn(transition_duration, Easing.OutQuint);
|
|
||||||
|
|
||||||
if (buttonsContainer.Position.X == 1 || Alpha == 0)
|
|
||||||
buttonsContainer.MoveToX(x_position - x_movement);
|
|
||||||
|
|
||||||
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
|
|
||||||
|
|
||||||
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
|
|
||||||
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void PopOut()
|
|
||||||
{
|
|
||||||
base.PopOut();
|
|
||||||
|
|
||||||
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
|
|
||||||
|
|
||||||
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
|
|
||||||
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
|
|
||||||
|
|
||||||
this.FadeOut(transition_duration, Easing.InQuint);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BeatmapOptionsOverlay()
|
public BeatmapOptionsOverlay()
|
||||||
{
|
{
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
@ -87,9 +62,8 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
/// <param name="secondLine">Text in the second line.</param>
|
/// <param name="secondLine">Text in the second line.</param>
|
||||||
/// <param name="colour">Colour of the button.</param>
|
/// <param name="colour">Colour of the button.</param>
|
||||||
/// <param name="icon">Icon of the button.</param>
|
/// <param name="icon">Icon of the button.</param>
|
||||||
/// <param name="hotkey">Hotkey of the button.</param>
|
|
||||||
/// <param name="action">Binding the button does.</param>
|
/// <param name="action">Binding the button does.</param>
|
||||||
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null)
|
public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action)
|
||||||
{
|
{
|
||||||
var button = new BeatmapOptionsButton
|
var button = new BeatmapOptionsButton
|
||||||
{
|
{
|
||||||
@ -102,10 +76,58 @@ namespace osu.Game.Screens.Select.Options
|
|||||||
Hide();
|
Hide();
|
||||||
action?.Invoke();
|
action?.Invoke();
|
||||||
},
|
},
|
||||||
HotKey = hotkey
|
|
||||||
};
|
};
|
||||||
|
|
||||||
buttonsContainer.Add(button);
|
buttonsContainer.Add(button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void PopIn()
|
||||||
|
{
|
||||||
|
base.PopIn();
|
||||||
|
|
||||||
|
this.FadeIn(transition_duration, Easing.OutQuint);
|
||||||
|
|
||||||
|
if (buttonsContainer.Position.X == 1 || Alpha == 0)
|
||||||
|
buttonsContainer.MoveToX(x_position - x_movement);
|
||||||
|
|
||||||
|
holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint);
|
||||||
|
|
||||||
|
buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint);
|
||||||
|
buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void PopOut()
|
||||||
|
{
|
||||||
|
base.PopOut();
|
||||||
|
|
||||||
|
holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine);
|
||||||
|
|
||||||
|
buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine);
|
||||||
|
buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine);
|
||||||
|
|
||||||
|
this.FadeOut(transition_duration, Easing.InQuint);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
// don't absorb control as ToolbarRulesetSelector uses control + number to navigate
|
||||||
|
if (e.ControlPressed) return false;
|
||||||
|
|
||||||
|
if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9)
|
||||||
|
{
|
||||||
|
int requested = e.Key - Key.Number1;
|
||||||
|
|
||||||
|
// go reverse as buttonsContainer is a ReverseChildIDFillFlowContainer
|
||||||
|
BeatmapOptionsButton found = buttonsContainer.Children.ElementAtOrDefault((buttonsContainer.Children.Count - 1) - requested);
|
||||||
|
|
||||||
|
if (found != null)
|
||||||
|
{
|
||||||
|
found.Click();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.OnKeyDown(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select
|
|||||||
{
|
{
|
||||||
ValidForResume = false;
|
ValidForResume = false;
|
||||||
Edit();
|
Edit();
|
||||||
}, Key.Number4);
|
});
|
||||||
|
|
||||||
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore;
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select
|
|||||||
private MusicController music { get; set; }
|
private MusicController music { get; set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader(true)]
|
[BackgroundDependencyLoader(true)]
|
||||||
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections)
|
private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog)
|
||||||
{
|
{
|
||||||
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
|
// initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter).
|
||||||
transferRulesetValue();
|
transferRulesetValue();
|
||||||
@ -275,9 +275,10 @@ namespace osu.Game.Screens.Select
|
|||||||
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
|
Footer.AddButton(new FooterButtonRandom { Action = triggerRandom });
|
||||||
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
|
Footer.AddButton(new FooterButtonOptions(), BeatmapOptions);
|
||||||
|
|
||||||
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null, Key.Number1);
|
BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show());
|
||||||
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2);
|
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo));
|
||||||
BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number3);
|
BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null);
|
||||||
|
BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo));
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogOverlay = dialog;
|
dialogOverlay = dialog;
|
||||||
@ -517,6 +518,8 @@ namespace osu.Game.Screens.Select
|
|||||||
FilterControl.Activate();
|
FilterControl.Activate();
|
||||||
|
|
||||||
ModSelect.SelectedMods.BindTo(selectedMods);
|
ModSelect.SelectedMods.BindTo(selectedMods);
|
||||||
|
|
||||||
|
music.TrackChanged += ensureTrackLooping;
|
||||||
}
|
}
|
||||||
|
|
||||||
private const double logo_transition = 250;
|
private const double logo_transition = 250;
|
||||||
@ -568,6 +571,7 @@ namespace osu.Game.Screens.Select
|
|||||||
BeatmapDetails.Refresh();
|
BeatmapDetails.Refresh();
|
||||||
|
|
||||||
music.CurrentTrack.Looping = true;
|
music.CurrentTrack.Looping = true;
|
||||||
|
music.TrackChanged += ensureTrackLooping;
|
||||||
music.ResetTrackAdjustments();
|
music.ResetTrackAdjustments();
|
||||||
|
|
||||||
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
|
if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending)
|
||||||
@ -593,6 +597,7 @@ namespace osu.Game.Screens.Select
|
|||||||
BeatmapOptions.Hide();
|
BeatmapOptions.Hide();
|
||||||
|
|
||||||
music.CurrentTrack.Looping = false;
|
music.CurrentTrack.Looping = false;
|
||||||
|
music.TrackChanged -= ensureTrackLooping;
|
||||||
|
|
||||||
this.ScaleTo(1.1f, 250, Easing.InSine);
|
this.ScaleTo(1.1f, 250, Easing.InSine);
|
||||||
|
|
||||||
@ -614,10 +619,14 @@ namespace osu.Game.Screens.Select
|
|||||||
FilterControl.Deactivate();
|
FilterControl.Deactivate();
|
||||||
|
|
||||||
music.CurrentTrack.Looping = false;
|
music.CurrentTrack.Looping = false;
|
||||||
|
music.TrackChanged -= ensureTrackLooping;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection)
|
||||||
|
=> music.CurrentTrack.Looping = true;
|
||||||
|
|
||||||
public override bool OnBackButton()
|
public override bool OnBackButton()
|
||||||
{
|
{
|
||||||
if (ModSelect.State.Value == Visibility.Visible)
|
if (ModSelect.State.Value == Visibility.Visible)
|
||||||
@ -634,6 +643,9 @@ namespace osu.Game.Screens.Select
|
|||||||
base.Dispose(isDisposing);
|
base.Dispose(isDisposing);
|
||||||
|
|
||||||
decoupledRuleset.UnbindAll();
|
decoupledRuleset.UnbindAll();
|
||||||
|
|
||||||
|
if (music != null)
|
||||||
|
music.TrackChanged -= ensureTrackLooping;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -653,8 +665,6 @@ namespace osu.Game.Screens.Select
|
|||||||
beatmapInfoWedge.Beatmap = beatmap;
|
beatmapInfoWedge.Beatmap = beatmap;
|
||||||
|
|
||||||
BeatmapDetails.Beatmap = beatmap;
|
BeatmapDetails.Beatmap = beatmap;
|
||||||
|
|
||||||
music.CurrentTrack.Looping = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly WeakReference<ITrack> lastTrack = new WeakReference<ITrack>(null);
|
private readonly WeakReference<ITrack> lastTrack = new WeakReference<ITrack>(null);
|
||||||
|
@ -15,14 +15,16 @@ namespace osu.Game.Tests.Beatmaps
|
|||||||
{
|
{
|
||||||
public class TestBeatmap : Beatmap
|
public class TestBeatmap : Beatmap
|
||||||
{
|
{
|
||||||
public TestBeatmap(RulesetInfo ruleset)
|
public TestBeatmap(RulesetInfo ruleset, bool withHitObjects = true)
|
||||||
{
|
{
|
||||||
var baseBeatmap = CreateBeatmap();
|
var baseBeatmap = CreateBeatmap();
|
||||||
|
|
||||||
BeatmapInfo = baseBeatmap.BeatmapInfo;
|
BeatmapInfo = baseBeatmap.BeatmapInfo;
|
||||||
ControlPointInfo = baseBeatmap.ControlPointInfo;
|
ControlPointInfo = baseBeatmap.ControlPointInfo;
|
||||||
Breaks = baseBeatmap.Breaks;
|
Breaks = baseBeatmap.Breaks;
|
||||||
HitObjects = baseBeatmap.HitObjects;
|
|
||||||
|
if (withHitObjects)
|
||||||
|
HitObjects = baseBeatmap.HitObjects;
|
||||||
|
|
||||||
BeatmapInfo.Ruleset = ruleset;
|
BeatmapInfo.Ruleset = ruleset;
|
||||||
BeatmapInfo.RulesetID = ruleset.ID ?? 0;
|
BeatmapInfo.RulesetID = ruleset.ID ?? 0;
|
||||||
|
@ -14,7 +14,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
{
|
{
|
||||||
public abstract class EditorTestScene : ScreenTestScene
|
public abstract class EditorTestScene : ScreenTestScene
|
||||||
{
|
{
|
||||||
protected Editor Editor { get; private set; }
|
protected EditorBeatmap EditorBeatmap;
|
||||||
|
|
||||||
|
protected TestEditor Editor { get; private set; }
|
||||||
|
|
||||||
|
protected EditorClock EditorClock { get; private set; }
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load()
|
private void load()
|
||||||
@ -29,6 +33,8 @@ namespace osu.Game.Tests.Visual
|
|||||||
AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
|
AddStep("load editor", () => LoadScreen(Editor = CreateEditor()));
|
||||||
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
|
AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType<HitObjectComposer>().FirstOrDefault()?.IsLoaded == true
|
||||||
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
|
&& Editor.ChildrenOfType<TimelineArea>().FirstOrDefault()?.IsLoaded == true);
|
||||||
|
AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType<EditorBeatmap>().Single());
|
||||||
|
AddStep("get clock", () => EditorClock = Editor.ChildrenOfType<EditorClock>().Single());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -39,6 +45,23 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset();
|
protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset();
|
||||||
|
|
||||||
protected virtual Editor CreateEditor() => new Editor();
|
protected virtual TestEditor CreateEditor() => new TestEditor();
|
||||||
|
|
||||||
|
protected class TestEditor : Editor
|
||||||
|
{
|
||||||
|
public new void Undo() => base.Undo();
|
||||||
|
|
||||||
|
public new void Redo() => base.Redo();
|
||||||
|
|
||||||
|
public new void Save() => base.Save();
|
||||||
|
|
||||||
|
public new void Cut() => base.Cut();
|
||||||
|
|
||||||
|
public new void Copy() => base.Copy();
|
||||||
|
|
||||||
|
public new void Paste() => base.Paste();
|
||||||
|
|
||||||
|
public new bool HasUnsavedChanges => base.HasUnsavedChanges;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
private Lazy<Storage> localStorage;
|
private Lazy<Storage> localStorage;
|
||||||
protected Storage LocalStorage => localStorage.Value;
|
protected Storage LocalStorage => localStorage.Value;
|
||||||
|
|
||||||
private readonly Lazy<DatabaseContextFactory> contextFactory;
|
private Lazy<DatabaseContextFactory> contextFactory;
|
||||||
|
|
||||||
protected IAPIProvider API
|
protected IAPIProvider API
|
||||||
{
|
{
|
||||||
@ -71,6 +71,17 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent)
|
||||||
{
|
{
|
||||||
|
contextFactory = new Lazy<DatabaseContextFactory>(() =>
|
||||||
|
{
|
||||||
|
var factory = new DatabaseContextFactory(LocalStorage);
|
||||||
|
factory.ResetDatabase();
|
||||||
|
using (var usage = factory.Get())
|
||||||
|
usage.Migrate();
|
||||||
|
return factory;
|
||||||
|
});
|
||||||
|
|
||||||
|
RecycleLocalStorage();
|
||||||
|
|
||||||
var baseDependencies = base.CreateChildDependencies(parent);
|
var baseDependencies = base.CreateChildDependencies(parent);
|
||||||
|
|
||||||
var providedRuleset = CreateRuleset();
|
var providedRuleset = CreateRuleset();
|
||||||
@ -104,19 +115,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
|
|
||||||
protected OsuTestScene()
|
protected OsuTestScene()
|
||||||
{
|
{
|
||||||
RecycleLocalStorage();
|
|
||||||
contextFactory = new Lazy<DatabaseContextFactory>(() =>
|
|
||||||
{
|
|
||||||
var factory = new DatabaseContextFactory(LocalStorage);
|
|
||||||
factory.ResetDatabase();
|
|
||||||
using (var usage = factory.Get())
|
|
||||||
usage.Migrate();
|
|
||||||
return factory;
|
|
||||||
});
|
|
||||||
|
|
||||||
base.Content.Add(content = new DrawSizePreservingFillContainer());
|
base.Content.Add(content = new DrawSizePreservingFillContainer());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual bool UseFreshStoragePerRun => false;
|
||||||
|
|
||||||
public virtual void RecycleLocalStorage()
|
public virtual void RecycleLocalStorage()
|
||||||
{
|
{
|
||||||
if (localStorage?.IsValueCreated == true)
|
if (localStorage?.IsValueCreated == true)
|
||||||
@ -131,9 +134,13 @@ namespace osu.Game.Tests.Visual
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage = new Lazy<Storage>(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}")));
|
localStorage =
|
||||||
|
new Lazy<Storage>(() => !UseFreshStoragePerRun && host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private GameHost host { get; set; }
|
||||||
|
|
||||||
[Resolved]
|
[Resolved]
|
||||||
protected AudioManager Audio { get; private set; }
|
protected AudioManager Audio { get; private set; }
|
||||||
|
|
||||||
@ -172,7 +179,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
if (MusicController?.TrackLoaded == true)
|
if (MusicController?.TrackLoaded == true)
|
||||||
MusicController.CurrentTrack.Stop();
|
MusicController.CurrentTrack.Stop();
|
||||||
|
|
||||||
if (contextFactory.IsValueCreated)
|
if (contextFactory?.IsValueCreated == true)
|
||||||
contextFactory.Value.ResetDatabase();
|
contextFactory.Value.ResetDatabase();
|
||||||
|
|
||||||
RecycleLocalStorage();
|
RecycleLocalStorage();
|
||||||
|
@ -81,6 +81,12 @@ namespace osu.Game.Tests.Visual
|
|||||||
LoadScreen(Player);
|
LoadScreen(Player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void Dispose(bool isDisposing)
|
||||||
|
{
|
||||||
|
LocalConfig?.Dispose();
|
||||||
|
base.Dispose(isDisposing);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the ruleset for setting up the <see cref="Player"/> component.
|
/// Creates the ruleset for setting up the <see cref="Player"/> component.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -24,7 +24,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2020.910.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2020.911.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||||
<PackageReference Include="Sentry" Version="2.1.6" />
|
<PackageReference Include="Sentry" Version="2.1.6" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||||
|
@ -70,7 +70,7 @@
|
|||||||
<Reference Include="System.Net.Http" />
|
<Reference Include="System.Net.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup Label="Package References">
|
<ItemGroup Label="Package References">
|
||||||
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.910.0" />
|
<PackageReference Include="ppy.osu.Framework.iOS" Version="2020.911.0" />
|
||||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
<!-- Xamarin.iOS does not automatically handle transitive dependencies from NuGet packages. -->
|
||||||
@ -80,7 +80,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.2.6" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.2.6" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
|
||||||
<PackageReference Include="ppy.osu.Framework" Version="2020.910.0" />
|
<PackageReference Include="ppy.osu.Framework" Version="2020.911.0" />
|
||||||
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
<PackageReference Include="SharpCompress" Version="0.26.0" />
|
||||||
<PackageReference Include="NUnit" Version="3.12.0" />
|
<PackageReference Include="NUnit" Version="3.12.0" />
|
||||||
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
<PackageReference Include="SharpRaven" Version="2.4.0" />
|
||||||
|
Loading…
Reference in New Issue
Block a user