mirror of
https://github.com/osukey/osukey.git
synced 2025-08-06 16:13:57 +09:00
Merge remote-tracking branch 'origin/master' into editor-composer
This commit is contained in:
Submodule osu-framework updated: d92cec7645...4fc866eee3
@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double Calculate(Dictionary<string, string> categoryDifficulty = null) => 0;
|
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => 0;
|
||||||
|
|
||||||
protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter();
|
protected override BeatmapConverter<CatchHitObject> CreateBeatmapConverter(Beatmap beatmap) => new CatchBeatmapConverter();
|
||||||
}
|
}
|
||||||
|
16
osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs
Normal file
16
osu.Game.Rulesets.Catch/Tests/TestCasePerformancePoints.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2007-2017 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
|
||||||
|
{
|
||||||
|
[Ignore("getting CI working")]
|
||||||
|
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
|
||||||
|
{
|
||||||
|
public TestCasePerformancePoints()
|
||||||
|
: base(new CatchRuleset(new RulesetInfo()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,7 @@
|
|||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Tests\TestCaseCatcherArea.cs" />
|
<Compile Include="Tests\TestCaseCatcherArea.cs" />
|
||||||
<Compile Include="Tests\TestCaseCatchStacker.cs" />
|
<Compile Include="Tests\TestCaseCatchStacker.cs" />
|
||||||
|
<Compile Include="Tests\TestCasePerformancePoints.cs" />
|
||||||
<Compile Include="Tests\TestCaseCatchPlayer.cs" />
|
<Compile Include="Tests\TestCaseCatchPlayer.cs" />
|
||||||
<Compile Include="UI\CatcherArea.cs" />
|
<Compile Include="UI\CatcherArea.cs" />
|
||||||
<Compile Include="UI\CatchRulesetContainer.cs" />
|
<Compile Include="UI\CatchRulesetContainer.cs" />
|
||||||
|
@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Mania
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double Calculate(Dictionary<string, string> categoryDifficulty = null) => 0;
|
public override double Calculate(Dictionary<string, double> categoryDifficulty = null) => 0;
|
||||||
|
|
||||||
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize)));
|
protected override BeatmapConverter<ManiaHitObject> CreateBeatmapConverter(Beatmap beatmap) => new ManiaBeatmapConverter(true, (int)Math.Max(1, Math.Round(beatmap.BeatmapInfo.BaseDifficulty.CircleSize)));
|
||||||
}
|
}
|
||||||
|
@ -176,22 +176,10 @@ namespace osu.Game.Rulesets.Mania.Mods
|
|||||||
|
|
||||||
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
|
public class ManiaModAutoplay : ModAutoplay<ManiaHitObject>
|
||||||
{
|
{
|
||||||
private int availableColumns;
|
|
||||||
|
|
||||||
public override void ApplyToRulesetContainer(RulesetContainer<ManiaHitObject> rulesetContainer)
|
|
||||||
{
|
|
||||||
// Todo: This shouldn't be done, we should be getting a ManiaBeatmap which should store AvailableColumns
|
|
||||||
// But this is dependent on a _lot_ of refactoring
|
|
||||||
var maniaRulesetContainer = (ManiaRulesetContainer)rulesetContainer;
|
|
||||||
availableColumns = maniaRulesetContainer.AvailableColumns;
|
|
||||||
|
|
||||||
base.ApplyToRulesetContainer(rulesetContainer);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Score CreateReplayScore(Beatmap<ManiaHitObject> beatmap) => new Score
|
protected override Score CreateReplayScore(Beatmap<ManiaHitObject> beatmap) => new Score
|
||||||
{
|
{
|
||||||
User = new User { Username = "osu!topus!" },
|
User = new User { Username = "osu!topus!" },
|
||||||
Replay = new ManiaAutoGenerator(beatmap, availableColumns).Generate(),
|
Replay = new ManiaAutoGenerator(beatmap).Generate(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using osu.Game.Beatmaps;
|
using osu.Game.Beatmaps;
|
||||||
using osu.Game.Rulesets.Mania.Objects;
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
@ -13,15 +13,11 @@ namespace osu.Game.Rulesets.Mania.Replays
|
|||||||
{
|
{
|
||||||
internal class ManiaAutoGenerator : AutoGenerator<ManiaHitObject>
|
internal class ManiaAutoGenerator : AutoGenerator<ManiaHitObject>
|
||||||
{
|
{
|
||||||
private const double release_delay = 20;
|
public const double RELEASE_DELAY = 20;
|
||||||
|
|
||||||
private readonly int availableColumns;
|
public ManiaAutoGenerator(Beatmap<ManiaHitObject> beatmap)
|
||||||
|
|
||||||
public ManiaAutoGenerator(Beatmap<ManiaHitObject> beatmap, int availableColumns)
|
|
||||||
: base(beatmap)
|
: base(beatmap)
|
||||||
{
|
{
|
||||||
this.availableColumns = availableColumns;
|
|
||||||
|
|
||||||
Replay = new Replay { User = new User { Username = @"Autoplay" } };
|
Replay = new Replay { User = new User { Username = @"Autoplay" } };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,104 +26,52 @@ namespace osu.Game.Rulesets.Mania.Replays
|
|||||||
public override Replay Generate()
|
public override Replay Generate()
|
||||||
{
|
{
|
||||||
// Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
|
// Todo: Realistically this shouldn't be needed, but the first frame is skipped with the way replays are currently handled
|
||||||
Replay.Frames.Add(new ReplayFrame(-100000, null, null, ReplayButtonState.None));
|
Replay.Frames.Add(new ManiaReplayFrame(-100000, 0));
|
||||||
|
|
||||||
double[] holdEndTimes = new double[availableColumns];
|
var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time);
|
||||||
for (int i = 0; i < availableColumns; i++)
|
|
||||||
holdEndTimes[i] = double.NegativeInfinity;
|
|
||||||
|
|
||||||
// Notes are handled row-by-row
|
int activeColumns = 0;
|
||||||
foreach (var objGroup in Beatmap.HitObjects.GroupBy(h => h.StartTime))
|
foreach (var group in pointGroups)
|
||||||
{
|
{
|
||||||
double groupTime = objGroup.Key;
|
foreach (var point in group)
|
||||||
|
|
||||||
int activeColumns = 0;
|
|
||||||
|
|
||||||
// Get the previously held-down active columns
|
|
||||||
for (int i = 0; i < availableColumns; i++)
|
|
||||||
{
|
{
|
||||||
if (holdEndTimes[i] > groupTime)
|
if (point is HitPoint)
|
||||||
activeColumns |= 1 << i;
|
activeColumns |= 1 << point.Column;
|
||||||
|
if (point is ReleasePoint)
|
||||||
|
activeColumns ^= 1 << point.Column;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add on the group columns, keeping track of the held notes for the next rows
|
Replay.Frames.Add(new ManiaReplayFrame(group.First().Time, activeColumns));
|
||||||
foreach (var obj in objGroup)
|
|
||||||
{
|
|
||||||
var holdNote = obj as HoldNote;
|
|
||||||
if (holdNote != null)
|
|
||||||
holdEndTimes[obj.Column] = Math.Max(holdEndTimes[obj.Column], holdNote.EndTime);
|
|
||||||
|
|
||||||
activeColumns |= 1 << obj.Column;
|
|
||||||
}
|
|
||||||
|
|
||||||
Replay.Frames.Add(new ReplayFrame(groupTime, activeColumns, null, ReplayButtonState.None));
|
|
||||||
|
|
||||||
// Add the release frames. We can't do this with the loop above because we need activeColumns to be fully populated
|
|
||||||
foreach (var obj in objGroup.GroupBy(h => (h as IHasEndTime)?.EndTime ?? h.StartTime + release_delay).OrderBy(h => h.Key))
|
|
||||||
{
|
|
||||||
var groupEndTime = obj.Key;
|
|
||||||
|
|
||||||
int activeColumnsAtEnd = 0;
|
|
||||||
for (int i = 0; i < availableColumns; i++)
|
|
||||||
{
|
|
||||||
if (holdEndTimes[i] > groupEndTime)
|
|
||||||
activeColumnsAtEnd |= 1 << i;
|
|
||||||
}
|
|
||||||
|
|
||||||
Replay.Frames.Add(new ReplayFrame(groupEndTime, activeColumnsAtEnd, 0, ReplayButtonState.None));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Replay.Frames = Replay.Frames
|
|
||||||
// Pick the maximum activeColumns for all frames at the same time
|
|
||||||
.GroupBy(f => f.Time)
|
|
||||||
.Select(g => new ReplayFrame(g.First().Time, maxMouseX(g), 0, ReplayButtonState.None))
|
|
||||||
// The addition of release frames above maybe result in unordered frames, but we need them ordered
|
|
||||||
.OrderBy(f => f.Time)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
return Replay;
|
return Replay;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private IEnumerable<IActionPoint> generateActionPoints()
|
||||||
/// Finds the maximum <see cref="ReplayFrame.MouseX"/> by count of bits from a grouping of <see cref="ReplayFrame"/>s.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="group">The <see cref="ReplayFrame"/> grouping to search.</param>
|
|
||||||
/// <returns>The maximum <see cref="ReplayFrame.MouseX"/> by count of bits.</returns>
|
|
||||||
private float maxMouseX(IGrouping<double, ReplayFrame> group)
|
|
||||||
{
|
{
|
||||||
int currentCount = -1;
|
foreach (var obj in Beatmap.HitObjects)
|
||||||
int currentMax = 0;
|
|
||||||
|
|
||||||
foreach (var val in group)
|
|
||||||
{
|
{
|
||||||
int newCount = countBits((int)(val.MouseX ?? 0));
|
yield return new HitPoint { Time = obj.StartTime, Column = obj.Column };
|
||||||
if (newCount > currentCount)
|
yield return new ReleasePoint { Time = ((obj as IHasEndTime)?.EndTime ?? obj.StartTime) + RELEASE_DELAY, Column = obj.Column };
|
||||||
{
|
|
||||||
currentCount = newCount;
|
|
||||||
currentMax = (int)(val.MouseX ?? 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return currentMax;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
private interface IActionPoint
|
||||||
/// Counts the number of bits set in a value.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="value">The value to count.</param>
|
|
||||||
/// <returns>The number of set bits.</returns>
|
|
||||||
private int countBits(int value)
|
|
||||||
{
|
{
|
||||||
int count = 0;
|
double Time { get; set; }
|
||||||
while (value > 0)
|
int Column { get; set; }
|
||||||
{
|
}
|
||||||
if ((value & 1) > 0)
|
|
||||||
count++;
|
|
||||||
value >>= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
private struct HitPoint : IActionPoint
|
||||||
|
{
|
||||||
|
public double Time { get; set; }
|
||||||
|
public int Column { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct ReleasePoint : IActionPoint
|
||||||
|
{
|
||||||
|
public double Time { get; set; }
|
||||||
|
public int Column { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,29 +2,37 @@
|
|||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using osu.Framework.Input;
|
using osu.Framework.Input;
|
||||||
|
using osu.Game.Rulesets.Mania.UI;
|
||||||
using osu.Game.Rulesets.Replays;
|
using osu.Game.Rulesets.Replays;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Mania.Replays
|
namespace osu.Game.Rulesets.Mania.Replays
|
||||||
{
|
{
|
||||||
internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler
|
internal class ManiaFramedReplayInputHandler : FramedReplayInputHandler
|
||||||
{
|
{
|
||||||
public ManiaFramedReplayInputHandler(Replay replay)
|
private readonly ManiaRulesetContainer container;
|
||||||
|
|
||||||
|
public ManiaFramedReplayInputHandler(Replay replay, ManiaRulesetContainer container)
|
||||||
: base(replay)
|
: base(replay)
|
||||||
{
|
{
|
||||||
|
this.container = container;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private ManiaPlayfield playfield;
|
||||||
public override List<InputState> GetPendingStates()
|
public override List<InputState> GetPendingStates()
|
||||||
{
|
{
|
||||||
var actions = new List<ManiaAction>();
|
var actions = new List<ManiaAction>();
|
||||||
|
|
||||||
int activeColumns = (int)(CurrentFrame.MouseX ?? 0);
|
if (playfield == null)
|
||||||
|
playfield = (ManiaPlayfield)container.Playfield;
|
||||||
|
|
||||||
|
int activeColumns = (int)(CurrentFrame.MouseX ?? 0);
|
||||||
int counter = 0;
|
int counter = 0;
|
||||||
while (activeColumns > 0)
|
while (activeColumns > 0)
|
||||||
{
|
{
|
||||||
if ((activeColumns & 1) > 0)
|
if ((activeColumns & 1) > 0)
|
||||||
actions.Add(ManiaAction.Key1 + counter);
|
actions.Add(playfield.Columns.ElementAt(counter).Action);
|
||||||
counter++;
|
counter++;
|
||||||
activeColumns >>= 1;
|
activeColumns >>= 1;
|
||||||
}
|
}
|
||||||
|
17
osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
Normal file
17
osu.Game.Rulesets.Mania/Replays/ManiaReplayFrame.cs
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using osu.Game.Rulesets.Replays;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Replays
|
||||||
|
{
|
||||||
|
public class ManiaReplayFrame : ReplayFrame
|
||||||
|
{
|
||||||
|
public override bool IsImportant => MouseX > 0;
|
||||||
|
|
||||||
|
public ManiaReplayFrame(double time, int activeColumns)
|
||||||
|
: base(time, activeColumns, null, ReplayButtonState.None)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
173
osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs
Normal file
173
osu.Game.Rulesets.Mania/Tests/TestCaseAutoGeneration.cs
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
|
using NUnit.Framework;
|
||||||
|
using osu.Game.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Mania.Objects;
|
||||||
|
using osu.Game.Rulesets.Mania.Replays;
|
||||||
|
using osu.Game.Tests.Visual;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Mania.Tests
|
||||||
|
{
|
||||||
|
[Ignore("getting CI working")]
|
||||||
|
public class TestCaseAutoGeneration : OsuTestCase
|
||||||
|
{
|
||||||
|
[Test]
|
||||||
|
public void TestSingleNote()
|
||||||
|
{
|
||||||
|
// | |
|
||||||
|
// | - |
|
||||||
|
// | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
|
||||||
|
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
|
||||||
|
Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleHoldNote()
|
||||||
|
{
|
||||||
|
// | |
|
||||||
|
// | * |
|
||||||
|
// | * |
|
||||||
|
// | * |
|
||||||
|
// | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
|
||||||
|
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
|
||||||
|
Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 0 has not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 0 has not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleNoteChord()
|
||||||
|
{
|
||||||
|
// | | |
|
||||||
|
// | - | - |
|
||||||
|
// | | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 1000, Column = 1 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
|
||||||
|
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
|
||||||
|
Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHoldNoteChord()
|
||||||
|
{
|
||||||
|
// | | |
|
||||||
|
// | * | * |
|
||||||
|
// | * | * |
|
||||||
|
// | * | * |
|
||||||
|
// | | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000, Column = 1 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 3, "Replay must have 3 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect hit time");
|
||||||
|
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect release time");
|
||||||
|
Assert.AreEqual(3, generated.Frames[1].MouseX, "Keys 1 and 2 have not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[2].MouseX, "Keys 1 and 2 have not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestSingleNoteStair()
|
||||||
|
{
|
||||||
|
// | | |
|
||||||
|
// | | - |
|
||||||
|
// | - | |
|
||||||
|
// | | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 1000 });
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 2000, Column = 1 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
|
||||||
|
Assert.AreEqual(1000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[2].Time, "Incorrect first note release time");
|
||||||
|
Assert.AreEqual(2000, generated.Frames[3].Time, "Incorrect second note hit time");
|
||||||
|
Assert.AreEqual(2000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time");
|
||||||
|
Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[2].MouseX, "Key 1 has not been released");
|
||||||
|
Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 2 has not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHoldNoteStair()
|
||||||
|
{
|
||||||
|
// | | |
|
||||||
|
// | | * |
|
||||||
|
// | * | * |
|
||||||
|
// | * | * |
|
||||||
|
// | * | |
|
||||||
|
// | | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 });
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 2000, Duration = 2000, Column = 1 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 5, "Replay must have 5 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
|
||||||
|
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect first note release time");
|
||||||
|
Assert.AreEqual(2000, generated.Frames[2].Time, "Incorrect second note hit time");
|
||||||
|
Assert.AreEqual(4000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[4].Time, "Incorrect second note release time");
|
||||||
|
Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed");
|
||||||
|
Assert.AreEqual(3, generated.Frames[2].MouseX, "Keys 1 and 2 have not been pressed");
|
||||||
|
Assert.AreEqual(2, generated.Frames[3].MouseX, "Key 1 has not been released");
|
||||||
|
Assert.AreEqual(0, generated.Frames[4].MouseX, "Key 2 has not been released");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void TestHoldNoteWithReleasePress()
|
||||||
|
{
|
||||||
|
// | | |
|
||||||
|
// | * | - |
|
||||||
|
// | * | |
|
||||||
|
// | * | |
|
||||||
|
// | | |
|
||||||
|
|
||||||
|
var beatmap = new Beatmap<ManiaHitObject>();
|
||||||
|
beatmap.HitObjects.Add(new HoldNote { StartTime = 1000, Duration = 2000 - ManiaAutoGenerator.RELEASE_DELAY });
|
||||||
|
beatmap.HitObjects.Add(new Note { StartTime = 3000, Column = 1 });
|
||||||
|
|
||||||
|
var generated = new ManiaAutoGenerator(beatmap).Generate();
|
||||||
|
|
||||||
|
Assert.IsTrue(generated.Frames.Count == 4, "Replay must have 4 frames");
|
||||||
|
Assert.AreEqual(1000, generated.Frames[1].Time, "Incorrect first note hit time");
|
||||||
|
Assert.AreEqual(3000, generated.Frames[2].Time, "Incorrect second note press time + first note release time");
|
||||||
|
Assert.AreEqual(3000 + ManiaAutoGenerator.RELEASE_DELAY, generated.Frames[3].Time, "Incorrect second note release time");
|
||||||
|
Assert.AreEqual(1, generated.Frames[1].MouseX, "Key 1 has not been pressed");
|
||||||
|
Assert.AreEqual(2, generated.Frames[2].MouseX, "Key 1 has not been released or key 2 has not been pressed");
|
||||||
|
Assert.AreEqual(0, generated.Frames[3].MouseX, "Keys 1 and 2 have not been released");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
16
osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs
Normal file
16
osu.Game.Rulesets.Mania/Tests/TestCasePerformancePoints.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2007-2017 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
|
||||||
|
{
|
||||||
|
[Ignore("getting CI working")]
|
||||||
|
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
|
||||||
|
{
|
||||||
|
public TestCasePerformancePoints()
|
||||||
|
: base(new ManiaRuleset(new RulesetInfo()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -124,6 +124,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
|||||||
|
|
||||||
protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic);
|
protected override SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new ManiaSpeedAdjustmentContainer(controlPoint, ScrollingAlgorithm.Basic);
|
||||||
|
|
||||||
protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
|
protected override FramedReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay, this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,6 +72,7 @@
|
|||||||
<Compile Include="Objects\Types\IHasColumn.cs" />
|
<Compile Include="Objects\Types\IHasColumn.cs" />
|
||||||
<Compile Include="Replays\ManiaAutoGenerator.cs" />
|
<Compile Include="Replays\ManiaAutoGenerator.cs" />
|
||||||
<Compile Include="Replays\ManiaFramedReplayInputHandler.cs" />
|
<Compile Include="Replays\ManiaFramedReplayInputHandler.cs" />
|
||||||
|
<Compile Include="Replays\ManiaReplayFrame.cs" />
|
||||||
<Compile Include="Scoring\ManiaScoreProcessor.cs" />
|
<Compile Include="Scoring\ManiaScoreProcessor.cs" />
|
||||||
<Compile Include="Objects\BarLine.cs" />
|
<Compile Include="Objects\BarLine.cs" />
|
||||||
<Compile Include="Objects\HoldNote.cs" />
|
<Compile Include="Objects\HoldNote.cs" />
|
||||||
@ -80,8 +81,10 @@
|
|||||||
<Compile Include="Objects\Note.cs" />
|
<Compile Include="Objects\Note.cs" />
|
||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="ManiaInputManager.cs" />
|
<Compile Include="ManiaInputManager.cs" />
|
||||||
|
<Compile Include="Tests\TestCaseAutoGeneration.cs" />
|
||||||
<Compile Include="Tests\TestCaseManiaHitObjects.cs" />
|
<Compile Include="Tests\TestCaseManiaHitObjects.cs" />
|
||||||
<Compile Include="Tests\TestCaseManiaPlayfield.cs" />
|
<Compile Include="Tests\TestCaseManiaPlayfield.cs" />
|
||||||
|
<Compile Include="Tests\TestCasePerformancePoints.cs" />
|
||||||
<Compile Include="Timing\GravityScrollingContainer.cs" />
|
<Compile Include="Timing\GravityScrollingContainer.cs" />
|
||||||
<Compile Include="Timing\ScrollingAlgorithm.cs" />
|
<Compile Include="Timing\ScrollingAlgorithm.cs" />
|
||||||
<Compile Include="UI\Column.cs" />
|
<Compile Include="UI\Column.cs" />
|
||||||
|
@ -45,6 +45,18 @@ namespace osu.Game.Rulesets.Osu.Objects
|
|||||||
set { Curve.Distance = value; }
|
set { Curve.Distance = value; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The position of the cursor at the point of completion of this <see cref="Slider"/> if it was hit
|
||||||
|
/// with as few movements as possible. This is set and used by difficulty calculation.
|
||||||
|
/// </summary>
|
||||||
|
internal Vector2? LazyEndPosition;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The distance travelled by the cursor upon completion of this <see cref="Slider"/> if it was hit
|
||||||
|
/// with as few movements as possible. This is set and used by difficulty calculation.
|
||||||
|
/// </summary>
|
||||||
|
internal float LazyTravelDistance;
|
||||||
|
|
||||||
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
|
public List<SampleInfoList> RepeatSamples { get; set; } = new List<SampleInfoList>();
|
||||||
public int RepeatCount { get; set; } = 1;
|
public int RepeatCount { get; set; } = 1;
|
||||||
|
|
||||||
|
@ -33,9 +33,9 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty
|
|||||||
(h as Slider)?.Curve?.Calculate();
|
(h as Slider)?.Curve?.Calculate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double Calculate(Dictionary<string, string> categoryDifficulty = null)
|
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||||
{
|
{
|
||||||
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects);
|
OsuDifficultyBeatmap beatmap = new OsuDifficultyBeatmap(Beatmap.HitObjects, TimeRate);
|
||||||
Skill[] skills =
|
Skill[] skills =
|
||||||
{
|
{
|
||||||
new Aim(),
|
new Aim(),
|
||||||
@ -67,8 +67,8 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty
|
|||||||
|
|
||||||
if (categoryDifficulty != null)
|
if (categoryDifficulty != null)
|
||||||
{
|
{
|
||||||
categoryDifficulty.Add("Aim", aimRating.ToString("0.00"));
|
categoryDifficulty.Add("Aim", aimRating);
|
||||||
categoryDifficulty.Add("Speed", speedRating.ToString("0.00"));
|
categoryDifficulty.Add("Speed", speedRating);
|
||||||
}
|
}
|
||||||
|
|
||||||
return starRating;
|
return starRating;
|
||||||
|
@ -20,12 +20,12 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
|||||||
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
|
/// Creates an enumerator, which preprocesses a list of <see cref="OsuHitObject"/>s recieved as input, wrapping them as
|
||||||
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
|
/// <see cref="OsuDifficultyHitObject"/> which contains extra data required for difficulty calculation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public OsuDifficultyBeatmap(List<OsuHitObject> objects)
|
public OsuDifficultyBeatmap(List<OsuHitObject> objects, double timeRate)
|
||||||
{
|
{
|
||||||
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
|
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
|
||||||
// This should probably happen before the objects reach the difficulty calculator.
|
// This should probably happen before the objects reach the difficulty calculator.
|
||||||
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
|
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
|
||||||
difficultyObjects = createDifficultyObjectEnumerator(objects);
|
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
|||||||
|
|
||||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||||
|
|
||||||
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects)
|
private IEnumerator<OsuDifficultyHitObject> createDifficultyObjectEnumerator(List<OsuHitObject> objects, double timeRate)
|
||||||
{
|
{
|
||||||
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
|
// We will process OsuHitObjects in groups of three to form a triangle, so we can calculate an angle for each object.
|
||||||
OsuHitObject[] triangle = new OsuHitObject[3];
|
OsuHitObject[] triangle = new OsuHitObject[3];
|
||||||
@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
|||||||
triangle[1] = triangle[0];
|
triangle[1] = triangle[0];
|
||||||
triangle[0] = objects[i];
|
triangle[0] = objects[i];
|
||||||
|
|
||||||
yield return new OsuDifficultyHitObject(triangle);
|
yield return new OsuDifficultyHitObject(triangle, timeRate);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using OpenTK;
|
||||||
using osu.Game.Rulesets.Osu.Objects;
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
||||||
@ -33,13 +35,17 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
|||||||
|
|
||||||
private const int normalized_radius = 52;
|
private const int normalized_radius = 52;
|
||||||
|
|
||||||
|
private readonly double timeRate;
|
||||||
|
|
||||||
private readonly OsuHitObject[] t;
|
private readonly OsuHitObject[] t;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes the object calculating extra data required for difficulty calculation.
|
/// Initializes the object calculating extra data required for difficulty calculation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public OsuDifficultyHitObject(OsuHitObject[] triangle)
|
public OsuDifficultyHitObject(OsuHitObject[] triangle, double timeRate)
|
||||||
{
|
{
|
||||||
|
this.timeRate = timeRate;
|
||||||
|
|
||||||
t = triangle;
|
t = triangle;
|
||||||
BaseObject = t[0];
|
BaseObject = t[0];
|
||||||
setDistances();
|
setDistances();
|
||||||
@ -57,14 +63,53 @@ namespace osu.Game.Rulesets.Osu.OsuDifficulty.Preprocessing
|
|||||||
scalingFactor *= 1 + smallCircleBonus;
|
scalingFactor *= 1 + smallCircleBonus;
|
||||||
}
|
}
|
||||||
|
|
||||||
Distance = (t[0].StackedPosition - t[1].StackedPosition).Length * scalingFactor;
|
Vector2 lastCursorPosition = t[1].StackedPosition;
|
||||||
|
float lastTravelDistance = 0;
|
||||||
|
|
||||||
|
var lastSlider = t[1] as Slider;
|
||||||
|
if (lastSlider != null)
|
||||||
|
{
|
||||||
|
computeSliderCursorPosition(lastSlider);
|
||||||
|
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
|
||||||
|
lastTravelDistance = lastSlider.LazyTravelDistance;
|
||||||
|
}
|
||||||
|
|
||||||
|
Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setTimingValues()
|
private void setTimingValues()
|
||||||
{
|
{
|
||||||
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
|
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
|
||||||
DeltaTime = Math.Max(40, t[0].StartTime - t[1].StartTime);
|
DeltaTime = Math.Max(40, (t[0].StartTime - t[1].StartTime) / timeRate);
|
||||||
TimeUntilHit = 450; // BaseObject.PreEmpt;
|
TimeUntilHit = 450; // BaseObject.PreEmpt;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void computeSliderCursorPosition(Slider slider)
|
||||||
|
{
|
||||||
|
if (slider.LazyEndPosition != null)
|
||||||
|
return;
|
||||||
|
slider.LazyEndPosition = slider.StackedPosition;
|
||||||
|
|
||||||
|
float approxFollowCircleRadius = (float)(slider.Radius * 3);
|
||||||
|
var computeVertex = new Action<double>(t =>
|
||||||
|
{
|
||||||
|
var diff = slider.PositionAt(t) - slider.LazyEndPosition.Value;
|
||||||
|
float dist = diff.Length;
|
||||||
|
|
||||||
|
if (dist > approxFollowCircleRadius)
|
||||||
|
{
|
||||||
|
// The cursor would be outside the follow circle, we need to move it
|
||||||
|
diff.Normalize(); // Obtain direction of diff
|
||||||
|
dist -= approxFollowCircleRadius;
|
||||||
|
slider.LazyEndPosition += diff * dist;
|
||||||
|
slider.LazyTravelDistance += dist;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var scoringTimes = slider.Ticks.Select(t => t.StartTime).Concat(slider.RepeatPoints.Select(r => r.StartTime)).OrderBy(t => t);
|
||||||
|
foreach (var time in scoringTimes)
|
||||||
|
computeVertex(time);
|
||||||
|
computeVertex(slider.EndTime);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,8 @@ using System.Linq;
|
|||||||
using osu.Framework.Graphics;
|
using osu.Framework.Graphics;
|
||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Framework.Input.Bindings;
|
using osu.Framework.Input.Bindings;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
using osu.Game.Rulesets.Osu.Scoring;
|
||||||
using osu.Game.Rulesets.Osu.Edit;
|
using osu.Game.Rulesets.Osu.Edit;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
|
|
||||||
@ -116,6 +118,8 @@ namespace osu.Game.Rulesets.Osu
|
|||||||
|
|
||||||
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
|
public override DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null) => new OsuDifficultyCalculator(beatmap, mods);
|
||||||
|
|
||||||
|
public override PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => new OsuPerformanceCalculator(this, beatmap, score);
|
||||||
|
|
||||||
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
|
||||||
|
|
||||||
public override string Description => "osu!";
|
public override string Description => "osu!";
|
||||||
|
199
osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs
Normal file
199
osu.Game.Rulesets.Osu/Scoring/OsuPerformanceCalculator.cs
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
// Copyright (c) 2007-2017 ppy Pty Ltd <contact@ppy.sh>.
|
||||||
|
// 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.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||||
|
using osu.Game.Rulesets.Osu.Mods;
|
||||||
|
using osu.Game.Rulesets.Osu.Objects;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Osu.Scoring
|
||||||
|
{
|
||||||
|
public class OsuPerformanceCalculator : PerformanceCalculator<OsuHitObject>
|
||||||
|
{
|
||||||
|
private readonly int countHitCircles;
|
||||||
|
private readonly int beatmapMaxCombo;
|
||||||
|
|
||||||
|
private Mod[] mods;
|
||||||
|
private double realApproachRate;
|
||||||
|
private double accuracy;
|
||||||
|
private int scoreMaxCombo;
|
||||||
|
private int count300;
|
||||||
|
private int count100;
|
||||||
|
private int count50;
|
||||||
|
private int countMiss;
|
||||||
|
|
||||||
|
public OsuPerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
|
||||||
|
: base(ruleset, beatmap, score)
|
||||||
|
{
|
||||||
|
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
|
||||||
|
|
||||||
|
beatmapMaxCombo = Beatmap.HitObjects.Count;
|
||||||
|
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.RepeatCount + s.Ticks.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
public override double Calculate(Dictionary<string, double> categoryRatings = null)
|
||||||
|
{
|
||||||
|
mods = Score.Mods;
|
||||||
|
accuracy = Score.Accuracy;
|
||||||
|
scoreMaxCombo = Score.MaxCombo;
|
||||||
|
count300 = Convert.ToInt32(Score.Statistics["300"]);
|
||||||
|
count100 = Convert.ToInt32(Score.Statistics["100"]);
|
||||||
|
count50 = Convert.ToInt32(Score.Statistics["50"]);
|
||||||
|
countMiss = Convert.ToInt32(Score.Statistics["x"]);
|
||||||
|
|
||||||
|
// Don't count scores made with supposedly unranked mods
|
||||||
|
if (mods.Any(m => !m.Ranked))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Todo: In the future we should apply changes to PreEmpt/AR at an OsuHitObject/BaseDifficulty level, but this is done
|
||||||
|
// locally for now as doing so would modify animations and other things unexpectedly
|
||||||
|
// DO NOT MODIFY THIS
|
||||||
|
double ar = Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate;
|
||||||
|
if (mods.Any(m => m is OsuModHardRock))
|
||||||
|
ar = Math.Min(10, ar * 1.4);
|
||||||
|
if (mods.Any(m => m is OsuModEasy))
|
||||||
|
ar = Math.Max(0, ar / 2);
|
||||||
|
double preEmpt = BeatmapDifficulty.DifficultyRange(ar, 1800, 1200, 450);
|
||||||
|
realApproachRate = preEmpt > 1200 ? (1800 - preEmpt) / 120 : (1200 - preEmpt) / 150 + 5;
|
||||||
|
|
||||||
|
// Custom multipliers for NoFail and SpunOut.
|
||||||
|
double multiplier = 1.12f; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModNoFail))
|
||||||
|
multiplier *= 0.90f;
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModSpunOut))
|
||||||
|
multiplier *= 0.95f;
|
||||||
|
|
||||||
|
double aimValue = computeAimValue();
|
||||||
|
double speedValue = computeSpeedValue();
|
||||||
|
double accuracyValue = computeAccuracyValue();
|
||||||
|
double totalValue =
|
||||||
|
Math.Pow(
|
||||||
|
Math.Pow(aimValue, 1.1f) +
|
||||||
|
Math.Pow(speedValue, 1.1f) +
|
||||||
|
Math.Pow(accuracyValue, 1.1f), 1.0f / 1.1f
|
||||||
|
) * multiplier;
|
||||||
|
|
||||||
|
if (categoryRatings != null)
|
||||||
|
{
|
||||||
|
categoryRatings.Add("Aim", aimValue);
|
||||||
|
categoryRatings.Add("Speed", speedValue);
|
||||||
|
categoryRatings.Add("Accuracy", accuracyValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double computeAimValue()
|
||||||
|
{
|
||||||
|
double aimValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Aim"] / 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) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
|
||||||
|
|
||||||
|
aimValue *= lengthBonus;
|
||||||
|
|
||||||
|
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||||
|
aimValue *= Math.Pow(0.97f, countMiss);
|
||||||
|
|
||||||
|
// Combo scaling
|
||||||
|
if (beatmapMaxCombo > 0)
|
||||||
|
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
|
||||||
|
|
||||||
|
double approachRateFactor = 1.0f;
|
||||||
|
if (realApproachRate > 10.33f)
|
||||||
|
approachRateFactor += 0.45f * (realApproachRate - 10.33f);
|
||||||
|
else if (realApproachRate < 8.0f)
|
||||||
|
{
|
||||||
|
// HD is worth more with lower ar!
|
||||||
|
if (mods.Any(h => h is OsuModHidden))
|
||||||
|
approachRateFactor += 0.02f * (8.0f - realApproachRate);
|
||||||
|
else
|
||||||
|
approachRateFactor += 0.01f * (8.0f - realApproachRate);
|
||||||
|
}
|
||||||
|
|
||||||
|
aimValue *= approachRateFactor;
|
||||||
|
|
||||||
|
if (mods.Any(h => h is OsuModHidden))
|
||||||
|
aimValue *= 1.18f;
|
||||||
|
|
||||||
|
if (mods.Any(h => h is OsuModFlashlight))
|
||||||
|
{
|
||||||
|
// Apply length bonus again if flashlight is on simply because it becomes a lot harder on longer maps.
|
||||||
|
aimValue *= 1.45f * lengthBonus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scale the aim value with accuracy _slightly_
|
||||||
|
aimValue *= 0.5f + accuracy / 2.0f;
|
||||||
|
// It is important to also consider accuracy difficulty when doing that
|
||||||
|
aimValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
|
||||||
|
|
||||||
|
return aimValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double computeSpeedValue()
|
||||||
|
{
|
||||||
|
double speedValue = Math.Pow(5.0f * Math.Max(1.0f, Attributes["Speed"] / 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) +
|
||||||
|
(totalHits > 2000 ? Math.Log10(totalHits / 2000.0f) * 0.5f : 0.0f);
|
||||||
|
|
||||||
|
// Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available
|
||||||
|
speedValue *= Math.Pow(0.97f, countMiss);
|
||||||
|
|
||||||
|
// Combo scaling
|
||||||
|
if (beatmapMaxCombo > 0)
|
||||||
|
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8f) / Math.Pow(beatmapMaxCombo, 0.8f), 1.0f);
|
||||||
|
|
||||||
|
// Scale the speed value with accuracy _slightly_
|
||||||
|
speedValue *= 0.5f + accuracy / 2.0f;
|
||||||
|
// It is important to also consider accuracy difficulty when doing that
|
||||||
|
speedValue *= 0.98f + Math.Pow(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty, 2) / 2500;
|
||||||
|
|
||||||
|
return speedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double computeAccuracyValue()
|
||||||
|
{
|
||||||
|
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
|
||||||
|
double betterAccuracyPercentage;
|
||||||
|
int amountHitObjectsWithAccuracy = countHitCircles;
|
||||||
|
|
||||||
|
if (amountHitObjectsWithAccuracy > 0)
|
||||||
|
betterAccuracyPercentage = ((count300 - (totalHits - amountHitObjectsWithAccuracy)) * 6 + count100 * 2 + count50) / (amountHitObjectsWithAccuracy * 6);
|
||||||
|
else
|
||||||
|
betterAccuracyPercentage = 0;
|
||||||
|
|
||||||
|
// It is possible to reach a negative accuracy with this formula. Cap it at zero - zero points
|
||||||
|
if (betterAccuracyPercentage < 0)
|
||||||
|
betterAccuracyPercentage = 0;
|
||||||
|
|
||||||
|
// Lots of arbitrary values from testing.
|
||||||
|
// Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution
|
||||||
|
double accuracyValue = Math.Pow(1.52163f, Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) * Math.Pow(betterAccuracyPercentage, 24) * 2.83f;
|
||||||
|
|
||||||
|
// Bonus for many hitcircles - it's harder to keep good accuracy up for longer
|
||||||
|
accuracyValue *= Math.Min(1.15f, Math.Pow(amountHitObjectsWithAccuracy / 1000.0f, 0.3f));
|
||||||
|
|
||||||
|
if (mods.Any(m => m is OsuModHidden))
|
||||||
|
accuracyValue *= 1.02f;
|
||||||
|
if (mods.Any(m => m is OsuModFlashlight))
|
||||||
|
accuracyValue *= 1.02f;
|
||||||
|
|
||||||
|
return accuracyValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double totalHits => count300 + count100 + count50 + countMiss;
|
||||||
|
private double totalSuccessfulHits => count300 + count100 + count50;
|
||||||
|
|
||||||
|
protected override BeatmapConverter<OsuHitObject> CreateBeatmapConverter() => new OsuBeatmapConverter();
|
||||||
|
}
|
||||||
|
}
|
16
osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs
Normal file
16
osu.Game.Rulesets.Osu/Tests/TestCasePerformancePoints.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2007-2017 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
|
||||||
|
{
|
||||||
|
[Ignore("getting CI working")]
|
||||||
|
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
|
||||||
|
{
|
||||||
|
public TestCasePerformancePoints()
|
||||||
|
: base(new OsuRuleset(new RulesetInfo()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -86,9 +86,11 @@
|
|||||||
<Compile Include="OsuInputManager.cs" />
|
<Compile Include="OsuInputManager.cs" />
|
||||||
<Compile Include="Replays\OsuReplayInputHandler.cs" />
|
<Compile Include="Replays\OsuReplayInputHandler.cs" />
|
||||||
<Compile Include="Tests\TestCaseHitObjects.cs" />
|
<Compile Include="Tests\TestCaseHitObjects.cs" />
|
||||||
|
<Compile Include="Tests\TestCasePerformancePoints.cs" />
|
||||||
<Compile Include="UI\Cursor\CursorTrail.cs" />
|
<Compile Include="UI\Cursor\CursorTrail.cs" />
|
||||||
<Compile Include="UI\Cursor\GameplayCursor.cs" />
|
<Compile Include="UI\Cursor\GameplayCursor.cs" />
|
||||||
<Compile Include="UI\OsuSettings.cs" />
|
<Compile Include="UI\OsuSettings.cs" />
|
||||||
|
<Compile Include="Scoring\OsuPerformanceCalculator.cs" />
|
||||||
<Compile Include="Scoring\OsuScoreProcessor.cs" />
|
<Compile Include="Scoring\OsuScoreProcessor.cs" />
|
||||||
<Compile Include="UI\OsuRulesetContainer.cs" />
|
<Compile Include="UI\OsuRulesetContainer.cs" />
|
||||||
<Compile Include="UI\OsuPlayfield.cs" />
|
<Compile Include="UI\OsuPlayfield.cs" />
|
||||||
|
@ -5,7 +5,6 @@ using osu.Game.Beatmaps;
|
|||||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||||
using osu.Game.Rulesets.Taiko.Objects;
|
using osu.Game.Rulesets.Taiko.Objects;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets.Taiko
|
namespace osu.Game.Rulesets.Taiko
|
||||||
@ -36,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public override double Calculate(Dictionary<string, string> categoryDifficulty = null)
|
public override double Calculate(Dictionary<string, double> categoryDifficulty = null)
|
||||||
{
|
{
|
||||||
// Fill our custom DifficultyHitObject class, that carries additional information
|
// Fill our custom DifficultyHitObject class, that carries additional information
|
||||||
difficultyHitObjects.Clear();
|
difficultyHitObjects.Clear();
|
||||||
@ -53,8 +52,8 @@ namespace osu.Game.Rulesets.Taiko
|
|||||||
|
|
||||||
if (categoryDifficulty != null)
|
if (categoryDifficulty != null)
|
||||||
{
|
{
|
||||||
categoryDifficulty.Add("Strain", starRating.ToString("0.00", CultureInfo.InvariantCulture));
|
categoryDifficulty.Add("Strain", starRating);
|
||||||
categoryDifficulty.Add("Hit window 300", (35 /*HitObjectManager.HitWindow300*/ / TimeRate).ToString("0.00", CultureInfo.InvariantCulture));
|
categoryDifficulty.Add("Hit window 300", 35 /*HitObjectManager.HitWindow300*/ / TimeRate);
|
||||||
}
|
}
|
||||||
|
|
||||||
return starRating;
|
return starRating;
|
||||||
|
16
osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs
Normal file
16
osu.Game.Rulesets.Taiko/Tests/TestCasePerformancePoints.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
// Copyright (c) 2007-2017 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
|
||||||
|
{
|
||||||
|
[Ignore("getting CI working")]
|
||||||
|
public class TestCasePerformancePoints : Game.Tests.Visual.TestCasePerformancePoints
|
||||||
|
{
|
||||||
|
public TestCasePerformancePoints()
|
||||||
|
: base(new TaikoRuleset(new RulesetInfo()))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -83,6 +83,7 @@
|
|||||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||||
<Compile Include="Scoring\TaikoScoreProcessor.cs" />
|
<Compile Include="Scoring\TaikoScoreProcessor.cs" />
|
||||||
<Compile Include="TaikoInputManager.cs" />
|
<Compile Include="TaikoInputManager.cs" />
|
||||||
|
<Compile Include="Tests\TestCasePerformancePoints.cs" />
|
||||||
<Compile Include="Tests\TestCaseTaikoPlayfield.cs" />
|
<Compile Include="Tests\TestCaseTaikoPlayfield.cs" />
|
||||||
<Compile Include="UI\HitTarget.cs" />
|
<Compile Include="UI\HitTarget.cs" />
|
||||||
<Compile Include="UI\InputDrum.cs" />
|
<Compile Include="UI\InputDrum.cs" />
|
||||||
|
@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps
|
|||||||
{
|
{
|
||||||
protected double TimeRate = 1;
|
protected double TimeRate = 1;
|
||||||
|
|
||||||
public abstract double Calculate(Dictionary<string, string> categoryDifficulty = null);
|
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public abstract class DifficultyCalculator<T> : DifficultyCalculator where T : HitObject
|
public abstract class DifficultyCalculator<T> : DifficultyCalculator where T : HitObject
|
||||||
|
@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Replays
|
|||||||
{
|
{
|
||||||
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
|
public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0);
|
||||||
|
|
||||||
public bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight);
|
public virtual bool IsImportant => MouseX.HasValue && MouseY.HasValue && (MouseLeft || MouseRight);
|
||||||
|
|
||||||
public float? MouseX;
|
public float? MouseX;
|
||||||
public float? MouseY;
|
public float? MouseY;
|
||||||
@ -68,4 +68,4 @@ namespace osu.Game.Rulesets.Replays
|
|||||||
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
|
return $"{Time}\t({MouseX},{MouseY})\t{MouseLeft}\t{MouseRight}\t{MouseLeft1}\t{MouseRight1}\t{MouseLeft2}\t{MouseRight2}\t{ButtonState}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ using osu.Game.Graphics;
|
|||||||
using osu.Game.Overlays.Settings;
|
using osu.Game.Overlays.Settings;
|
||||||
using osu.Game.Rulesets.Edit;
|
using osu.Game.Rulesets.Edit;
|
||||||
using osu.Game.Rulesets.Mods;
|
using osu.Game.Rulesets.Mods;
|
||||||
|
using osu.Game.Rulesets.Scoring;
|
||||||
using osu.Game.Rulesets.UI;
|
using osu.Game.Rulesets.UI;
|
||||||
|
|
||||||
namespace osu.Game.Rulesets
|
namespace osu.Game.Rulesets
|
||||||
@ -50,6 +51,8 @@ namespace osu.Game.Rulesets
|
|||||||
|
|
||||||
public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null);
|
public abstract DifficultyCalculator CreateDifficultyCalculator(Beatmap beatmap, Mod[] mods = null);
|
||||||
|
|
||||||
|
public virtual PerformanceCalculator CreatePerformanceCalculator(Beatmap beatmap, Score score) => null;
|
||||||
|
|
||||||
public virtual HitObjectComposer CreateHitObjectComposer() => null;
|
public virtual HitObjectComposer CreateHitObjectComposer() => null;
|
||||||
|
|
||||||
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle };
|
public virtual Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.fa_question_circle };
|
||||||
|
35
osu.Game/Rulesets/Scoring/PerformanceCalculator.cs
Normal file
35
osu.Game/Rulesets/Scoring/PerformanceCalculator.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2007-2017 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.Objects;
|
||||||
|
|
||||||
|
namespace osu.Game.Rulesets.Scoring
|
||||||
|
{
|
||||||
|
public abstract class PerformanceCalculator
|
||||||
|
{
|
||||||
|
public abstract double Calculate(Dictionary<string, double> categoryDifficulty = null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract class PerformanceCalculator<TObject> : PerformanceCalculator
|
||||||
|
where TObject : HitObject
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, double> attributes = new Dictionary<string, double>();
|
||||||
|
protected IDictionary<string, double> Attributes => attributes;
|
||||||
|
|
||||||
|
protected readonly Beatmap<TObject> Beatmap;
|
||||||
|
protected readonly Score Score;
|
||||||
|
|
||||||
|
protected PerformanceCalculator(Ruleset ruleset, Beatmap beatmap, Score score)
|
||||||
|
{
|
||||||
|
Beatmap = CreateBeatmapConverter().Convert(beatmap);
|
||||||
|
Score = score;
|
||||||
|
|
||||||
|
var diffCalc = ruleset.CreateDifficultyCalculator(beatmap, score.Mods);
|
||||||
|
diffCalc.Calculate(attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract BeatmapConverter<TObject> CreateBeatmapConverter();
|
||||||
|
}
|
||||||
|
}
|
@ -261,6 +261,7 @@ namespace osu.Game.Screens.Select
|
|||||||
description.Text = null;
|
description.Text = null;
|
||||||
source.Text = null;
|
source.Text = null;
|
||||||
tags.Text = null;
|
tags.Text = null;
|
||||||
|
|
||||||
advanced.Beatmap = new BeatmapInfo
|
advanced.Beatmap = new BeatmapInfo
|
||||||
{
|
{
|
||||||
StarDifficulty = 0,
|
StarDifficulty = 0,
|
||||||
@ -306,36 +307,16 @@ namespace osu.Game.Screens.Select
|
|||||||
|
|
||||||
private class MetadataSection : Container
|
private class MetadataSection : Container
|
||||||
{
|
{
|
||||||
private readonly TextFlowContainer textFlow;
|
private readonly FillFlowContainer textContainer;
|
||||||
|
private TextFlowContainer textFlow;
|
||||||
public string Text
|
|
||||||
{
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(value))
|
|
||||||
{
|
|
||||||
this.FadeOut(transition_duration);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.FadeIn(transition_duration);
|
|
||||||
textFlow.Clear();
|
|
||||||
textFlow.AddText(value, s => s.TextSize = 14);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Color4 TextColour
|
|
||||||
{
|
|
||||||
get { return textFlow.Colour; }
|
|
||||||
set { textFlow.Colour = value; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public MetadataSection(string title)
|
public MetadataSection(string title)
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X;
|
RelativeSizeAxes = Axes.X;
|
||||||
AutoSizeAxes = Axes.Y;
|
AutoSizeAxes = Axes.Y;
|
||||||
|
Alpha = 0;
|
||||||
|
|
||||||
InternalChild = new FillFlowContainer
|
InternalChild = textContainer = new FillFlowContainer
|
||||||
{
|
{
|
||||||
RelativeSizeAxes = Axes.X,
|
RelativeSizeAxes = Axes.X,
|
||||||
AutoSizeAxes = Axes.Y,
|
AutoSizeAxes = Axes.Y,
|
||||||
@ -361,6 +342,44 @@ namespace osu.Game.Screens.Select
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string Text
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
this.FadeOut(transition_duration);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTextAsync(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTextAsync(string text)
|
||||||
|
{
|
||||||
|
LoadComponentAsync(new TextFlowContainer(s => s.TextSize = 14)
|
||||||
|
{
|
||||||
|
RelativeSizeAxes = Axes.X,
|
||||||
|
AutoSizeAxes = Axes.Y,
|
||||||
|
Colour = textFlow.Colour,
|
||||||
|
Text = text
|
||||||
|
}, loaded =>
|
||||||
|
{
|
||||||
|
textFlow?.Expire();
|
||||||
|
textContainer.Add(textFlow = loaded);
|
||||||
|
|
||||||
|
// fade in if we haven't yet.
|
||||||
|
this.FadeIn(transition_duration);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public Color4 TextColour
|
||||||
|
{
|
||||||
|
get { return textFlow.Colour; }
|
||||||
|
set { textFlow.Colour = value; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class DimmedLoadingAnimation : VisibilityContainer
|
private class DimmedLoadingAnimation : VisibilityContainer
|
||||||
|
@ -211,7 +211,7 @@ namespace osu.Game.Screens.Select
|
|||||||
if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false)
|
if (Beatmap.Value.BeatmapSetInfo?.DeletePending == false)
|
||||||
carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false);
|
carousel.SelectBeatmap(Beatmap.Value.BeatmapInfo, false);
|
||||||
else
|
else
|
||||||
carousel.SelectNext();
|
carousel.SelectNextRandom();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void carouselRaisedStart(InputState state = null)
|
private void carouselRaisedStart(InputState state = null)
|
||||||
|
395
osu.Game/Tests/Visual/TestCasePerformancePoints.cs
Normal file
395
osu.Game/Tests/Visual/TestCasePerformancePoints.cs
Normal file
@ -0,0 +1,395 @@
|
|||||||
|
// Copyright (c) 2007-2017 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.Extensions.IEnumerableExtensions;
|
||||||
|
using osu.Framework.Graphics;
|
||||||
|
using osu.Framework.Graphics.Containers;
|
||||||
|
using osu.Framework.Graphics.Cursor;
|
||||||
|
using osu.Framework.Graphics.Shapes;
|
||||||
|
using osu.Framework.Graphics.Sprites;
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
|
||||||
|
public BeatmapList(Ruleset ruleset)
|
||||||
|
{
|
||||||
|
this.ruleset = ruleset;
|
||||||
|
|
||||||
|
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 < 0 || b.RulesetID == ruleset.LegacyID);
|
||||||
|
|
||||||
|
allBeatmaps.ForEach(b => beatmapDisplays.Add(new BeatmapDisplay(b)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BeatmapDisplay : CompositeDrawable, IHasTooltip
|
||||||
|
{
|
||||||
|
private readonly OsuSpriteText text;
|
||||||
|
private readonly BeatmapInfo beatmap;
|
||||||
|
|
||||||
|
private BeatmapManager beatmaps;
|
||||||
|
private OsuGameBase osuGame;
|
||||||
|
|
||||||
|
private bool isSelected;
|
||||||
|
|
||||||
|
public string TooltipText => text.Text;
|
||||||
|
|
||||||
|
public BeatmapDisplay(BeatmapInfo beatmap)
|
||||||
|
{
|
||||||
|
this.beatmap = beatmap;
|
||||||
|
|
||||||
|
AutoSizeAxes = Axes.Both;
|
||||||
|
InternalChild = text = new OsuSpriteText();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool OnClick(InputState state)
|
||||||
|
{
|
||||||
|
if (osuGame.Beatmap.Value.BeatmapInfo.ID == beatmap.ID)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
osuGame.Beatmap.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[BackgroundDependencyLoader]
|
||||||
|
private void load(OsuGameBase osuGame, BeatmapManager beatmaps)
|
||||||
|
{
|
||||||
|
this.osuGame = osuGame;
|
||||||
|
this.beatmaps = beatmaps;
|
||||||
|
|
||||||
|
var working = beatmaps.GetWorkingBeatmap(beatmap);
|
||||||
|
text.Text = $"{working.Metadata.Artist} - {working.Metadata.Title} ({working.Metadata.AuthorString}) [{working.BeatmapInfo.Version}]";
|
||||||
|
|
||||||
|
osuGame.Beatmap.ValueChanged += beatmapChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void beatmapChanged(WorkingBeatmap newBeatmap)
|
||||||
|
{
|
||||||
|
if (isSelected)
|
||||||
|
this.FadeColour(Color4.White, 100);
|
||||||
|
isSelected = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PerformanceList : CompositeDrawable
|
||||||
|
{
|
||||||
|
private readonly FillFlowContainer<PerformanceDisplay> scores;
|
||||||
|
private APIAccess api;
|
||||||
|
|
||||||
|
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(OsuGameBase osuGame, APIAccess api)
|
||||||
|
{
|
||||||
|
this.api = api;
|
||||||
|
|
||||||
|
if (!api.IsLoggedIn)
|
||||||
|
{
|
||||||
|
InternalChild = new SpriteText
|
||||||
|
{
|
||||||
|
Anchor = Anchor.TopCentre,
|
||||||
|
Origin = Anchor.TopCentre,
|
||||||
|
Text = "Please login to see online scores",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
osuGame.Beatmap.ValueChanged += beatmapChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GetScoresRequest lastRequest;
|
||||||
|
private void beatmapChanged(WorkingBeatmap newBeatmap)
|
||||||
|
{
|
||||||
|
lastRequest?.Cancel();
|
||||||
|
scores.Clear();
|
||||||
|
|
||||||
|
if (!api.IsLoggedIn)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lastRequest = new GetScoresRequest(newBeatmap.BeatmapInfo);
|
||||||
|
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 Beatmap beatmap;
|
||||||
|
|
||||||
|
public PerformanceDisplay(Score score, Beatmap 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(OsuGameBase osuGame)
|
||||||
|
{
|
||||||
|
osuGame.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -50,7 +50,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
string instantiation = ruleset?.AssemblyQualifiedName;
|
string instantiation = ruleset?.AssemblyQualifiedName;
|
||||||
|
|
||||||
foreach (var r in rulesets.AvailableRulesets.Where(rs => instantiation == null || rs.InstantiationInfo == instantiation))
|
foreach (var r in rulesets.AvailableRulesets.Where(rs => instantiation == null || rs.InstantiationInfo == instantiation))
|
||||||
AddStep(r.Name, () => loadPlayerFor(r));
|
{
|
||||||
|
Player p = null;
|
||||||
|
AddStep(r.Name, () => p = loadPlayerFor(r));
|
||||||
|
AddUntilStep(() => p.IsLoaded);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Beatmap CreateBeatmap()
|
protected virtual Beatmap CreateBeatmap()
|
||||||
@ -64,7 +68,7 @@ namespace osu.Game.Tests.Visual
|
|||||||
return beatmap;
|
return beatmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPlayerFor(RulesetInfo r)
|
private Player loadPlayerFor(RulesetInfo r)
|
||||||
{
|
{
|
||||||
var beatmap = CreateBeatmap();
|
var beatmap = CreateBeatmap();
|
||||||
|
|
||||||
@ -78,7 +82,11 @@ namespace osu.Game.Tests.Visual
|
|||||||
if (Player != null)
|
if (Player != null)
|
||||||
Remove(Player);
|
Remove(Player);
|
||||||
|
|
||||||
LoadScreen(CreatePlayer(working, instance));
|
var player = CreatePlayer(working, instance);
|
||||||
|
|
||||||
|
LoadComponentAsync(player, LoadScreen);
|
||||||
|
|
||||||
|
return player;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player
|
protected virtual Player CreatePlayer(WorkingBeatmap beatmap, Ruleset ruleset) => new Player
|
||||||
|
@ -633,6 +633,7 @@
|
|||||||
<Compile Include="Rulesets\Ruleset.cs" />
|
<Compile Include="Rulesets\Ruleset.cs" />
|
||||||
<Compile Include="Rulesets\RulesetInfo.cs" />
|
<Compile Include="Rulesets\RulesetInfo.cs" />
|
||||||
<Compile Include="Rulesets\RulesetStore.cs" />
|
<Compile Include="Rulesets\RulesetStore.cs" />
|
||||||
|
<Compile Include="Rulesets\Scoring\PerformanceCalculator.cs" />
|
||||||
<Compile Include="Rulesets\Scoring\Score.cs" />
|
<Compile Include="Rulesets\Scoring\Score.cs" />
|
||||||
<Compile Include="Rulesets\Scoring\ScoreProcessor.cs" />
|
<Compile Include="Rulesets\Scoring\ScoreProcessor.cs" />
|
||||||
<Compile Include="Rulesets\Scoring\ScoreRank.cs" />
|
<Compile Include="Rulesets\Scoring\ScoreRank.cs" />
|
||||||
@ -788,6 +789,7 @@
|
|||||||
<Compile Include="Tests\Beatmaps\TestWorkingBeatmap.cs" />
|
<Compile Include="Tests\Beatmaps\TestWorkingBeatmap.cs" />
|
||||||
<Compile Include="Tests\Platform\TestStorage.cs" />
|
<Compile Include="Tests\Platform\TestStorage.cs" />
|
||||||
<Compile Include="Tests\Visual\OsuTestCase.cs" />
|
<Compile Include="Tests\Visual\OsuTestCase.cs" />
|
||||||
|
<Compile Include="Tests\Visual\TestCasePerformancePoints.cs" />
|
||||||
<Compile Include="Tests\Visual\ScreenTestCase.cs" />
|
<Compile Include="Tests\Visual\ScreenTestCase.cs" />
|
||||||
<Compile Include="Tests\Visual\TestCasePlayer.cs" />
|
<Compile Include="Tests\Visual\TestCasePlayer.cs" />
|
||||||
<Compile Include="Users\Avatar.cs" />
|
<Compile Include="Users\Avatar.cs" />
|
||||||
|
Reference in New Issue
Block a user