mirror of
https://github.com/ppy/osu.git
synced 2024-09-22 07:27:25 +08:00
Merge branch 'master' into fix-hit-error-when-not-visible
This commit is contained in:
commit
36599d1174
@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
{
|
{
|
||||||
CatchHitObject lastObject = null;
|
CatchHitObject lastObject = null;
|
||||||
|
|
||||||
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
// In 2B beatmaps, it is possible that a normal Fruit is placed in the middle of a JuiceStream.
|
||||||
foreach (var hitObject in beatmap.HitObjects
|
foreach (var hitObject in beatmap.HitObjects
|
||||||
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
|
.SelectMany(obj => obj is JuiceStream stream ? stream.NestedHitObjects.AsEnumerable() : new[] { obj })
|
||||||
@ -60,10 +62,12 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (lastObject != null)
|
if (lastObject != null)
|
||||||
yield return new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth);
|
objects.Add(new CatchDifficultyHitObject(hitObject, lastObject, clockRate, halfCatcherWidth, objects, objects.Count));
|
||||||
|
|
||||||
lastObject = hitObject;
|
lastObject = hitObject;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Catch.Objects;
|
using osu.Game.Rulesets.Catch.Objects;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -24,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly double StrainTime;
|
public readonly double StrainTime;
|
||||||
|
|
||||||
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth)
|
public CatchDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, float halfCatcherWidth, List<DifficultyHitObject> objects, int index)
|
||||||
: base(hitObject, lastObject, clockRate)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
// We will scale everything by this factor, so we can assume a uniform CircleSize among beatmaps.
|
||||||
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
float scalingFactor = normalized_hitobject_radius / halfCatcherWidth;
|
||||||
|
@ -70,8 +70,12 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
|||||||
|
|
||||||
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
|
||||||
|
|
||||||
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
for (int i = 1; i < sortedObjects.Length; i++)
|
for (int i = 1; i < sortedObjects.Length; i++)
|
||||||
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
|
objects.Add(new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate, objects, objects.Count));
|
||||||
|
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
|
||||||
|
@ -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.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
@ -11,8 +12,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Preprocessing
|
|||||||
{
|
{
|
||||||
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
public new ManiaHitObject BaseObject => (ManiaHitObject)base.BaseObject;
|
||||||
|
|
||||||
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
public ManiaDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||||
: base(hitObject, lastObject, clockRate)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,9 +84,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills
|
|||||||
return individualStrain + overallStrain - CurrentStrain;
|
return individualStrain + overallStrain - CurrentStrain;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double offset)
|
protected override double CalculateInitialStrain(double offset, DifficultyHitObject current)
|
||||||
=> applyDecay(individualStrain, offset - Previous[0].StartTime, individual_decay_base)
|
=> applyDecay(individualStrain, offset - current.Previous(0).StartTime, individual_decay_base)
|
||||||
+ applyDecay(overallStrain, offset - Previous[0].StartTime, overall_decay_base);
|
+ applyDecay(overallStrain, offset - current.Previous(0).StartTime, overall_decay_base);
|
||||||
|
|
||||||
private double applyDecay(double value, double deltaTime, double decayBase)
|
private double applyDecay(double value, double deltaTime, double decayBase)
|
||||||
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
=> value * Math.Pow(decayBase, deltaTime / 1000);
|
||||||
|
141
osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
Normal file
141
osu.Game.Rulesets.Osu/Difficulty/Evaluators/AimEvaluator.cs
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
// 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.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class AimEvaluator
|
||||||
|
{
|
||||||
|
private const double wide_angle_multiplier = 1.5;
|
||||||
|
private const double acute_angle_multiplier = 2.0;
|
||||||
|
private const double slider_multiplier = 1.5;
|
||||||
|
private const double velocity_change_multiplier = 0.75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates the difficulty of aiming the current object, based on:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>cursor velocity to the current object,</description></item>
|
||||||
|
/// <item><description>angle difficulty,</description></item>
|
||||||
|
/// <item><description>sharp velocity increases,</description></item>
|
||||||
|
/// <item><description>and slider difficulty.</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool withSliders)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner || current.Index <= 1 || current.Previous(0).BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
|
var osuLastObj = (OsuDifficultyHitObject)current.Previous(0);
|
||||||
|
var osuLastLastObj = (OsuDifficultyHitObject)current.Previous(1);
|
||||||
|
|
||||||
|
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
||||||
|
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
||||||
|
|
||||||
|
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
||||||
|
if (osuLastObj.BaseObject is Slider && withSliders)
|
||||||
|
{
|
||||||
|
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
||||||
|
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
|
||||||
|
|
||||||
|
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
||||||
|
}
|
||||||
|
|
||||||
|
// As above, do the same for the previous hitobject.
|
||||||
|
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
||||||
|
|
||||||
|
if (osuLastLastObj.BaseObject is Slider && withSliders)
|
||||||
|
{
|
||||||
|
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
|
||||||
|
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
|
||||||
|
|
||||||
|
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
||||||
|
}
|
||||||
|
|
||||||
|
double wideAngleBonus = 0;
|
||||||
|
double acuteAngleBonus = 0;
|
||||||
|
double sliderBonus = 0;
|
||||||
|
double velocityChangeBonus = 0;
|
||||||
|
|
||||||
|
double aimStrain = currVelocity; // Start strain with regular velocity.
|
||||||
|
|
||||||
|
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
||||||
|
{
|
||||||
|
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
|
||||||
|
{
|
||||||
|
double currAngle = osuCurrObj.Angle.Value;
|
||||||
|
double lastAngle = osuLastObj.Angle.Value;
|
||||||
|
double lastLastAngle = osuLastLastObj.Angle.Value;
|
||||||
|
|
||||||
|
// Rewarding angles, take the smaller velocity as base.
|
||||||
|
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
||||||
|
|
||||||
|
wideAngleBonus = calcWideAngleBonus(currAngle);
|
||||||
|
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
||||||
|
|
||||||
|
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
|
||||||
|
acuteAngleBonus = 0;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
||||||
|
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
||||||
|
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
||||||
|
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
||||||
|
}
|
||||||
|
|
||||||
|
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
||||||
|
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
||||||
|
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
|
||||||
|
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Math.Max(prevVelocity, currVelocity) != 0)
|
||||||
|
{
|
||||||
|
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
||||||
|
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
|
||||||
|
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
|
||||||
|
|
||||||
|
// Scale with ratio of difference compared to 0.5 * max dist.
|
||||||
|
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
||||||
|
|
||||||
|
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
||||||
|
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
||||||
|
|
||||||
|
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
|
||||||
|
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
|
||||||
|
// do not award overlap
|
||||||
|
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
|
||||||
|
|
||||||
|
// Choose the largest bonus, multiplied by ratio.
|
||||||
|
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
|
||||||
|
|
||||||
|
// Penalize for rhythm changes.
|
||||||
|
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (osuLastObj.TravelTime != 0)
|
||||||
|
{
|
||||||
|
// Reward sliders based on velocity.
|
||||||
|
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
||||||
|
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
||||||
|
|
||||||
|
// Add in additional slider velocity bonus.
|
||||||
|
if (withSliders)
|
||||||
|
aimStrain += sliderBonus * slider_multiplier;
|
||||||
|
|
||||||
|
return aimStrain;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
||||||
|
|
||||||
|
private static double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
// 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.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class FlashlightEvaluator
|
||||||
|
{
|
||||||
|
private const double max_opacity_bonus = 0.4;
|
||||||
|
private const double hidden_bonus = 0.2;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates the difficulty of memorising and hitting an object, based on:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>distance between the previous and current object,</description></item>
|
||||||
|
/// <item><description>the visual opacity of the current object,</description></item>
|
||||||
|
/// <item><description>and whether the hidden mod is enabled.</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, bool hidden)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var osuCurrent = (OsuDifficultyHitObject)current;
|
||||||
|
var osuHitObject = (OsuHitObject)(osuCurrent.BaseObject);
|
||||||
|
|
||||||
|
double scalingFactor = 52.0 / osuHitObject.Radius;
|
||||||
|
double smallDistNerf = 1.0;
|
||||||
|
double cumulativeStrainTime = 0.0;
|
||||||
|
|
||||||
|
double result = 0.0;
|
||||||
|
|
||||||
|
OsuDifficultyHitObject lastObj = osuCurrent;
|
||||||
|
|
||||||
|
// This is iterating backwards in time from the current object.
|
||||||
|
for (int i = 0; i < Math.Min(current.Index, 10); i++)
|
||||||
|
{
|
||||||
|
var currentObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||||
|
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
||||||
|
|
||||||
|
if (!(currentObj.BaseObject is Spinner))
|
||||||
|
{
|
||||||
|
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
|
||||||
|
|
||||||
|
cumulativeStrainTime += lastObj.StrainTime;
|
||||||
|
|
||||||
|
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
||||||
|
if (i == 0)
|
||||||
|
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
||||||
|
|
||||||
|
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
||||||
|
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
||||||
|
|
||||||
|
// Bonus based on how visible the object is.
|
||||||
|
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
||||||
|
|
||||||
|
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
lastObj = currentObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = Math.Pow(smallDistNerf * result, 2.0);
|
||||||
|
|
||||||
|
// Additional bonus for Hidden due to there being no approach circles.
|
||||||
|
if (hidden)
|
||||||
|
result *= 1.0 + hidden_bonus;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
108
osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
Normal file
108
osu.Game.Rulesets.Osu/Difficulty/Evaluators/RhythmEvaluator.cs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
// 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.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class RhythmEvaluator
|
||||||
|
{
|
||||||
|
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
||||||
|
private const double rhythm_multiplier = 0.75;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
int previousIslandSize = 0;
|
||||||
|
|
||||||
|
double rhythmComplexitySum = 0;
|
||||||
|
int islandSize = 1;
|
||||||
|
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
||||||
|
|
||||||
|
bool firstDeltaSwitch = false;
|
||||||
|
|
||||||
|
int historicalNoteCount = Math.Min(current.Index, 32);
|
||||||
|
|
||||||
|
int rhythmStart = 0;
|
||||||
|
|
||||||
|
while (rhythmStart < historicalNoteCount - 2 && current.StartTime - current.Previous(rhythmStart).StartTime < history_time_max)
|
||||||
|
rhythmStart++;
|
||||||
|
|
||||||
|
for (int i = rhythmStart; i > 0; i--)
|
||||||
|
{
|
||||||
|
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)current.Previous(i - 1);
|
||||||
|
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)current.Previous(i);
|
||||||
|
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)current.Previous(i + 1);
|
||||||
|
|
||||||
|
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
||||||
|
|
||||||
|
currHistoricalDecay = Math.Min((double)(historicalNoteCount - i) / historicalNoteCount, currHistoricalDecay); // either we're limited by time or limited by object count.
|
||||||
|
|
||||||
|
double currDelta = currObj.StrainTime;
|
||||||
|
double prevDelta = prevObj.StrainTime;
|
||||||
|
double lastDelta = lastObj.StrainTime;
|
||||||
|
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
||||||
|
|
||||||
|
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
||||||
|
|
||||||
|
windowPenalty = Math.Min(1, windowPenalty);
|
||||||
|
|
||||||
|
double effectiveRatio = windowPenalty * currRatio;
|
||||||
|
|
||||||
|
if (firstDeltaSwitch)
|
||||||
|
{
|
||||||
|
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
||||||
|
{
|
||||||
|
if (islandSize < 7)
|
||||||
|
islandSize++; // island is still progressing, count size.
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (current.Previous(i - 1).BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
||||||
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
|
if (current.Previous(i).BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
||||||
|
effectiveRatio *= 0.25;
|
||||||
|
|
||||||
|
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
||||||
|
effectiveRatio *= 0.25;
|
||||||
|
|
||||||
|
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
||||||
|
effectiveRatio *= 0.50;
|
||||||
|
|
||||||
|
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
||||||
|
effectiveRatio *= 0.125;
|
||||||
|
|
||||||
|
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
||||||
|
|
||||||
|
startRatio = effectiveRatio;
|
||||||
|
|
||||||
|
previousIslandSize = islandSize; // log the last island size.
|
||||||
|
|
||||||
|
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
||||||
|
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
||||||
|
|
||||||
|
islandSize = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
||||||
|
{
|
||||||
|
// Begin counting island until we change speed again.
|
||||||
|
firstDeltaSwitch = true;
|
||||||
|
startRatio = effectiveRatio;
|
||||||
|
islandSize = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||||
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using osu.Framework.Utils;
|
||||||
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Difficulty.Evaluators
|
||||||
|
{
|
||||||
|
public static class SpeedEvaluator
|
||||||
|
{
|
||||||
|
private const double single_spacing_threshold = 125;
|
||||||
|
private const double min_speed_bonus = 75; // ~200BPM
|
||||||
|
private const double speed_balancing_factor = 40;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates the difficulty of tapping the current object, based on:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><description>time between pressing the previous and current object,</description></item>
|
||||||
|
/// <item><description>distance between those objects,</description></item>
|
||||||
|
/// <item><description>and how easily they can be cheesed.</description></item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public static double EvaluateDifficultyOf(DifficultyHitObject current, double greatWindow)
|
||||||
|
{
|
||||||
|
if (current.BaseObject is Spinner)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// derive strainTime for calculation
|
||||||
|
var osuCurrObj = (OsuDifficultyHitObject)current;
|
||||||
|
var osuPrevObj = current.Index > 0 ? (OsuDifficultyHitObject)current.Previous(0) : null;
|
||||||
|
|
||||||
|
double strainTime = osuCurrObj.StrainTime;
|
||||||
|
double greatWindowFull = greatWindow * 2;
|
||||||
|
double speedWindowRatio = strainTime / greatWindowFull;
|
||||||
|
|
||||||
|
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
|
||||||
|
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
|
||||||
|
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
|
||||||
|
|
||||||
|
// Cap deltatime to the OD 300 hitwindow.
|
||||||
|
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
||||||
|
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
||||||
|
|
||||||
|
// derive speedBonus for calculation
|
||||||
|
double speedBonus = 1.0;
|
||||||
|
|
||||||
|
if (strainTime < min_speed_bonus)
|
||||||
|
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
||||||
|
|
||||||
|
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
||||||
|
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
|
||||||
|
|
||||||
|
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -87,16 +87,17 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
|||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
|
List<DifficultyHitObject> objects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
// The first jump is formed by the first two hitobjects of the map.
|
// The first jump is formed by the first two hitobjects of the map.
|
||||||
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
// If the map has less than two OsuHitObjects, the enumerator will not return anything.
|
||||||
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
for (int i = 1; i < beatmap.HitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null;
|
var lastLast = i > 1 ? beatmap.HitObjects[i - 2] : null;
|
||||||
var last = beatmap.HitObjects[i - 1];
|
objects.Add(new OsuDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], lastLast, clockRate, objects, objects.Count));
|
||||||
var current = beatmap.HitObjects[i];
|
|
||||||
|
|
||||||
yield return new OsuDifficultyHitObject(current, lastLast, last, clockRate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return objects;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods, double clockRate)
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
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.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
@ -74,8 +75,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
|
|||||||
private readonly OsuHitObject lastLastObject;
|
private readonly OsuHitObject lastLastObject;
|
||||||
private readonly OsuHitObject lastObject;
|
private readonly OsuHitObject lastObject;
|
||||||
|
|
||||||
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastLastObject, HitObject lastObject, double clockRate)
|
public OsuDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||||
: base(hitObject, lastObject, clockRate)
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
this.lastLastObject = (OsuHitObject)lastLastObject;
|
this.lastLastObject = (OsuHitObject)lastLastObject;
|
||||||
this.lastObject = (OsuHitObject)lastObject;
|
this.lastObject = (OsuHitObject)lastObject;
|
||||||
|
@ -4,8 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -22,142 +21,19 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
private readonly bool withSliders;
|
private readonly bool withSliders;
|
||||||
|
|
||||||
protected override int HistoryLength => 2;
|
|
||||||
|
|
||||||
private const double wide_angle_multiplier = 1.5;
|
|
||||||
private const double acute_angle_multiplier = 2.0;
|
|
||||||
private const double slider_multiplier = 1.5;
|
|
||||||
private const double velocity_change_multiplier = 0.75;
|
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double skillMultiplier => 23.25;
|
private double skillMultiplier => 23.25;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
|
|
||||||
private double strainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner || Previous.Count <= 1 || Previous[0].BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
|
||||||
var osuLastObj = (OsuDifficultyHitObject)Previous[0];
|
|
||||||
var osuLastLastObj = (OsuDifficultyHitObject)Previous[1];
|
|
||||||
|
|
||||||
// Calculate the velocity to the current hitobject, which starts with a base distance / time assuming the last object is a hitcircle.
|
|
||||||
double currVelocity = osuCurrObj.LazyJumpDistance / osuCurrObj.StrainTime;
|
|
||||||
|
|
||||||
// But if the last object is a slider, then we extend the travel velocity through the slider into the current object.
|
|
||||||
if (osuLastObj.BaseObject is Slider && withSliders)
|
|
||||||
{
|
|
||||||
double travelVelocity = osuLastObj.TravelDistance / osuLastObj.TravelTime; // calculate the slider velocity from slider head to slider end.
|
|
||||||
double movementVelocity = osuCurrObj.MinimumJumpDistance / osuCurrObj.MinimumJumpTime; // calculate the movement velocity from slider end to current object
|
|
||||||
|
|
||||||
currVelocity = Math.Max(currVelocity, movementVelocity + travelVelocity); // take the larger total combined velocity.
|
|
||||||
}
|
|
||||||
|
|
||||||
// As above, do the same for the previous hitobject.
|
|
||||||
double prevVelocity = osuLastObj.LazyJumpDistance / osuLastObj.StrainTime;
|
|
||||||
|
|
||||||
if (osuLastLastObj.BaseObject is Slider && withSliders)
|
|
||||||
{
|
|
||||||
double travelVelocity = osuLastLastObj.TravelDistance / osuLastLastObj.TravelTime;
|
|
||||||
double movementVelocity = osuLastObj.MinimumJumpDistance / osuLastObj.MinimumJumpTime;
|
|
||||||
|
|
||||||
prevVelocity = Math.Max(prevVelocity, movementVelocity + travelVelocity);
|
|
||||||
}
|
|
||||||
|
|
||||||
double wideAngleBonus = 0;
|
|
||||||
double acuteAngleBonus = 0;
|
|
||||||
double sliderBonus = 0;
|
|
||||||
double velocityChangeBonus = 0;
|
|
||||||
|
|
||||||
double aimStrain = currVelocity; // Start strain with regular velocity.
|
|
||||||
|
|
||||||
if (Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime) < 1.25 * Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime)) // If rhythms are the same.
|
|
||||||
{
|
|
||||||
if (osuCurrObj.Angle != null && osuLastObj.Angle != null && osuLastLastObj.Angle != null)
|
|
||||||
{
|
|
||||||
double currAngle = osuCurrObj.Angle.Value;
|
|
||||||
double lastAngle = osuLastObj.Angle.Value;
|
|
||||||
double lastLastAngle = osuLastLastObj.Angle.Value;
|
|
||||||
|
|
||||||
// Rewarding angles, take the smaller velocity as base.
|
|
||||||
double angleBonus = Math.Min(currVelocity, prevVelocity);
|
|
||||||
|
|
||||||
wideAngleBonus = calcWideAngleBonus(currAngle);
|
|
||||||
acuteAngleBonus = calcAcuteAngleBonus(currAngle);
|
|
||||||
|
|
||||||
if (osuCurrObj.StrainTime > 100) // Only buff deltaTime exceeding 300 bpm 1/2.
|
|
||||||
acuteAngleBonus = 0;
|
|
||||||
else
|
|
||||||
{
|
|
||||||
acuteAngleBonus *= calcAcuteAngleBonus(lastAngle) // Multiply by previous angle, we don't want to buff unless this is a wiggle type pattern.
|
|
||||||
* Math.Min(angleBonus, 125 / osuCurrObj.StrainTime) // The maximum velocity we buff is equal to 125 / strainTime
|
|
||||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, (100 - osuCurrObj.StrainTime) / 25)), 2) // scale buff from 150 bpm 1/4 to 200 bpm 1/4
|
|
||||||
* Math.Pow(Math.Sin(Math.PI / 2 * (Math.Clamp(osuCurrObj.LazyJumpDistance, 50, 100) - 50) / 50), 2); // Buff distance exceeding 50 (radius) up to 100 (diameter).
|
|
||||||
}
|
|
||||||
|
|
||||||
// Penalize wide angles if they're repeated, reducing the penalty as the lastAngle gets more acute.
|
|
||||||
wideAngleBonus *= angleBonus * (1 - Math.Min(wideAngleBonus, Math.Pow(calcWideAngleBonus(lastAngle), 3)));
|
|
||||||
// Penalize acute angles if they're repeated, reducing the penalty as the lastLastAngle gets more obtuse.
|
|
||||||
acuteAngleBonus *= 0.5 + 0.5 * (1 - Math.Min(acuteAngleBonus, Math.Pow(calcAcuteAngleBonus(lastLastAngle), 3)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Math.Max(prevVelocity, currVelocity) != 0)
|
|
||||||
{
|
|
||||||
// We want to use the average velocity over the whole object when awarding differences, not the individual jump and slider path velocities.
|
|
||||||
prevVelocity = (osuLastObj.LazyJumpDistance + osuLastLastObj.TravelDistance) / osuLastObj.StrainTime;
|
|
||||||
currVelocity = (osuCurrObj.LazyJumpDistance + osuLastObj.TravelDistance) / osuCurrObj.StrainTime;
|
|
||||||
|
|
||||||
// Scale with ratio of difference compared to 0.5 * max dist.
|
|
||||||
double distRatio = Math.Pow(Math.Sin(Math.PI / 2 * Math.Abs(prevVelocity - currVelocity) / Math.Max(prevVelocity, currVelocity)), 2);
|
|
||||||
|
|
||||||
// Reward for % distance up to 125 / strainTime for overlaps where velocity is still changing.
|
|
||||||
double overlapVelocityBuff = Math.Min(125 / Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime), Math.Abs(prevVelocity - currVelocity));
|
|
||||||
|
|
||||||
// Reward for % distance slowed down compared to previous, paying attention to not award overlap
|
|
||||||
double nonOverlapVelocityBuff = Math.Abs(prevVelocity - currVelocity)
|
|
||||||
// do not award overlap
|
|
||||||
* Math.Pow(Math.Sin(Math.PI / 2 * Math.Min(1, Math.Min(osuCurrObj.LazyJumpDistance, osuLastObj.LazyJumpDistance) / 100)), 2);
|
|
||||||
|
|
||||||
// Choose the largest bonus, multiplied by ratio.
|
|
||||||
velocityChangeBonus = Math.Max(overlapVelocityBuff, nonOverlapVelocityBuff) * distRatio;
|
|
||||||
|
|
||||||
// Penalize for rhythm changes.
|
|
||||||
velocityChangeBonus *= Math.Pow(Math.Min(osuCurrObj.StrainTime, osuLastObj.StrainTime) / Math.Max(osuCurrObj.StrainTime, osuLastObj.StrainTime), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (osuLastObj.TravelTime != 0)
|
|
||||||
{
|
|
||||||
// Reward sliders based on velocity.
|
|
||||||
sliderBonus = osuLastObj.TravelDistance / osuLastObj.TravelTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger.
|
|
||||||
aimStrain += Math.Max(acuteAngleBonus * acute_angle_multiplier, wideAngleBonus * wide_angle_multiplier + velocityChangeBonus * velocity_change_multiplier);
|
|
||||||
|
|
||||||
// Add in additional slider velocity bonus.
|
|
||||||
if (withSliders)
|
|
||||||
aimStrain += sliderBonus * slider_multiplier;
|
|
||||||
|
|
||||||
return aimStrain;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double calcWideAngleBonus(double angle) => Math.Pow(Math.Sin(3.0 / 4 * (Math.Min(5.0 / 6 * Math.PI, Math.Max(Math.PI / 6, angle)) - Math.PI / 6)), 2);
|
|
||||||
|
|
||||||
private double calcAcuteAngleBonus(double angle) => 1 - calcWideAngleBonus(angle);
|
|
||||||
|
|
||||||
private double applyDiminishingExp(double val) => Math.Pow(val, 0.99);
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
currentStrain += AimEvaluator.EvaluateDifficultyOf(current, withSliders) * skillMultiplier;
|
||||||
|
|
||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,8 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Mods;
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -16,85 +15,28 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Flashlight : OsuStrainSkill
|
public class Flashlight : OsuStrainSkill
|
||||||
{
|
{
|
||||||
|
private readonly bool hasHiddenMod;
|
||||||
|
|
||||||
public Flashlight(Mod[] mods)
|
public Flashlight(Mod[] mods)
|
||||||
: base(mods)
|
: base(mods)
|
||||||
{
|
{
|
||||||
hidden = mods.Any(m => m is OsuModHidden);
|
hasHiddenMod = mods.Any(m => m is OsuModHidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
private double skillMultiplier => 0.05;
|
private double skillMultiplier => 0.05;
|
||||||
private double strainDecayBase => 0.15;
|
private double strainDecayBase => 0.15;
|
||||||
protected override double DecayWeight => 1.0;
|
protected override double DecayWeight => 1.0;
|
||||||
protected override int HistoryLength => 10; // Look back for 10 notes is added for the sake of flashlight calculations.
|
|
||||||
|
|
||||||
private readonly bool hidden;
|
|
||||||
|
|
||||||
private const double max_opacity_bonus = 0.4;
|
|
||||||
private const double hidden_bonus = 0.2;
|
|
||||||
|
|
||||||
private double currentStrain;
|
private double currentStrain;
|
||||||
|
|
||||||
private double strainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
var osuCurrent = (OsuDifficultyHitObject)current;
|
|
||||||
var osuHitObject = (OsuHitObject)(osuCurrent.BaseObject);
|
|
||||||
|
|
||||||
double scalingFactor = 52.0 / osuHitObject.Radius;
|
|
||||||
double smallDistNerf = 1.0;
|
|
||||||
double cumulativeStrainTime = 0.0;
|
|
||||||
|
|
||||||
double result = 0.0;
|
|
||||||
|
|
||||||
OsuDifficultyHitObject lastObj = osuCurrent;
|
|
||||||
|
|
||||||
// This is iterating backwards in time from the current object.
|
|
||||||
for (int i = 0; i < Previous.Count; i++)
|
|
||||||
{
|
|
||||||
var currentObj = (OsuDifficultyHitObject)Previous[i];
|
|
||||||
var currentHitObject = (OsuHitObject)(currentObj.BaseObject);
|
|
||||||
|
|
||||||
if (!(currentObj.BaseObject is Spinner))
|
|
||||||
{
|
|
||||||
double jumpDistance = (osuHitObject.StackedPosition - currentHitObject.EndPosition).Length;
|
|
||||||
|
|
||||||
cumulativeStrainTime += lastObj.StrainTime;
|
|
||||||
|
|
||||||
// We want to nerf objects that can be easily seen within the Flashlight circle radius.
|
|
||||||
if (i == 0)
|
|
||||||
smallDistNerf = Math.Min(1.0, jumpDistance / 75.0);
|
|
||||||
|
|
||||||
// We also want to nerf stacks so that only the first object of the stack is accounted for.
|
|
||||||
double stackNerf = Math.Min(1.0, (currentObj.LazyJumpDistance / scalingFactor) / 25.0);
|
|
||||||
|
|
||||||
// Bonus based on how visible the object is.
|
|
||||||
double opacityBonus = 1.0 + max_opacity_bonus * (1.0 - osuCurrent.OpacityAt(currentHitObject.StartTime, hidden));
|
|
||||||
|
|
||||||
result += stackNerf * opacityBonus * scalingFactor * jumpDistance / cumulativeStrainTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
lastObj = currentObj;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = Math.Pow(smallDistNerf * result, 2.0);
|
|
||||||
|
|
||||||
// Additional bonus for Hidden due to there being no approach circles.
|
|
||||||
if (hidden)
|
|
||||||
result *= 1.0 + hidden_bonus;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time) => currentStrain * strainDecay(time - Previous[0].StartTime);
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => currentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
currentStrain += FlashlightEvaluator.EvaluateDifficultyOf(current, hasHiddenMod) * skillMultiplier;
|
||||||
|
|
||||||
return currentStrain;
|
return currentStrain;
|
||||||
}
|
}
|
||||||
|
@ -4,9 +4,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
using osu.Game.Rulesets.Osu.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Osu.Difficulty.Evaluators;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
|
||||||
using osu.Framework.Utils;
|
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
||||||
{
|
{
|
||||||
@ -15,12 +13,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Speed : OsuStrainSkill
|
public class Speed : OsuStrainSkill
|
||||||
{
|
{
|
||||||
private const double single_spacing_threshold = 125;
|
|
||||||
private const double rhythm_multiplier = 0.75;
|
|
||||||
private const int history_time_max = 5000; // 5 seconds of calculatingRhythmBonus max.
|
|
||||||
private const double min_speed_bonus = 75; // ~200BPM
|
|
||||||
private const double speed_balancing_factor = 40;
|
|
||||||
|
|
||||||
private double skillMultiplier => 1375;
|
private double skillMultiplier => 1375;
|
||||||
private double strainDecayBase => 0.3;
|
private double strainDecayBase => 0.3;
|
||||||
|
|
||||||
@ -29,8 +21,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
|
|
||||||
protected override int ReducedSectionCount => 5;
|
protected override int ReducedSectionCount => 5;
|
||||||
protected override double DifficultyMultiplier => 1.04;
|
protected override double DifficultyMultiplier => 1.04;
|
||||||
protected override int HistoryLength => 32;
|
|
||||||
|
|
||||||
private readonly double greatWindow;
|
private readonly double greatWindow;
|
||||||
|
|
||||||
public Speed(Mod[] mods, double hitWindowGreat)
|
public Speed(Mod[] mods, double hitWindowGreat)
|
||||||
@ -39,139 +29,16 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
|
|||||||
greatWindow = hitWindowGreat;
|
greatWindow = hitWindowGreat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Calculates a rhythm multiplier for the difficulty of the tap associated with historic data of the current <see cref="OsuDifficultyHitObject"/>.
|
|
||||||
/// </summary>
|
|
||||||
private double calculateRhythmBonus(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
int previousIslandSize = 0;
|
|
||||||
|
|
||||||
double rhythmComplexitySum = 0;
|
|
||||||
int islandSize = 1;
|
|
||||||
double startRatio = 0; // store the ratio of the current start of an island to buff for tighter rhythms
|
|
||||||
|
|
||||||
bool firstDeltaSwitch = false;
|
|
||||||
|
|
||||||
int rhythmStart = 0;
|
|
||||||
|
|
||||||
while (rhythmStart < Previous.Count - 2 && current.StartTime - Previous[rhythmStart].StartTime < history_time_max)
|
|
||||||
rhythmStart++;
|
|
||||||
|
|
||||||
for (int i = rhythmStart; i > 0; i--)
|
|
||||||
{
|
|
||||||
OsuDifficultyHitObject currObj = (OsuDifficultyHitObject)Previous[i - 1];
|
|
||||||
OsuDifficultyHitObject prevObj = (OsuDifficultyHitObject)Previous[i];
|
|
||||||
OsuDifficultyHitObject lastObj = (OsuDifficultyHitObject)Previous[i + 1];
|
|
||||||
|
|
||||||
double currHistoricalDecay = (history_time_max - (current.StartTime - currObj.StartTime)) / history_time_max; // scales note 0 to 1 from history to now
|
|
||||||
|
|
||||||
currHistoricalDecay = Math.Min((double)(Previous.Count - i) / Previous.Count, currHistoricalDecay); // either we're limited by time or limited by object count.
|
|
||||||
|
|
||||||
double currDelta = currObj.StrainTime;
|
|
||||||
double prevDelta = prevObj.StrainTime;
|
|
||||||
double lastDelta = lastObj.StrainTime;
|
|
||||||
double currRatio = 1.0 + 6.0 * Math.Min(0.5, Math.Pow(Math.Sin(Math.PI / (Math.Min(prevDelta, currDelta) / Math.Max(prevDelta, currDelta))), 2)); // fancy function to calculate rhythmbonuses.
|
|
||||||
|
|
||||||
double windowPenalty = Math.Min(1, Math.Max(0, Math.Abs(prevDelta - currDelta) - greatWindow * 0.6) / (greatWindow * 0.6));
|
|
||||||
|
|
||||||
windowPenalty = Math.Min(1, windowPenalty);
|
|
||||||
|
|
||||||
double effectiveRatio = windowPenalty * currRatio;
|
|
||||||
|
|
||||||
if (firstDeltaSwitch)
|
|
||||||
{
|
|
||||||
if (!(prevDelta > 1.25 * currDelta || prevDelta * 1.25 < currDelta))
|
|
||||||
{
|
|
||||||
if (islandSize < 7)
|
|
||||||
islandSize++; // island is still progressing, count size.
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (Previous[i - 1].BaseObject is Slider) // bpm change is into slider, this is easy acc window
|
|
||||||
effectiveRatio *= 0.125;
|
|
||||||
|
|
||||||
if (Previous[i].BaseObject is Slider) // bpm change was from a slider, this is easier typically than circle -> circle
|
|
||||||
effectiveRatio *= 0.25;
|
|
||||||
|
|
||||||
if (previousIslandSize == islandSize) // repeated island size (ex: triplet -> triplet)
|
|
||||||
effectiveRatio *= 0.25;
|
|
||||||
|
|
||||||
if (previousIslandSize % 2 == islandSize % 2) // repeated island polartiy (2 -> 4, 3 -> 5)
|
|
||||||
effectiveRatio *= 0.50;
|
|
||||||
|
|
||||||
if (lastDelta > prevDelta + 10 && prevDelta > currDelta + 10) // previous increase happened a note ago, 1/1->1/2-1/4, dont want to buff this.
|
|
||||||
effectiveRatio *= 0.125;
|
|
||||||
|
|
||||||
rhythmComplexitySum += Math.Sqrt(effectiveRatio * startRatio) * currHistoricalDecay * Math.Sqrt(4 + islandSize) / 2 * Math.Sqrt(4 + previousIslandSize) / 2;
|
|
||||||
|
|
||||||
startRatio = effectiveRatio;
|
|
||||||
|
|
||||||
previousIslandSize = islandSize; // log the last island size.
|
|
||||||
|
|
||||||
if (prevDelta * 1.25 < currDelta) // we're slowing down, stop counting
|
|
||||||
firstDeltaSwitch = false; // if we're speeding up, this stays true and we keep counting island size.
|
|
||||||
|
|
||||||
islandSize = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (prevDelta > 1.25 * currDelta) // we want to be speeding up.
|
|
||||||
{
|
|
||||||
// Begin counting island until we change speed again.
|
|
||||||
firstDeltaSwitch = true;
|
|
||||||
startRatio = effectiveRatio;
|
|
||||||
islandSize = 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Math.Sqrt(4 + rhythmComplexitySum * rhythm_multiplier) / 2; //produces multiplier that can be applied to strain. range [1, infinity) (not really though)
|
|
||||||
}
|
|
||||||
|
|
||||||
private double strainValueOf(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
if (current.BaseObject is Spinner)
|
|
||||||
return 0;
|
|
||||||
|
|
||||||
// derive strainTime for calculation
|
|
||||||
var osuCurrObj = (OsuDifficultyHitObject)current;
|
|
||||||
var osuPrevObj = Previous.Count > 0 ? (OsuDifficultyHitObject)Previous[0] : null;
|
|
||||||
|
|
||||||
double strainTime = osuCurrObj.StrainTime;
|
|
||||||
double greatWindowFull = greatWindow * 2;
|
|
||||||
double speedWindowRatio = strainTime / greatWindowFull;
|
|
||||||
|
|
||||||
// Aim to nerf cheesy rhythms (Very fast consecutive doubles with large deltatimes between)
|
|
||||||
if (osuPrevObj != null && strainTime < greatWindowFull && osuPrevObj.StrainTime > strainTime)
|
|
||||||
strainTime = Interpolation.Lerp(osuPrevObj.StrainTime, strainTime, speedWindowRatio);
|
|
||||||
|
|
||||||
// Cap deltatime to the OD 300 hitwindow.
|
|
||||||
// 0.93 is derived from making sure 260bpm OD8 streams aren't nerfed harshly, whilst 0.92 limits the effect of the cap.
|
|
||||||
strainTime /= Math.Clamp((strainTime / greatWindowFull) / 0.93, 0.92, 1);
|
|
||||||
|
|
||||||
// derive speedBonus for calculation
|
|
||||||
double speedBonus = 1.0;
|
|
||||||
|
|
||||||
if (strainTime < min_speed_bonus)
|
|
||||||
speedBonus = 1 + 0.75 * Math.Pow((min_speed_bonus - strainTime) / speed_balancing_factor, 2);
|
|
||||||
|
|
||||||
double travelDistance = osuPrevObj?.TravelDistance ?? 0;
|
|
||||||
double distance = Math.Min(single_spacing_threshold, travelDistance + osuCurrObj.MinimumJumpDistance);
|
|
||||||
|
|
||||||
return (speedBonus + speedBonus * Math.Pow(distance / single_spacing_threshold, 3.5)) / strainTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
private double strainDecay(double ms) => Math.Pow(strainDecayBase, ms / 1000);
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time) => (currentStrain * currentRhythm) * strainDecay(time - Previous[0].StartTime);
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => (currentStrain * currentRhythm) * strainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
currentStrain *= strainDecay(current.DeltaTime);
|
currentStrain *= strainDecay(current.DeltaTime);
|
||||||
currentStrain += strainValueOf(current) * skillMultiplier;
|
currentStrain += SpeedEvaluator.EvaluateDifficultyOf(current, greatWindow) * skillMultiplier;
|
||||||
|
|
||||||
currentRhythm = calculateRhythmBonus(current);
|
currentRhythm = RhythmEvaluator.EvaluateDifficultyOf(current, greatWindow);
|
||||||
|
|
||||||
return currentStrain * currentRhythm;
|
return currentStrain * currentRhythm;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
// See the LICENCE file in the repository root for full licence text.
|
// See the LICENCE file in the repository root for full licence text.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
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;
|
||||||
@ -24,11 +25,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly HitType? HitType;
|
public readonly HitType? HitType;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// The index of the object in the beatmap.
|
|
||||||
/// </summary>
|
|
||||||
public readonly int ObjectIndex;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Whether the object should carry a penalty due to being hittable using special techniques
|
/// Whether the object should carry a penalty due to being hittable using special techniques
|
||||||
/// making it easier to do so.
|
/// making it easier to do so.
|
||||||
@ -42,16 +38,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing
|
|||||||
/// <param name="lastObject">The gameplay <see cref="HitObject"/> preceding <paramref name="hitObject"/>.</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="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="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>
|
/// <param name="objects">The list of <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||||
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex)
|
/// /// <param name="index">The position of this <see cref="DifficultyHitObject"/> in the <paramref name="objects"/> list.</param>
|
||||||
: base(hitObject, lastObject, clockRate)
|
public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||||
|
: base(hitObject, lastObject, clockRate, objects, index)
|
||||||
{
|
{
|
||||||
var currentHit = hitObject as Hit;
|
var currentHit = hitObject as Hit;
|
||||||
|
|
||||||
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate);
|
||||||
HitType = currentHit?.Type;
|
HitType = currentHit?.Type;
|
||||||
|
|
||||||
ObjectIndex = objectIndex;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills
|
|||||||
if (!samePattern(start, mostRecentPatternsToCompare))
|
if (!samePattern(start, mostRecentPatternsToCompare))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex;
|
int notesSince = hitObject.Index - rhythmHistory[start].Index;
|
||||||
penalty *= repetitionPenalty(notesSince);
|
penalty *= repetitionPenalty(notesSince);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -46,13 +46,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
|||||||
|
|
||||||
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
|
||||||
{
|
{
|
||||||
List<TaikoDifficultyHitObject> taikoDifficultyHitObjects = new List<TaikoDifficultyHitObject>();
|
List<DifficultyHitObject> taikoDifficultyHitObjects = new List<DifficultyHitObject>();
|
||||||
|
|
||||||
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
for (int i = 2; i < beatmap.HitObjects.Count; i++)
|
||||||
{
|
{
|
||||||
taikoDifficultyHitObjects.Add(
|
taikoDifficultyHitObjects.Add(
|
||||||
new TaikoDifficultyHitObject(
|
new TaikoDifficultyHitObject(
|
||||||
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i
|
beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, taikoDifficultyHitObjects, taikoDifficultyHitObjects.Count
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -225,10 +225,10 @@ namespace osu.Game.Tests.Online
|
|||||||
this.testBeatmapManager = testBeatmapManager;
|
this.testBeatmapManager = testBeatmapManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public override Live<BeatmapSetInfo> Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
testBeatmapManager.AllowImport.Task.WaitSafely();
|
testBeatmapManager.AllowImport.Task.WaitSafely();
|
||||||
return (testBeatmapManager.CurrentImport = base.Import(item, archive, lowPriority, cancellationToken));
|
return (testBeatmapManager.CurrentImport = base.Import(item, archive, cancellationToken));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,16 @@ using Moq;
|
|||||||
using NUnit.Framework;
|
using NUnit.Framework;
|
||||||
using osu.Framework.Allocation;
|
using osu.Framework.Allocation;
|
||||||
using osu.Framework.Bindables;
|
using osu.Framework.Bindables;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Colour;
|
||||||
using osu.Framework.Graphics.Containers;
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
using osu.Framework.Testing;
|
using osu.Framework.Testing;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Overlays;
|
using osu.Game.Overlays;
|
||||||
using osu.Game.Overlays.Toolbar;
|
using osu.Game.Overlays.Toolbar;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
|
using osuTK.Graphics;
|
||||||
using osuTK.Input;
|
using osuTK.Input;
|
||||||
|
|
||||||
namespace osu.Game.Tests.Visual.Menus
|
namespace osu.Game.Tests.Visual.Menus
|
||||||
@ -95,6 +100,28 @@ namespace osu.Game.Tests.Visual.Menus
|
|||||||
AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible);
|
AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollInput()
|
||||||
|
{
|
||||||
|
OsuScrollContainer scroll = null;
|
||||||
|
|
||||||
|
AddStep("add scroll layer", () => Add(scroll = new OsuScrollContainer
|
||||||
|
{
|
||||||
|
Depth = 1f,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Child = new Box
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
Height = DrawHeight * 2,
|
||||||
|
Colour = ColourInfo.GradientVertical(Color4.Gray, Color4.DarkGray),
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
AddStep("hover toolbar", () => InputManager.MoveMouseTo(toolbar));
|
||||||
|
AddStep("perform scroll", () => InputManager.ScrollVerticalBy(500));
|
||||||
|
AddAssert("not scrolled", () => scroll.Current == 0);
|
||||||
|
}
|
||||||
|
|
||||||
public class TestToolbar : Toolbar
|
public class TestToolbar : Toolbar
|
||||||
{
|
{
|
||||||
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
|
public new Bindable<OverlayActivation> OverlayActivationMode => base.OverlayActivationMode as Bindable<OverlayActivation>;
|
||||||
|
@ -36,6 +36,12 @@ namespace osu.Game.Tests.Visual.Settings
|
|||||||
AddStep("set visual mode to circles", () => latencyCertifier.VisualMode.Value = LatencyVisualMode.CircleGameplay);
|
AddStep("set visual mode to circles", () => latencyCertifier.VisualMode.Value = LatencyVisualMode.CircleGameplay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestScrollingGameplay()
|
||||||
|
{
|
||||||
|
AddStep("set visual mode to scrolling", () => latencyCertifier.VisualMode.Value = LatencyVisualMode.ScrollingGameplay);
|
||||||
|
}
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public void TestCycleVisualModes()
|
public void TestCycleVisualModes()
|
||||||
{
|
{
|
||||||
|
@ -347,35 +347,17 @@ namespace osu.Game.Beatmaps
|
|||||||
|
|
||||||
#region Implementation of ICanAcceptFiles
|
#region Implementation of ICanAcceptFiles
|
||||||
|
|
||||||
public Task Import(params string[] paths)
|
public Task Import(params string[] paths) => beatmapModelManager.Import(paths);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Import(params ImportTask[] tasks)
|
public Task Import(params ImportTask[] tasks) => beatmapModelManager.Import(tasks);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
public Task<IEnumerable<Live<BeatmapSetInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => beatmapModelManager.Import(notification, tasks);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(notification, tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Task<Live<BeatmapSetInfo>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(task, lowPriority, cancellationToken);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(task, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Task<Live<BeatmapSetInfo>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) => beatmapModelManager.Import(archive, lowPriority, cancellationToken);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Live<BeatmapSetInfo>? Import(BeatmapSetInfo item, ArchiveReader? archive = null, CancellationToken cancellationToken = default) => beatmapModelManager.Import(item, archive, cancellationToken);
|
||||||
{
|
|
||||||
return beatmapModelManager.Import(item, archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> HandledExtensions => beatmapModelManager.HandledExtensions;
|
public IEnumerable<string> HandledExtensions => beatmapModelManager.HandledExtensions;
|
||||||
|
|
||||||
|
@ -12,14 +12,22 @@ namespace osu.Game.Database
|
|||||||
public interface ICanAcceptFiles
|
public interface ICanAcceptFiles
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Import the specified paths.
|
/// Import one or more items from filesystem <paramref name="paths"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will be treated as a low priority batch import if more than one path is specified.
|
||||||
|
/// This will post notifications tracking progress.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="paths">The files which should be imported.</param>
|
/// <param name="paths">The files which should be imported.</param>
|
||||||
Task Import(params string[] paths);
|
Task Import(params string[] paths);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Import the specified files from the given import tasks.
|
/// Import the specified files from the given import tasks.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This will be treated as a low priority batch import if more than one path is specified.
|
||||||
|
/// This will post notifications tracking progress.
|
||||||
|
/// </remarks>
|
||||||
/// <param name="tasks">The import tasks from which the files should be imported.</param>
|
/// <param name="tasks">The import tasks from which the files should be imported.</param>
|
||||||
Task Import(params ImportTask[] tasks);
|
Task Import(params ImportTask[] tasks);
|
||||||
|
|
||||||
|
@ -1,14 +1,12 @@
|
|||||||
// 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.Collections.Generic;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using osu.Game.IO.Archives;
|
|
||||||
using osu.Game.Overlays.Notifications;
|
|
||||||
|
|
||||||
#nullable enable
|
#nullable enable
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using osu.Game.Overlays.Notifications;
|
||||||
|
|
||||||
namespace osu.Game.Database
|
namespace osu.Game.Database
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -18,35 +16,14 @@ namespace osu.Game.Database
|
|||||||
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
|
public interface IModelImporter<TModel> : IPostNotifications, IPostImports<TModel>, ICanAcceptFiles
|
||||||
where TModel : class, IHasGuidPrimaryKey
|
where TModel : class, IHasGuidPrimaryKey
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Process multiple import tasks, updating a tracking notification with progress.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="notification">The notification to update.</param>
|
||||||
|
/// <param name="tasks">The import tasks.</param>
|
||||||
|
/// <returns>The imported models.</returns>
|
||||||
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
Task<IEnumerable<Live<TModel>>> Import(ProgressNotification notification, params ImportTask[] tasks);
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Import one <typeparamref name="TModel"/> from the filesystem and delete the file on success.
|
|
||||||
/// Note that this bypasses the UI flow and should only be used for special cases or testing.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="task">The <see cref="ImportTask"/> containing data about the <typeparamref name="TModel"/> to import.</param>
|
|
||||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
|
||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
|
||||||
/// <returns>The imported model, if successful.</returns>
|
|
||||||
Task<Live<TModel>?> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Silently import an item from an <see cref="ArchiveReader"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="archive">The archive to be imported.</param>
|
|
||||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
|
||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
|
||||||
Task<Live<TModel>?> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Silently import an item from a <typeparamref name="TModel"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="item">The model to be imported.</param>
|
|
||||||
/// <param name="archive">An optional archive to use for model population.</param>
|
|
||||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
|
||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
|
||||||
Live<TModel>? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A user displayable name for the model type associated with this manager.
|
/// A user displayable name for the model type associated with this manager.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -34,11 +34,6 @@ namespace osu.Game.Localisation
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static LocalisableString ShowFPS => new TranslatableString(getKey(@"show_fps"), @"Show FPS");
|
public static LocalisableString ShowFPS => new TranslatableString(getKey(@"show_fps"), @"Show FPS");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// "Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. "2x refresh rate" is recommended."
|
|
||||||
/// </summary>
|
|
||||||
public static LocalisableString UnlimitedFramesNote => new TranslatableString(getKey(@"unlimited_frames_note"), @"Using unlimited frame limiter can lead to stutters, bad performance and overheating. It will not improve perceived latency. ""2x refresh rate"" is recommended.");
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// "Layout"
|
/// "Layout"
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -15,8 +15,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
{
|
{
|
||||||
protected override LocalisableString Header => GraphicsSettingsStrings.RendererHeader;
|
protected override LocalisableString Header => GraphicsSettingsStrings.RendererHeader;
|
||||||
|
|
||||||
private SettingsEnumDropdown<FrameSync> frameLimiterDropdown;
|
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
|
private void load(FrameworkConfigManager config, OsuConfigManager osuConfig)
|
||||||
{
|
{
|
||||||
@ -24,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
// TODO: this needs to be a custom dropdown at some point
|
// TODO: this needs to be a custom dropdown at some point
|
||||||
frameLimiterDropdown = new SettingsEnumDropdown<FrameSync>
|
new SettingsEnumDropdown<FrameSync>
|
||||||
{
|
{
|
||||||
LabelText = GraphicsSettingsStrings.FrameLimiter,
|
LabelText = GraphicsSettingsStrings.FrameLimiter,
|
||||||
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
|
Current = config.GetBindable<FrameSync>(FrameworkSetting.FrameSync)
|
||||||
@ -41,24 +39,5 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void LoadComplete()
|
|
||||||
{
|
|
||||||
base.LoadComplete();
|
|
||||||
|
|
||||||
frameLimiterDropdown.Current.BindValueChanged(limit =>
|
|
||||||
{
|
|
||||||
switch (limit.NewValue)
|
|
||||||
{
|
|
||||||
case FrameSync.Unlimited:
|
|
||||||
frameLimiterDropdown.SetNoticeText(GraphicsSettingsStrings.UnlimitedFramesNote, true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
frameLimiterDropdown.ClearNoticeText();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ using osu.Framework.Bindables;
|
|||||||
using osu.Framework.Input.Events;
|
using osu.Framework.Input.Events;
|
||||||
using osu.Game.Rulesets;
|
using osu.Game.Rulesets;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Graphics.Containers;
|
||||||
using osu.Game.Input.Bindings;
|
using osu.Game.Input.Bindings;
|
||||||
|
|
||||||
namespace osu.Game.Overlays.Toolbar
|
namespace osu.Game.Overlays.Toolbar
|
||||||
@ -41,8 +42,6 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
// Toolbar and its components need keyboard input even when hidden.
|
// Toolbar and its components need keyboard input even when hidden.
|
||||||
public override bool PropagateNonPositionalInputSubTree => true;
|
public override bool PropagateNonPositionalInputSubTree => true;
|
||||||
|
|
||||||
protected override bool BlockScrollInput => false;
|
|
||||||
|
|
||||||
public Toolbar()
|
public Toolbar()
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
@ -67,45 +66,115 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
Children = new Drawable[]
|
Children = new Drawable[]
|
||||||
{
|
{
|
||||||
new ToolbarBackground(),
|
new ToolbarBackground(),
|
||||||
new FillFlowContainer
|
new GridContainer
|
||||||
{
|
{
|
||||||
Direction = FillDirection.Horizontal,
|
RelativeSizeAxes = Axes.Both,
|
||||||
RelativeSizeAxes = Axes.Y,
|
ColumnDimensions = new[]
|
||||||
AutoSizeAxes = Axes.X,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
{
|
||||||
new ToolbarSettingsButton(),
|
new Dimension(GridSizeMode.AutoSize),
|
||||||
new ToolbarHomeButton
|
new Dimension(),
|
||||||
|
new Dimension(GridSizeMode.AutoSize)
|
||||||
|
},
|
||||||
|
Content = new[]
|
||||||
|
{
|
||||||
|
new Drawable[]
|
||||||
{
|
{
|
||||||
Action = () => OnHome?.Invoke()
|
new Container
|
||||||
|
{
|
||||||
|
Name = "Left buttons",
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Depth = float.MinValue,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = OsuColour.Gray(0.1f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ToolbarSettingsButton(),
|
||||||
|
new ToolbarHomeButton
|
||||||
|
{
|
||||||
|
Action = () => OnHome?.Invoke()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Name = "Ruleset selector",
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new OsuScrollContainer(Direction.Horizontal)
|
||||||
|
{
|
||||||
|
ScrollbarVisible = false,
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
Masking = false,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
rulesetSelector = new ToolbarRulesetSelector()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = ColourInfo.GradientHorizontal(OsuColour.Gray(0.1f).Opacity(0), OsuColour.Gray(0.1f)),
|
||||||
|
Width = 50,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new Container
|
||||||
|
{
|
||||||
|
Name = "Right buttons",
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Colour = OsuColour.Gray(0.1f),
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new FillFlowContainer
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopRight,
|
||||||
|
Origin = Anchor.TopRight,
|
||||||
|
Direction = FillDirection.Horizontal,
|
||||||
|
RelativeSizeAxes = Axes.Y,
|
||||||
|
AutoSizeAxes = Axes.X,
|
||||||
|
Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ToolbarNewsButton(),
|
||||||
|
new ToolbarChangelogButton(),
|
||||||
|
new ToolbarRankingsButton(),
|
||||||
|
new ToolbarBeatmapListingButton(),
|
||||||
|
new ToolbarChatButton(),
|
||||||
|
new ToolbarSocialButton(),
|
||||||
|
new ToolbarWikiButton(),
|
||||||
|
new ToolbarMusicButton(),
|
||||||
|
//new ToolbarButton
|
||||||
|
//{
|
||||||
|
// Icon = FontAwesome.Solid.search
|
||||||
|
//},
|
||||||
|
userButton = new ToolbarUserButton(),
|
||||||
|
new ToolbarClock(),
|
||||||
|
new ToolbarNotificationButton(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
rulesetSelector = new ToolbarRulesetSelector()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new FillFlowContainer
|
|
||||||
{
|
|
||||||
Anchor = Anchor.TopRight,
|
|
||||||
Origin = Anchor.TopRight,
|
|
||||||
Direction = FillDirection.Horizontal,
|
|
||||||
RelativeSizeAxes = Axes.Y,
|
|
||||||
AutoSizeAxes = Axes.X,
|
|
||||||
Children = new Drawable[]
|
|
||||||
{
|
|
||||||
new ToolbarNewsButton(),
|
|
||||||
new ToolbarChangelogButton(),
|
|
||||||
new ToolbarRankingsButton(),
|
|
||||||
new ToolbarBeatmapListingButton(),
|
|
||||||
new ToolbarChatButton(),
|
|
||||||
new ToolbarSocialButton(),
|
|
||||||
new ToolbarWikiButton(),
|
|
||||||
new ToolbarMusicButton(),
|
|
||||||
//new ToolbarButton
|
|
||||||
//{
|
|
||||||
// Icon = FontAwesome.Solid.search
|
|
||||||
//},
|
|
||||||
userButton = new ToolbarUserButton(),
|
|
||||||
new ToolbarClock(),
|
|
||||||
new ToolbarNotificationButton(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -161,7 +161,7 @@ namespace osu.Game.Overlays.Toolbar
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
protected override bool OnMouseDown(MouseDownEvent e) => false;
|
||||||
|
|
||||||
protected override bool OnClick(ClickEvent e)
|
protected override bool OnClick(ClickEvent e)
|
||||||
{
|
{
|
||||||
|
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
foreach (var skill in skills)
|
foreach (var skill in skills)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
skill.ProcessInternal(hitObject);
|
skill.Process(hitObject);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Difficulty
|
|||||||
foreach (var skill in skills)
|
foreach (var skill in skills)
|
||||||
{
|
{
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
skill.ProcessInternal(hitObject);
|
skill.Process(hitObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
|
attribs.Add(new TimedDifficultyAttributes(hitObject.EndTime * clockRate, CreateDifficultyAttributes(progressiveBeatmap, playableMods, skills, clockRate)));
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
// 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.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Game.Rulesets.Objects;
|
using osu.Game.Rulesets.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
||||||
@ -10,6 +12,13 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class DifficultyHitObject
|
public class DifficultyHitObject
|
||||||
{
|
{
|
||||||
|
private readonly IReadOnlyList<DifficultyHitObject> difficultyHitObjects;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The index of this <see cref="DifficultyHitObject"/> in the list of all <see cref="DifficultyHitObject"/>s.
|
||||||
|
/// </summary>
|
||||||
|
public int Index;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The <see cref="HitObject"/> this <see cref="DifficultyHitObject"/> wraps.
|
/// The <see cref="HitObject"/> this <see cref="DifficultyHitObject"/> wraps.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -41,13 +50,21 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
|
|||||||
/// <param name="hitObject">The <see cref="HitObject"/> which this <see cref="DifficultyHitObject"/> wraps.</param>
|
/// <param name="hitObject">The <see cref="HitObject"/> which this <see cref="DifficultyHitObject"/> wraps.</param>
|
||||||
/// <param name="lastObject">The last <see cref="HitObject"/> which occurs before <paramref name="hitObject"/> in the beatmap.</param>
|
/// <param name="lastObject">The last <see cref="HitObject"/> which occurs before <paramref name="hitObject"/> in the beatmap.</param>
|
||||||
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
/// <param name="clockRate">The rate at which the gameplay clock is run at.</param>
|
||||||
public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate)
|
/// <param name="objects">The list of <see cref="DifficultyHitObject"/>s in the current beatmap.</param>
|
||||||
|
/// <param name="index">The index of this <see cref="DifficultyHitObject"/> in <paramref name="objects"/> list.</param>
|
||||||
|
public DifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate, List<DifficultyHitObject> objects, int index)
|
||||||
{
|
{
|
||||||
|
difficultyHitObjects = objects;
|
||||||
|
Index = index;
|
||||||
BaseObject = hitObject;
|
BaseObject = hitObject;
|
||||||
LastObject = lastObject;
|
LastObject = lastObject;
|
||||||
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
|
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
|
||||||
StartTime = hitObject.StartTime / clockRate;
|
StartTime = hitObject.StartTime / clockRate;
|
||||||
EndTime = hitObject.GetEndTime() / clockRate;
|
EndTime = hitObject.GetEndTime() / clockRate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public DifficultyHitObject Previous(int backwardsIndex) => difficultyHitObjects.ElementAtOrDefault(Index - (backwardsIndex + 1));
|
||||||
|
|
||||||
|
public DifficultyHitObject Next(int forwardsIndex) => difficultyHitObjects.ElementAtOrDefault(Index + (forwardsIndex + 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
using osu.Game.Rulesets.Difficulty.Preprocessing;
|
||||||
using osu.Game.Rulesets.Difficulty.Utils;
|
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Difficulty.Skills
|
namespace osu.Game.Rulesets.Difficulty.Skills
|
||||||
@ -12,21 +11,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// A bare minimal abstract skill for fully custom skill implementations.
|
/// A bare minimal abstract skill for fully custom skill implementations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// This class should be considered a "processing" class and not persisted, as it keeps references to
|
/// This class should be considered a "processing" class and not persisted.
|
||||||
/// gameplay objects after processing is run (see <see cref="Previous"/>).
|
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
public abstract class Skill
|
public abstract class Skill
|
||||||
{
|
{
|
||||||
/// <summary>
|
|
||||||
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
|
|
||||||
/// </summary>
|
|
||||||
protected readonly ReverseQueue<DifficultyHitObject> Previous;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Number of previous <see cref="DifficultyHitObject"/>s to keep inside the <see cref="Previous"/> queue.
|
|
||||||
/// </summary>
|
|
||||||
protected virtual int HistoryLength => 1;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Mods for use in skill calculations.
|
/// Mods for use in skill calculations.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -37,24 +25,13 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
protected Skill(Mod[] mods)
|
protected Skill(Mod[] mods)
|
||||||
{
|
{
|
||||||
this.mods = mods;
|
this.mods = mods;
|
||||||
Previous = new ReverseQueue<DifficultyHitObject>(HistoryLength + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
internal void ProcessInternal(DifficultyHitObject current)
|
|
||||||
{
|
|
||||||
while (Previous.Count > HistoryLength)
|
|
||||||
Previous.Dequeue();
|
|
||||||
|
|
||||||
Process(current);
|
|
||||||
|
|
||||||
Previous.Enqueue(current);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a <see cref="DifficultyHitObject"/>.
|
/// Process a <see cref="DifficultyHitObject"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="current">The <see cref="DifficultyHitObject"/> to process.</param>
|
/// <param name="current">The <see cref="DifficultyHitObject"/> to process.</param>
|
||||||
protected abstract void Process(DifficultyHitObject current);
|
public abstract void Process(DifficultyHitObject current);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
|
||||||
|
@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override double CalculateInitialStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
|
protected override double CalculateInitialStrain(double time, DifficultyHitObject current) => CurrentStrain * strainDecay(time - current.Previous(0).StartTime);
|
||||||
|
|
||||||
protected override double StrainValueAt(DifficultyHitObject current)
|
protected override double StrainValueAt(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
|
@ -44,16 +44,16 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
|
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected sealed override void Process(DifficultyHitObject current)
|
public sealed override void Process(DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
// The first object doesn't generate a strain, so we begin with an incremented section end
|
// The first object doesn't generate a strain, so we begin with an incremented section end
|
||||||
if (Previous.Count == 0)
|
if (current.Index == 0)
|
||||||
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
|
||||||
|
|
||||||
while (current.StartTime > currentSectionEnd)
|
while (current.StartTime > currentSectionEnd)
|
||||||
{
|
{
|
||||||
saveCurrentPeak();
|
saveCurrentPeak();
|
||||||
startNewSectionFrom(currentSectionEnd);
|
startNewSectionFrom(currentSectionEnd, current);
|
||||||
currentSectionEnd += SectionLength;
|
currentSectionEnd += SectionLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,19 +72,21 @@ namespace osu.Game.Rulesets.Difficulty.Skills
|
|||||||
/// Sets the initial strain level for a new section.
|
/// Sets the initial strain level for a new section.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
/// <param name="time">The beginning of the new section in milliseconds.</param>
|
||||||
private void startNewSectionFrom(double time)
|
/// <param name="current">The current hit object.</param>
|
||||||
|
private void startNewSectionFrom(double time, DifficultyHitObject current)
|
||||||
{
|
{
|
||||||
// The maximum strain of the new section is not zero by default
|
// The maximum strain of the new section is not zero by default
|
||||||
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
|
||||||
currentSectionPeak = CalculateInitialStrain(time);
|
currentSectionPeak = CalculateInitialStrain(time, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Retrieves the peak strain at a point in time.
|
/// Retrieves the peak strain at a point in time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="time">The time to retrieve the peak strain at.</param>
|
/// <param name="time">The time to retrieve the peak strain at.</param>
|
||||||
|
/// <param name="current">The current hit object.</param>
|
||||||
/// <returns>The peak strain.</returns>
|
/// <returns>The peak strain.</returns>
|
||||||
protected abstract double CalculateInitialStrain(double time);
|
protected abstract double CalculateInitialStrain(double time, DifficultyHitObject current);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
|
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
|
||||||
|
@ -266,57 +266,23 @@ namespace osu.Game.Scoring
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Delete(List<ScoreInfo> items, bool silent = false)
|
public void Delete(List<ScoreInfo> items, bool silent = false) => scoreModelManager.Delete(items, silent);
|
||||||
{
|
|
||||||
scoreModelManager.Delete(items, silent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Undelete(List<ScoreInfo> items, bool silent = false)
|
public void Undelete(List<ScoreInfo> items, bool silent = false) => scoreModelManager.Undelete(items, silent);
|
||||||
{
|
|
||||||
scoreModelManager.Undelete(items, silent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Undelete(ScoreInfo item)
|
public void Undelete(ScoreInfo item) => scoreModelManager.Undelete(item);
|
||||||
{
|
|
||||||
scoreModelManager.Undelete(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Import(params string[] paths)
|
public Task Import(params string[] paths) => scoreModelManager.Import(paths);
|
||||||
{
|
|
||||||
return scoreModelManager.Import(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Import(params ImportTask[] tasks)
|
public Task Import(params ImportTask[] tasks) => scoreModelManager.Import(tasks);
|
||||||
{
|
|
||||||
return scoreModelManager.Import(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> HandledExtensions => scoreModelManager.HandledExtensions;
|
public IEnumerable<string> HandledExtensions => scoreModelManager.HandledExtensions;
|
||||||
|
|
||||||
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
public Task<IEnumerable<Live<ScoreInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => scoreModelManager.Import(notification, tasks);
|
||||||
{
|
|
||||||
return scoreModelManager.Import(notification, tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<ScoreInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => scoreModelManager.Import(item, archive, cancellationToken);
|
||||||
{
|
|
||||||
return scoreModelManager.Import(task, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<ScoreInfo>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public bool IsAvailableLocally(ScoreInfo model) => scoreModelManager.IsAvailableLocally(model);
|
||||||
{
|
|
||||||
return scoreModelManager.Import(archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Live<ScoreInfo> Import(ScoreInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
|
||||||
{
|
|
||||||
return scoreModelManager.Import(item, archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAvailableLocally(ScoreInfo model)
|
|
||||||
{
|
|
||||||
return scoreModelManager.IsAvailableLocally(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
@ -422,6 +422,8 @@ namespace osu.Game.Screens.Edit
|
|||||||
|
|
||||||
protected override bool OnKeyDown(KeyDownEvent e)
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
{
|
{
|
||||||
|
if (e.ControlPressed || e.AltPressed || e.SuperPressed) return false;
|
||||||
|
|
||||||
switch (e.Key)
|
switch (e.Key)
|
||||||
{
|
{
|
||||||
case Key.Left:
|
case Key.Left:
|
||||||
|
@ -23,6 +23,9 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
private Sample sample;
|
private Sample sample;
|
||||||
|
|
||||||
|
public Action RepeatBegan;
|
||||||
|
public Action RepeatEnded;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// An additive modifier for the frequency of the sample played on next actuation.
|
/// An additive modifier for the frequency of the sample played on next actuation.
|
||||||
/// This can be adjusted during the button's <see cref="Drawable.OnClick"/> event to affect the repeat sample playback of that click.
|
/// This can be adjusted during the button's <see cref="Drawable.OnClick"/> event to affect the repeat sample playback of that click.
|
||||||
@ -44,6 +47,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
|
|
||||||
protected override bool OnMouseDown(MouseDownEvent e)
|
protected override bool OnMouseDown(MouseDownEvent e)
|
||||||
{
|
{
|
||||||
|
RepeatBegan?.Invoke();
|
||||||
beginRepeat();
|
beginRepeat();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -51,6 +55,7 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
protected override void OnMouseUp(MouseUpEvent e)
|
protected override void OnMouseUp(MouseUpEvent e)
|
||||||
{
|
{
|
||||||
adjustDelegate?.Cancel();
|
adjustDelegate?.Cancel();
|
||||||
|
RepeatEnded?.Invoke();
|
||||||
base.OnMouseUp(e);
|
base.OnMouseUp(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,9 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
[Resolved]
|
[Resolved]
|
||||||
private OverlayColourProvider colourProvider { get; set; }
|
private OverlayColourProvider colourProvider { get; set; }
|
||||||
|
|
||||||
|
[Resolved]
|
||||||
|
private EditorBeatmap editorBeatmap { get; set; }
|
||||||
|
|
||||||
public TimingAdjustButton(double adjustAmount)
|
public TimingAdjustButton(double adjustAmount)
|
||||||
{
|
{
|
||||||
this.adjustAmount = adjustAmount;
|
this.adjustAmount = adjustAmount;
|
||||||
@ -72,7 +75,11 @@ namespace osu.Game.Screens.Edit.Timing
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
AddInternal(repeatBehaviour = new RepeatingButtonBehaviour(this));
|
AddInternal(repeatBehaviour = new RepeatingButtonBehaviour(this)
|
||||||
|
{
|
||||||
|
RepeatBegan = () => editorBeatmap.BeginChange(),
|
||||||
|
RepeatEnded = () => editorBeatmap.EndChange()
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
[BackgroundDependencyLoader]
|
[BackgroundDependencyLoader]
|
||||||
|
@ -108,6 +108,20 @@ namespace osu.Game.Screens.Utility
|
|||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case LatencyVisualMode.ScrollingGameplay:
|
||||||
|
visualContent.Children = new Drawable[]
|
||||||
|
{
|
||||||
|
new ScrollingGameplay
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
new LatencyCursorContainer
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ namespace osu.Game.Screens.Utility
|
|||||||
public enum LatencyVisualMode
|
public enum LatencyVisualMode
|
||||||
{
|
{
|
||||||
CircleGameplay,
|
CircleGameplay,
|
||||||
|
ScrollingGameplay,
|
||||||
Simple,
|
Simple,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
201
osu.Game/Screens/Utility/ScrollingGameplay.cs
Normal file
201
osu.Game/Screens/Utility/ScrollingGameplay.cs
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using osu.Framework.Allocation;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Input.Events;
|
||||||
|
using osu.Framework.Input.States;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Screens.Utility.SampleComponents;
|
||||||
|
using osuTK;
|
||||||
|
using osuTK.Input;
|
||||||
|
|
||||||
|
namespace osu.Game.Screens.Utility
|
||||||
|
{
|
||||||
|
public class ScrollingGameplay : LatencySampleComponent
|
||||||
|
{
|
||||||
|
private const float judgement_position = 0.8f;
|
||||||
|
private const float bar_height = 20;
|
||||||
|
|
||||||
|
private int nextLocation;
|
||||||
|
|
||||||
|
private readonly List<HitEvent> hitEvents = new List<HitEvent>();
|
||||||
|
|
||||||
|
private double? lastGeneratedBeatTime;
|
||||||
|
|
||||||
|
private Container circles = null!;
|
||||||
|
|
||||||
|
protected override void LoadComplete()
|
||||||
|
{
|
||||||
|
base.LoadComplete();
|
||||||
|
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
new Box
|
||||||
|
{
|
||||||
|
Name = "judgement bar",
|
||||||
|
Colour = OverlayColourProvider.Content2,
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
RelativePositionAxes = Axes.Y,
|
||||||
|
Y = judgement_position,
|
||||||
|
Height = bar_height,
|
||||||
|
},
|
||||||
|
circles = new Container
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.Both,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
SampleBPM.BindValueChanged(_ =>
|
||||||
|
{
|
||||||
|
circles.Clear();
|
||||||
|
lastGeneratedBeatTime = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateAtLimitedRate(InputState inputState)
|
||||||
|
{
|
||||||
|
double beatLength = 60000 / SampleBPM.Value;
|
||||||
|
|
||||||
|
int nextBeat = (int)(Clock.CurrentTime / beatLength) + 1;
|
||||||
|
|
||||||
|
// We want to generate a few hit objects ahead of the current time (to allow them to animate).
|
||||||
|
double generateUpTo = (nextBeat + 2) * beatLength;
|
||||||
|
|
||||||
|
while (lastGeneratedBeatTime == null || lastGeneratedBeatTime < generateUpTo)
|
||||||
|
{
|
||||||
|
double time = ++nextBeat * beatLength;
|
||||||
|
|
||||||
|
if (time <= lastGeneratedBeatTime)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
newBeat(time);
|
||||||
|
lastGeneratedBeatTime = time;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newBeat(double time)
|
||||||
|
{
|
||||||
|
const float columns = 4;
|
||||||
|
|
||||||
|
float adjustedXPos = ((1f + nextLocation++ % columns) - columns / 2) / columns;
|
||||||
|
|
||||||
|
circles.Add(new SampleNote(time)
|
||||||
|
{
|
||||||
|
RelativePositionAxes = Axes.Both,
|
||||||
|
X = 0.5f + SampleVisualSpacing.Value * (adjustedXPos * 0.5f),
|
||||||
|
Scale = new Vector2(0.4f + (0.8f * SampleVisualSpacing.Value), 1),
|
||||||
|
Hit = hit,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hit(HitEvent h)
|
||||||
|
{
|
||||||
|
hitEvents.Add(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SampleNote : LatencySampleComponent
|
||||||
|
{
|
||||||
|
public HitEvent? HitEvent;
|
||||||
|
|
||||||
|
public Action<HitEvent>? Hit { get; set; }
|
||||||
|
|
||||||
|
public readonly double HitTime;
|
||||||
|
|
||||||
|
private Box box = null!;
|
||||||
|
|
||||||
|
private const float size = 100;
|
||||||
|
private const float duration = 200;
|
||||||
|
|
||||||
|
public SampleNote(double hitTime)
|
||||||
|
{
|
||||||
|
HitTime = hitTime;
|
||||||
|
|
||||||
|
Origin = Anchor.Centre;
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
AlwaysPresent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load()
|
||||||
|
{
|
||||||
|
InternalChildren = new Drawable[]
|
||||||
|
{
|
||||||
|
box = new Box
|
||||||
|
{
|
||||||
|
Colour = OverlayColourProvider.Content1,
|
||||||
|
Size = new Vector2(size, bar_height),
|
||||||
|
Anchor = Anchor.Centre,
|
||||||
|
Origin = Anchor.Centre,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnKeyDown(KeyDownEvent e)
|
||||||
|
{
|
||||||
|
if (!IsActive.Value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Math.Abs(Clock.CurrentTime - HitTime) > duration)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Allow using any key that isn't used by the latency certifier itself.
|
||||||
|
switch (e.Key)
|
||||||
|
{
|
||||||
|
case Key.Space:
|
||||||
|
case Key.Number1:
|
||||||
|
case Key.Number2:
|
||||||
|
case Key.Tab:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
attemptHit();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void UpdateAtLimitedRate(InputState inputState)
|
||||||
|
{
|
||||||
|
if (HitEvent == null)
|
||||||
|
{
|
||||||
|
double preempt = (float)IBeatmapDifficultyInfo.DifficultyRange(SampleApproachRate.Value, 1800, 1200, 450);
|
||||||
|
|
||||||
|
Alpha = (float)MathHelper.Clamp((Clock.CurrentTime - HitTime + 600) / 400, 0, 1);
|
||||||
|
Y = judgement_position - (float)((HitTime - Clock.CurrentTime) / preempt);
|
||||||
|
|
||||||
|
if (Clock.CurrentTime > HitTime + duration)
|
||||||
|
Expire();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void attemptHit() => Schedule(() =>
|
||||||
|
{
|
||||||
|
if (HitEvent != null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// in case it was hit outside of display range, show immediately
|
||||||
|
// so the user isn't confused.
|
||||||
|
this.FadeIn();
|
||||||
|
|
||||||
|
box
|
||||||
|
.FadeOut(duration / 2)
|
||||||
|
.ScaleTo(1.5f, duration / 2);
|
||||||
|
|
||||||
|
HitEvent = new HitEvent(Clock.CurrentTime - HitTime, HitResult.Good, new HitObject
|
||||||
|
{
|
||||||
|
HitWindows = new HitWindows(),
|
||||||
|
}, null, null);
|
||||||
|
|
||||||
|
Hit?.Invoke(HitEvent.Value);
|
||||||
|
|
||||||
|
this.Delay(duration).Expire();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -268,37 +268,21 @@ namespace osu.Game.Skinning
|
|||||||
set => skinModelManager.PostImport = value;
|
set => skinModelManager.PostImport = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Import(params string[] paths)
|
public Task Import(params string[] paths) => skinModelManager.Import(paths);
|
||||||
{
|
|
||||||
return skinModelManager.Import(paths);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Import(params ImportTask[] tasks)
|
public Task Import(params ImportTask[] tasks) => skinModelManager.Import(tasks);
|
||||||
{
|
|
||||||
return skinModelManager.Import(tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public IEnumerable<string> HandledExtensions => skinModelManager.HandledExtensions;
|
public IEnumerable<string> HandledExtensions => skinModelManager.HandledExtensions;
|
||||||
|
|
||||||
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks)
|
public Task<IEnumerable<Live<SkinInfo>>> Import(ProgressNotification notification, params ImportTask[] tasks) => skinModelManager.Import(notification, tasks);
|
||||||
{
|
|
||||||
return skinModelManager.Import(notification, tasks);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<SkinInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Task<Live<SkinInfo>> Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) => skinModelManager.Import(task, lowPriority, cancellationToken);
|
||||||
{
|
|
||||||
return skinModelManager.Import(task, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task<Live<SkinInfo>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Task<Live<SkinInfo>> Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) =>
|
||||||
{
|
skinModelManager.Import(archive, lowPriority, cancellationToken);
|
||||||
return skinModelManager.Import(archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Live<SkinInfo> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public Live<SkinInfo> Import(SkinInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) =>
|
||||||
{
|
skinModelManager.Import(item, archive, cancellationToken);
|
||||||
return skinModelManager.Import(item, archive, lowPriority, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@ -323,46 +307,22 @@ namespace osu.Game.Skinning
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool Delete(SkinInfo item) => skinModelManager.Delete(item);
|
||||||
|
|
||||||
|
public void Delete(List<SkinInfo> items, bool silent = false) => skinModelManager.Delete(items, silent);
|
||||||
|
|
||||||
|
public void Undelete(List<SkinInfo> items, bool silent = false) => skinModelManager.Undelete(items, silent);
|
||||||
|
|
||||||
|
public void Undelete(SkinInfo item) => skinModelManager.Undelete(item);
|
||||||
|
|
||||||
|
public bool IsAvailableLocally(SkinInfo model) => skinModelManager.IsAvailableLocally(model);
|
||||||
|
|
||||||
|
public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents) => skinModelManager.ReplaceFile(model, file, contents);
|
||||||
|
|
||||||
|
public void DeleteFile(SkinInfo model, RealmNamedFileUsage file) => skinModelManager.DeleteFile(model, file);
|
||||||
|
|
||||||
|
public void AddFile(SkinInfo model, Stream contents, string filename) => skinModelManager.AddFile(model, contents, filename);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public bool Delete(SkinInfo item)
|
|
||||||
{
|
|
||||||
return skinModelManager.Delete(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Delete(List<SkinInfo> items, bool silent = false)
|
|
||||||
{
|
|
||||||
skinModelManager.Delete(items, silent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Undelete(List<SkinInfo> items, bool silent = false)
|
|
||||||
{
|
|
||||||
skinModelManager.Undelete(items, silent);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Undelete(SkinInfo item)
|
|
||||||
{
|
|
||||||
skinModelManager.Undelete(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsAvailableLocally(SkinInfo model)
|
|
||||||
{
|
|
||||||
return skinModelManager.IsAvailableLocally(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void ReplaceFile(SkinInfo model, RealmNamedFileUsage file, Stream contents)
|
|
||||||
{
|
|
||||||
skinModelManager.ReplaceFile(model, file, contents);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteFile(SkinInfo model, RealmNamedFileUsage file)
|
|
||||||
{
|
|
||||||
skinModelManager.DeleteFile(model, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void AddFile(SkinInfo model, Stream contents, string filename)
|
|
||||||
{
|
|
||||||
skinModelManager.AddFile(model, contents, filename);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,9 +39,6 @@ namespace osu.Game.Stores
|
|||||||
|
|
||||||
protected override string[] HashableFileTypes => new[] { ".osu" };
|
protected override string[] HashableFileTypes => new[] { ".osu" };
|
||||||
|
|
||||||
// protected override bool CheckLocalAvailability(RealmBeatmapSet model, System.Linq.IQueryable<RealmBeatmapSet> items)
|
|
||||||
// => base.CheckLocalAvailability(model, items) || (model.OnlineID > -1));
|
|
||||||
|
|
||||||
private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
|
private readonly BeatmapOnlineLookupQueue? onlineLookupQueue;
|
||||||
|
|
||||||
protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
protected BeatmapImporter(RealmAccess realm, Storage storage, BeatmapOnlineLookupQueue? onlineLookupQueue = null)
|
||||||
|
@ -78,22 +78,7 @@ namespace osu.Game.Stores
|
|||||||
Files = new RealmFileStore(realm, storage);
|
Files = new RealmFileStore(realm, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
public Task Import(params string[] paths) => Import(paths.Select(p => new ImportTask(p)).ToArray());
|
||||||
/// Import one or more <typeparamref name="TModel"/> items from filesystem <paramref name="paths"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This will be treated as a low priority import if more than one path is specified; use <see cref="Import(ImportTask[])"/> to always import at standard priority.
|
|
||||||
/// This will post notifications tracking progress.
|
|
||||||
/// </remarks>
|
|
||||||
/// <param name="paths">One or more archive locations on disk.</param>
|
|
||||||
public Task Import(params string[] paths)
|
|
||||||
{
|
|
||||||
var notification = new ProgressNotification { State = ProgressNotificationState.Active };
|
|
||||||
|
|
||||||
PostNotification?.Invoke(notification);
|
|
||||||
|
|
||||||
return Import(notification, paths.Select(p => new ImportTask(p)).ToArray());
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task Import(params ImportTask[] tasks)
|
public Task Import(params ImportTask[] tasks)
|
||||||
{
|
{
|
||||||
@ -250,7 +235,7 @@ namespace osu.Game.Stores
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, lowPriority, cancellationToken),
|
var scheduledImport = Task.Factory.StartNew(() => Import(model, archive, cancellationToken),
|
||||||
cancellationToken,
|
cancellationToken,
|
||||||
TaskCreationOptions.HideScheduler,
|
TaskCreationOptions.HideScheduler,
|
||||||
lowPriority ? import_scheduler_low_priority : import_scheduler);
|
lowPriority ? import_scheduler_low_priority : import_scheduler);
|
||||||
@ -258,69 +243,13 @@ namespace osu.Game.Stores
|
|||||||
return await scheduledImport.ConfigureAwait(false);
|
return await scheduledImport.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Any file extensions which should be included in hash creation.
|
|
||||||
/// Generally should include all file types which determine the file's uniqueness.
|
|
||||||
/// Large files should be avoided if possible.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
|
|
||||||
/// </remarks>
|
|
||||||
protected abstract string[] HashableFileTypes { get; }
|
|
||||||
|
|
||||||
internal static void LogForModel(TModel? model, string message, Exception? e = null)
|
|
||||||
{
|
|
||||||
string trimmedHash;
|
|
||||||
if (model == null || !model.IsValid || string.IsNullOrEmpty(model.Hash))
|
|
||||||
trimmedHash = "?????";
|
|
||||||
else
|
|
||||||
trimmedHash = model.Hash.Substring(0, 5);
|
|
||||||
|
|
||||||
string prefix = $"[{trimmedHash}]";
|
|
||||||
|
|
||||||
if (e != null)
|
|
||||||
Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database);
|
|
||||||
else
|
|
||||||
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
|
|
||||||
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
|
|
||||||
/// </summary>
|
|
||||||
protected virtual bool HasCustomHashFunction => false;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
|
||||||
/// </summary>
|
|
||||||
/// <remarks>
|
|
||||||
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
|
||||||
/// </remarks>
|
|
||||||
protected virtual string ComputeHash(TModel item)
|
|
||||||
{
|
|
||||||
// for now, concatenate all hashable files in the set to create a unique hash.
|
|
||||||
MemoryStream hashable = new MemoryStream();
|
|
||||||
|
|
||||||
foreach (RealmNamedFileUsage file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
|
|
||||||
{
|
|
||||||
using (Stream s = Files.Store.GetStream(file.File.GetStoragePath()))
|
|
||||||
s.CopyTo(hashable);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hashable.Length > 0)
|
|
||||||
return hashable.ComputeSHA2Hash();
|
|
||||||
|
|
||||||
return item.Hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Silently import an item from a <typeparamref name="TModel"/>.
|
/// Silently import an item from a <typeparamref name="TModel"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="item">The model to be imported.</param>
|
/// <param name="item">The model to be imported.</param>
|
||||||
/// <param name="archive">An optional archive to use for model population.</param>
|
/// <param name="archive">An optional archive to use for model population.</param>
|
||||||
/// <param name="lowPriority">Whether this is a low priority import.</param>
|
|
||||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||||
public virtual Live<TModel>? Import(TModel item, ArchiveReader? archive = null, bool lowPriority = false, CancellationToken cancellationToken = default)
|
public virtual Live<TModel>? Import(TModel item, ArchiveReader? archive = null, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
return Realm.Run(realm =>
|
return Realm.Run(realm =>
|
||||||
{
|
{
|
||||||
@ -420,6 +349,61 @@ namespace osu.Game.Stores
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Any file extensions which should be included in hash creation.
|
||||||
|
/// Generally should include all file types which determine the file's uniqueness.
|
||||||
|
/// Large files should be avoided if possible.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This is only used by the default hash implementation. If <see cref="ComputeHash"/> is overridden, it will not be used.
|
||||||
|
/// </remarks>
|
||||||
|
protected abstract string[] HashableFileTypes { get; }
|
||||||
|
|
||||||
|
internal static void LogForModel(TModel? model, string message, Exception? e = null)
|
||||||
|
{
|
||||||
|
string trimmedHash;
|
||||||
|
if (model == null || !model.IsValid || string.IsNullOrEmpty(model.Hash))
|
||||||
|
trimmedHash = "?????";
|
||||||
|
else
|
||||||
|
trimmedHash = model.Hash.Substring(0, 5);
|
||||||
|
|
||||||
|
string prefix = $"[{trimmedHash}]";
|
||||||
|
|
||||||
|
if (e != null)
|
||||||
|
Logger.Error(e, $"{prefix} {message}", LoggingTarget.Database);
|
||||||
|
else
|
||||||
|
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
|
||||||
|
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
|
||||||
|
/// </summary>
|
||||||
|
protected virtual bool HasCustomHashFunction => false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// In the case of no matching files, a hash will be generated from the passed archive's <see cref="ArchiveReader.Name"/>.
|
||||||
|
/// </remarks>
|
||||||
|
protected virtual string ComputeHash(TModel item)
|
||||||
|
{
|
||||||
|
// for now, concatenate all hashable files in the set to create a unique hash.
|
||||||
|
MemoryStream hashable = new MemoryStream();
|
||||||
|
|
||||||
|
foreach (RealmNamedFileUsage file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
|
||||||
|
{
|
||||||
|
using (Stream s = Files.Store.GetStream(file.File.GetStoragePath()))
|
||||||
|
s.CopyTo(hashable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashable.Length > 0)
|
||||||
|
return hashable.ComputeSHA2Hash();
|
||||||
|
|
||||||
|
return item.Hash;
|
||||||
|
}
|
||||||
|
|
||||||
private string computeHashFast(ArchiveReader reader)
|
private string computeHashFast(ArchiveReader reader)
|
||||||
{
|
{
|
||||||
MemoryStream hashable = new MemoryStream();
|
MemoryStream hashable = new MemoryStream();
|
||||||
@ -521,8 +505,7 @@ namespace osu.Game.Stores
|
|||||||
// for the best or worst, we copy and import files of a new import before checking whether
|
// for the best or worst, we copy and import files of a new import before checking whether
|
||||||
// it is a duplicate. so to check if anything has changed, we can just compare all File IDs.
|
// it is a duplicate. so to check if anything has changed, we can just compare all File IDs.
|
||||||
getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) &&
|
getIDs(existing.Files).SequenceEqual(getIDs(import.Files)) &&
|
||||||
getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files)) &&
|
getFilenames(existing.Files).SequenceEqual(getFilenames(import.Files));
|
||||||
checkAllFilesExist(existing);
|
|
||||||
|
|
||||||
private bool checkAllFilesExist(TModel model) =>
|
private bool checkAllFilesExist(TModel model) =>
|
||||||
model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath()));
|
model.Files.All(f => Files.Storage.Exists(f.File.GetStoragePath()));
|
||||||
|
Loading…
Reference in New Issue
Block a user