Merge branch 'master' into timeline-markers

This commit is contained in:
Dean Herbert 2018-06-18 02:09:20 +09:00 committed by GitHub
commit 48613ea1f6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
56 changed files with 801 additions and 893 deletions

View File

@ -1,36 +0,0 @@
# Linux
### 1. Requirements:
Mono >= 5.4.0 (>= 5.8.0 recommended)
Please check [here](http://www.mono-project.com/download/) for stable or [here](http://www.mono-project.com/download/alpha/) for an alpha release.
NuGet >= 4.4.0
msbuild
git
### 2. Cloning project
Clone the entire repository with submodules using
```
git clone https://github.com/ppy/osu --recursive
```
Then restore NuGet packages from the repository
```
nuget restore
```
### 3. Compiling
Simply run `msbuild` where `osu.sln` is located, this will create all binaries in `osu/osu.Desktop/bin/Debug`.
### 4. Optimizing
If you want additional performance you can change build type to Release with
```
msbuild -p:Configuration=Release
```
Additionally, mono provides an AOT utility which attempts to precompile binaries. You can utilize that by running
```
mono --aot ./osu\!.exe
```
### 5. Troubleshooting
You may run into trouble with NuGet versioning, as the one in packaging system is almost always out of date. Simply run
```
nuget
sudo nuget update -self
```
**Warning** NuGet creates few config files when it's run for the first time.
Do not run NuGet as root on the first run or you might run into very peculiar issues.

View File

@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
namespace osu.Game.Rulesets.Catch.Tests
{
[TestFixture]
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
{
public TestCasePerformancePoints()
: base(new CatchRuleset())
{
}
}
}

View File

@ -112,7 +112,7 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_fruits_o };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new CatchDifficultyCalculator(beatmap);
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override int? LegacyID => 2;

View File

@ -1,18 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Catch.Difficulty
{
public class CatchDifficultyCalculator : DifficultyCalculator
{
public CatchDifficultyCalculator(IBeatmap beatmap) : base(beatmap)
public CatchDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => 0;
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => new DifficultyAttributes(mods, 0);
}
}

View File

@ -42,6 +42,8 @@ public abstract class DrawableCatchHitObject : DrawableHitObject<CatchHitObject>
{
public virtual bool CanBePlated => false;
public virtual bool StaysOnPlate => CanBePlated;
protected DrawableCatchHitObject(CatchHitObject hitObject)
: base(hitObject)
{

View File

@ -13,6 +13,8 @@ public class DrawableDroplet : PalpableCatchHitObject<Droplet>
{
private Pulp pulp;
public override bool StaysOnPlate => false;
public DrawableDroplet(Droplet h)
: base(h)
{

View File

@ -124,6 +124,9 @@ private void createTicks()
X = X + Curve.PositionAt(reversed ? 0 : 1).X / CatchPlayfield.BASE_WIDTH
});
}
if (NestedHitObjects.LastOrDefault() is IHasComboInformation lastNested)
lastNested.LastInCombo = LastInCombo;
}
public double EndTime => StartTime + this.SpanCount() * Curve.Distance / Velocity;

View File

@ -48,6 +48,16 @@ public CatcherArea(BeatmapDifficulty difficulty = null)
public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement)
{
void runAfterLoaded(Action action)
{
// this is required to make this run after the last caught fruit runs UpdateState at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
action();
else
lastPlateableFruit.OnLoadComplete = _ => action();
}
if (judgement.IsHit && fruit.CanBePlated)
{
var caughtFruit = (DrawableCatchHitObject)GetVisualRepresentation?.Invoke(fruit.HitObject);
@ -63,21 +73,17 @@ public void OnJudgement(DrawableCatchHitObject fruit, Judgement judgement)
caughtFruit.LifetimeEnd = double.MaxValue;
MovableCatcher.Add(caughtFruit);
lastPlateableFruit = caughtFruit;
if (!fruit.StaysOnPlate)
runAfterLoaded(() => MovableCatcher.Explode(caughtFruit));
}
if (fruit.HitObject.LastInCombo)
{
if (judgement.IsHit)
{
// this is required to make this run after the last caught fruit runs UpdateState at least once.
// TODO: find a better alternative
if (lastPlateableFruit.IsLoaded)
MovableCatcher.Explode();
else
lastPlateableFruit.OnLoadComplete = _ => { MovableCatcher.Explode(); };
}
runAfterLoaded(() => MovableCatcher.Explode());
else
MovableCatcher.Drop();
}
@ -378,28 +384,31 @@ public void Explode()
var fruit = caughtFruit.ToArray();
foreach (var f in fruit)
Explode(f);
}
public void Explode(DrawableHitObject fruit)
{
var originalX = fruit.X * Scale.X;
if (ExplodingFruitTarget != null)
{
var originalX = f.X * Scale.X;
fruit.Anchor = Anchor.TopLeft;
fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
if (ExplodingFruitTarget != null)
{
f.Anchor = Anchor.TopLeft;
f.Position = caughtFruit.ToSpaceOfOtherDrawable(f.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(fruit);
caughtFruit.Remove(f);
ExplodingFruitTarget.Add(f);
}
f.MoveToY(f.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(f.Y + 50, 500, Easing.InSine);
f.MoveToX(f.X + originalX * 6, 1000);
f.FadeOut(750);
f.Expire();
ExplodingFruitTarget.Add(fruit);
}
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToX(fruit.X + originalX * 6, 1000);
fruit.FadeOut(750);
fruit.Expire();
}
private class CatcherSprite : Sprite

View File

@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
namespace osu.Game.Rulesets.Mania.Tests
{
[TestFixture]
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
{
public TestCasePerformancePoints()
: base(new ManiaRuleset())
{
}
}
}

View File

@ -58,6 +58,13 @@ public DistanceObjectPatternGenerator(FastRandom random, HitObject hitObject, Ma
public override Pattern Generate()
{
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0, HitObject.StartTime, endTime);
return pattern;
}
if (spanCount > 1)
{
if (segmentDuration <= 90)

View File

@ -77,10 +77,25 @@ public HitObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBe
}
else
convertType |= PatternType.LowProbability;
if ((convertType & PatternType.KeepSingle) == 0)
{
if (HitObject.Samples.Any(s => s.Name == SampleInfo.HIT_FINISH) && TotalColumns != 8)
convertType |= PatternType.Mirror;
else
convertType |= PatternType.Gathered;
}
}
public override Pattern Generate()
{
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0);
return pattern;
}
int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0;
if ((convertType & PatternType.Reverse) > 0 && PreviousPattern.HitObjects.Any())
@ -346,7 +361,7 @@ private int getRandomNoteCountMirrored(double centreProbability, double p2, doub
addToCentre = false;
if ((convertType & PatternType.ForceNotStack) > 0)
return getRandomNoteCount(p2 / 2, p2, (p2 + p3) / 2, p3);
return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
switch (TotalColumns)
{

View File

@ -29,47 +29,36 @@ internal class ManiaDifficultyCalculator : DifficultyCalculator
/// </summary>
private const double decay_weight = 0.9;
/// <summary>
/// HitObjects are stored as a member variable.
/// </summary>
private readonly List<ManiaHitObjectDifficulty> difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
private readonly bool isForCurrentRuleset;
public ManiaDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
}
public ManiaDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
}
var difficultyHitObjects = new List<ManiaHitObjectDifficulty>();
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
// Fill our custom DifficultyHitObject class, that carries additional information
difficultyHitObjects.Clear();
int columnCount = (Beatmap as ManiaBeatmap)?.TotalColumns ?? 7;
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
// Note: Stable sort is done so that the ordering of hitobjects with equal start times doesn't change
difficultyHitObjects.AddRange(Beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
difficultyHitObjects.AddRange(beatmap.HitObjects.Select(h => new ManiaHitObjectDifficulty((ManiaHitObject)h, columnCount)).OrderBy(h => h.BaseHitObject.StartTime));
if (!calculateStrainValues())
return 0;
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new DifficultyAttributes(mods, 0);
double starRating = calculateDifficulty() * star_scaling_factor;
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
if (categoryDifficulty != null)
categoryDifficulty["Strain"] = starRating;
return starRating;
return new DifficultyAttributes(mods, starRating);
}
private bool calculateStrainValues()
private bool calculateStrainValues(List<ManiaHitObjectDifficulty> objects, double timeRate)
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
using (List<ManiaHitObjectDifficulty>.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator())
using (var hitObjectsEnumerator = objects.GetEnumerator())
{
if (!hitObjectsEnumerator.MoveNext())
return false;
@ -80,7 +69,7 @@ private bool calculateStrainValues()
while (hitObjectsEnumerator.MoveNext())
{
var next = hitObjectsEnumerator.Current;
next?.CalculateStrains(current, TimeRate);
next?.CalculateStrains(current, timeRate);
current = next;
}
@ -88,9 +77,9 @@ private bool calculateStrainValues()
}
}
private double calculateDifficulty()
private double calculateDifficulty(List<ManiaHitObjectDifficulty> objects, double timeRate)
{
double actualStrainStep = strain_step * TimeRate;
double actualStrainStep = strain_step * timeRate;
// Find the highest strain value within each strain step
List<double> highestStrains = new List<double>();
@ -98,7 +87,7 @@ private double calculateDifficulty()
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
ManiaHitObjectDifficulty previousHitObject = null;
foreach (var hitObject in difficultyHitObjects)
foreach (var hitObject in objects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
@ -143,21 +132,35 @@ private double calculateDifficulty()
return difficulty;
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
protected override Mod[] DifficultyAdjustmentMods
{
new ManiaModDoubleTime(),
new ManiaModHalfTime(),
new ManiaModEasy(),
new ManiaModHardRock(),
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
new ManiaModKey6(),
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
};
get
{
var mods = new Mod[]
{
new ManiaModDoubleTime(),
new ManiaModHalfTime(),
new ManiaModEasy(),
new ManiaModHardRock(),
};
if (isForCurrentRuleset)
return mods;
// if we are a convert, we can be played in any key mod.
return mods.Concat(new Mod[]
{
new ManiaModKey1(),
new ManiaModKey2(),
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
new ManiaModKey6(),
new ManiaModKey7(),
new ManiaModKey8(),
new ManiaModKey9(),
}).ToArray();
}
}
}
}

View File

@ -25,7 +25,7 @@ public class ManiaPerformanceCalculator : PerformanceCalculator
private int countMeh;
private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
}
@ -82,7 +82,7 @@ public override double Calculate(Dictionary<string, double> categoryDifficulty =
private double computeStrainValue()
{
// Obtain strain difficulty
double strainValue = Math.Pow(5 * Math.Max(1, Attributes["Strain"] / 0.2) - 4.0, 2.2) / 135.0;
double strainValue = Math.Pow(5 * Math.Max(1, Attributes.StarRating / 0.2) - 4.0, 2.2) / 135.0;
// Longer maps are worth more
strainValue *= 1.0 + 0.1 * Math.Min(1.0, totalHits / 1500.0);

View File

@ -29,7 +29,7 @@ public class ManiaRuleset : Ruleset
{
public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatmap) => new ManiaRulesetContainer(this, beatmap);
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override IEnumerable<Mod> ConvertLegacyMods(LegacyMods mods)
{
@ -147,7 +147,7 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_mania_o };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new ManiaDifficultyCalculator(beatmap, mods);
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(this, beatmap);
public override int? LegacyID => 3;

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Mania.UI
{
internal class HitExplosion : CompositeDrawable
{
public override bool RemoveWhenNotAlive => true;
private readonly CircularContainer circle;
public HitExplosion(DrawableHitObject judgedObject)

View File

@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
{
public TestCasePerformancePoints()
: base(new OsuRuleset())
{
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuDifficultyAttributes : DifficultyAttributes
{
public double AimStrain;
public double SpeedStrain;
public OsuDifficultyAttributes(Mod[] mods, double starRating)
: base(mods, starRating)
{
}
}
}

View File

@ -2,7 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -18,31 +18,26 @@ public class OsuDifficultyCalculator : DifficultyCalculator
private const int section_length = 400;
private const double difficulty_multiplier = 0.0675;
public OsuDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
public OsuDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
public OsuDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap((List<OsuHitObject>)Beatmap.HitObjects, TimeRate);
OsuDifficultyBeatmap difficultyBeatmap = new OsuDifficultyBeatmap(beatmap.HitObjects.Cast<OsuHitObject>().ToList(), timeRate);
Skill[] skills =
{
new Aim(),
new Speed()
};
double sectionLength = section_length * TimeRate;
double sectionLength = section_length * timeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;
foreach (OsuDifficultyHitObject h in beatmap)
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
{
while (h.BaseObject.StartTime > currentSectionEnd)
{
@ -61,16 +56,13 @@ public override double Calculate(Dictionary<string, double> categoryDifficulty =
double aimRating = Math.Sqrt(skills[0].DifficultyValue()) * difficulty_multiplier;
double speedRating = Math.Sqrt(skills[1].DifficultyValue()) * difficulty_multiplier;
double starRating = aimRating + speedRating + Math.Abs(aimRating - speedRating) / 2;
if (categoryDifficulty != null)
return new OsuDifficultyAttributes(mods, starRating)
{
categoryDifficulty.Add("Aim", aimRating);
categoryDifficulty.Add("Speed", speedRating);
}
return starRating;
AimStrain = aimRating,
SpeedStrain = speedRating
};
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]

View File

@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public class OsuPerformanceCalculator : PerformanceCalculator
{
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
private readonly int countHitCircles;
private readonly int beatmapMaxCombo;
@ -37,7 +39,7 @@ public class OsuPerformanceCalculator : PerformanceCalculator
private int countMeh;
private int countMiss;
public OsuPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
@ -102,7 +104,7 @@ public override double Calculate(Dictionary<string, double> categoryRatings = nu
private double computeAimValue()
{
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Aim"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.AimStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
// Longer maps are worth more
double lengthBonus = 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +
@ -151,7 +153,7 @@ private double computeAimValue()
private double computeSpeedValue()
{
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Speed"] / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes.SpeedStrain / 0.0675f) - 4.0f, 3.0f) / 100000.0f;
// Longer maps are worth more
speedValue *= 0.95f + 0.4f * Math.Min(1.0f, totalHits / 2000.0f) +

View File

@ -120,9 +120,9 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_osu_o };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);

View File

@ -161,7 +161,7 @@ private void load(OsuConfigManager config, IBindableBeatmap beatmap)
};
this.beatmap.BindTo(beatmap);
beatmap.ValueChanged += v => calculateScale();
this.beatmap.ValueChanged += v => calculateScale();
cursorScale = config.GetBindable<double>(OsuSetting.GameplayCursorSize);
cursorScale.ValueChanged += v => calculateScale();

View File

@ -1,16 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using NUnit.Framework;
namespace osu.Game.Rulesets.Taiko.Tests
{
[TestFixture]
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
{
public TestCasePerformancePoints()
: base(new TaikoRuleset())
{
}
}
}

View File

@ -27,54 +27,33 @@ internal class TaikoDifficultyCalculator : DifficultyCalculator
/// </summary>
private const double decay_weight = 0.9;
/// <summary>
/// HitObjects are stored as a member variable.
/// </summary>
private readonly List<TaikoHitObjectDifficulty> difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
public TaikoDifficultyCalculator(IBeatmap beatmap)
: base(beatmap)
public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
}
public TaikoDifficultyCalculator(IBeatmap beatmap, Mod[] mods)
: base(beatmap, mods)
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate)
{
}
var difficultyHitObjects = new List<TaikoHitObjectDifficulty>();
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
{
// Fill our custom DifficultyHitObject class, that carries additional information
difficultyHitObjects.Clear();
foreach (var hitObject in Beatmap.HitObjects)
foreach (var hitObject in beatmap.HitObjects)
difficultyHitObjects.Add(new TaikoHitObjectDifficulty((TaikoHitObject)hitObject));
// Sort DifficultyHitObjects by StartTime of the HitObjects - just to make sure.
difficultyHitObjects.Sort((a, b) => a.BaseHitObject.StartTime.CompareTo(b.BaseHitObject.StartTime));
if (!calculateStrainValues()) return 0;
if (!calculateStrainValues(difficultyHitObjects, timeRate))
return new DifficultyAttributes(mods, 0);
double starRating = calculateDifficulty() * star_scaling_factor;
double starRating = calculateDifficulty(difficultyHitObjects, timeRate) * star_scaling_factor;
if (categoryDifficulty != null)
categoryDifficulty["Strain"] = starRating;
return starRating;
return new DifficultyAttributes(mods, starRating);
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
new TaikoModDoubleTime(),
new TaikoModHalfTime(),
new TaikoModEasy(),
new TaikoModHardRock(),
};
private bool calculateStrainValues()
private bool calculateStrainValues(List<TaikoHitObjectDifficulty> objects, double timeRate)
{
// Traverse hitObjects in pairs to calculate the strain value of NextHitObject from the strain value of CurrentHitObject and environment.
using (List<TaikoHitObjectDifficulty>.Enumerator hitObjectsEnumerator = difficultyHitObjects.GetEnumerator())
using (var hitObjectsEnumerator = objects.GetEnumerator())
{
if (!hitObjectsEnumerator.MoveNext()) return false;
@ -84,7 +63,7 @@ private bool calculateStrainValues()
while (hitObjectsEnumerator.MoveNext())
{
var next = hitObjectsEnumerator.Current;
next?.CalculateStrains(current, TimeRate);
next?.CalculateStrains(current, timeRate);
current = next;
}
@ -92,9 +71,9 @@ private bool calculateStrainValues()
}
}
private double calculateDifficulty()
private double calculateDifficulty(List<TaikoHitObjectDifficulty> objects, double timeRate)
{
double actualStrainStep = strain_step * TimeRate;
double actualStrainStep = strain_step * timeRate;
// Find the highest strain value within each strain step
List<double> highestStrains = new List<double>();
@ -102,7 +81,7 @@ private double calculateDifficulty()
double maximumStrain = 0; // We need to keep track of the maximum strain in the current interval
TaikoHitObjectDifficulty previousHitObject = null;
foreach (var hitObject in difficultyHitObjects)
foreach (var hitObject in objects)
{
// While we are beyond the current interval push the currently available maximum to our strain list
while (hitObject.BaseHitObject.StartTime > intervalEndTime)
@ -144,5 +123,13 @@ private double calculateDifficulty()
return difficulty;
}
protected override Mod[] DifficultyAdjustmentMods => new Mod[]
{
new TaikoModDoubleTime(),
new TaikoModHalfTime(),
new TaikoModEasy(),
new TaikoModHardRock(),
};
}
}

View File

@ -22,10 +22,10 @@ public class TaikoPerformanceCalculator : PerformanceCalculator
private int countMeh;
private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
: base(ruleset, beatmap, score)
{
beatmapMaxCombo = beatmap.HitObjects.Count(h => h is Hit);
beatmapMaxCombo = Beatmap.HitObjects.Count(h => h is Hit);
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
@ -68,7 +68,7 @@ public override double Calculate(Dictionary<string, double> categoryDifficulty =
private double computeStrainValue()
{
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes["Strain"] / 0.0075) - 4.0, 2.0) / 100000.0;
double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0;
// Longer maps are worth more
double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0);

View File

@ -86,6 +86,7 @@ protected override void UpdateState(ArmedState state)
switch (State.Value)
{
case ArmedState.Idle:
UnproxyContent();
this.Delay(HitObject.HitWindows.HalfWindowFor(HitResult.Miss)).Expire();
break;
case ArmedState.Miss:
@ -93,6 +94,10 @@ protected override void UpdateState(ArmedState state)
.Expire();
break;
case ArmedState.Hit:
// If we're far enough away from the left stage, we should bring outselves in front of it
if (X >= -0.05f)
ProxyContent();
var flash = circlePiece?.FlashBox;
if (flash != null)
{

View File

@ -20,12 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public class DrawableSwell : DrawableTaikoHitObject<Swell>
{
/// <summary>
/// Invoked when the swell has reached the hit target, i.e. when CurrentTime >= StartTime.
/// This is only ever invoked once.
/// </summary>
public event Action OnStart;
private const float target_ring_thick_border = 1.4f;
private const float target_ring_thin_border = 1f;
private const float target_ring_scale = 5f;
@ -40,7 +34,6 @@ public class DrawableSwell : DrawableTaikoHitObject<Swell>
/// </summary>
private int userHits;
private bool hasStarted;
private readonly SwellSymbolPiece symbol;
public DrawableSwell(Swell swell)
@ -48,7 +41,7 @@ public DrawableSwell(Swell swell)
{
FillMode = FillMode.Fit;
AddInternal(bodyContainer = new Container
Content.Add(bodyContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
@ -177,6 +170,9 @@ protected override void UpdateState(ArmedState state)
switch (state)
{
case ArmedState.Idle:
UnproxyContent();
break;
case ArmedState.Hit:
bodyContainer.Delay(untilJudgement).ScaleTo(1.4f, out_transition_time);
break;
@ -195,11 +191,8 @@ protected override void Update()
X = Math.Max(0, X);
double t = Math.Min(HitObject.StartTime, Time.Current);
if (t == HitObject.StartTime && !hasStarted)
{
OnStart?.Invoke();
hasStarted = true;
}
if (t == HitObject.StartTime)
ProxyContent();
}
private bool? lastWasCentre;

View File

@ -9,10 +9,75 @@
using System.Linq;
using osu.Game.Audio;
using System.Collections.Generic;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Primitives;
namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
public abstract class DrawableTaikoHitObject<TaikoHitType> : DrawableHitObject<TaikoHitObject>, IKeyBindingHandler<TaikoAction>
public abstract class DrawableTaikoHitObject : DrawableHitObject<TaikoHitObject>, IKeyBindingHandler<TaikoAction>
{
protected readonly Container Content;
private readonly Container proxiedContent;
private readonly Container nonProxiedContent;
protected DrawableTaikoHitObject(TaikoHitObject hitObject)
: base(hitObject)
{
InternalChildren = new[]
{
nonProxiedContent = new Container
{
RelativeSizeAxes = Axes.Both,
Child = Content = new Container { RelativeSizeAxes = Axes.Both }
},
proxiedContent = new Container { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// <see cref="proxiedContent"/> is proxied into an upper layer. We don't want to get masked away otherwise <see cref="proxiedContent"/> would too.
/// </summary>
protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false;
private bool isProxied;
/// <summary>
/// Moves <see cref="Content"/> to a layer proxied above the playfield.
/// Does nothing is content is already proxied.
/// </summary>
protected void ProxyContent()
{
if (isProxied) return;
isProxied = true;
nonProxiedContent.Remove(Content);
proxiedContent.Add(Content);
}
/// <summary>
/// Moves <see cref="Content"/> to the normal hitobject layer.
/// Does nothing is content is not currently proxied.
/// </summary>
protected void UnproxyContent()
{
if (!isProxied) return;
isProxied = false;
proxiedContent.Remove(Content);
nonProxiedContent.Add(Content);
}
/// <summary>
/// Creates a proxy for the content of this <see cref="DrawableTaikoHitObject"/>.
/// </summary>
public Drawable CreateProxiedContent() => proxiedContent.CreateProxy();
public abstract bool OnPressed(TaikoAction action);
public virtual bool OnReleased(TaikoAction action) => false;
}
public abstract class DrawableTaikoHitObject<TaikoHitType> : DrawableTaikoHitObject
where TaikoHitType : TaikoHitObject
{
public override Vector2 OriginPosition => new Vector2(DrawHeight / 2);
@ -34,7 +99,7 @@ protected DrawableTaikoHitObject(TaikoHitType hitObject)
RelativeSizeAxes = Axes.Both;
Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE);
InternalChild = MainPiece = CreateMainPiece();
Content.Add(MainPiece = CreateMainPiece());
MainPiece.KiaiMode = HitObject.Kiai;
}
@ -44,9 +109,5 @@ protected DrawableTaikoHitObject(TaikoHitType hitObject)
protected override string SampleNamespace => "Taiko";
protected virtual TaikoPiece CreateMainPiece() => new CirclePiece();
public abstract bool OnPressed(TaikoAction action);
public virtual bool OnReleased(TaikoAction action) => false;
}
}

View File

@ -91,7 +91,7 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
{
new TaikoModHardRock(),
new MultiMod(new TaikoModSuddenDeath(), new TaikoModPerfect()),
new MultiMod(new TaikoModDoubleTime(), new TaikoModDaycore()),
new MultiMod(new TaikoModDoubleTime(), new TaikoModNightcore()),
new TaikoModHidden(),
new TaikoModFlashlight(),
};
@ -114,9 +114,9 @@ public override IEnumerable<Mod> GetModsFor(ModType type)
public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_osu_taiko_o };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => new TaikoDifficultyCalculator(beatmap, mods);
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => new TaikoPerformanceCalculator(this, beatmap, score);
public override int? LegacyID => 1;

View File

@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Taiko.UI
/// </summary>
internal class HitExplosion : CircularContainer
{
public override bool RemoveWhenNotAlive => true;
public readonly DrawableHitObject JudgedObject;
private readonly Box innerFill;
@ -66,7 +68,7 @@ protected override void LoadComplete()
this.ScaleTo(3f, 1000, Easing.OutQuint);
this.FadeOut(500);
Expire();
Expire(true);
}
/// <summary>

View File

@ -14,6 +14,8 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public class KiaiHitExplosion : CircularContainer
{
public override bool RemoveWhenNotAlive => true;
public readonly DrawableHitObject JudgedObject;
private readonly bool isRim;
@ -62,7 +64,7 @@ protected override void LoadComplete()
this.ScaleTo(new Vector2(1, 3f), 500, Easing.OutQuint);
this.FadeOut(250);
Expire();
Expire(true);
}
}
}

View File

@ -216,10 +216,9 @@ public override void Add(DrawableHitObject h)
if (barline != null)
barlineContainer.Add(barline.CreateProxy());
// Swells should be moved at the very top of the playfield when they reach the hit target
var swell = h as DrawableSwell;
if (swell != null)
swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy());
var taikoObject = h as DrawableTaikoHitObject;
if (taikoObject != null)
topLevelHitContainer.Add(taikoObject.CreateProxiedContent());
}
internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
@ -244,19 +243,6 @@ internal void OnJudgement(DrawableHitObject judgedObject, Judgement judgement)
hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == judgedObject)?.VisualiseSecondHit();
else
{
if (judgedObject.X >= -0.05f && judgedObject is DrawableHit)
{
// If we're far enough away from the left stage, we should bring outselves in front of it
// Todo: The following try-catch is temporary for replay rewinding support
try
{
topLevelHitContainer.Add(judgedObject.CreateProxy());
}
catch
{
}
}
hitExplosionContainer.Add(new HitExplosion(judgedObject, isRim));
if (judgedObject.HitObject.Kiai)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Tests.Beatmaps.IO
[TestFixture]
public class ImportBeatmapTest
{
private const string osz_path = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz";
public const string TEST_OSZ_PATH = @"../../../../osu-resources/osu.Game.Resources/Beatmaps/241526 Soleily - Renatus.osz";
[Test]
public void TestImportWhenClosed()
@ -265,7 +265,7 @@ public void TestImportWhenFileOpen()
private string createTemporaryBeatmap()
{
var temp = Path.GetTempFileName() + ".osz";
File.Copy(osz_path, temp, true);
File.Copy(TEST_OSZ_PATH, temp, true);
Assert.IsTrue(File.Exists(temp));
return temp;
}

View File

@ -2,8 +2,8 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
@ -139,14 +139,14 @@ private class ModIncompatibleWithAAndB : Mod
private class TestDifficultyCalculator : DifficultyCalculator
{
public TestDifficultyCalculator(params Mod[] mods)
: base(null)
: base(null, null)
{
DifficultyAdjustmentMods = mods;
}
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => throw new NotImplementedException();
protected override Mod[] DifficultyAdjustmentMods { get; }
protected override DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate) => throw new NotImplementedException();
}
}
}

View File

@ -4,11 +4,17 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Configuration;
using OpenTK;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Overlays;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Timing;
using osu.Game.Beatmaps;
using osu.Game.Screens.Edit.Screens.Compose.Timeline;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
{
@ -23,15 +29,23 @@ public class TestCaseEditorComposeTimeline : EditorClockTestCase
typeof(CentreMarker)
};
public TestCaseEditorComposeTimeline()
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = new WaveformTestBeatmap();
Children = new Drawable[]
{
new MusicController
new FillFlowContainer
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
State = Visibility.Visible
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 5),
Children = new Drawable[]
{
new StartStopButton(),
new AudioVisualiser(),
}
},
new TimelineArea
{
@ -42,5 +56,85 @@ public TestCaseEditorComposeTimeline()
}
};
}
private class AudioVisualiser : CompositeDrawable
{
private readonly Drawable marker;
private readonly IBindable<WorkingBeatmap> beatmap = new Bindable<WorkingBeatmap>();
private IAdjustableClock adjustableClock;
public AudioVisualiser()
{
Size = new Vector2(250, 25);
InternalChildren = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.25f,
},
marker = new Box
{
RelativePositionAxes = Axes.X,
RelativeSizeAxes = Axes.Y,
Width = 2,
}
};
}
[BackgroundDependencyLoader]
private void load(IAdjustableClock adjustableClock, IBindableBeatmap beatmap)
{
this.adjustableClock = adjustableClock;
this.beatmap.BindTo(beatmap);
}
protected override void Update()
{
base.Update();
if (beatmap.Value.Track.IsLoaded)
marker.X = (float)(adjustableClock.CurrentTime / beatmap.Value.Track.Length);
}
}
private class StartStopButton : Button
{
private IAdjustableClock adjustableClock;
private bool started;
public StartStopButton()
{
BackgroundColour = Color4.SlateGray;
Size = new Vector2(100, 50);
Text = "Start";
Action = onClick;
}
[BackgroundDependencyLoader]
private void load(IAdjustableClock adjustableClock)
{
this.adjustableClock = adjustableClock;
}
private void onClick()
{
if (started)
{
adjustableClock.Stop();
Text = "Start";
}
else
{
adjustableClock.Start();
Text = "Stop";
}
started = !started;
}
}
}
}

View File

@ -5,6 +5,7 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Game.Overlays.Volume;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Tests.Visual
@ -17,13 +18,21 @@ protected override void LoadComplete()
{
VolumeMeter meter;
MuteButton mute;
Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue));
Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) });
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
Add(new VolumeMeter("BIG", 250, Color4.Red)
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Position = new Vector2(10),
});
Add(mute = new MuteButton
{
Margin = new MarginPadding { Top = 200 }
});
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
AddToggleStep("mute", b => mute.Current.Value = b);
}
}

View File

@ -10,7 +10,6 @@
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays;
namespace osu.Game.Tests.Visual
{
@ -20,22 +19,14 @@ public class TestCaseWaveform : OsuTestCase
[BackgroundDependencyLoader]
private void load()
{
Beatmap.Value = new WaveformTestBeatmap();
FillFlowContainer flow;
Child = flow = new FillFlowContainer
{
RelativeSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 10),
Children = new Drawable[]
{
new MusicController
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Y = 100,
State = Visibility.Visible
},
}
};
for (int i = 1; i <= 16; i *= 2)
@ -44,10 +35,9 @@ private void load()
{
RelativeSizeAxes = Axes.Both,
Resolution = 1f / i,
Waveform = Beatmap.Value.Waveform,
};
Beatmap.ValueChanged += b => newDisplay.Waveform = b.Waveform;
flow.Add(new Container
{
RelativeSizeAxes = Axes.X,

View File

@ -0,0 +1,55 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.IO;
using System.Linq;
using osu.Framework.Audio.Track;
using osu.Framework.Graphics.Textures;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using osu.Game.IO.Archives;
using osu.Game.Tests.Beatmaps.IO;
namespace osu.Game.Tests
{
/// <summary>
/// A <see cref="WorkingBeatmap"/> that is used for testcases that include waveforms.
/// </summary>
public class WaveformTestBeatmap : WorkingBeatmap
{
private readonly ZipArchiveReader reader;
private readonly FileStream stream;
public WaveformTestBeatmap()
: base(new BeatmapInfo())
{
stream = File.OpenRead(ImportBeatmapTest.TEST_OSZ_PATH);
reader = new ZipArchiveReader(stream);
}
public override void Dispose()
{
base.Dispose();
stream?.Dispose();
reader?.Dispose();
}
protected override IBeatmap GetBeatmap() => createTestBeatmap();
protected override Texture GetBackground() => null;
protected override Waveform GetWaveform() => new Waveform(getAudioStream());
protected override Track GetTrack() => new TrackBass(getAudioStream());
private Stream getAudioStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".mp3")));
private Stream getBeatmapStream() => reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")));
private Beatmap createTestBeatmap()
{
using (var beatmapStream = getBeatmapStream())
using (var beatmapReader = new StreamReader(beatmapStream))
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
}
}
}

View File

@ -366,8 +366,7 @@ private List<BeatmapInfo> createBeatmapDifficulties(ArchiveReader reader)
if (ruleset != null)
{
// TODO: this should be done in a better place once we actually need to dynamically update it.
var converted = new DummyConversionBeatmap(beatmap).GetPlayableBeatmap(ruleset);
beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(converted).Calculate();
beatmap.BeatmapInfo.StarDifficulty = ruleset.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating;
}
else
beatmap.BeatmapInfo.StarDifficulty = 0;

View File

@ -62,7 +62,7 @@ public override RulesetContainer CreateRulesetContainerWith(WorkingBeatmap beatm
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new DummyBeatmapConverter { Beatmap = beatmap };
public override DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null) => null;
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => null;
public override string Description => "dummy";

View File

@ -181,24 +181,6 @@ public bool IsEnabled(LogLevel logLevel)
}
}
public void Migrate()
{
try
{
Database.Migrate();
}
catch (Exception e)
{
throw new MigrationFailedException(e);
}
}
}
public class MigrationFailedException : Exception
{
public MigrationFailedException(Exception exception)
: base("sqlite-net migration failed", exception)
{
}
public void Migrate() => Database.Migrate();
}
}

View File

@ -33,7 +33,7 @@ private void load(OsuGame osuGame, AudioManager audio)
/// <summary>
/// Whether mouse input should be blocked screen-wide while this overlay is visible.
/// Performing mouse actions outside of the valid extents will hide the overlay but pass the events through.
/// Performing mouse actions outside of the valid extents will hide the overlay.
/// </summary>
public virtual bool BlockScreenWideMouse => BlockPassThroughMouse;

View File

@ -149,7 +149,7 @@ private void load(OsuColour colours, LocalisationEngine localisation)
{
new OsuSpriteText
{
Text = $"{SetInfo.Metadata.Source}",
Text = SetInfo.Metadata.Source,
TextSize = 14,
Shadow = false,
Colour = colours.Gray5,

View File

@ -160,7 +160,7 @@ private void load(LocalisationEngine localisation, OsuColour colours)
},
new OsuSpriteText
{
Text = $"from {SetInfo.Metadata.Source}",
Text = SetInfo.Metadata.Source,
Anchor = Anchor.TopRight,
Origin = Anchor.TopRight,
TextSize = 14,

View File

@ -66,34 +66,6 @@ public MusicController()
AlwaysPresent = true;
}
private Vector2 dragStart;
protected override bool OnDragStart(InputState state)
{
base.OnDragStart(state);
dragStart = state.Mouse.Position;
return true;
}
protected override bool OnDrag(InputState state)
{
if (base.OnDrag(state)) return true;
Vector2 change = state.Mouse.Position - dragStart;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length;
dragContainer.MoveTo(change);
return true;
}
protected override bool OnDragEnd(InputState state)
{
dragContainer.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
return base.OnDragEnd(state);
}
[BackgroundDependencyLoader]
private void load(BindableBeatmap beatmap, BeatmapManager beatmaps, OsuColour colours, LocalisationEngine localisation)
{
@ -103,7 +75,7 @@ private void load(BindableBeatmap beatmap, BeatmapManager beatmaps, OsuColour co
Children = new Drawable[]
{
dragContainer = new Container
dragContainer = new DragContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
@ -470,5 +442,36 @@ private void load(TextureStore textures)
sprite.Texture = beatmap?.Background ?? textures.Get(@"Backgrounds/bg4");
}
}
private class DragContainer : Container
{
private Vector2 dragStart;
protected override bool OnDragStart(InputState state)
{
base.OnDragStart(state);
dragStart = state.Mouse.Position;
return true;
}
protected override bool OnDrag(InputState state)
{
if (base.OnDrag(state)) return true;
Vector2 change = state.Mouse.Position - dragStart;
// Diminish the drag distance as we go further to simulate "rubber band" feeling.
change *= change.Length <= 0 ? 0 : (float)Math.Pow(change.Length, 0.7f) / change.Length;
this.MoveTo(change);
return true;
}
protected override bool OnDragEnd(InputState state)
{
this.MoveTo(Vector2.Zero, 800, Easing.OutElastic);
return base.OnDragEnd(state);
}
}
}
}

View File

@ -24,6 +24,8 @@ namespace osu.Game.Overlays.Volume
public class VolumeMeter : Container, IKeyBindingHandler<GlobalAction>
{
private CircularProgress volumeCircle;
private CircularProgress volumeCircleGlow;
public BindableDouble Bindable { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 };
private readonly float circleSize;
private readonly Color4 meterColour;
@ -44,90 +46,143 @@ public VolumeMeter(string name, float circleSize, Color4 meterColour)
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
Add(new Container
{
Size = new Vector2(120, 20),
CornerRadius = 10,
Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray1,
Alpha = 0.9f,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Exo2.0-Bold",
Text = name
}
}
});
Color4 backgroundColour = colours.Gray1;
CircularProgress bgProgress;
Add(new CircularContainer
const float progress_start_radius = 0.75f;
const float progress_size = 0.03f;
const float progress_end_radius = progress_start_radius + progress_size;
const float blur_amount = 5;
Children = new Drawable[]
{
Masking = true,
Size = new Vector2(circleSize),
Children = new Drawable[]
new Container
{
new Box
Size = new Vector2(circleSize),
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Colour = colours.Gray1,
Alpha = 0.9f,
},
bgProgress = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = 0.05f,
Rotation = 180,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Colour = colours.Gray2,
Size = new Vector2(0.8f)
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(0.8f),
Padding = new MarginPadding(-Blur.KernelSize(5)),
Rotation = 180,
Child = (volumeCircle = new CircularProgress
new BufferedContainer
{
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both,
InnerRadius = 0.05f,
Children = new Drawable[]
{
new Circle
{
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new CircularContainer
{
Masking = true,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_end_radius),
Children = new Drawable[]
{
bgProgress = new CircularProgress
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Colour = backgroundColour,
},
new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Name = "Progress under covers for smoothing",
RelativeSizeAxes = Axes.Both,
Rotation = 180,
Child = volumeCircle = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
}
},
}
},
new Circle
{
Name = "Inner Cover",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
Size = new Vector2(progress_start_radius),
},
new Container
{
Name = "Progress overlay for glow",
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
Size = new Vector2(progress_start_radius + progress_size / 1.5f),
Rotation = 180,
Padding = new MarginPadding(-Blur.KernelSize(blur_amount)),
Child = (volumeCircleGlow = new CircularProgress
{
RelativeSizeAxes = Axes.Both,
InnerRadius = progress_size * 0.8f,
}).WithEffect(new GlowEffect
{
Colour = meterColour,
BlurSigma = new Vector2(blur_amount),
Strength = 5,
PadExtent = true
}),
},
},
},
maxGlow = (text = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Venera",
TextSize = 0.16f * circleSize
}).WithEffect(new GlowEffect
{
Colour = meterColour,
Strength = 2,
PadExtent = true
}),
},
maxGlow = (text = new OsuSpriteText
Colour = Color4.Transparent,
PadExtent = true,
})
}
},
new Container
{
Size = new Vector2(120, 20),
CornerRadius = 10,
Masking = true,
Margin = new MarginPadding { Left = circleSize + 10 },
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Children = new Drawable[]
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Venera",
TextSize = 0.16f * circleSize
}).WithEffect(new GlowEffect
{
Colour = Color4.Transparent,
PadExtent = true,
})
new Box
{
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both,
Colour = backgroundColour,
},
new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = "Exo2.0-Bold",
Text = name
}
}
}
});
Bindable.ValueChanged += newVolume => { this.TransformTo("DisplayVolume", newVolume, 400, Easing.OutQuint); };
};
Bindable.ValueChanged += newVolume =>
{
this.TransformTo("DisplayVolume",
newVolume,
400,
Easing.OutQuint);
};
bgProgress.Current.Value = 0.75f;
}
@ -158,6 +213,7 @@ protected double DisplayVolume
}
volumeCircle.Current.Value = displayVolume * 0.75f;
volumeCircleGlow.Current.Value = displayVolume * 0.75f;
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.Difficulty
{
public class DifficultyAttributes
{
public readonly Mod[] Mods;
public readonly double StarRating;
public DifficultyAttributes(Mod[] mods, double starRating)
{
Mods = mods;
StarRating = starRating;
}
}
}

View File

@ -13,28 +13,44 @@ namespace osu.Game.Rulesets.Difficulty
{
public abstract class DifficultyCalculator
{
protected readonly IBeatmap Beatmap;
protected readonly Mod[] Mods;
private readonly Ruleset ruleset;
private readonly WorkingBeatmap beatmap;
protected double TimeRate { get; private set; } = 1;
protected DifficultyCalculator(IBeatmap beatmap, Mod[] mods = null)
protected DifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
{
Beatmap = beatmap;
Mods = mods ?? new Mod[0];
ApplyMods(Mods);
this.ruleset = ruleset;
this.beatmap = beatmap;
}
protected virtual void ApplyMods(Mod[] mods)
/// <summary>
/// Calculates the difficulty of the beatmap using a specific mod combination.
/// </summary>
/// <param name="mods">The mods that should be applied to the beatmap.</param>
/// <returns>A structure describing the difficulty of the beatmap.</returns>
public DifficultyAttributes Calculate(params Mod[] mods)
{
beatmap.Mods.Value = mods;
IBeatmap playableBeatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
var clock = new StopwatchClock();
mods.OfType<IApplicableToClock>().ForEach(m => m.ApplyToClock(clock));
TimeRate = clock.Rate;
return Calculate(playableBeatmap, mods, clock.Rate);
}
protected virtual void PreprocessHitObjects()
/// <summary>
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary>
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
public IEnumerable<DifficultyAttributes> CalculateAll()
{
foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{
if (combination is MultiMod multi)
yield return Calculate(multi.Mods);
else
yield return Calculate(combination);
}
}
/// <summary>
@ -75,6 +91,13 @@ IEnumerable<Mod> createDifficultyAdjustmentModCombinations(IEnumerable<Mod> curr
/// </summary>
protected virtual Mod[] DifficultyAdjustmentMods => Array.Empty<Mod>();
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
/// <summary>
/// Calculates the difficulty of a <see cref="Beatmap"/> using a specific <see cref="Mod"/> combination.
/// </summary>
/// <param name="beatmap">The <see cref="IBeatmap"/> to compute the difficulty for.</param>
/// <param name="mods">The <see cref="Mod"/>s that should be applied.</param>
/// <param name="timeRate">The rate of time in <paramref name="beatmap"/>.</param>
/// <returns>A structure containing the difficulty attributes.</returns>
protected abstract DifficultyAttributes Calculate(IBeatmap beatmap, Mod[] mods, double timeRate);
}
}

View File

@ -13,8 +13,7 @@ namespace osu.Game.Rulesets.Difficulty
{
public abstract class PerformanceCalculator
{
private readonly Dictionary<string, double> attributes = new Dictionary<string, double>();
protected IDictionary<string, double> Attributes => attributes;
protected readonly DifficultyAttributes Attributes;
protected readonly Ruleset Ruleset;
protected readonly IBeatmap Beatmap;
@ -22,14 +21,15 @@ public abstract class PerformanceCalculator
protected double TimeRate { get; private set; } = 1;
protected PerformanceCalculator(Ruleset ruleset, IBeatmap beatmap, Score score)
protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, Score score)
{
Ruleset = ruleset;
Beatmap = beatmap;
Score = score;
var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods);
diffCalc.Calculate(attributes);
beatmap.Mods.Value = score.Mods;
Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo);
Attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods);
ApplyMods(score.Mods);
}

View File

@ -61,9 +61,9 @@ protected Ruleset(RulesetInfo rulesetInfo = null)
public virtual IBeatmapProcessor CreateBeatmapProcessor(IBeatmap beatmap) => null;
public abstract DifficultyCalculator CreateDifficultyCalculator(IBeatmap beatmap, Mod[] mods = null);
public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap);
public virtual PerformanceCalculator CreatePerformanceCalculator(IBeatmap beatmap, Score score) => null;
public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, Score score) => null;
public virtual HitObjectComposer CreateHitObjectComposer() => null;

View File

@ -31,7 +31,7 @@ public abstract class ScrollingPlayfield : Playfield
/// <summary>
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
/// </summary>
private const double time_span_step = 50;
private const double time_span_step = 200;
/// <summary>
/// The span of time that is visible by the length of the scrolling axes.
@ -88,10 +88,10 @@ protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
switch (args.Key)
{
case Key.Minus:
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange + time_span_step, 600, Easing.OutQuint);
break;
case Key.Plus:
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
this.TransformBindableTo(VisibleTimeRange, VisibleTimeRange - time_span_step, 600, Easing.OutQuint);
break;
}
}

View File

@ -18,7 +18,8 @@ public class KeyCounterCollection : FillFlowContainer<KeyCounter>
{
private const int duration = 100;
private Bindable<bool> showKeyCounter;
public readonly Bindable<bool> Visible = new Bindable<bool>(true);
private readonly Bindable<bool> configVisibility = new Bindable<bool>();
public KeyCounterCollection()
{
@ -46,9 +47,10 @@ public void ResetCount()
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
showKeyCounter = config.GetBindable<bool>(OsuSetting.KeyOverlay);
showKeyCounter.ValueChanged += keyCounterVisibility => this.FadeTo(keyCounterVisibility ? 1 : 0, duration);
showKeyCounter.TriggerChange();
config.BindWith(OsuSetting.KeyOverlay, configVisibility);
Visible.BindValueChanged(_ => updateVisibility());
configVisibility.BindValueChanged(_ => updateVisibility(), true);
}
//further: change default values here and in KeyCounter if needed, instead of passing them in every constructor
@ -111,6 +113,8 @@ public Color4 KeyUpTextColor
}
}
private void updateVisibility() => this.FadeTo(Visible.Value || configVisibility.Value ? 1 : 0, duration);
public override bool HandleKeyboardInput => receptor == null;
public override bool HandleMouseInput => receptor == null;

View File

@ -229,6 +229,7 @@ private void load(AudioManager audio, APIAccess api, OsuConfigManager config)
};
hudOverlay.HoldToQuit.Action = Exit;
hudOverlay.KeyCounter.Visible.BindTo(RulesetContainer.HasReplayLoaded);
if (ShowStoryboard)
initializeStoryboard(false);

View File

@ -16,7 +16,8 @@
namespace osu.Game.Tests.Beatmaps
{
[TestFixture]
public abstract class BeatmapConversionTest<TConvertValue>
public abstract class BeatmapConversionTest<TConvertMapping, TConvertValue>
where TConvertMapping : ConvertMapping<TConvertValue>, IEquatable<TConvertMapping>, new()
where TConvertValue : IEquatable<TConvertValue>
{
private const string resource_namespace = "Testing.Beatmaps";
@ -59,9 +60,13 @@ protected void Test(string name)
else if (objectCounter >= expectedMapping.Objects.Count)
Assert.Fail($"The conversion generated a hitobject, but should not have, for hitobject at time: {ourMapping.StartTime}:\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n");
else if (!EqualityComparer<TConvertValue>.Default.Equals(expectedMapping.Objects[objectCounter], ourMapping.Objects[objectCounter]))
else if (!expectedMapping.Equals(ourMapping))
Assert.Fail($"The conversion mapping differed for object at time {expectedMapping.StartTime}:\n"
+ $"Expected {JsonConvert.SerializeObject(expectedMapping)}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping)}\n");
else if (!expectedMapping.Objects[objectCounter].Equals(ourMapping.Objects[objectCounter]))
{
Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}\n"
Assert.Fail($"The conversion generated differing hitobjects for object at time: {expectedMapping.StartTime}:\n"
+ $"Expected: {JsonConvert.SerializeObject(expectedMapping.Objects[objectCounter])}\n"
+ $"Received: {JsonConvert.SerializeObject(ourMapping.Objects[objectCounter])}\n");
}
@ -84,19 +89,22 @@ private ConvertResult convert(string name)
beatmap.BeatmapInfo.Ruleset = beatmap.BeatmapInfo.RulesetID == rulesetInstance.RulesetInfo.ID ? rulesetInstance.RulesetInfo : new RulesetInfo();
var result = new ConvertResult();
var converter = rulesetInstance.CreateBeatmapConverter(beatmap);
converter.ObjectConverted += (orig, converted) =>
{
converted.ForEach(h => h.ApplyDefaults(beatmap.ControlPointInfo, beatmap.BeatmapInfo.BaseDifficulty));
var mapping = new ConvertMapping { StartTime = orig.StartTime };
var mapping = CreateConvertMapping();
mapping.StartTime = orig.StartTime;
foreach (var obj in converted)
mapping.Objects.AddRange(CreateConvertValue(obj));
result.Mappings.Add(mapping);
};
converter.Convert();
IBeatmap convertedBeatmap = converter.Convert();
rulesetInstance.CreateBeatmapProcessor(convertedBeatmap)?.PostProcess();
return result;
}
@ -128,21 +136,54 @@ private Stream openResource(string name)
return Assembly.LoadFrom(Path.Combine(localPath, $"{ResourceAssembly}.dll")).GetManifestResourceStream($@"{ResourceAssembly}.Resources.{name}");
}
protected abstract IEnumerable<TConvertValue> CreateConvertValue(HitObject hitObject);
protected abstract Ruleset CreateRuleset();
/// <summary>
/// Creates the conversion mapping for a <see cref="HitObject"/>. A conversion mapping stores important information about the conversion process.
/// This is generated _after_ the <see cref="HitObject"/> has been converted.
/// <para>
/// This should be used to validate the integrity of the conversion process after a conversion has occurred.
/// </para>
/// </summary>
protected virtual TConvertMapping CreateConvertMapping() => new TConvertMapping();
private class ConvertMapping
{
[JsonProperty]
public double StartTime;
[JsonProperty]
public List<TConvertValue> Objects = new List<TConvertValue>();
}
/// <summary>
/// Creates the conversion value for a <see cref="HitObject"/>. A conversion value stores information about the converted <see cref="HitObject"/>.
/// <para>
/// This should be used to validate the integrity of the converted <see cref="HitObject"/>.
/// </para>
/// </summary>
/// <param name="hitObject">The converted <see cref="HitObject"/>.</param>
protected abstract IEnumerable<TConvertValue> CreateConvertValue(HitObject hitObject);
/// <summary>
/// Creates the <see cref="Ruleset"/> applicable to this <see cref="BeatmapConversionTest{TConvertMapping,TConvertValue}"/>.
/// </summary>
/// <returns></returns>
protected abstract Ruleset CreateRuleset();
private class ConvertResult
{
[JsonProperty]
public List<ConvertMapping> Mappings = new List<ConvertMapping>();
public List<TConvertMapping> Mappings = new List<TConvertMapping>();
}
}
public abstract class BeatmapConversionTest<TConvertValue> : BeatmapConversionTest<ConvertMapping<TConvertValue>, TConvertValue>
where TConvertValue : IEquatable<TConvertValue>
{
}
public class ConvertMapping<TConvertValue> : IEquatable<ConvertMapping<TConvertValue>>
where TConvertValue : IEquatable<TConvertValue>
{
[JsonProperty]
public double StartTime;
[JsonIgnore]
public List<TConvertValue> Objects = new List<TConvertValue>();
[JsonProperty("Objects")]
private List<TConvertValue> setObjects { set => Objects = value; }
public virtual bool Equals(ConvertMapping<TConvertValue> other) => StartTime.Equals(other?.StartTime);
}
}

View File

@ -25,13 +25,20 @@ protected EditorClockTestCase()
Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false };
}
protected override IReadOnlyDependencyContainer CreateLocalDependencies(IReadOnlyDependencyContainer parent)
{
var dependencies = new DependencyContainer(base.CreateLocalDependencies(parent));
dependencies.Cache(BeatDivisor);
dependencies.CacheAs<IFrameBasedClock>(Clock);
dependencies.CacheAs<IAdjustableClock>(Clock);
return dependencies;
}
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(BeatDivisor);
Dependencies.CacheAs<IFrameBasedClock>(Clock);
Dependencies.CacheAs<IAdjustableClock>(Clock);
Beatmap.BindValueChanged(beatmapChanged, true);
}

View File

@ -1,403 +0,0 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using OpenTK;
using OpenTK.Graphics;
using osu.Framework.Allocation;
using osu.Framework.Caching;
using osu.Framework.Configuration;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input;
using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Tests.Visual
{
public abstract class TestCasePerformancePoints : OsuTestCase
{
protected TestCasePerformancePoints(Ruleset ruleset)
{
Child = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[]
{
new Drawable[]
{
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new BeatmapList(ruleset, Beatmap)
}
}
},
null,
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new StarRatingGrid()
}
}
},
null,
new Container
{
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = Color4.Black,
Alpha = 0.5f,
},
new ScrollContainer
{
RelativeSizeAxes = Axes.Both,
Child = new PerformanceList()
}
}
},
}
},
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20),
new Dimension(),
new Dimension(GridSizeMode.Absolute, 20)
}
};
}
private class BeatmapList : CompositeDrawable
{
private readonly Container<BeatmapDisplay> beatmapDisplays;
private readonly Ruleset ruleset;
private readonly BindableBeatmap beatmapBindable;
public BeatmapList(Ruleset ruleset, BindableBeatmap beatmapBindable)
{
this.ruleset = ruleset;
this.beatmapBindable = beatmapBindable;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = beatmapDisplays = new FillFlowContainer<BeatmapDisplay>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4)
};
}
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
{
var sets = beatmaps.GetAllUsableBeatmapSets();
var allBeatmaps = sets.SelectMany(s => s.Beatmaps).Where(b => ruleset.LegacyID == null || b.RulesetID == ruleset.LegacyID);
allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b, beatmapBindable)));
}
private class BeatmapDisplay : CompositeDrawable, IHasTooltip
{
private readonly OsuSpriteText text;
private readonly BeatmapInfo beatmap;
private readonly BindableBeatmap beatmapBindable;
private BeatmapManager beatmaps;
private bool isSelected;
public string TooltipText => text.Text;
public BeatmapDisplay(BeatmapInfo beatmap, BindableBeatmap beatmapBindable)
{
this.beatmap = beatmap;
this.beatmapBindable = beatmapBindable;
AutoSizeAxes = Axes.Both;
InternalChild = text = new OsuSpriteText();
this.beatmapBindable.ValueChanged += beatmapChanged;
}
[BackgroundDependencyLoader]
private void load(BeatmapManager beatmaps)
{
this.beatmaps = beatmaps;
var working = beatmaps.GetWorkingBeatmap(beatmap);
text.Text = $"{working.Metadata.Artist} - {working.Metadata.Title} ({working.Metadata.AuthorString}) [{working.BeatmapInfo.Version}]";
}
private void beatmapChanged(WorkingBeatmap newBeatmap)
{
if (isSelected)
this.FadeColour(Color4.White, 100);
isSelected = false;
}
protected override bool OnClick(InputState state)
{
if (beatmapBindable.Value.BeatmapInfo.ID == beatmap.ID)
return false;
beatmapBindable.Value = beatmaps.GetWorkingBeatmap(beatmap);
isSelected = true;
return true;
}
protected override bool OnHover(InputState state)
{
if (isSelected)
return false;
this.FadeColour(Color4.Yellow, 100);
return true;
}
protected override void OnHoverLost(InputState state)
{
if (isSelected)
return;
this.FadeColour(Color4.White, 100);
}
}
}
private class PerformanceList : CompositeDrawable
{
private readonly FillFlowContainer<PerformanceDisplay> scores;
private APIAccess api;
private readonly IBindable<WorkingBeatmap> currentBeatmap = new Bindable<WorkingBeatmap>();
public PerformanceList()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = scores = new FillFlowContainer<PerformanceDisplay>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Spacing = new Vector2(0, 4)
};
}
[BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap, APIAccess api)
{
this.api = api;
if (!api.IsLoggedIn)
{
InternalChild = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Text = "Please sign in to see online scores",
};
}
currentBeatmap.ValueChanged += beatmapChanged;
currentBeatmap.BindTo(beatmap);
}
private GetScoresRequest lastRequest;
private void beatmapChanged(WorkingBeatmap newBeatmap)
{
if (!IsAlive) return;
lastRequest?.Cancel();
scores.Clear();
if (!api.IsLoggedIn)
return;
lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo, newBeatmap.BeatmapInfo.Ruleset);
lastRequest.Success += res => res.Scores.ForEach(s => scores.Add(new PerformanceDisplay(s, newBeatmap.Beatmap)));
api.Queue(lastRequest);
}
private class PerformanceDisplay : CompositeDrawable
{
private readonly OsuSpriteText text;
private readonly Score score;
private readonly IBeatmap beatmap;
public PerformanceDisplay(Score score, IBeatmap beatmap)
{
this.score = score;
this.beatmap = beatmap;
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = text = new OsuSpriteText();
}
[BackgroundDependencyLoader]
private void load()
{
var ruleset = beatmap.BeatmapInfo.Ruleset.CreateInstance();
var calculator = ruleset.CreatePerformanceCalculator(beatmap, score);
if (calculator == null)
return;
var attributes = new Dictionary<string, double>();
double performance = calculator.Calculate(attributes);
text.Text = $"{score.User.Username} -> online: {score.PP:n2}pp | local: {performance:n2}pp";
}
}
}
private class StarRatingGrid : CompositeDrawable
{
private readonly FillFlowContainer<OsuCheckbox> modFlow;
private readonly OsuSpriteText totalText;
private readonly FillFlowContainer categoryTexts;
public StarRatingGrid()
{
RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y;
InternalChild = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
modFlow = new FillFlowContainer<OsuCheckbox>
{
Name = "Checkbox flow",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(4, 4)
},
new FillFlowContainer
{
Name = "Information display",
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(0, 4),
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
totalText = new OsuSpriteText { TextSize = 24 },
categoryTexts = new FillFlowContainer
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical
}
}
}
}
};
}
[BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap)
{
beatmap.ValueChanged += beatmapChanged;
}
private Cached informationCache = new Cached();
private Ruleset ruleset;
private WorkingBeatmap beatmap;
private void beatmapChanged(WorkingBeatmap newBeatmap)
{
beatmap = newBeatmap;
modFlow.Clear();
ruleset = newBeatmap.BeatmapInfo.Ruleset.CreateInstance();
foreach (var mod in ruleset.GetAllMods())
{
var checkBox = new OsuCheckbox
{
RelativeSizeAxes = Axes.None,
Width = 50,
LabelText = mod.ShortenedName
};
checkBox.Current.ValueChanged += v => informationCache.Invalidate();
modFlow.Add(checkBox);
}
informationCache.Invalidate();
}
protected override void Update()
{
base.Update();
if (ruleset == null)
return;
if (!informationCache.IsValid)
{
totalText.Text = string.Empty;
categoryTexts.Clear();
var allMods = ruleset.GetAllMods().ToList();
Mod[] activeMods = modFlow.Where(c => c.Current.Value).Select(c => allMods.First(m => m.ShortenedName == c.LabelText)).ToArray();
var diffCalc = ruleset.CreateDifficultyCalculator(beatmap.Beatmap, activeMods);
if (diffCalc != null)
{
var categories = new Dictionary<string, double>();
double totalSr = diffCalc.Calculate(categories);
totalText.Text = $"Star rating: {totalSr:n2}";
foreach (var kvp in categories)
categoryTexts.Add(new OsuSpriteText { Text = $"{kvp.Key}: {kvp.Value:n2}" });
}
informationCache.Validate();
}
}
}
}
}

View File

@ -1,9 +1,11 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Lists;
using osu.Game.Beatmaps;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
@ -51,6 +53,28 @@ private void load(RulesetStore rulesets)
Player p = null;
AddStep(r.Name, () => p = loadPlayerFor(r));
AddUntilStep(() => ContinueCondition(p));
AddAssert("no leaked beatmaps", () =>
{
p = null;
GC.Collect();
GC.WaitForPendingFinalizers();
int count = 0;
workingWeakReferences.ForEachAlive(_ => count++);
return count == 1;
});
AddAssert("no leaked players", () =>
{
GC.Collect();
GC.WaitForPendingFinalizers();
int count = 0;
playerWeakReferences.ForEachAlive(_ => count++);
return count == 1;
});
}
}
}
@ -59,21 +83,32 @@ private void load(RulesetStore rulesets)
protected virtual IBeatmap CreateBeatmap(Ruleset ruleset) => new TestBeatmap(ruleset.RulesetInfo);
private readonly WeakList<WorkingBeatmap> workingWeakReferences = new WeakList<WorkingBeatmap>();
private readonly WeakList<Player> playerWeakReferences = new WeakList<Player>();
private Player loadPlayerFor(RulesetInfo ri) => loadPlayerFor(ri.CreateInstance());
private Player loadPlayerFor(Ruleset r)
{
var beatmap = CreateBeatmap(r);
var working = new TestWorkingBeatmap(beatmap);
Beatmap.Value = new TestWorkingBeatmap(beatmap);
workingWeakReferences.Add(working);
Beatmap.Value = working;
Beatmap.Value.Mods.Value = new[] { r.GetAllMods().First(m => m is ModNoFail) };
if (Player != null)
Remove(Player);
Player?.Exit();
var player = CreatePlayer(r);
LoadComponentAsync(player, LoadScreen);
playerWeakReferences.Add(player);
LoadComponentAsync(player, p =>
{
Player = p;
LoadScreen(p);
});
return player;
}