1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 19:22:54 +08:00

Merge pull request #1609 from smoogipoo/mania-auto-generation-fixes

Fix osu!mania autoplay generation
This commit is contained in:
Dean Herbert 2017-11-29 19:22:52 +09:00 committed by GitHub
commit 8103849273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 104 deletions

View File

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

View File

@ -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" } };
} }
@ -32,103 +28,50 @@ namespace osu.Game.Rulesets.Mania.Replays
// 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 ManiaReplayFrame(-100000, 0)); 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 ManiaReplayFrame(groupTime, activeColumns));
// 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 ManiaReplayFrame(groupEndTime, activeColumnsAtEnd));
}
} }
Replay.Frames = Replay.Frames
// Pick the maximum activeColumns for all frames at the same time
.GroupBy(f => f.Time)
.Select(g => new ManiaReplayFrame(g.First().Time, maxMouseX(g)))
.Cast<ReplayFrame>()
// 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 int 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; }
} }
} }
} }

View File

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

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

View File

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

View File

@ -81,6 +81,7 @@
<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="Tests\TestCasePerformancePoints.cs" />