1
0
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:
Salman Ahmed 2022-06-14 22:47:11 +03:00 committed by GitHub
commit 36599d1174
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 954 additions and 702 deletions

View File

@ -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)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; 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;

View File

@ -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.

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System.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)
{ {
} }
} }

View File

@ -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);

View 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);
}
}

View File

@ -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;
}
}
}

View 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)
}
}
}

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; 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;

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -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;
} }

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; 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>

View File

@ -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;
} }

View File

@ -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
) )
); );
} }

View File

@ -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));
} }
} }
} }

View File

@ -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>;

View File

@ -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()
{ {

View File

@ -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;

View File

@ -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);

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
} }
} }

View File

@ -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(),
} }
} }
}; };

View File

@ -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)
{ {

View File

@ -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)));

View File

@ -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));
} }
} }

View File

@ -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.

View File

@ -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)
{ {

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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);
} }

View File

@ -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]

View File

@ -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();
} }

View File

@ -7,6 +7,7 @@ namespace osu.Game.Screens.Utility
public enum LatencyVisualMode public enum LatencyVisualMode
{ {
CircleGameplay, CircleGameplay,
ScrollingGameplay,
Simple, Simple,
} }
} }

View 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();
});
}
}
}

View File

@ -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);
}
} }
} }

View File

@ -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)

View File

@ -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()));