mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 18:29:58 +08:00
91f3be5fea
Closes https://github.com/ppy/osu/issues/32420. The failure cause here is that in editor the beatmap version for the beatmap affected (or... any beatmap, really), is 0 (ZERO). That is probably a regression from https://github.com/ppy/osu/pull/32315, but like... can we universally agree that calling that change "a regression" in any capacity is dumb? Like what was that code *doing* playing dumb reference games and copying stuff into an arbitrary instance that could get or not get used later on? And now you have a 50/50 chance of accessing the *correct* model's field, depending on whether you go via `BeatmapInfo` or `Beatmap.BeatmapInfo`? Moving the field to `IBeatmap`, i.e. what is by now - by consensus, since https://github.com/ppy/osu/pull/28473 - supposed to be the "decoded and materialised" beatmap, fixes this issue. I probably should have done this as part of https://github.com/ppy/osu/pull/28473 but it slipped my mind. Probably for the better too because this change has rather large chances of breaking stuff so maybe better to examine it in isolation (via diffcalc runs or whatever). For added humour points, you'd say that the field on `BeatmapInfo` was not `[Ignore]`d, so this is a realm schema change, right? No. As far as I can tell, it's not. I opened realm studio and `BeatmapVersion` *is not a listed column` on `Beatmap` models. I'm also not gonna get into the fact that I think `EditorBeatmap` doing dumb games with juggling two `BeatmapInfo` references since https://github.com/ppy/osu/pull/15075 is bad, because I don't think I have the mental capacity to hotfix this by going down that train of thought.
892 lines
36 KiB
C#
892 lines
36 KiB
C#
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.CompilerServices;
|
|
using System.Text;
|
|
using NUnit.Framework;
|
|
using osu.Framework.Extensions;
|
|
using osu.Framework.Extensions.TypeExtensions;
|
|
using osu.Framework.Screens;
|
|
using osu.Framework.Testing;
|
|
using osu.Game.Beatmaps;
|
|
using osu.Game.Beatmaps.ControlPoints;
|
|
using osu.Game.Beatmaps.Formats;
|
|
using osu.Game.Replays;
|
|
using osu.Game.Rulesets.Judgements;
|
|
using osu.Game.Rulesets.Mods;
|
|
using osu.Game.Rulesets.Objects;
|
|
using osu.Game.Rulesets.Objects.Drawables;
|
|
using osu.Game.Rulesets.Objects.Types;
|
|
using osu.Game.Rulesets.Osu.Mods;
|
|
using osu.Game.Rulesets.Osu.Objects;
|
|
using osu.Game.Rulesets.Osu.Replays;
|
|
using osu.Game.Rulesets.Osu.Scoring;
|
|
using osu.Game.Rulesets.Osu.UI;
|
|
using osu.Game.Rulesets.Replays;
|
|
using osu.Game.Rulesets.Scoring;
|
|
using osu.Game.Scoring;
|
|
using osu.Game.Scoring.Legacy;
|
|
using osu.Game.Screens.Play;
|
|
using osu.Game.Tests.Visual;
|
|
using osuTK;
|
|
|
|
namespace osu.Game.Rulesets.Osu.Tests
|
|
{
|
|
public partial class TestSceneLegacyHitPolicy : RateAdjustedBeatmapTestScene
|
|
{
|
|
private readonly OsuHitWindows referenceHitWindows;
|
|
|
|
/// <summary>
|
|
/// This is provided as a convenience for testing note lock behaviour against osu!stable.
|
|
/// Setting this field to a non-null path will cause beatmap files and replays used in all test cases
|
|
/// to be exported to disk so that they can be cross-checked against stable.
|
|
/// </summary>
|
|
private readonly string? exportLocation = null;
|
|
|
|
public TestSceneLegacyHitPolicy()
|
|
{
|
|
referenceHitWindows = new OsuHitWindows();
|
|
referenceHitWindows.SetDifficulty(0);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestClickSecondCircleBeforeFirstCircleTime()
|
|
{
|
|
const double time_first_circle = 1500;
|
|
const double time_second_circle = 1600;
|
|
Vector2 positionFirstCircle = Vector2.Zero;
|
|
Vector2 positionSecondCircle = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
|
addClickActionAssert(0, ClickAction.Shake);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestClickSecondCircleAtFirstCircleTime()
|
|
{
|
|
const double time_first_circle = 1500;
|
|
const double time_second_circle = 1600;
|
|
Vector2 positionFirstCircle = Vector2.Zero;
|
|
Vector2 positionSecondCircle = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
|
addClickActionAssert(0, ClickAction.Shake);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestClickSecondCircleAfterFirstCircleTime()
|
|
{
|
|
const double time_first_circle = 1500;
|
|
const double time_second_circle = 1600;
|
|
Vector2 positionFirstCircle = Vector2.Zero;
|
|
Vector2 positionSecondCircle = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
|
addClickActionAssert(0, ClickAction.Shake);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged()
|
|
{
|
|
const double time_first_circle = 1500;
|
|
const double time_second_circle = 1600;
|
|
Vector2 positionFirstCircle = Vector2.Zero;
|
|
Vector2 positionSecondCircle = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_first_circle - 90, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
|
addJudgementOffsetAssert(hitObjects[1], -190); // time_second_circle - first_circle_time - 90
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged()
|
|
{
|
|
const double time_first_circle = 1500;
|
|
const double time_second_circle = 1600;
|
|
Vector2 positionFirstCircle = Vector2.Zero;
|
|
Vector2 positionSecondCircle = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle - 190, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Meh);
|
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
|
addJudgementOffsetAssert(hitObjects[0], -190); // time_first_circle - 190
|
|
addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle after a slider's start time, but hitting the slider head and all slider ticks.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestHitCircleBeforeSliderHead()
|
|
{
|
|
const double time_slider = 1500;
|
|
const double time_circle = 1510;
|
|
Vector2 positionCircle = Vector2.Zero;
|
|
Vector2 positionSlider = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_circle,
|
|
Position = positionCircle
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_slider,
|
|
Position = positionSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(50, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_slider, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_slider + 10, Position = positionSlider, Actions = { OsuAction.RightButton } }
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking hitting future slider ticks before a circle.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestHitSliderTicksBeforeCircle()
|
|
{
|
|
const double time_slider = 1500;
|
|
const double time_circle = 1510;
|
|
Vector2 positionCircle = Vector2.Zero;
|
|
Vector2 positionSlider = new Vector2(30);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_circle,
|
|
Position = positionCircle
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_slider,
|
|
Position = positionSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(50, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 100, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_circle + referenceHitWindows.WindowFor(HitResult.Meh) - 90, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
|
addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit);
|
|
addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit);
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tests clicking a future circle before a spinner.
|
|
/// </summary>
|
|
[Test]
|
|
public void TestHitCircleBeforeSpinner()
|
|
{
|
|
const double time_spinner = 1500;
|
|
const double time_circle = 1600;
|
|
Vector2 positionCircle = Vector2.Zero;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new TestSpinner
|
|
{
|
|
StartTime = time_spinner,
|
|
Position = new Vector2(256, 192),
|
|
EndTime = time_spinner + 1000,
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_circle,
|
|
Position = positionCircle
|
|
},
|
|
};
|
|
|
|
List<ReplayFrame> frames = new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_spinner - 90, Position = positionCircle, Actions = { OsuAction.LeftButton } },
|
|
};
|
|
|
|
frames.AddRange(new SpinFramesGenerator(time_spinner + 10)
|
|
.Spin(360, 500)
|
|
.Build());
|
|
|
|
performTest(hitObjects, frames);
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
}
|
|
|
|
[Test]
|
|
public void TestHitSliderHeadBeforeHitCircle()
|
|
{
|
|
const double time_circle = 1000;
|
|
const double time_slider = 1200;
|
|
Vector2 positionCircle = Vector2.Zero;
|
|
Vector2 positionSlider = new Vector2(80);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_circle,
|
|
Position = positionCircle
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_slider,
|
|
Position = positionSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_circle - 100, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_circle, Position = positionCircle, Actions = { OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_slider, Position = positionSlider, Actions = { OsuAction.LeftButton } },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
|
addClickActionAssert(0, ClickAction.Shake);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
addClickActionAssert(2, ClickAction.Hit);
|
|
}
|
|
|
|
[Test]
|
|
public void TestOverlappingSliders()
|
|
{
|
|
const double time_first_slider = 1000;
|
|
const double time_second_slider = 1200;
|
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new Slider
|
|
{
|
|
StartTime = time_first_slider,
|
|
Position = positionFirstSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_second_slider,
|
|
Position = positionSecondSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton, OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
|
new OsuReplayFrame { Time = time_second_slider, Position = positionSecondSlider + new Vector2(0, 10), Actions = { OsuAction.LeftButton } },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
[Test]
|
|
public void TestStacksDoNotShake()
|
|
{
|
|
const double time_stack_start = 1000;
|
|
Vector2 position = new Vector2(80);
|
|
|
|
var hitObjects = Enumerable.Range(0, 20).Select(i => new HitCircle
|
|
{
|
|
StartTime = time_stack_start + i * 100,
|
|
Position = position
|
|
}).Cast<OsuHitObject>().ToList();
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_stack_start - 450, Position = new Vector2(55), Actions = { OsuAction.LeftButton } },
|
|
});
|
|
|
|
addClickActionAssert(0, ClickAction.Ignore);
|
|
}
|
|
|
|
[Test]
|
|
public void TestAutopilotReducesHittableRange()
|
|
{
|
|
const double time_circle = 1500;
|
|
Vector2 positionCircle = Vector2.Zero;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_circle,
|
|
Position = positionCircle
|
|
},
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_circle - 250, Position = positionCircle, Actions = { OsuAction.LeftButton } }
|
|
}, new Mod[] { new OsuModAutopilot() });
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Miss);
|
|
// note lock prevented the object from being hit, so the judgement offset should be very late.
|
|
addJudgementOffsetAssert(hitObjects[0], referenceHitWindows.WindowFor(HitResult.Meh));
|
|
addClickActionAssert(0, ClickAction.Shake);
|
|
}
|
|
|
|
[Test]
|
|
public void TestInputDoesNotFallThroughOverlappingSliders()
|
|
{
|
|
const double time_first_slider = 1000;
|
|
const double time_second_slider = 1250;
|
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new Slider
|
|
{
|
|
StartTime = time_first_slider,
|
|
Position = positionFirstSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_second_slider,
|
|
Position = positionSecondSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_first_slider + 50, Position = midpoint },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
|
addJudgementAssert(hitObjects[1], HitResult.Miss);
|
|
// the slider head of the first slider prevents the second slider's head from being hit, so the judgement offset should be very late.
|
|
// this is not strictly done by the hit policy implementation itself (see `OsuModClassic.blockInputToObjectsUnderSliderHead()`),
|
|
// but we're testing this here anyways to just keep everything related to input handling and note lock in one place.
|
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, referenceHitWindows.WindowFor(HitResult.Meh));
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
}
|
|
|
|
[Test]
|
|
public void TestOverlappingSlidersDontBlockEachOtherWhenFullyJudged()
|
|
{
|
|
const double time_first_slider = 1000;
|
|
const double time_second_slider = 1600;
|
|
Vector2 positionFirstSlider = new Vector2(100, 50);
|
|
Vector2 positionSecondSlider = new Vector2(100, 80);
|
|
var midpoint = (positionFirstSlider + positionSecondSlider) / 2;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new Slider
|
|
{
|
|
StartTime = time_first_slider,
|
|
Position = positionFirstSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
},
|
|
new Slider
|
|
{
|
|
StartTime = time_second_slider,
|
|
Position = positionSecondSlider,
|
|
Path = new SliderPath(PathType.LINEAR, new[]
|
|
{
|
|
Vector2.Zero,
|
|
new Vector2(25, 0),
|
|
})
|
|
}
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_slider, Position = midpoint, Actions = { OsuAction.RightButton } },
|
|
new OsuReplayFrame { Time = time_first_slider + 25, Position = midpoint },
|
|
// this frame doesn't do anything on lazer, but is REQUIRED for correct playback on stable,
|
|
// because stable during replay playback only updates game state _when it encounters a replay frame_
|
|
new OsuReplayFrame { Time = 1250, Position = midpoint },
|
|
new OsuReplayFrame { Time = time_second_slider + 50, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_second_slider + 75, Position = midpoint },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Ok);
|
|
addJudgementOffsetAssert("first slider head", () => ((Slider)hitObjects[0]).HeadCircle, 0);
|
|
addJudgementAssert(hitObjects[1], HitResult.Ok);
|
|
addJudgementOffsetAssert("second slider head", () => ((Slider)hitObjects[1]).HeadCircle, 50);
|
|
addClickActionAssert(0, ClickAction.Hit);
|
|
addClickActionAssert(1, ClickAction.Hit);
|
|
}
|
|
|
|
[Test]
|
|
public void TestOverlappingHitCirclesDontBlockEachOtherWhenBothVisible()
|
|
{
|
|
const double time_first_circle = 1000;
|
|
const double time_second_circle = 1200;
|
|
Vector2 positionFirstCircle = new Vector2(100);
|
|
Vector2 positionSecondCircle = new Vector2(120);
|
|
var midpoint = (positionFirstCircle + positionSecondCircle) / 2;
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle,
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle,
|
|
},
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle, Position = midpoint, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_first_circle + 25, Position = midpoint },
|
|
new OsuReplayFrame { Time = time_first_circle + 50, Position = midpoint, Actions = { OsuAction.RightButton } },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
|
addJudgementOffsetAssert(hitObjects[0], 0);
|
|
|
|
addJudgementAssert(hitObjects[1], HitResult.Meh);
|
|
addJudgementOffsetAssert(hitObjects[1], -150);
|
|
}
|
|
|
|
[Test]
|
|
public void TestOverlappingHitCirclesDontBlockEachOtherWhenFullyFadedOut()
|
|
{
|
|
const double time_first_circle = 1000;
|
|
const double time_second_circle = 1200;
|
|
const double time_third_circle = 1400;
|
|
Vector2 positionFirstCircle = new Vector2(100);
|
|
Vector2 positionSecondCircle = new Vector2(200);
|
|
|
|
var hitObjects = new List<OsuHitObject>
|
|
{
|
|
new HitCircle
|
|
{
|
|
StartTime = time_first_circle,
|
|
Position = positionFirstCircle,
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_second_circle,
|
|
Position = positionSecondCircle,
|
|
},
|
|
new HitCircle
|
|
{
|
|
StartTime = time_third_circle,
|
|
Position = positionFirstCircle,
|
|
},
|
|
};
|
|
|
|
performTest(hitObjects, new List<ReplayFrame>
|
|
{
|
|
new OsuReplayFrame { Time = time_first_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_first_circle + 50, Position = positionFirstCircle },
|
|
new OsuReplayFrame { Time = time_second_circle - 50, Position = positionSecondCircle },
|
|
new OsuReplayFrame { Time = time_second_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_second_circle + 50, Position = positionSecondCircle },
|
|
new OsuReplayFrame { Time = time_third_circle - 50, Position = positionFirstCircle },
|
|
new OsuReplayFrame { Time = time_third_circle, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } },
|
|
new OsuReplayFrame { Time = time_third_circle + 50, Position = positionFirstCircle },
|
|
});
|
|
|
|
addJudgementAssert(hitObjects[0], HitResult.Great);
|
|
addJudgementOffsetAssert(hitObjects[0], 0);
|
|
|
|
addJudgementAssert(hitObjects[1], HitResult.Great);
|
|
addJudgementOffsetAssert(hitObjects[1], 0);
|
|
|
|
addJudgementAssert(hitObjects[2], HitResult.Great);
|
|
addJudgementOffsetAssert(hitObjects[2], 0);
|
|
}
|
|
|
|
private void addJudgementAssert(OsuHitObject hitObject, HitResult result)
|
|
{
|
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}",
|
|
() => judgementResults.Single(r => r.HitObject == hitObject).Type, () => Is.EqualTo(result));
|
|
}
|
|
|
|
private void addJudgementAssert(string name, Func<OsuHitObject?> hitObject, HitResult result)
|
|
{
|
|
AddAssert($"{name} judgement is {result}",
|
|
() => judgementResults.Single(r => r.HitObject == hitObject()).Type, () => Is.EqualTo(result));
|
|
}
|
|
|
|
private void addJudgementOffsetAssert(OsuHitObject hitObject, double offset)
|
|
{
|
|
AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}",
|
|
() => judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
|
}
|
|
|
|
private void addJudgementOffsetAssert(string name, Func<OsuHitObject?> hitObject, double offset)
|
|
{
|
|
AddAssert($"{name} @ judged at {offset}",
|
|
() => judgementResults.Single(r => r.HitObject == hitObject()).TimeOffset, () => Is.EqualTo(offset).Within(50));
|
|
}
|
|
|
|
private void addClickActionAssert(int inputIndex, ClickAction action)
|
|
=> AddAssert($"input #{inputIndex} resulted in {action}", () => testPolicy.ClickActions[inputIndex], () => Is.EqualTo(action));
|
|
|
|
private ScoreAccessibleReplayPlayer currentPlayer = null!;
|
|
private List<JudgementResult> judgementResults = null!;
|
|
private TestLegacyHitPolicy testPolicy = null!;
|
|
|
|
private void performTest(List<OsuHitObject> hitObjects, List<ReplayFrame> frames, IEnumerable<Mod>? extraMods = null, [CallerMemberName] string testCaseName = "")
|
|
{
|
|
List<Mod> mods = null!;
|
|
IBeatmap playableBeatmap = null!;
|
|
Score score = null!;
|
|
|
|
AddStep("set up mods", () =>
|
|
{
|
|
mods = new List<Mod> { new OsuModClassic() };
|
|
|
|
if (extraMods != null)
|
|
mods.AddRange(extraMods);
|
|
});
|
|
|
|
AddStep("create beatmap", () =>
|
|
{
|
|
var cpi = new ControlPointInfo();
|
|
cpi.Add(0, new TimingControlPoint { BeatLength = 1000 });
|
|
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<OsuHitObject>
|
|
{
|
|
Metadata =
|
|
{
|
|
Title = testCaseName
|
|
},
|
|
HitObjects = hitObjects,
|
|
Difficulty = new BeatmapDifficulty
|
|
{
|
|
OverallDifficulty = 0,
|
|
SliderTickRate = 3
|
|
},
|
|
BeatmapInfo =
|
|
{
|
|
Ruleset = new OsuRuleset().RulesetInfo,
|
|
},
|
|
ControlPointInfo = cpi,
|
|
BeatmapVersion = LegacyBeatmapEncoder.FIRST_LAZER_VERSION // for correct offset treatment by score encoder
|
|
});
|
|
playableBeatmap = Beatmap.Value.GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
|
|
});
|
|
|
|
AddStep("create score", () =>
|
|
{
|
|
score = new Score
|
|
{
|
|
Replay = new Replay
|
|
{
|
|
Frames = new List<ReplayFrame>
|
|
{
|
|
// required for correct playback in stable
|
|
new OsuReplayFrame(0, new Vector2(256, -500)),
|
|
new OsuReplayFrame(0, new Vector2(256, -500))
|
|
}.Concat(frames).ToList()
|
|
},
|
|
ScoreInfo =
|
|
{
|
|
Ruleset = new OsuRuleset().RulesetInfo,
|
|
BeatmapInfo = playableBeatmap.BeatmapInfo,
|
|
Mods = mods.ToArray()
|
|
}
|
|
};
|
|
});
|
|
|
|
if (exportLocation != null)
|
|
{
|
|
AddStep("export beatmap", () =>
|
|
{
|
|
var beatmapEncoder = new LegacyBeatmapEncoder(playableBeatmap, null);
|
|
|
|
using (var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osu"), FileMode.Create))
|
|
{
|
|
var memoryStream = new MemoryStream();
|
|
using (var writer = new StreamWriter(memoryStream, Encoding.UTF8, leaveOpen: true))
|
|
beatmapEncoder.Encode(writer);
|
|
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
memoryStream.CopyTo(stream);
|
|
memoryStream.Seek(0, SeekOrigin.Begin);
|
|
playableBeatmap.BeatmapInfo.MD5Hash = memoryStream.ComputeMD5Hash();
|
|
}
|
|
});
|
|
|
|
AddStep("export score", () =>
|
|
{
|
|
using var stream = File.Open(Path.Combine(exportLocation, $"{testCaseName}.osr"), FileMode.Create);
|
|
var encoder = new LegacyScoreEncoder(score, playableBeatmap);
|
|
encoder.Encode(stream);
|
|
});
|
|
}
|
|
|
|
AddStep("load player", () =>
|
|
{
|
|
SelectedMods.Value = mods.ToArray();
|
|
|
|
var p = new ScoreAccessibleReplayPlayer(score);
|
|
|
|
p.OnLoadComplete += _ =>
|
|
{
|
|
p.ScoreProcessor.NewJudgement += result =>
|
|
{
|
|
if (currentPlayer == p) judgementResults.Add(result);
|
|
};
|
|
};
|
|
|
|
LoadScreen(currentPlayer = p);
|
|
judgementResults = new List<JudgementResult>();
|
|
});
|
|
|
|
AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0);
|
|
AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen());
|
|
AddStep("Substitute hit policy", () =>
|
|
{
|
|
var playfield = currentPlayer.ChildrenOfType<OsuPlayfield>().Single();
|
|
var currentPolicy = playfield.HitPolicy;
|
|
playfield.HitPolicy = testPolicy = new TestLegacyHitPolicy(currentPolicy);
|
|
});
|
|
AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value);
|
|
}
|
|
|
|
private class TestSpinner : Spinner
|
|
{
|
|
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
|
{
|
|
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
|
|
SpinsRequired = 1;
|
|
}
|
|
}
|
|
|
|
private partial class ScoreAccessibleReplayPlayer : ReplayPlayer
|
|
{
|
|
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
|
|
|
|
protected override bool PauseOnFocusLost => false;
|
|
|
|
public ScoreAccessibleReplayPlayer(Score score)
|
|
: base(score, new PlayerConfiguration
|
|
{
|
|
AllowPause = false,
|
|
ShowResults = false,
|
|
})
|
|
{
|
|
}
|
|
}
|
|
|
|
private class TestLegacyHitPolicy : LegacyHitPolicy
|
|
{
|
|
private readonly IHitPolicy currentPolicy;
|
|
|
|
public TestLegacyHitPolicy(IHitPolicy currentPolicy)
|
|
{
|
|
this.currentPolicy = currentPolicy;
|
|
}
|
|
|
|
public List<ClickAction> ClickActions { get; } = new List<ClickAction>();
|
|
|
|
public override ClickAction CheckHittable(DrawableHitObject hitObject, double time, HitResult result)
|
|
{
|
|
var action = currentPolicy.CheckHittable(hitObject, time, result);
|
|
ClickActions.Add(action);
|
|
return action;
|
|
}
|
|
}
|
|
}
|
|
}
|