Merge branch 'master' into circular-arc-freeze

This commit is contained in:
Dean Herbert
2021-04-08 14:33:03 +09:00
committed by GitHub
217 changed files with 10785 additions and 755 deletions

View File

@ -16,11 +16,6 @@ namespace osu.Game.Rulesets.Difficulty
{
public abstract class DifficultyCalculator
{
/// <summary>
/// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
private readonly Ruleset ruleset;
private readonly WorkingBeatmap beatmap;
@ -71,32 +66,14 @@ namespace osu.Game.Rulesets.Difficulty
var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList();
double sectionLength = SectionLength * clockRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
foreach (DifficultyHitObject h in difficultyHitObjects)
foreach (var hitObject in difficultyHitObjects)
{
while (h.BaseObject.StartTime > currentSectionEnd)
foreach (var skill in skills)
{
foreach (Skill s in skills)
{
s.SaveCurrentPeak();
s.StartNewSectionFrom(currentSectionEnd);
}
currentSectionEnd += sectionLength;
skill.ProcessInternal(hitObject);
}
foreach (Skill s in skills)
s.Process(h);
}
// The peak strain will not be saved for the last section in the above loop
foreach (Skill s in skills)
s.SaveCurrentPeak();
return CreateDifficultyAttributes(beatmap, mods, skills, clockRate);
}

View File

@ -21,10 +21,20 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
public readonly HitObject LastObject;
/// <summary>
/// Amount of time elapsed between <see cref="BaseObject"/> and <see cref="LastObject"/>.
/// Amount of time elapsed between <see cref="BaseObject"/> and <see cref="LastObject"/>, adjusted by clockrate.
/// </summary>
public readonly double DeltaTime;
/// <summary>
/// Clockrate adjusted start time of <see cref="BaseObject"/>.
/// </summary>
public readonly double StartTime;
/// <summary>
/// Clockrate adjusted end time of <see cref="BaseObject"/>.
/// </summary>
public readonly double EndTime;
/// <summary>
/// Creates a new <see cref="DifficultyHitObject"/>.
/// </summary>
@ -36,6 +46,8 @@ namespace osu.Game.Rulesets.Difficulty.Preprocessing
BaseObject = hitObject;
LastObject = lastObject;
DeltaTime = (hitObject.StartTime - lastObject.StartTime) / clockRate;
StartTime = hitObject.StartTime / clockRate;
EndTime = hitObject.GetEndTime() / clockRate;
}
}
}

View File

@ -1,9 +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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Utils;
using osu.Game.Rulesets.Mods;
@ -11,51 +9,20 @@ using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// A bare minimal abstract skill for fully custom skill implementations.
/// </summary>
public abstract class Skill
{
/// <summary>
/// The peak strain for each <see cref="DifficultyCalculator.SectionLength"/> section of the beatmap.
/// </summary>
public IReadOnlyList<double> StrainPeaks => strainPeaks;
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// The weight by which each strain value decays.
/// </summary>
protected virtual double DecayWeight => 0.9;
/// <summary>
/// <see cref="DifficultyHitObject"/>s that were processed previously. They can affect the strain values of the following objects.
/// </summary>
protected readonly LimitedCapacityStack<DifficultyHitObject> Previous = new LimitedCapacityStack<DifficultyHitObject>(2); // Contained objects not used yet
/// <summary>
/// The current strain level.
/// </summary>
protected double CurrentStrain { get; private set; } = 1;
/// <summary>
/// Mods for use in skill calculations.
/// </summary>
protected IReadOnlyList<Mod> Mods => mods;
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private readonly List<double> strainPeaks = new List<double>();
private readonly Mod[] mods;
protected Skill(Mod[] mods)
@ -63,71 +30,21 @@ namespace osu.Game.Rulesets.Difficulty.Skills
this.mods = mods;
}
/// <summary>
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
public void Process(DifficultyHitObject current)
internal void ProcessInternal(DifficultyHitObject current)
{
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
Process(current);
Previous.Push(current);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// Process a <see cref="DifficultyHitObject"/>.
/// </summary>
public void SaveCurrentPeak()
{
if (Previous.Count > 0)
strainPeaks.Add(currentSectionPeak);
}
/// <param name="current">The <see cref="DifficultyHitObject"/> to process.</param>
protected abstract void Process(DifficultyHitObject current);
/// <summary>
/// Sets the initial strain level for a new section.
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
/// </summary>
/// <param name="time">The beginning of the new section in milliseconds.</param>
public void StartNewSectionFrom(double time)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level.
if (Previous.Count > 0)
currentSectionPeak = GetPeakStrain(time);
}
/// <summary>
/// Retrieves the peak strain at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the peak strain at.</param>
/// <returns>The peak strain.</returns>
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime);
/// <summary>
/// Returns the calculated difficulty value representing all processed <see cref="DifficultyHitObject"/>s.
/// </summary>
public double DifficultyValue()
{
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in strainPeaks.OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(DifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
public abstract double DifficultyValue();
}
}

View File

@ -0,0 +1,135 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty.Skills
{
/// <summary>
/// Used to processes strain values of <see cref="DifficultyHitObject"/>s, keep track of strain levels caused by the processed objects
/// and to calculate a final difficulty value representing the difficulty of hitting all the processed objects.
/// </summary>
public abstract class StrainSkill : Skill
{
/// <summary>
/// Strain values are multiplied by this number for the given skill. Used to balance the value of different skills between each other.
/// </summary>
protected abstract double SkillMultiplier { get; }
/// <summary>
/// Determines how quickly strain decays for the given skill.
/// For example a value of 0.15 indicates that strain decays to 15% of its original value in one second.
/// </summary>
protected abstract double StrainDecayBase { get; }
/// <summary>
/// The weight by which each strain value decays.
/// </summary>
protected virtual double DecayWeight => 0.9;
/// <summary>
/// The current strain level.
/// </summary>
protected double CurrentStrain { get; private set; } = 1;
/// <summary>
/// The length of each strain section.
/// </summary>
protected virtual int SectionLength => 400;
private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section.
private double currentSectionEnd;
private readonly List<double> strainPeaks = new List<double>();
protected StrainSkill(Mod[] mods)
: base(mods)
{
}
/// <summary>
/// Process a <see cref="DifficultyHitObject"/> and update current strain values accordingly.
/// </summary>
protected sealed override void Process(DifficultyHitObject current)
{
// The first object doesn't generate a strain, so we begin with an incremented section end
if (Previous.Count == 0)
currentSectionEnd = Math.Ceiling(current.StartTime / SectionLength) * SectionLength;
while (current.StartTime > currentSectionEnd)
{
saveCurrentPeak();
startNewSectionFrom(currentSectionEnd);
currentSectionEnd += SectionLength;
}
CurrentStrain *= strainDecay(current.DeltaTime);
CurrentStrain += StrainValueOf(current) * SkillMultiplier;
currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak);
}
/// <summary>
/// Saves the current peak strain level to the list of strain peaks, which will be used to calculate an overall difficulty.
/// </summary>
private void saveCurrentPeak()
{
strainPeaks.Add(currentSectionPeak);
}
/// <summary>
/// Sets the initial strain level for a new section.
/// </summary>
/// <param name="time">The beginning of the new section in milliseconds.</param>
private void startNewSectionFrom(double time)
{
// The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries.
// 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 = GetPeakStrain(time);
}
/// <summary>
/// Retrieves the peak strain at a point in time.
/// </summary>
/// <param name="time">The time to retrieve the peak strain at.</param>
/// <returns>The peak strain.</returns>
protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].StartTime);
/// <summary>
/// Returns a live enumerable of the peak strains for each <see cref="SectionLength"/> section of the beatmap,
/// including the peak of the current section.
/// </summary>
public IEnumerable<double> GetCurrentStrainPeaks() => strainPeaks.Append(currentSectionPeak);
/// <summary>
/// Returns the calculated difficulty value representing all <see cref="DifficultyHitObject"/>s that have been processed up to this point.
/// </summary>
public sealed override double DifficultyValue()
{
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
// We're sorting from highest to lowest strain.
foreach (double strain in GetCurrentStrainPeaks().OrderByDescending(d => d))
{
difficulty += strain * weight;
weight *= DecayWeight;
}
return difficulty;
}
/// <summary>
/// Calculates the strain value of a <see cref="DifficultyHitObject"/>. This value is affected by previously processed objects.
/// </summary>
protected abstract double StrainValueOf(DifficultyHitObject current);
private double strainDecay(double ms) => Math.Pow(StrainDecayBase, ms / 1000);
}
}

View File

@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Judgements
return 300;
case HitResult.Perfect:
return 350;
return 315;
case HitResult.SmallBonus:
return SMALL_BONUS_SCORE;

View File

@ -336,9 +336,14 @@ namespace osu.Game.Rulesets.Objects.Legacy
while (++endIndex < vertices.Length - endPointLength)
{
// Keep incrementing while an implicit segment doesn't need to be started
if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value)
continue;
// The last control point of each segment is not allowed to start a new implicit segment.
if (endIndex == vertices.Length - endPointLength - 1)
continue;
// Force a type on the last point, and return the current control point set as a segment.
vertices[endIndex - 1].Type.Value = type;
yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex);

View File

@ -173,7 +173,7 @@ namespace osu.Game.Rulesets
{
var filename = Path.GetFileNameWithoutExtension(file);
if (loadedAssemblies.Values.Any(t => t.Namespace == filename))
if (loadedAssemblies.Values.Any(t => Path.GetFileNameWithoutExtension(t.Assembly.Location) == filename))
return;
try