1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-14 21:02:55 +08:00

Merge branch 'master' into ArrangeMod

This commit is contained in:
Dean Herbert 2018-09-05 02:33:00 +09:00 committed by GitHub
commit 34c42aed89
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1185 additions and 403 deletions

View File

@ -4,7 +4,11 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using osu.Framework; using osu.Framework;
using osu.Framework.Development;
using osu.Framework.Logging;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Game.IPC; using osu.Game.IPC;
@ -20,6 +24,8 @@ namespace osu.Desktop
using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true)) using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true))
{ {
host.ExceptionThrown += handleException;
if (!host.IsPrimaryInstance) if (!host.IsPrimaryInstance)
{ {
var importer = new ArchiveImportIPCChannel(host); var importer = new ArchiveImportIPCChannel(host);
@ -45,5 +51,24 @@ namespace osu.Desktop
return 0; return 0;
} }
} }
private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1;
/// <summary>
/// Allow a maximum of one unhandled exception, per second of execution.
/// </summary>
/// <param name="arg"></param>
/// <returns></returns>
private static bool handleException(Exception arg)
{
bool continueExecution = Interlocked.Decrement(ref allowableExceptions) >= 0;
Logger.Log($"Unhandled exception has been {(continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied")} .");
// restore the stock of allowable exceptions after a short delay.
Task.Delay(1000).ContinueWith(_ => Interlocked.Increment(ref allowableExceptions));
return continueExecution;
}
} }
} }

View File

@ -28,8 +28,8 @@
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="System.IO.Packaging" Version="4.5.0" /> <PackageReference Include="System.IO.Packaging" Version="4.5.0" />
<PackageReference Include="ppy.squirrel.windows" Version="1.8.0.5" /> <PackageReference Include="ppy.squirrel.windows" Version="1.8.0.5" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.2" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Resources"> <ItemGroup Label="Resources">
<EmbeddedResource Include="lazer.ico" /> <EmbeddedResource Include="lazer.ico" />

View File

@ -41,6 +41,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
RepeatCount = curveData.RepeatCount, RepeatCount = curveData.RepeatCount,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH, X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0 LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset ?? 0
}; };
} }
@ -51,7 +52,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
Duration = endTime.Duration, Duration = endTime.Duration,
NewCombo = comboData?.NewCombo ?? false NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}; };
} }
else else
@ -61,6 +63,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH X = (positionData?.X ?? 0) / CatchPlayfield.BASE_WIDTH
}; };
} }

View File

@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
public int IndexInCurrentCombo { get; set; } public int IndexInCurrentCombo { get; set; }
public int ComboIndex { get; set; } public int ComboIndex { get; set; }

View File

@ -173,19 +173,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var pattern = new Pattern(); var pattern = new Pattern();
int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects; int usableColumns = TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects;
int nextColumn = Random.Next(RandomStart, TotalColumns); int nextColumn = GetRandomColumn();
for (int i = 0; i < Math.Min(usableColumns, noteCount); i++) for (int i = 0; i < Math.Min(usableColumns, noteCount); i++)
{ {
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn)) //find available column // Find available column
nextColumn = Random.Next(RandomStart, TotalColumns); nextColumn = FindAvailableColumn(nextColumn, pattern, PreviousPattern);
addToPattern(pattern, nextColumn, startTime, EndTime); addToPattern(pattern, nextColumn, startTime, EndTime);
} }
// This is can't be combined with the above loop due to RNG // This is can't be combined with the above loop due to RNG
for (int i = 0; i < noteCount - usableColumns; i++) for (int i = 0; i < noteCount - usableColumns; i++)
{ {
while (pattern.ColumnHasObject(nextColumn)) nextColumn = FindAvailableColumn(nextColumn, pattern);
nextColumn = Random.Next(RandomStart, TotalColumns);
addToPattern(pattern, nextColumn, startTime, EndTime); addToPattern(pattern, nextColumn, startTime, EndTime);
} }
@ -210,18 +209,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
}
int lastColumn = nextColumn; int lastColumn = nextColumn;
for (int i = 0; i < noteCount; i++) for (int i = 0; i < noteCount; i++)
{ {
addToPattern(pattern, nextColumn, startTime, startTime); addToPattern(pattern, nextColumn, startTime, startTime);
while (nextColumn == lastColumn) nextColumn = FindAvailableColumn(nextColumn, validation: c => c != lastColumn);
nextColumn = Random.Next(RandomStart, TotalColumns);
lastColumn = nextColumn; lastColumn = nextColumn;
startTime += SegmentDuration; startTime += SegmentDuration;
} }
@ -313,7 +307,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns > 2) if (TotalColumns > 2)
addToPattern(pattern, nextColumn, startTime, startTime); addToPattern(pattern, nextColumn, startTime, startTime);
nextColumn = Random.Next(RandomStart, TotalColumns); nextColumn = GetRandomColumn();
startTime += SegmentDuration; startTime += SegmentDuration;
} }
@ -392,16 +386,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{ nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(RandomStart, TotalColumns);
}
for (int i = 0; i < columnRepeat; i++) for (int i = 0; i < columnRepeat; i++)
{ {
while (pattern.ColumnHasObject(nextColumn)) nextColumn = FindAvailableColumn(nextColumn, pattern);
nextColumn = Random.Next(RandomStart, TotalColumns);
addToPattern(pattern, nextColumn, startTime, EndTime); addToPattern(pattern, nextColumn, startTime, EndTime);
startTime += SegmentDuration; startTime += SegmentDuration;
} }
@ -426,15 +415,12 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
{ holdColumn = FindAvailableColumn(holdColumn, PreviousPattern);
while (PreviousPattern.ColumnHasObject(holdColumn))
holdColumn = Random.Next(RandomStart, TotalColumns);
}
// Create the hold note // Create the hold note
addToPattern(pattern, holdColumn, startTime, EndTime); addToPattern(pattern, holdColumn, startTime, EndTime);
int nextColumn = Random.Next(RandomStart, TotalColumns); int nextColumn = GetRandomColumn();
int noteCount; int noteCount;
if (ConversionDifficulty > 6.5) if (ConversionDifficulty > 6.5)
noteCount = GetRandomNoteCount(0.63, 0); noteCount = GetRandomNoteCount(0.63, 0);
@ -455,8 +441,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
for (int j = 0; j < noteCount; j++) for (int j = 0; j < noteCount; j++)
{ {
while (rowPattern.ColumnHasObject(nextColumn) || nextColumn == holdColumn) nextColumn = FindAvailableColumn(nextColumn, validation: c => c != holdColumn, patterns: rowPattern);
nextColumn = Random.Next(RandomStart, TotalColumns);
addToPattern(rowPattern, nextColumn, startTime, startTime); addToPattern(rowPattern, nextColumn, startTime, startTime);
} }
} }

View File

@ -39,32 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToPattern(pattern, 0, generateHold); addToPattern(pattern, 0, generateHold);
break; break;
case 8: case 8:
addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold); addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
break; break;
default: default:
if (TotalColumns > 0) if (TotalColumns > 0)
addToPattern(pattern, getNextRandomColumn(0), generateHold); addToPattern(pattern, GetRandomColumn(), generateHold);
break; break;
} }
return pattern; return pattern;
} }
/// <summary>
/// Picks a random column after a column.
/// </summary>
/// <param name="start">The starting column.</param>
/// <returns>A random column after <paramref name="start"/>.</returns>
private int getNextRandomColumn(int start)
{
int nextColumn = Random.Next(start, TotalColumns);
while (PreviousPattern.ColumnHasObject(nextColumn))
nextColumn = Random.Next(start, TotalColumns);
return nextColumn;
}
/// <summary> /// <summary>
/// Constructs and adds a note to a pattern. /// Constructs and adds a note to a pattern.
/// </summary> /// </summary>

View File

@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
PatternType lastStair, IBeatmap originalBeatmap) PatternType lastStair, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap) : base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{ {
if (previousTime > hitObject.StartTime) throw new ArgumentOutOfRangeException(nameof(previousTime));
if (density < 0) throw new ArgumentOutOfRangeException(nameof(density));
StairType = lastStair; StairType = lastStair;
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
@ -234,22 +231,27 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
for (int i = 0; i < noteCount; i++) for (int i = 0; i < noteCount; i++)
{ {
while (pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking) nextColumn = allowStacking
{ ? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern)
if (convertType.HasFlag(PatternType.Gathered)) : FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern });
{
nextColumn++;
if (nextColumn == TotalColumns)
nextColumn = RandomStart;
}
else
nextColumn = Random.Next(RandomStart, TotalColumns);
}
addToPattern(pattern, nextColumn); addToPattern(pattern, nextColumn);
} }
return pattern; return pattern;
int getNextColumn(int last)
{
if (convertType.HasFlag(PatternType.Gathered))
{
last++;
if (last == TotalColumns)
last = RandomStart;
}
else
last = GetRandomColumn();
return last;
}
} }
/// <summary> /// <summary>
@ -286,17 +288,19 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns> /// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3)
{ {
if (convertType.HasFlag(PatternType.ForceNotStack))
return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
var pattern = new Pattern(); var pattern = new Pattern();
bool addToCentre; bool addToCentre;
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre); int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2; int columnLimit = (TotalColumns % 2 == 0 ? TotalColumns : TotalColumns - 1) / 2;
int nextColumn = Random.Next(RandomStart, columnLimit); int nextColumn = GetRandomColumn(upperBound: columnLimit);
for (int i = 0; i < noteCount; i++) for (int i = 0; i < noteCount; i++)
{ {
while (pattern.ColumnHasObject(nextColumn)) nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern);
nextColumn = Random.Next(RandomStart, columnLimit);
// Add normal note // Add normal note
addToPattern(pattern, nextColumn); addToPattern(pattern, nextColumn);
@ -368,9 +372,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{ {
addToCentre = false; addToCentre = false;
if (convertType.HasFlag(PatternType.ForceNotStack))
return getRandomNoteCount(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3);
switch (TotalColumns) switch (TotalColumns)
{ {
case 2: case 2:

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
} }
private double? conversionDifficulty; private double? conversionDifficulty;
/// <summary> /// <summary>
/// A difficulty factor used for various conversion methods from osu!stable. /// A difficulty factor used for various conversion methods from osu!stable.
/// </summary> /// </summary>
@ -116,5 +118,82 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
return conversionDifficulty.Value; return conversionDifficulty.Value;
} }
} }
/// <summary>
/// Finds a new column in which a <see cref="HitObject"/> can be placed.
/// This uses <see cref="GetRandomColumn"/> to pick the next candidate column.
/// </summary>
/// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column for which there are no <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, params Pattern[] patterns)
=> FindAvailableColumn(initialColumn, null, patterns: patterns);
/// <summary>
/// Finds a new column in which a <see cref="HitObject"/> can be placed.
/// </summary>
/// <param name="initialColumn">The initial column to test. This may be returned if it is already a valid column.</param>
/// <param name="nextColumn">A function to retrieve the next column. If null, a randomisation scheme will be used.</param>
/// <param name="validation">A function to perform additional validation checks to determine if a column is a valid candidate for a <see cref="HitObject"/>.</param>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
/// <param name="patterns">A list of patterns for which the validity of a column should be checked against.
/// A column is not a valid candidate if a <see cref="HitObject"/> occupies the same column in any of the patterns.</param>
/// <returns>A column which has passed the <paramref name="validation"/> check and for which there are no
/// <see cref="HitObject"/>s in any of <paramref name="patterns"/> occupying the same column.</returns>
/// <exception cref="NotEnoughColumnsException">If there are no valid candidate columns.</exception>
protected int FindAvailableColumn(int initialColumn, int? lowerBound = null, int? upperBound = null, Func<int, int> nextColumn = null, [InstantHandle] Func<int, bool> validation = null,
params Pattern[] patterns)
{
lowerBound = lowerBound ?? RandomStart;
upperBound = upperBound ?? TotalColumns;
nextColumn = nextColumn ?? (_ => GetRandomColumn(lowerBound, upperBound));
// Check for the initial column
if (isValid(initialColumn))
return initialColumn;
// Ensure that we have at least one free column, so that an endless loop is avoided
bool hasValidColumns = false;
for (int i = lowerBound.Value; i < upperBound.Value; i++)
{
hasValidColumns = isValid(i);
if (hasValidColumns)
break;
}
if (!hasValidColumns)
throw new NotEnoughColumnsException();
// Iterate until a valid column is found. This is a random iteration in the default case.
do
{
initialColumn = nextColumn(initialColumn);
} while (!isValid(initialColumn));
return initialColumn;
bool isValid(int column) => validation?.Invoke(column) != false && !patterns.Any(p => p.ColumnHasObject(column));
}
/// <summary>
/// Returns a random column index in the range [<paramref name="lowerBound"/>, <paramref name="upperBound"/>).
/// </summary>
/// <param name="lowerBound">The minimum column index. If null, <see cref="RandomStart"/> is used.</param>
/// <param name="upperBound">The maximum column index. If null, <see cref="PatternGenerator.TotalColumns"/> is used.</param>
protected int GetRandomColumn(int? lowerBound = null, int? upperBound = null) => Random.Next(lowerBound ?? RandomStart, upperBound ?? TotalColumns);
/// <summary>
/// Occurs when mania conversion is stuck in an infinite loop unable to find columns to place new hitobjects in.
/// </summary>
public class NotEnoughColumnsException : Exception
{
public NotEnoughColumnsException()
: base("There were not enough columns to complete conversion.")
{
}
}
} }
} }

View File

@ -70,9 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects
TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime); TimingControlPoint timingPoint = controlPointInfo.TimingPointAt(StartTime);
tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate; tickSpacing = timingPoint.BeatLength / difficulty.SliderTickRate;
Head.ApplyDefaults(controlPointInfo, difficulty);
Tail.ApplyDefaults(controlPointInfo, difficulty);
} }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects()
@ -80,6 +77,9 @@ namespace osu.Game.Rulesets.Mania.Objects
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();
createTicks(); createTicks();
AddNested(Head);
AddNested(Tail);
} }
private void createTicks() private void createTicks()

View File

@ -26,11 +26,11 @@ namespace osu.Game.Rulesets.Mania.UI
throw new ArgumentException("Can't have zero or fewer stages."); throw new ArgumentException("Can't have zero or fewer stages.");
GridContainer playfieldGrid; GridContainer playfieldGrid;
InternalChild = playfieldGrid = new GridContainer AddInternal(playfieldGrid = new GridContainer
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Content = new[] { new Drawable[stageDefinitions.Count] } Content = new[] { new Drawable[stageDefinitions.Count] }
}; });
var normalColumnAction = ManiaAction.Key1; var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1; var specialColumnAction = ManiaAction.Special1;

View File

@ -0,0 +1,64 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.IO;
using System.Linq;
using System.Text;
using NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Tests.Beatmaps;
using Decoder = osu.Game.Beatmaps.Formats.Decoder;
namespace osu.Game.Rulesets.Osu.Tests
{
[TestFixture]
public class StackingTest
{
[Test]
public void TestStacking()
{
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(beatmap_data)))
using (var reader = new StreamReader(stream))
{
var beatmap = Decoder.GetDecoder<Beatmap>(reader).Decode(reader);
var converted = new TestWorkingBeatmap(beatmap).GetPlayableBeatmap(new OsuRuleset().RulesetInfo);
var objects = converted.HitObjects.ToList();
// The last hitobject triggers the stacking
for (int i = 0; i < objects.Count - 1; i++)
Assert.AreEqual(0, ((OsuHitObject)objects[i]).StackHeight);
}
}
private const string beatmap_data = @"
osu file format v14
[General]
StackLeniency: 0.2
[Difficulty]
ApproachRate:9.2
SliderMultiplier:1
SliderTickRate:0.5
[TimingPoints]
217871,6400,4,2,1,20,1,0
217871,-800,4,2,1,20,0,0
218071,-787.5,4,2,1,20,0,0
218271,-775,4,2,1,20,0,0
218471,-762.5,4,2,1,20,0,0
218671,-750,4,2,1,20,0,0
240271,-10,4,2,0,5,0,0
[HitObjects]
311,185,217871,6,0,L|318:158,1,25
311,185,218071,2,0,L|335:170,1,25
311,185,218271,2,0,L|338:192,1,25
311,185,218471,2,0,L|325:209,1,25
311,185,218671,2,0,L|304:212,1,25
311,185,240271,5,0,0:0:0:0:
";
}
}

View File

@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Tests
} }
} }
private class TestDrawableHitCircle : DrawableHitCircle protected class TestDrawableHitCircle : DrawableHitCircle
{ {
private readonly bool auto; private readonly bool auto;
@ -94,6 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests
this.auto = auto; this.auto = auto;
} }
public void TriggerJudgement() => UpdateResult(true);
protected override void CheckForResult(bool userTriggered, double timeOffset) protected override void CheckForResult(bool userTriggered, double timeOffset)
{ {
if (auto && !userTriggered && timeOffset > 0) if (auto && !userTriggered && timeOffset > 0)

View File

@ -0,0 +1,23 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestCaseShaking : TestCaseHitCircle
{
public override void Add(Drawable drawable)
{
base.Add(drawable);
if (drawable is TestDrawableHitCircle hitObject)
{
Scheduler.AddDelayed(() => hitObject.TriggerJudgement(),
hitObject.HitObject.StartTime - (hitObject.HitObject.HitWindows.HalfWindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current);
}
}
}
}

View File

@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.UI;
namespace osu.Game.Rulesets.Osu.Beatmaps namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
internal class OsuBeatmapConverter : BeatmapConverter<OsuHitObject> public class OsuBeatmapConverter : BeatmapConverter<OsuHitObject>
{ {
public OsuBeatmapConverter(IBeatmap beatmap) public OsuBeatmapConverter(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
@ -42,6 +42,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
RepeatCount = curveData.RepeatCount, RepeatCount = curveData.RepeatCount,
Position = positionData?.Position ?? Vector2.Zero, Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false, NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset LegacyLastTickOffset = legacyOffset?.LegacyLastTickOffset
}; };
} }
@ -52,7 +53,9 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime, StartTime = original.StartTime,
Samples = original.Samples, Samples = original.Samples,
EndTime = endTimeData.EndTime, EndTime = endTimeData.EndTime,
Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2 Position = positionData?.Position ?? OsuPlayfield.BASE_SIZE / 2,
NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}; };
} }
else else
@ -62,7 +65,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
StartTime = original.StartTime, StartTime = original.StartTime,
Samples = original.Samples, Samples = original.Samples,
Position = positionData?.Position ?? Vector2.Zero, Position = positionData?.Position ?? Vector2.Zero,
NewCombo = comboData?.NewCombo ?? false NewCombo = comboData?.NewCombo ?? false,
ComboOffset = comboData?.ComboOffset ?? 0,
}; };
} }
} }

View File

@ -8,16 +8,16 @@ using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Beatmaps namespace osu.Game.Rulesets.Osu.Beatmaps
{ {
internal class OsuBeatmapProcessor : BeatmapProcessor public class OsuBeatmapProcessor : BeatmapProcessor
{ {
public OsuBeatmapProcessor(IBeatmap beatmap) public OsuBeatmapProcessor(IBeatmap beatmap)
: base(beatmap) : base(beatmap)
{ {
} }
public override void PreProcess() public override void PostProcess()
{ {
base.PreProcess(); base.PostProcess();
applyStacking((Beatmap<OsuHitObject>)Beatmap); applyStacking((Beatmap<OsuHitObject>)Beatmap);
} }

View File

@ -2,14 +2,89 @@
// 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.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Input.States;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.UI;
using static osu.Game.Input.Handlers.ReplayInputHandler;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModRelax : ModRelax public class OsuModRelax : ModRelax, IApplicableFailOverride, IUpdatableByPlayfield, IApplicableToRulesetContainer<OsuHitObject>
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).ToArray();
public bool AllowFail => false;
public void Update(Playfield playfield)
{
bool requiresHold = false;
bool requiresHit = false;
const float relax_leniency = 3;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
if (!(drawable is DrawableOsuHitObject osuHit))
continue;
double time = osuHit.Clock.CurrentTime;
double relativetime = time - osuHit.HitObject.StartTime;
if (time < osuHit.HitObject.StartTime - relax_leniency) continue;
if (osuHit.HitObject is IHasEndTime hasEnd && time > hasEnd.EndTime || osuHit.IsHit)
continue;
requiresHit |= osuHit is DrawableHitCircle && osuHit.IsHovered && osuHit.HitObject.HitWindows.CanBeHit(relativetime);
requiresHold |= osuHit is DrawableSlider slider && (slider.Ball.IsHovered || osuHit.IsHovered) || osuHit is DrawableSpinner;
}
if (requiresHit)
{
addAction(false);
addAction(true);
}
addAction(requiresHold);
}
private bool wasHit;
private bool wasLeft;
private OsuInputManager osuInputManager;
private void addAction(bool hitting)
{
if (wasHit == hitting)
return;
wasHit = hitting;
var state = new ReplayState<OsuAction>
{
PressedActions = new List<OsuAction>()
};
if (hitting)
{
state.PressedActions.Add(wasLeft ? OsuAction.LeftButton : OsuAction.RightButton);
wasLeft = !wasLeft;
}
osuInputManager.HandleCustomInput(new InputState(), state);
}
public void ApplyToRulesetContainer(RulesetContainer<OsuHitObject> rulesetContainer)
{
// grab the input manager for future use.
osuInputManager = (OsuInputManager)rulesetContainer.KeyBindingInputManager;
osuInputManager.AllowUserPresses = false;
}
} }
} }

View File

@ -88,7 +88,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
var result = HitObject.HitWindows.ResultFor(timeOffset); var result = HitObject.HitWindows.ResultFor(timeOffset);
if (result == HitResult.None) if (result == HitResult.None)
{
Shake(Math.Abs(timeOffset) - HitObject.HitWindows.HalfWindowFor(HitResult.Miss));
return; return;
}
ApplyResult(r => r.Type = result); ApplyResult(r => r.Type = result);
} }

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning; using osu.Game.Skinning;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
@ -17,12 +18,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Time.Current >= HitObject.StartTime - HitObject.TimePreempt; public override bool IsPresent => base.IsPresent || State.Value == ArmedState.Idle && Time.Current >= HitObject.StartTime - HitObject.TimePreempt;
private readonly ShakeContainer shakeContainer;
protected DrawableOsuHitObject(OsuHitObject hitObject) protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject) : base(hitObject)
{ {
base.AddInternal(shakeContainer = new ShakeContainer { RelativeSizeAxes = Axes.Both });
Alpha = 0; Alpha = 0;
} }
// Forward all internal management to shakeContainer.
// This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690)
protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable);
protected override void ClearInternal(bool disposeChildren = true) => shakeContainer.Clear(disposeChildren);
protected override bool RemoveInternal(Drawable drawable) => shakeContainer.Remove(drawable);
protected sealed override void UpdateState(ArmedState state) protected sealed override void UpdateState(ArmedState state)
{ {
double transformTime = HitObject.StartTime - HitObject.TimePreempt; double transformTime = HitObject.StartTime - HitObject.TimePreempt;
@ -68,6 +78,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private OsuInputManager osuActionInputManager; private OsuInputManager osuActionInputManager;
internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager); internal OsuInputManager OsuActionInputManager => osuActionInputManager ?? (osuActionInputManager = GetContainingInputManager() as OsuInputManager);
protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength);
protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(judgement);
} }
} }

View File

@ -44,14 +44,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}, },
ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both }, ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both }, repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s) Ball = new SliderBall(s, this)
{ {
BypassAutoSizeAxes = Axes.Both, BypassAutoSizeAxes = Axes.Both,
Scale = new Vector2(s.Scale), Scale = new Vector2(s.Scale),
AlwaysPresent = true, AlwaysPresent = true,
Alpha = 0 Alpha = 0
}, },
HeadCircle = new DrawableSliderHead(s, s.HeadCircle), HeadCircle = new DrawableSliderHead(s, s.HeadCircle)
{
OnShake = Shake
},
TailCircle = new DrawableSliderTail(s, s.TailCircle) TailCircle = new DrawableSliderTail(s, s.TailCircle)
}; };

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 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 osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using OpenTK; using OpenTK;
@ -28,5 +29,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (!IsHit) if (!IsHit)
Position = slider.CurvePositionAt(completionProgress); Position = slider.CurvePositionAt(completionProgress);
} }
public Action<double> OnShake;
protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength);
} }
} }

View File

@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.EventArgs; using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using OpenTK;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -37,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider; private readonly Slider slider;
public readonly Drawable FollowCircle; public readonly Drawable FollowCircle;
private Drawable drawableBall; private Drawable drawableBall;
private readonly DrawableSlider drawableSlider;
public SliderBall(Slider slider) public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{ {
this.drawableSlider = drawableSlider;
this.slider = slider; this.slider = slider;
Masking = true; Masking = true;
AutoSizeAxes = Axes.Both; AutoSizeAxes = Axes.Both;
@ -121,9 +122,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return base.OnMouseMove(state); return base.OnMouseMove(state);
} }
// If the current time is between the start and end of the slider, we should track mouse input regardless of the cursor position.
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => canCurrentlyTrack || base.ReceiveMouseInputAt(screenSpacePos);
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
{ {
// Consider the case of rewinding - children's transforms are handled internally, so propagating down // Consider the case of rewinding - children's transforms are handled internally, so propagating down
@ -158,8 +156,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
// Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position. // Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position.
Tracking = canCurrentlyTrack Tracking = canCurrentlyTrack
&& lastState != null && lastState != null
&& base.ReceiveMouseInputAt(lastState.Mouse.NativeState.Position) && ReceiveMouseInputAt(lastState.Mouse.NativeState.Position)
&& ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); && (drawableSlider?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
} }
} }

View File

@ -54,6 +54,8 @@ namespace osu.Game.Rulesets.Osu.Objects
public virtual bool NewCombo { get; set; } public virtual bool NewCombo { get; set; }
public int ComboOffset { get; set; }
public virtual int IndexInCurrentCombo { get; set; } public virtual int IndexInCurrentCombo { get; set; }
public virtual int ComboIndex { get; set; } public virtual int ComboIndex { get; set; }

View File

@ -20,8 +20,6 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int SpinsRequired { get; protected set; } = 1; public int SpinsRequired { get; protected set; } = 1;
public override bool NewCombo => true;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);

View File

@ -4,6 +4,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using osu.Framework.Input.Bindings; using osu.Framework.Input.Bindings;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu namespace osu.Game.Rulesets.Osu
@ -12,8 +14,35 @@ namespace osu.Game.Rulesets.Osu
{ {
public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions; public IEnumerable<OsuAction> PressedActions => KeyBindingContainer.PressedActions;
public OsuInputManager(RulesetInfo ruleset) : base(ruleset, 0, SimultaneousBindingMode.Unique) public bool AllowUserPresses
{ {
set => ((OsuKeyBindingContainer)KeyBindingContainer).AllowUserPresses = value;
}
protected override RulesetKeyBindingContainer CreateKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
=> new OsuKeyBindingContainer(ruleset, variant, unique);
public OsuInputManager(RulesetInfo ruleset)
: base(ruleset, 0, SimultaneousBindingMode.Unique)
{
}
private class OsuKeyBindingContainer : RulesetKeyBindingContainer
{
public bool AllowUserPresses = true;
public OsuKeyBindingContainer(RulesetInfo ruleset, int variant, SimultaneousBindingMode unique)
: base(ruleset, variant, unique)
{
}
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args) => AllowUserPresses && base.OnKeyDown(state, args);
protected override bool OnKeyUp(InputState state, KeyUpEventArgs args) => AllowUserPresses && base.OnKeyUp(state, args);
protected override bool OnJoystickPress(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickPress(state, args);
protected override bool OnJoystickRelease(InputState state, JoystickEventArgs args) => AllowUserPresses && base.OnJoystickRelease(state, args);
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args) => AllowUserPresses && base.OnMouseDown(state, args);
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args) => AllowUserPresses && base.OnMouseUp(state, args);
protected override bool OnScroll(InputState state) => AllowUserPresses && base.OnScroll(state);
} }
} }
@ -21,6 +50,7 @@ namespace osu.Game.Rulesets.Osu
{ {
[Description("Left Button")] [Description("Left Button")]
LeftButton, LeftButton,
[Description("Right Button")] [Description("Right Button")]
RightButton RightButton
} }

View File

@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
if (Shared.VertexBuffer == null) if (Shared.VertexBuffer == null)
Shared.VertexBuffer = new QuadVertexBuffer<TexturedTrailVertex>(max_sprites, BufferUsageHint.DynamicDraw); Shared.VertexBuffer = new QuadVertexBuffer<TexturedTrailVertex>(max_sprites, BufferUsageHint.DynamicDraw);
Shader.GetUniform<float>("g_FadeClock").Value = Time; Shader.GetUniform<float>("g_FadeClock").UpdateValue(ref Time);
int updateStart = -1, updateEnd = 0; int updateStart = -1, updateEnd = 0;
for (int i = 0; i < Parts.Length; ++i) for (int i = 0; i < Parts.Length; ++i)

View File

@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.UI
public override void PostProcess() public override void PostProcess()
{ {
connectionLayer.HitObjects = HitObjects.Objects.Select(d => d.HitObject).OfType<OsuHitObject>(); connectionLayer.HitObjects = HitObjectContainer.Objects.Select(d => d.HitObject).OfType<OsuHitObject>();
} }
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{ {
TaikoHitObject first = x.First(); TaikoHitObject first = x.First();
if (x.Skip(1).Any()) if (x.Skip(1).Any() && !(first is Swell))
first.IsStrong = true; first.IsStrong = true;
return first; return first;
}).ToList(); }).ToList();
@ -168,7 +168,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
{ {
StartTime = obj.StartTime, StartTime = obj.StartTime,
Samples = obj.Samples, Samples = obj.Samples,
IsStrong = strong,
Duration = endTimeData.Duration, Duration = endTimeData.Duration,
RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier) RequiredHits = (int)Math.Max(1, endTimeData.Duration / 1000 * hitMultiplier)
}; };

View File

@ -1,6 +1,7 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 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 osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Taiko.Objects namespace osu.Game.Rulesets.Taiko.Objects
@ -16,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// </summary> /// </summary>
public int RequiredHits = 10; public int RequiredHits = 10;
public override bool IsStrong { set => throw new NotSupportedException($"{nameof(Swell)} cannot be a strong hitobject."); }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects()
{ {
base.CreateNestedHitObjects(); base.CreateNestedHitObjects();

View File

@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// Whether this HitObject is a "strong" type. /// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys. /// Strong hit objects give more points for hitting the hit object with both keys.
/// </summary> /// </summary>
public bool IsStrong; public virtual bool IsStrong { get; set; }
protected override void CreateNestedHitObjects() protected override void CreateNestedHitObjects()
{ {

View File

@ -11,7 +11,9 @@ using osu.Game.Audio;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Catch.Beatmaps;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Beatmaps;
using osu.Game.Skinning; using osu.Game.Skinning;
namespace osu.Game.Tests.Beatmaps.Formats namespace osu.Game.Tests.Beatmaps.Formats
@ -186,6 +188,50 @@ namespace osu.Game.Tests.Beatmaps.Formats
} }
} }
[Test]
public void TestDecodeBeatmapComboOffsetsOsu()
{
var decoder = new LegacyBeatmapDecoder();
using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu"))
using (var stream = new StreamReader(resStream))
{
var beatmap = decoder.Decode(stream);
var converted = new OsuBeatmapConverter(beatmap).Convert();
new OsuBeatmapProcessor(converted).PreProcess();
new OsuBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
}
}
[Test]
public void TestDecodeBeatmapComboOffsetsCatch()
{
var decoder = new LegacyBeatmapDecoder();
using (var resStream = Resource.OpenResource("hitobject-combo-offset.osu"))
using (var stream = new StreamReader(resStream))
{
var beatmap = decoder.Decode(stream);
var converted = new CatchBeatmapConverter(beatmap).Convert();
new CatchBeatmapProcessor(converted).PreProcess();
new CatchBeatmapProcessor(converted).PostProcess();
Assert.AreEqual(4, ((IHasComboInformation)converted.HitObjects.ElementAt(0)).ComboIndex);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(2)).ComboIndex);
Assert.AreEqual(5, ((IHasComboInformation)converted.HitObjects.ElementAt(4)).ComboIndex);
Assert.AreEqual(6, ((IHasComboInformation)converted.HitObjects.ElementAt(6)).ComboIndex);
Assert.AreEqual(11, ((IHasComboInformation)converted.HitObjects.ElementAt(8)).ComboIndex);
Assert.AreEqual(14, ((IHasComboInformation)converted.HitObjects.ElementAt(11)).ComboIndex);
}
}
[Test] [Test]
public void TestDecodeBeatmapHitObjects() public void TestDecodeBeatmapHitObjects()
{ {

View File

@ -0,0 +1,32 @@
osu file format v14
[HitObjects]
// Circle with combo offset (3)
255,193,1000,49,0,0:0:0:0:
// Combo index = 4
// Slider with new combo followed by circle with no new combo
256,192,2000,12,0,2000,0:0:0:0:
255,193,3000,1,0,0:0:0:0:
// Combo index = 5
// Slider without new combo followed by circle with no new combo
256,192,4000,8,0,5000,0:0:0:0:
255,193,6000,1,0,0:0:0:0:
// Combo index = 5
// Slider without new combo followed by circle with new combo
256,192,7000,8,0,8000,0:0:0:0:
255,193,9000,5,0,0:0:0:0:
// Combo index = 6
// Slider with new combo and offset (1) followed by circle with new combo and offset (3)
256,192,10000,28,0,11000,0:0:0:0:
255,193,12000,53,0,0:0:0:0:
// Combo index = 11
// Slider with new combo and offset (2) followed by slider with no new combo followed by circle with no new combo
256,192,13000,44,0,14000,0:0:0:0:
256,192,15000,8,0,16000,0:0:0:0:
255,193,17000,1,0,0:0:0:0:
// Combo index = 14

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using NUnit.Framework; using NUnit.Framework;
using OpenTK; using OpenTK;
using osu.Framework.Allocation; using osu.Framework.Allocation;
@ -116,7 +117,7 @@ namespace osu.Game.Tests.Visual
private void testNullBeatmap() private void testNullBeatmap()
{ {
selectNullBeatmap(); selectBeatmap(null);
AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text)); AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text));
AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title);
AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist);
@ -124,28 +125,19 @@ namespace osu.Game.Tests.Visual
AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any());
} }
private void selectBeatmap(IBeatmap b) private void selectBeatmap([CanBeNull] IBeatmap b)
{ {
BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null; BeatmapInfoWedge.BufferedWedgeInfo infoBefore = null;
AddStep($"select {b.Metadata.Title} beatmap", () => AddStep($"select {b?.Metadata.Title ?? "null"} beatmap", () =>
{ {
infoBefore = infoWedge.Info; infoBefore = infoWedge.Info;
infoWedge.Beatmap = Beatmap.Value = new TestWorkingBeatmap(b); infoWedge.Beatmap = Beatmap.Value = b == null ? Beatmap.Default : new TestWorkingBeatmap(b);
}); });
AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load"); AddUntilStep(() => infoWedge.Info != infoBefore, "wait for async load");
} }
private void selectNullBeatmap()
{
AddStep("select null beatmap", () =>
{
Beatmap.Value = Beatmap.Default;
infoWedge.Beatmap = Beatmap;
});
}
private IBeatmap createTestBeatmap(RulesetInfo ruleset) private IBeatmap createTestBeatmap(RulesetInfo ruleset)
{ {
List<HitObject> objects = new List<HitObject>(); List<HitObject> objects = new List<HitObject>();

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
AddInternal(trackManager); Add(trackManager);
} }
[Test] [Test]
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
TestTrackOwner owner = null; TestTrackOwner owner = null;
PreviewTrack track = null; PreviewTrack track = null;
AddStep("get track", () => AddInternal(owner = new TestTrackOwner(track = getTrack()))); AddStep("get track", () => Add(owner = new TestTrackOwner(track = getTrack())));
AddStep("start", () => track.Start()); AddStep("start", () => track.Start());
AddStep("attempt stop", () => trackManager.StopAnyPlaying(this)); AddStep("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning); AddAssert("not stopped", () => track.IsRunning);
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual
{ {
var track = getTrack(); var track = getTrack();
AddInternal(track); Add(track);
return track; return track;
} }

View File

@ -319,17 +319,17 @@ namespace osu.Game.Beatmaps
/// <summary> /// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary> /// </summary>
public async Task ImportFromStable() public Task ImportFromStable()
{ {
var stable = GetStableStorage?.Invoke(); var stable = GetStableStorage?.Invoke();
if (stable == null) if (stable == null)
{ {
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return; return Task.CompletedTask;
} }
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs")), TaskCreationOptions.LongRunning); return Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning);
} }
/// <summary> /// <summary>
@ -350,7 +350,11 @@ namespace osu.Game.Beatmaps
{ {
// let's make sure there are actually .osu files to import. // let's make sure there are actually .osu files to import.
string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu"));
if (string.IsNullOrEmpty(mapName)) throw new InvalidOperationException("No beatmap files found in this beatmap archive."); if (string.IsNullOrEmpty(mapName))
{
Logger.Log($"No beatmap files found in the beatmap archive ({reader.Name}).", LoggingTarget.Database);
return null;
}
Beatmap beatmap; Beatmap beatmap;
using (var stream = new StreamReader(reader.GetStream(mapName))) using (var stream = new StreamReader(reader.GetStream(mapName)))

View File

@ -27,11 +27,10 @@ namespace osu.Game.Beatmaps
if (obj.NewCombo) if (obj.NewCombo)
{ {
obj.IndexInCurrentCombo = 0; obj.IndexInCurrentCombo = 0;
obj.ComboIndex = (lastObj?.ComboIndex ?? 0) + obj.ComboOffset + 1;
if (lastObj != null) if (lastObj != null)
{
lastObj.LastInCombo = true; lastObj.LastInCombo = true;
obj.ComboIndex = lastObj.ComboIndex + 1;
}
} }
else if (lastObj != null) else if (lastObj != null)
{ {

View File

@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps.Formats
} while (line != null && line.Length == 0); } while (line != null && line.Length == 0);
if (line == null) if (line == null)
throw new IOException(@"Unknown file format"); throw new IOException(@"Unknown file format (null)");
var decoder = typedDecoders.Select(d => line.StartsWith(d.Key) ? d.Value : null).FirstOrDefault(); var decoder = typedDecoders.Select(d => line.StartsWith(d.Key, StringComparison.InvariantCulture) ? d.Value : null).FirstOrDefault();
if (decoder == null) if (decoder == null)
throw new IOException(@"Unknown file format"); throw new IOException($@"Unknown file format ({line})");
return (Decoder<T>)decoder.Invoke(line); return (Decoder<T>)decoder.Invoke(line);
} }

View File

@ -126,16 +126,16 @@ namespace osu.Game.Beatmaps.Formats
switch (beatmap.BeatmapInfo.RulesetID) switch (beatmap.BeatmapInfo.RulesetID)
{ {
case 0: case 0:
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break; break;
case 1: case 1:
parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Taiko.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break; break;
case 2: case 2:
parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Catch.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break; break;
case 3: case 3:
parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Mania.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
break; break;
} }
@ -405,14 +405,11 @@ namespace osu.Game.Beatmaps.Formats
{ {
// If the ruleset wasn't specified, assume the osu!standard ruleset. // If the ruleset wasn't specified, assume the osu!standard ruleset.
if (parser == null) if (parser == null)
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(); parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
var obj = parser.Parse(line, getOffsetTime());
var obj = parser.Parse(line);
if (obj != null) if (obj != null)
{
beatmap.HitObjects.Add(obj); beatmap.HitObjects.Add(obj);
}
} }
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0); private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);

View File

@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats
if (ShouldSkipLine(line)) if (ShouldSkipLine(line))
continue; continue;
if (line.StartsWith(@"[") && line.EndsWith(@"]")) if (line.StartsWith(@"[", StringComparison.Ordinal) && line.EndsWith(@"]", StringComparison.Ordinal))
{ {
if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section)) if (!Enum.TryParse(line.Substring(1, line.Length - 2), out section))
{ {
@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps.Formats
} }
} }
protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//"); protected virtual bool ShouldSkipLine(string line) => string.IsNullOrWhiteSpace(line) || line.StartsWith("//", StringComparison.Ordinal);
protected virtual void ParseLine(T output, Section section, string line) protected virtual void ParseLine(T output, Section section, string line)
{ {

View File

@ -60,7 +60,7 @@ namespace osu.Game.Beatmaps.Formats
private void handleEvents(string line) private void handleEvents(string line)
{ {
var depth = 0; var depth = 0;
while (line.StartsWith(" ") || line.StartsWith("_")) while (line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal))
{ {
++depth; ++depth;
line = line.Substring(1); line = line.Substring(1);
@ -269,9 +269,9 @@ namespace osu.Game.Beatmaps.Formats
return Anchor.BottomCentre; return Anchor.BottomCentre;
case LegacyOrigins.BottomRight: case LegacyOrigins.BottomRight:
return Anchor.BottomRight; return Anchor.BottomRight;
default:
return Anchor.TopLeft;
} }
throw new InvalidDataException($@"Unknown origin: {value}");
} }
private void handleVariables(string line) private void handleVariables(string line)

View File

@ -67,7 +67,7 @@ namespace osu.Game.Beatmaps
public bool BeatmapLoaded => beatmap.IsResultAvailable; public bool BeatmapLoaded => beatmap.IsResultAvailable;
public IBeatmap Beatmap => beatmap.Value.Result; public IBeatmap Beatmap => beatmap.Value.Result;
public async Task<IBeatmap> GetBeatmapAsync() => await beatmap.Value; public Task<IBeatmap> GetBeatmapAsync() => beatmap.Value;
private readonly AsyncLazy<IBeatmap> beatmap; private readonly AsyncLazy<IBeatmap> beatmap;
private IBeatmap populateBeatmap() private IBeatmap populateBeatmap()
@ -138,14 +138,14 @@ namespace osu.Game.Beatmaps
public bool BackgroundLoaded => background.IsResultAvailable; public bool BackgroundLoaded => background.IsResultAvailable;
public Texture Background => background.Value.Result; public Texture Background => background.Value.Result;
public async Task<Texture> GetBackgroundAsync() => await background.Value; public Task<Texture> GetBackgroundAsync() => background.Value;
private AsyncLazy<Texture> background; private AsyncLazy<Texture> background;
private Texture populateBackground() => GetBackground(); private Texture populateBackground() => GetBackground();
public bool TrackLoaded => track.IsResultAvailable; public bool TrackLoaded => track.IsResultAvailable;
public Track Track => track.Value.Result; public Track Track => track.Value.Result;
public async Task<Track> GetTrackAsync() => await track.Value; public Task<Track> GetTrackAsync() => track.Value;
private AsyncLazy<Track> track; private AsyncLazy<Track> track;
private Track populateTrack() private Track populateTrack()
@ -158,21 +158,21 @@ namespace osu.Game.Beatmaps
public bool WaveformLoaded => waveform.IsResultAvailable; public bool WaveformLoaded => waveform.IsResultAvailable;
public Waveform Waveform => waveform.Value.Result; public Waveform Waveform => waveform.Value.Result;
public async Task<Waveform> GetWaveformAsync() => await waveform.Value; public Task<Waveform> GetWaveformAsync() => waveform.Value;
private readonly AsyncLazy<Waveform> waveform; private readonly AsyncLazy<Waveform> waveform;
private Waveform populateWaveform() => GetWaveform(); private Waveform populateWaveform() => GetWaveform();
public bool StoryboardLoaded => storyboard.IsResultAvailable; public bool StoryboardLoaded => storyboard.IsResultAvailable;
public Storyboard Storyboard => storyboard.Value.Result; public Storyboard Storyboard => storyboard.Value.Result;
public async Task<Storyboard> GetStoryboardAsync() => await storyboard.Value; public Task<Storyboard> GetStoryboardAsync() => storyboard.Value;
private readonly AsyncLazy<Storyboard> storyboard; private readonly AsyncLazy<Storyboard> storyboard;
private Storyboard populateStoryboard() => GetStoryboard(); private Storyboard populateStoryboard() => GetStoryboard();
public bool SkinLoaded => skin.IsResultAvailable; public bool SkinLoaded => skin.IsResultAvailable;
public Skin Skin => skin.Value.Result; public Skin Skin => skin.Value.Result;
public async Task<Skin> GetSkinAsync() => await skin.Value; public Task<Skin> GetSkinAsync() => skin.Value;
private readonly AsyncLazy<Skin> skin; private readonly AsyncLazy<Skin> skin;
private Skin populateSkin() => GetSkin(); private Skin populateSkin() => GetSkin();

View File

@ -178,7 +178,8 @@ namespace osu.Game.Database
{ {
try try
{ {
return Import(CreateModel(archive), archive); var model = CreateModel(archive);
return model == null ? null : Import(model, archive);
} }
catch (Exception e) catch (Exception e)
{ {
@ -198,6 +199,8 @@ namespace osu.Game.Database
try try
{ {
Logger.Log($"Importing {item}...", LoggingTarget.Database);
using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes.
{ {
try try
@ -387,7 +390,7 @@ namespace osu.Game.Database
/// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking. /// Actual expensive population should be done in <see cref="Populate"/>; this should just prepare for duplicate checking.
/// </summary> /// </summary>
/// <param name="archive">The archive to create the model for.</param> /// <param name="archive">The archive to create the model for.</param>
/// <returns>A model populated with minimal information.</returns> /// <returns>A model populated with minimal information. Returning a null will abort importing silently.</returns>
protected abstract TModel CreateModel(ArchiveReader archive); protected abstract TModel CreateModel(ArchiveReader archive);
/// <summary> /// <summary>
@ -412,7 +415,7 @@ namespace osu.Game.Database
private ArchiveReader getReaderFrom(string path) private ArchiveReader getReaderFrom(string path)
{ {
if (ZipUtils.IsZipArchive(path)) if (ZipUtils.IsZipArchive(path))
return new ZipArchiveReader(Files.Storage.GetStream(path), Path.GetFileName(path)); return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path));
if (Directory.Exists(path)) if (Directory.Exists(path))
return new LegacyFilesystemReader(path); return new LegacyFilesystemReader(path);
throw new InvalidFormatException($"{path} is not a valid archive"); throw new InvalidFormatException($"{path} is not a valid archive");

View File

@ -5,7 +5,6 @@ using System;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Storage;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Platform; using osu.Framework.Platform;
namespace osu.Game.Database namespace osu.Game.Database
@ -118,7 +117,9 @@ namespace osu.Game.Database
private void recycleThreadContexts() private void recycleThreadContexts()
{ {
threadContexts?.Values.ForEach(c => c.Dispose()); // Contexts for other threads are not disposed as they may be in use elsewhere. Instead, fresh contexts are exposed
// for other threads to use, and we rely on the finalizer inside OsuDbContext to handle their previous contexts
threadContexts?.Value.Dispose();
threadContexts = new ThreadLocal<OsuDbContext>(CreateContext, true); threadContexts = new ThreadLocal<OsuDbContext>(CreateContext, true);
} }

View File

@ -75,6 +75,13 @@ namespace osu.Game.Database
} }
} }
~OsuDbContext()
{
// DbContext does not contain a finalizer (https://github.com/aspnet/EntityFrameworkCore/issues/8872)
// This is used to clean up previous contexts when fresh contexts are exposed via DatabaseContextFactory
Dispose();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{ {
base.OnConfiguring(optionsBuilder); base.OnConfiguring(optionsBuilder);

View File

@ -0,0 +1,39 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
namespace osu.Game.Graphics.Containers
{
/// <summary>
/// A container that adds the ability to shake its contents.
/// </summary>
public class ShakeContainer : Container
{
/// <summary>
/// Shake the contents of this container.
/// </summary>
/// <param name="maximumLength">The maximum length the shake should last.</param>
public void Shake(double maximumLength)
{
const float shake_amount = 8;
const float shake_duration = 30;
// if we don't have enough time, don't bother shaking.
if (maximumLength < shake_duration * 2)
return;
var sequence = this.MoveToX(shake_amount, shake_duration / 2, Easing.OutSine).Then()
.MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then();
// if we don't have enough time for the second shake, skip it.
if (maximumLength > shake_duration * 4)
sequence = sequence
.MoveToX(shake_amount, shake_duration, Easing.InOutSine).Then()
.MoveToX(-shake_amount, shake_duration, Easing.InOutSine).Then();
sequence.MoveToX(0, shake_duration / 2, Easing.InSine);
}
}
}

View File

@ -2,7 +2,6 @@
// 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.Drawing.Imaging;
using System.IO; using System.IO;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -19,6 +18,7 @@ using osu.Game.Configuration;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp;
namespace osu.Game.Graphics namespace osu.Game.Graphics
{ {
@ -71,7 +71,7 @@ namespace osu.Game.Graphics
private volatile int screenShotTasks; private volatile int screenShotTasks;
public async Task TakeScreenshotAsync() => await Task.Run(async () => public Task TakeScreenshotAsync() => Task.Run(async () =>
{ {
Interlocked.Increment(ref screenShotTasks); Interlocked.Increment(ref screenShotTasks);
@ -90,7 +90,7 @@ namespace osu.Game.Graphics
waitDelegate.Cancel(); waitDelegate.Cancel();
} }
using (var bitmap = await host.TakeScreenshotAsync()) using (var image = await host.TakeScreenshotAsync())
{ {
Interlocked.Decrement(ref screenShotTasks); Interlocked.Decrement(ref screenShotTasks);
@ -102,10 +102,10 @@ namespace osu.Game.Graphics
switch (screenshotFormat.Value) switch (screenshotFormat.Value)
{ {
case ScreenshotFormat.Png: case ScreenshotFormat.Png:
bitmap.Save(stream, ImageFormat.Png); image.SaveAsPng(stream);
break; break;
case ScreenshotFormat.Jpg: case ScreenshotFormat.Jpg:
bitmap.Save(stream, ImageFormat.Jpeg); image.SaveAsJpeg(stream);
break; break;
default: default:
throw new ArgumentOutOfRangeException(nameof(screenshotFormat)); throw new ArgumentOutOfRangeException(nameof(screenshotFormat));

View File

@ -71,7 +71,7 @@ namespace osu.Game.Graphics
if (loadableIcon == loadedIcon) return; if (loadableIcon == loadedIcon) return;
var texture = store?.Get(((char)loadableIcon).ToString()); var texture = store.Get(((char)loadableIcon).ToString());
spriteMain.Texture = texture; spriteMain.Texture = texture;
spriteShadow.Texture = texture; spriteShadow.Texture = texture;
@ -129,7 +129,7 @@ namespace osu.Game.Graphics
if (icon == value) return; if (icon == value) return;
icon = value; icon = value;
if (IsLoaded) if (LoadState == LoadState.Loaded)
updateTexture(); updateTexture();
} }
} }

View File

@ -1,7 +1,16 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 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 osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface; using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Game.Graphics.Sprites;
using OpenTK.Graphics;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -10,9 +19,73 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
public class OsuButton : Button public class OsuButton : Button
{ {
private Box hover;
public OsuButton() public OsuButton()
{ {
Add(new HoverClickSounds(HoverSampleSet.Loud)); Height = 40;
Content.Masking = true;
Content.CornerRadius = 5;
} }
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
BackgroundColour = colours.BlueDark;
AddRange(new Drawable[]
{
hover = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingMode.Additive,
Colour = Color4.White.Opacity(0.1f),
Alpha = 0,
Depth = -1
},
new HoverClickSounds(HoverSampleSet.Loud),
});
Enabled.ValueChanged += enabled_ValueChanged;
Enabled.TriggerChange();
}
private void enabled_ValueChanged(bool enabled)
{
this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
}
protected override bool OnHover(InputState state)
{
hover.FadeIn(200);
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
hover.FadeOut(200);
base.OnHoverLost(state);
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
Content.ScaleTo(0.9f, 4000, Easing.OutQuint);
return base.OnMouseDown(state, args);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{
Content.ScaleTo(1, 1000, Easing.OutElastic);
return base.OnMouseUp(state, args);
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Font = @"Exo2.0-Bold",
};
} }
} }

View File

@ -2,17 +2,10 @@
// 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 OpenTK.Graphics;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Graphics.UserInterface namespace osu.Game.Graphics.UserInterface
{ {
@ -21,79 +14,17 @@ namespace osu.Game.Graphics.UserInterface
/// </summary> /// </summary>
public class TriangleButton : OsuButton, IFilterable public class TriangleButton : OsuButton, IFilterable
{ {
private Box hover; protected Triangles Triangles { get; private set; }
protected Triangles Triangles;
public TriangleButton()
{
Height = 40;
}
protected override SpriteText CreateText() => new OsuSpriteText
{
Depth = -1,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Font = @"Exo2.0-Bold",
};
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
BackgroundColour = colours.BlueDark; Add(Triangles = new Triangles
Content.Masking = true;
Content.CornerRadius = 5;
AddRange(new Drawable[]
{ {
Triangles = new Triangles RelativeSizeAxes = Axes.Both,
{ ColourDark = colours.BlueDarker,
RelativeSizeAxes = Axes.Both, ColourLight = colours.Blue,
ColourDark = colours.BlueDarker,
ColourLight = colours.Blue,
},
hover = new Box
{
RelativeSizeAxes = Axes.Both,
Blending = BlendingMode.Additive,
Colour = Color4.White.Opacity(0.1f),
Alpha = 0,
},
}); });
Enabled.ValueChanged += enabled_ValueChanged;
Enabled.TriggerChange();
}
private void enabled_ValueChanged(bool enabled)
{
this.FadeColour(enabled ? Color4.White : Color4.Gray, 200, Easing.OutQuint);
}
protected override bool OnHover(InputState state)
{
hover.FadeIn(200);
return base.OnHover(state);
}
protected override void OnHoverLost(InputState state)
{
hover.FadeOut(200);
base.OnHoverLost(state);
}
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
Content.ScaleTo(0.9f, 4000, Easing.OutQuint);
return base.OnMouseDown(state, args);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{
Content.ScaleTo(1, 1000, Easing.OutElastic);
return base.OnMouseUp(state, args);
} }
public IEnumerable<string> FilterTerms => new[] { Text }; public IEnumerable<string> FilterTerms => new[] { Text };

View File

@ -3,6 +3,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
namespace osu.Game.IO.Archives namespace osu.Game.IO.Archives
@ -28,7 +29,9 @@ namespace osu.Game.IO.Archives
public abstract IEnumerable<string> Filenames { get; } public abstract IEnumerable<string> Filenames { get; }
public virtual byte[] Get(string name) public virtual byte[] Get(string name) => GetAsync(name).Result;
public async Task<byte[]> GetAsync(string name)
{ {
using (Stream input = GetStream(name)) using (Stream input = GetStream(name))
{ {
@ -36,7 +39,7 @@ namespace osu.Game.IO.Archives
return null; return null;
byte[] buffer = new byte[input.Length]; byte[] buffer = new byte[input.Length];
input.Read(buffer, 0, buffer.Length); await input.ReadAsync(buffer, 0, buffer.Length);
return buffer; return buffer;
} }
} }

View File

@ -36,6 +36,8 @@ using osu.Game.Skinning;
using OpenTK.Graphics; using OpenTK.Graphics;
using osu.Game.Overlays.Volume; using osu.Game.Overlays.Volume;
using osu.Game.Screens.Select; using osu.Game.Screens.Select;
using osu.Game.Utils;
using LogLevel = osu.Framework.Logging.LogLevel;
namespace osu.Game namespace osu.Game
{ {
@ -65,16 +67,18 @@ namespace osu.Game
private ScreenshotManager screenshotManager; private ScreenshotManager screenshotManager;
protected RavenLogger RavenLogger;
public virtual Storage GetStorageForStableInstall() => null; public virtual Storage GetStorageForStableInstall() => null;
private Intro intro private Intro intro
{ {
get get
{ {
Screen s = screenStack; Screen screen = screenStack;
while (s != null && !(s is Intro)) while (screen != null && !(screen is Intro))
s = s.ChildScreen; screen = screen.ChildScreen;
return s as Intro; return screen as Intro;
} }
} }
@ -108,6 +112,8 @@ namespace osu.Game
this.args = args; this.args = args;
forwardLoggedErrorsToNotifications(); forwardLoggedErrorsToNotifications();
RavenLogger = new RavenLogger(this);
} }
public void ToggleSettings() => settings.ToggleVisibility(); public void ToggleSettings() => settings.ToggleVisibility();
@ -120,8 +126,8 @@ namespace osu.Game
/// <param name="toolbar">Whether the toolbar should also be hidden.</param> /// <param name="toolbar">Whether the toolbar should also be hidden.</param>
public void CloseAllOverlays(bool toolbar = true) public void CloseAllOverlays(bool toolbar = true)
{ {
foreach (var o in overlays) foreach (var overlay in overlays)
o.State = Visibility.Hidden; overlay.State = Visibility.Hidden;
if (toolbar) Toolbar.State = Visibility.Hidden; if (toolbar) Toolbar.State = Visibility.Hidden;
} }
@ -145,13 +151,15 @@ namespace osu.Game
if (args?.Length > 0) if (args?.Length > 0)
{ {
var paths = args.Where(a => !a.StartsWith(@"-")); var paths = args.Where(a => !a.StartsWith(@"-")).ToArray();
if (paths.Length > 0)
Task.Run(() => Import(paths.ToArray())); Task.Run(() => Import(paths));
} }
dependencies.CacheAs(this); dependencies.CacheAs(this);
dependencies.Cache(RavenLogger);
dependencies.CacheAs(ruleset); dependencies.CacheAs(ruleset);
dependencies.CacheAs<IBindable<RulesetInfo>>(ruleset); dependencies.CacheAs<IBindable<RulesetInfo>>(ruleset);
@ -236,7 +244,7 @@ namespace osu.Game
/// <param name="beatmapId">The beatmap to show.</param> /// <param name="beatmapId">The beatmap to show.</param>
public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId); public void ShowBeatmap(int beatmapId) => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId);
protected void LoadScore(Score s) protected void LoadScore(Score score)
{ {
scoreLoad?.Cancel(); scoreLoad?.Cancel();
@ -244,18 +252,18 @@ namespace osu.Game
if (menu == null) if (menu == null)
{ {
scoreLoad = Schedule(() => LoadScore(s)); scoreLoad = Schedule(() => LoadScore(score));
return; return;
} }
if (!menu.IsCurrentScreen) if (!menu.IsCurrentScreen)
{ {
menu.MakeCurrent(); menu.MakeCurrent();
this.Delay(500).Schedule(() => LoadScore(s), out scoreLoad); this.Delay(500).Schedule(() => LoadScore(score), out scoreLoad);
return; return;
} }
if (s.Beatmap == null) if (score.Beatmap == null)
{ {
notifications.Post(new SimpleNotification notifications.Post(new SimpleNotification
{ {
@ -265,12 +273,18 @@ namespace osu.Game
return; return;
} }
ruleset.Value = s.Ruleset; ruleset.Value = score.Ruleset;
Beatmap.Value = BeatmapManager.GetWorkingBeatmap(s.Beatmap); Beatmap.Value = BeatmapManager.GetWorkingBeatmap(score.Beatmap);
Beatmap.Value.Mods.Value = s.Mods; Beatmap.Value.Mods.Value = score.Mods;
menu.Push(new PlayerLoader(new ReplayPlayer(s.Replay))); menu.Push(new PlayerLoader(new ReplayPlayer(score.Replay)));
}
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
RavenLogger.Dispose();
} }
protected override void LoadComplete() protected override void LoadComplete()
@ -449,7 +463,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification Schedule(() => notifications.Post(new SimpleNotification
{ {
Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb, Icon = entry.Level == LogLevel.Important ? FontAwesome.fa_exclamation_circle : FontAwesome.fa_bomb,
Text = entry.Message, Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
})); }));
} }
else if (recentLogCount == short_term_display_limit) else if (recentLogCount == short_term_display_limit)
@ -490,7 +504,27 @@ namespace osu.Game
// schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached). // schedule is here to ensure that all component loads are done after LoadComplete is run (and thus all dependencies are cached).
// with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile, // with some better organisation of LoadComplete to do construction and dependency caching in one step, followed by calls to loadComponentSingleFile,
// we could avoid the need for scheduling altogether. // we could avoid the need for scheduling altogether.
Schedule(() => { asyncLoadStream = asyncLoadStream?.ContinueWith(t => LoadComponentAsync(d, add).Wait()) ?? LoadComponentAsync(d, add); }); Schedule(() =>
{
var previousLoadStream = asyncLoadStream;
//chain with existing load stream
asyncLoadStream = Task.Run(async () =>
{
if (previousLoadStream != null)
await previousLoadStream;
try
{
Logger.Log($"Loading {d}...", LoggingTarget.Debug);
await LoadComponentAsync(d, add);
Logger.Log($"Loaded {d}!", LoggingTarget.Debug);
}
catch (OperationCanceledException)
{
}
});
});
} }
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
@ -601,6 +635,7 @@ namespace osu.Game
private void screenAdded(Screen newScreen) private void screenAdded(Screen newScreen)
{ {
currentScreen = (OsuScreen)newScreen; currentScreen = (OsuScreen)newScreen;
Logger.Log($"Screen changed → {currentScreen}");
newScreen.ModePushed += screenAdded; newScreen.ModePushed += screenAdded;
newScreen.Exited += screenRemoved; newScreen.Exited += screenRemoved;
@ -609,6 +644,7 @@ namespace osu.Game
private void screenRemoved(Screen newScreen) private void screenRemoved(Screen newScreen)
{ {
currentScreen = (OsuScreen)newScreen; currentScreen = (OsuScreen)newScreen;
Logger.Log($"Screen changed ← {currentScreen}");
if (newScreen == null) if (newScreen == null)
Exit(); Exit();

View File

@ -240,6 +240,15 @@ namespace osu.Game.Overlays
}); });
} }
protected override void PopIn()
{
base.PopIn();
// Queries are allowed to be run only on the first pop-in
if (getSetsRequest == null)
Scheduler.AddOnce(updateSearch);
}
private SearchBeatmapSetsRequest getSetsRequest; private SearchBeatmapSetsRequest getSetsRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>(); private readonly Bindable<string> currentQuery = new Bindable<string>();
@ -251,16 +260,22 @@ namespace osu.Game.Overlays
{ {
queryChangedDebounce?.Cancel(); queryChangedDebounce?.Cancel();
if (!IsLoaded) return; if (!IsLoaded)
return;
if (State == Visibility.Hidden)
return;
BeatmapSets = null; BeatmapSets = null;
ResultAmounts = null; ResultAmounts = null;
getSetsRequest?.Cancel(); getSetsRequest?.Cancel();
if (api == null) return; if (api == null)
return;
if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty)) return; if (Header.Tabs.Current.Value == DirectTab.Search && (Filter.Search.Text == string.Empty || currentQuery == string.Empty))
return;
previewTrackManager.StopAnyPlaying(this); previewTrackManager.StopAnyPlaying(this);

View File

@ -27,6 +27,11 @@ namespace osu.Game.Overlays.Mods
{ {
public class ModSelectOverlay : WaveOverlayContainer public class ModSelectOverlay : WaveOverlayContainer
{ {
/// <summary>
/// How much this container should overflow the sides of the screen to account for parallax shifting.
/// </summary>
private const float overflow_padding = 50;
private const float content_width = 0.8f; private const float content_width = 0.8f;
protected Color4 LowMultiplierColour, HighMultiplierColour; protected Color4 LowMultiplierColour, HighMultiplierColour;
@ -199,6 +204,11 @@ namespace osu.Game.Overlays.Mods
Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e"); Waves.FourthWaveColour = OsuColour.FromHex(@"003a4e");
Height = 510; Height = 510;
Padding = new MarginPadding
{
Left = -overflow_padding,
Right = -overflow_padding
};
Children = new Drawable[] Children = new Drawable[]
{ {
@ -258,6 +268,11 @@ namespace osu.Game.Overlays.Mods
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical, Direction = FillDirection.Vertical,
Width = content_width, Width = content_width,
Padding = new MarginPadding
{
Left = overflow_padding,
Right = overflow_padding
},
Children = new Drawable[] Children = new Drawable[]
{ {
new OsuSpriteText new OsuSpriteText
@ -295,7 +310,12 @@ namespace osu.Game.Overlays.Mods
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
Anchor = Anchor.TopCentre, Anchor = Anchor.TopCentre,
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Vertical = 10 }, Padding = new MarginPadding
{
Vertical = 10,
Left = overflow_padding,
Right = overflow_padding
},
Child = ModSectionsContainer = new FillFlowContainer<ModSection> Child = ModSectionsContainer = new FillFlowContainer<ModSection>
{ {
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -341,7 +361,9 @@ namespace osu.Game.Overlays.Mods
Direction = FillDirection.Horizontal, Direction = FillDirection.Horizontal,
Padding = new MarginPadding Padding = new MarginPadding
{ {
Vertical = 15 Vertical = 15,
Left = overflow_padding,
Right = overflow_padding
}, },
Children = new Drawable[] Children = new Drawable[]
{ {

View File

@ -96,8 +96,7 @@ namespace osu.Game.Overlays
base.LoadComplete(); base.LoadComplete();
StateChanged += _ => updateProcessingMode(); StateChanged += _ => updateProcessingMode();
OverlayActivationMode.ValueChanged += _ => updateProcessingMode(); OverlayActivationMode.BindValueChanged(_ => updateProcessingMode(), true);
OverlayActivationMode.TriggerChange();
} }
private int totalCount => sections.Select(c => c.DisplayedCount).Sum(); private int totalCount => sections.Select(c => c.DisplayedCount).Sum();

View File

@ -13,15 +13,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
{ {
protected override string Header => "Garbage Collector"; protected override string Header => "Garbage Collector";
private readonly Bindable<LatencyMode> latencyMode = new Bindable<LatencyMode>();
private Bindable<GCLatencyMode> configLatencyMode;
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(FrameworkDebugConfigManager config) private void load(FrameworkDebugConfigManager config)
{ {
Children = new Drawable[] Children = new Drawable[]
{ {
new SettingsEnumDropdown<GCLatencyMode> new SettingsEnumDropdown<LatencyMode>
{ {
LabelText = "Active mode", LabelText = "Active mode",
Bindable = config.GetBindable<GCLatencyMode>(DebugSetting.ActiveGCMode) Bindable = latencyMode
}, },
new SettingsButton new SettingsButton
{ {
@ -29,6 +32,18 @@ namespace osu.Game.Overlays.Settings.Sections.Debug
Action = GC.Collect Action = GC.Collect
}, },
}; };
configLatencyMode = config.GetBindable<GCLatencyMode>(DebugSetting.ActiveGCMode);
configLatencyMode.BindValueChanged(v => latencyMode.Value = (LatencyMode)v, true);
latencyMode.BindValueChanged(v => configLatencyMode.Value = (GCLatencyMode)v);
}
private enum LatencyMode
{
Batch = GCLatencyMode.Batch,
Interactive = GCLatencyMode.Interactive,
LowLatency = GCLatencyMode.LowLatency,
SustainedLowLatency = GCLatencyMode.SustainedLowLatency
} }
} }
} }

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Framework.Input.States; using osu.Framework.Input.States;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
@ -16,7 +17,7 @@ using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Settings namespace osu.Game.Overlays.Settings
{ {
public class SidebarButton : OsuButton public class SidebarButton : Button
{ {
private readonly SpriteIcon drawableIcon; private readonly SpriteIcon drawableIcon;
private readonly SpriteText headerText; private readonly SpriteText headerText;
@ -97,7 +98,8 @@ namespace osu.Game.Overlays.Settings
Width = 5, Width = 5,
Anchor = Anchor.CentreRight, Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight, Origin = Anchor.CentreRight,
} },
new HoverClickSounds(HoverSampleSet.Loud),
}); });
} }

View File

@ -0,0 +1,12 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Mods
{
public interface IUpdatableByPlayfield : IApplicableMod
{
void Update(Playfield playfield);
}
}

View File

@ -147,17 +147,15 @@ namespace osu.Game.Rulesets.Objects.Drawables
/// </summary> /// </summary>
public void PlaySamples() => Samples?.Play(); public void PlaySamples() => Samples?.Play();
private double lastUpdateTime;
protected override void Update() protected override void Update()
{ {
base.Update(); base.Update();
if (Result != null && lastUpdateTime > Time.Current) if (Result != null && Result.HasResult)
{ {
var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime; var endTime = (HitObject as IHasEndTime)?.EndTime ?? HitObject.StartTime;
if (Result.TimeOffset + endTime < Time.Current) if (Result.TimeOffset + endTime > Time.Current)
{ {
OnRevertResult?.Invoke(this, Result); OnRevertResult?.Invoke(this, Result);
@ -165,8 +163,6 @@ namespace osu.Game.Rulesets.Objects.Drawables
State.Value = ArmedState.Idle; State.Value = ArmedState.Idle;
} }
} }
lastUpdateTime = Time.Current;
} }
protected override void UpdateAfterChildren() protected override void UpdateAfterChildren()

View File

@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
public float X { get; set; } public float X { get; set; }
public bool NewCombo { get; set; } public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
} }
} }

View File

@ -13,21 +13,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
/// </summary> /// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{ {
protected override HitObject CreateHit(Vector2 position, bool newCombo) public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{ {
}
private bool forceNewCombo;
private int extraComboOffset;
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;
forceNewCombo = false;
extraComboOffset = 0;
return new ConvertHit return new ConvertHit
{ {
X = position.X, X = position.X,
NewCombo = newCombo, NewCombo = newCombo,
ComboOffset = comboOffset
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples) protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{ {
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;
forceNewCombo = false;
extraComboOffset = 0;
return new ConvertSlider return new ConvertSlider
{ {
X = position.X, X = position.X,
NewCombo = newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
ControlPoints = controlPoints, ControlPoints = controlPoints,
Distance = length, Distance = length,
CurveType = curveType, CurveType = curveType,
@ -36,15 +58,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
}; };
} }
protected override HitObject CreateSpinner(Vector2 position, double endTime) protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
// Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
// Their combo offset is still added to that next hitobject's combo index
forceNewCombo |= FormatVersion <= 8 || newCombo;
extraComboOffset += comboOffset;
return new ConvertSpinner return new ConvertSpinner
{ {
EndTime = endTime EndTime = endTime
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return null; return null;
} }

View File

@ -13,5 +13,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
public float X { get; set; } public float X { get; set; }
public bool NewCombo { get; set; } public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
} }
} }

View File

@ -8,10 +8,14 @@ namespace osu.Game.Rulesets.Objects.Legacy.Catch
/// <summary> /// <summary>
/// Legacy osu!catch Spinner-type, used for parsing Beatmaps. /// Legacy osu!catch Spinner-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertSpinner : HitObject, IHasEndTime internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasCombo
{ {
public double EndTime { get; set; } public double EndTime { get; set; }
public double Duration => EndTime - StartTime; public double Duration => EndTime - StartTime;
public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
} }
} }

View File

@ -10,6 +10,8 @@ using System.IO;
using osu.Game.Beatmaps.Formats; using osu.Game.Beatmaps.Formats;
using osu.Game.Audio; using osu.Game.Audio;
using System.Linq; using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Framework.MathUtils; using osu.Framework.MathUtils;
namespace osu.Game.Rulesets.Objects.Legacy namespace osu.Game.Rulesets.Objects.Legacy
@ -19,12 +21,26 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary> /// </summary>
public abstract class ConvertHitObjectParser : HitObjectParser public abstract class ConvertHitObjectParser : HitObjectParser
{ {
public override HitObject Parse(string text) /// <summary>
/// The offset to apply to all time values.
/// </summary>
protected readonly double Offset;
/// <summary>
/// The beatmap version.
/// </summary>
protected readonly int FormatVersion;
protected bool FirstObject { get; private set; } = true;
protected ConvertHitObjectParser(double offset, int formatVersion)
{ {
return Parse(text, 0); Offset = offset;
FormatVersion = formatVersion;
} }
public HitObject Parse(string text, double offset) [CanBeNull]
public override HitObject Parse(string text)
{ {
try try
{ {
@ -32,7 +48,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture)); Vector2 pos = new Vector2((int)Convert.ToSingle(split[0], CultureInfo.InvariantCulture), (int)Convert.ToSingle(split[1], CultureInfo.InvariantCulture));
ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]) & ~ConvertHitObjectType.ColourHax; ConvertHitObjectType type = (ConvertHitObjectType)int.Parse(split[3]);
int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4;
type &= ~ConvertHitObjectType.ComboOffset;
bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); bool combo = type.HasFlag(ConvertHitObjectType.NewCombo);
type &= ~ConvertHitObjectType.NewCombo; type &= ~ConvertHitObjectType.NewCombo;
@ -43,7 +63,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
if (type.HasFlag(ConvertHitObjectType.Circle)) if (type.HasFlag(ConvertHitObjectType.Circle))
{ {
result = CreateHit(pos, combo); result = CreateHit(pos, combo, comboOffset);
if (split.Length > 5) if (split.Length > 5)
readCustomSampleBanks(split[5], bankInfo); readCustomSampleBanks(split[5], bankInfo);
@ -148,11 +168,11 @@ namespace osu.Game.Rulesets.Objects.Legacy
for (int i = 0; i < nodes; i++) for (int i = 0; i < nodes; i++)
nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i]));
result = CreateSlider(pos, combo, points, length, curveType, repeatCount, nodeSamples); result = CreateSlider(pos, combo, comboOffset, points, length, curveType, repeatCount, nodeSamples);
} }
else if (type.HasFlag(ConvertHitObjectType.Spinner)) else if (type.HasFlag(ConvertHitObjectType.Spinner))
{ {
result = CreateSpinner(new Vector2(512, 384) / 2, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + offset); result = CreateSpinner(new Vector2(512, 384) / 2, combo, comboOffset, Convert.ToDouble(split[5], CultureInfo.InvariantCulture) + Offset);
if (split.Length > 6) if (split.Length > 6)
readCustomSampleBanks(split[6], bankInfo); readCustomSampleBanks(split[6], bankInfo);
@ -170,15 +190,20 @@ namespace osu.Game.Rulesets.Objects.Legacy
readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo);
} }
result = CreateHold(pos, combo, endTime + offset); result = CreateHold(pos, combo, comboOffset, endTime + Offset);
} }
if (result == null) if (result == null)
throw new InvalidOperationException($@"Unknown hit object type {type}."); {
Logger.Log($"Unknown hit object type: {type}. Skipped.", level: LogLevel.Error);
return null;
}
result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + offset; result.StartTime = Convert.ToDouble(split[2], CultureInfo.InvariantCulture) + Offset;
result.Samples = convertSoundType(soundType, bankInfo); result.Samples = convertSoundType(soundType, bankInfo);
FirstObject = false;
return result; return result;
} }
catch (FormatException) catch (FormatException)
@ -221,37 +246,42 @@ namespace osu.Game.Rulesets.Objects.Legacy
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param> /// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateHit(Vector2 position, bool newCombo); protected abstract HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset);
/// <summary> /// <summary>
/// Creats a legacy Slider-type hit object. /// Creats a legacy Slider-type hit object.
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param> /// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="controlPoints">The slider control points.</param> /// <param name="controlPoints">The slider control points.</param>
/// <param name="length">The slider length.</param> /// <param name="length">The slider length.</param>
/// <param name="curveType">The slider curve type.</param> /// <param name="curveType">The slider curve type.</param>
/// <param name="repeatCount">The slider repeat count.</param> /// <param name="repeatCount">The slider repeat count.</param>
/// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param> /// <param name="repeatSamples">The samples to be played when the repeat nodes are hit. This includes the head and tail of the slider.</param>
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples); protected abstract HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples);
/// <summary> /// <summary>
/// Creates a legacy Spinner-type hit object. /// Creates a legacy Spinner-type hit object.
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="endTime">The spinner end time.</param> /// <param name="endTime">The spinner end time.</param>
/// <returns>The hit object.</returns> /// <returns>The hit object.</returns>
protected abstract HitObject CreateSpinner(Vector2 position, double endTime); protected abstract HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime);
/// <summary> /// <summary>
/// Creates a legacy Hold-type hit object. /// Creates a legacy Hold-type hit object.
/// </summary> /// </summary>
/// <param name="position">The position of the hit object.</param> /// <param name="position">The position of the hit object.</param>
/// <param name="newCombo">Whether the hit object creates a new combo.</param> /// <param name="newCombo">Whether the hit object creates a new combo.</param>
/// <param name="comboOffset">When starting a new combo, the offset of the new combo relative to the current one.</param>
/// <param name="endTime">The hold end time.</param> /// <param name="endTime">The hold end time.</param>
protected abstract HitObject CreateHold(Vector2 position, bool newCombo, double endTime); protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime);
private List<SampleInfo> convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) private List<SampleInfo> convertSoundType(LegacySoundType type, SampleBankInfo bankInfo)
{ {

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Objects.Legacy
Slider = 1 << 1, Slider = 1 << 1,
NewCombo = 1 << 2, NewCombo = 1 << 2,
Spinner = 1 << 3, Spinner = 1 << 3,
ColourHax = 112, ComboOffset = 1 << 4 | 1 << 5 | 1 << 6,
Hold = 1 << 7 Hold = 1 << 7
} }
} }

View File

@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
/// <summary> /// <summary>
/// Legacy osu!mania Hit-type, used for parsing Beatmaps. /// Legacy osu!mania Hit-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertHit : HitObject, IHasXPosition, IHasCombo internal sealed class ConvertHit : HitObject, IHasXPosition
{ {
public float X { get; set; } public float X { get; set; }
public bool NewCombo { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -13,21 +13,24 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
/// </summary> /// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{ {
protected override HitObject CreateHit(Vector2 position, bool newCombo) public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{
}
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{ {
return new ConvertHit return new ConvertHit
{ {
X = position.X, X = position.X
NewCombo = newCombo,
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples) protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {
X = position.X, X = position.X,
NewCombo = newCombo,
ControlPoints = controlPoints, ControlPoints = controlPoints,
Distance = length, Distance = length,
CurveType = curveType, CurveType = curveType,
@ -36,7 +39,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
}; };
} }
protected override HitObject CreateSpinner(Vector2 position, double endTime) protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return new ConvertSpinner return new ConvertSpinner
{ {
@ -45,7 +48,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return new ConvertHold return new ConvertHold
{ {

View File

@ -8,12 +8,10 @@ namespace osu.Game.Rulesets.Objects.Legacy.Mania
/// <summary> /// <summary>
/// Legacy osu!mania Slider-type, used for parsing Beatmaps. /// Legacy osu!mania Slider-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition, IHasCombo internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasXPosition
{ {
public float X { get; set; } public float X { get; set; }
public bool NewCombo { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public bool NewCombo { get; set; } public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -14,21 +14,43 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
/// </summary> /// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{ {
protected override HitObject CreateHit(Vector2 position, bool newCombo) public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{ {
}
private bool forceNewCombo;
private int extraComboOffset;
protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;
forceNewCombo = false;
extraComboOffset = 0;
return new ConvertHit return new ConvertHit
{ {
Position = position, Position = position,
NewCombo = newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset
}; };
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples) protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{ {
newCombo |= forceNewCombo;
comboOffset += extraComboOffset;
forceNewCombo = false;
extraComboOffset = 0;
return new ConvertSlider return new ConvertSlider
{ {
Position = position, Position = position,
NewCombo = newCombo, NewCombo = FirstObject || newCombo,
ComboOffset = comboOffset,
ControlPoints = controlPoints, ControlPoints = controlPoints,
Distance = Math.Max(0, length), Distance = Math.Max(0, length),
CurveType = curveType, CurveType = curveType,
@ -37,8 +59,13 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
}; };
} }
protected override HitObject CreateSpinner(Vector2 position, double endTime) protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
// Convert spinners don't create the new combo themselves, but force the next non-spinner hitobject to create a new combo
// Their combo offset is still added to that next hitobject's combo index
forceNewCombo |= FormatVersion <= 8 || newCombo;
extraComboOffset += comboOffset;
return new ConvertSpinner return new ConvertSpinner
{ {
Position = position, Position = position,
@ -46,7 +73,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return null; return null;
} }

View File

@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public bool NewCombo { get; set; } public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
/// <summary> /// <summary>
/// Legacy osu! Spinner-type, used for parsing Beatmaps. /// Legacy osu! Spinner-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition internal sealed class ConvertSpinner : HitObject, IHasEndTime, IHasPosition, IHasCombo
{ {
public double EndTime { get; set; } public double EndTime { get; set; }
@ -22,5 +22,9 @@ namespace osu.Game.Rulesets.Objects.Legacy.Osu
public float Y => Position.Y; public float Y => Position.Y;
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
public bool NewCombo { get; set; }
public int ComboOffset { get; set; }
} }
} }

View File

@ -1,17 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 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 osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Taiko namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{ {
/// <summary> /// <summary>
/// Legacy osu!taiko Hit-type, used for parsing Beatmaps. /// Legacy osu!taiko Hit-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertHit : HitObject, IHasCombo internal sealed class ConvertHit : HitObject
{ {
public bool NewCombo { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -13,19 +13,20 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
/// </summary> /// </summary>
public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser public class ConvertHitObjectParser : Legacy.ConvertHitObjectParser
{ {
protected override HitObject CreateHit(Vector2 position, bool newCombo) public ConvertHitObjectParser(double offset, int formatVersion)
: base(offset, formatVersion)
{ {
return new ConvertHit
{
NewCombo = newCombo,
};
} }
protected override HitObject CreateSlider(Vector2 position, bool newCombo, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples) protected override HitObject CreateHit(Vector2 position, bool newCombo, int comboOffset)
{
return new ConvertHit();
}
protected override HitObject CreateSlider(Vector2 position, bool newCombo, int comboOffset, List<Vector2> controlPoints, double length, CurveType curveType, int repeatCount, List<List<SampleInfo>> repeatSamples)
{ {
return new ConvertSlider return new ConvertSlider
{ {
NewCombo = newCombo,
ControlPoints = controlPoints, ControlPoints = controlPoints,
Distance = length, Distance = length,
CurveType = curveType, CurveType = curveType,
@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
}; };
} }
protected override HitObject CreateSpinner(Vector2 position, double endTime) protected override HitObject CreateSpinner(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return new ConvertSpinner return new ConvertSpinner
{ {
@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Objects.Legacy.Taiko
}; };
} }
protected override HitObject CreateHold(Vector2 position, bool newCombo, double endTime) protected override HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime)
{ {
return null; return null;
} }

View File

@ -1,17 +1,13 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>. // Copyright (c) 2007-2018 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 osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Rulesets.Objects.Legacy.Taiko namespace osu.Game.Rulesets.Objects.Legacy.Taiko
{ {
/// <summary> /// <summary>
/// Legacy osu!taiko Slider-type, used for parsing Beatmaps. /// Legacy osu!taiko Slider-type, used for parsing Beatmaps.
/// </summary> /// </summary>
internal sealed class ConvertSlider : Legacy.ConvertSlider, IHasCombo internal sealed class ConvertSlider : Legacy.ConvertSlider
{ {
public bool NewCombo { get; set; }
protected override HitWindows CreateHitWindows() => null; protected override HitWindows CreateHitWindows() => null;
} }
} }

View File

@ -12,5 +12,10 @@ namespace osu.Game.Rulesets.Objects.Types
/// Whether the HitObject starts a new combo. /// Whether the HitObject starts a new combo.
/// </summary> /// </summary>
bool NewCombo { get; } bool NewCombo { get; }
/// <summary>
/// When starting a new combo, the offset of the new combo relative to the current one.
/// </summary>
int ComboOffset { get; }
} }
} }

View File

@ -9,6 +9,8 @@ using osu.Game.Rulesets.Objects.Drawables;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Configuration; using osu.Framework.Configuration;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Rulesets.UI namespace osu.Game.Rulesets.UI
{ {
@ -17,12 +19,12 @@ namespace osu.Game.Rulesets.UI
/// <summary> /// <summary>
/// The <see cref="DrawableHitObject"/> contained in this Playfield. /// The <see cref="DrawableHitObject"/> contained in this Playfield.
/// </summary> /// </summary>
public HitObjectContainer HitObjects { get; private set; } public HitObjectContainer HitObjectContainer { get; private set; }
/// <summary> /// <summary>
/// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>. /// All the <see cref="DrawableHitObject"/>s contained in this <see cref="Playfield"/> and all <see cref="NestedPlayfields"/>.
/// </summary> /// </summary>
public IEnumerable<DrawableHitObject> AllHitObjects => HitObjects?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty<DrawableHitObject>(); public IEnumerable<DrawableHitObject> AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty<DrawableHitObject>();
/// <summary> /// <summary>
/// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>. /// All <see cref="Playfield"/>s nested inside this <see cref="Playfield"/>.
@ -51,13 +53,17 @@ namespace osu.Game.Rulesets.UI
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
[BackgroundDependencyLoader] private WorkingBeatmap beatmap;
private void load()
{
HitObjects = CreateHitObjectContainer();
HitObjects.RelativeSizeAxes = Axes.Both;
Add(HitObjects); [BackgroundDependencyLoader]
private void load(IBindableBeatmap beatmap)
{
this.beatmap = beatmap.Value;
HitObjectContainer = CreateHitObjectContainer();
HitObjectContainer.RelativeSizeAxes = Axes.Both;
Add(HitObjectContainer);
} }
/// <summary> /// <summary>
@ -69,13 +75,13 @@ namespace osu.Game.Rulesets.UI
/// Adds a DrawableHitObject to this Playfield. /// Adds a DrawableHitObject to this Playfield.
/// </summary> /// </summary>
/// <param name="h">The DrawableHitObject to add.</param> /// <param name="h">The DrawableHitObject to add.</param>
public virtual void Add(DrawableHitObject h) => HitObjects.Add(h); public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h);
/// <summary> /// <summary>
/// Remove a DrawableHitObject from this Playfield. /// Remove a DrawableHitObject from this Playfield.
/// </summary> /// </summary>
/// <param name="h">The DrawableHitObject to remove.</param> /// <param name="h">The DrawableHitObject to remove.</param>
public virtual void Remove(DrawableHitObject h) => HitObjects.Remove(h); public virtual void Remove(DrawableHitObject h) => HitObjectContainer.Remove(h);
/// <summary> /// <summary>
/// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>. /// Registers a <see cref="Playfield"/> as a nested <see cref="Playfield"/>.
@ -92,5 +98,15 @@ namespace osu.Game.Rulesets.UI
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s. /// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
/// </summary> /// </summary>
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
protected override void Update()
{
base.Update();
if (beatmap != null)
foreach (var mod in beatmap.Mods.Value)
if (mod is IUpdatableByPlayfield updatable)
updatable.Update(this);
}
} }
} }

View File

@ -306,7 +306,7 @@ namespace osu.Game.Rulesets.UI
Playfield.PostProcess(); Playfield.PostProcess();
foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>()) foreach (var mod in Mods.OfType<IApplicableToDrawableHitObjects>())
mod.ApplyToDrawableHitObjects(Playfield.HitObjects.Objects); mod.ApplyToDrawableHitObjects(Playfield.HitObjectContainer.Objects);
} }
protected override void Update() protected override void Update()

View File

@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary> /// <summary>
/// The container that contains the <see cref="DrawableHitObject"/>s. /// The container that contains the <see cref="DrawableHitObject"/>s.
/// </summary> /// </summary>
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects; public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)HitObjectContainer;
/// <summary> /// <summary>
/// The direction in which <see cref="DrawableHitObject"/>s in this <see cref="ScrollingPlayfield"/> should scroll. /// The direction in which <see cref="DrawableHitObject"/>s in this <see cref="ScrollingPlayfield"/> should scroll.

View File

@ -0,0 +1,25 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Graphics.UserInterface;
using OpenTK;
namespace osu.Game.Screens.Edit.Components
{
public class CircularButton : OsuButton
{
private const float width = 125;
private const float height = 30;
public CircularButton()
{
Size = new Vector2(width, height);
}
protected override void Update()
{
base.Update();
Content.CornerRadius = DrawHeight / 2f;
}
}
}

View File

@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shaders;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using OpenTK; using OpenTK;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Overlays;
namespace osu.Game.Screens namespace osu.Game.Screens
{ {
@ -18,6 +19,8 @@ namespace osu.Game.Screens
protected override bool HideOverlaysOnEnter => true; protected override bool HideOverlaysOnEnter => true;
protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled;
protected override bool AllowBackButton => false; protected override bool AllowBackButton => false;
public Loader() public Loader()

View File

@ -174,6 +174,9 @@ namespace osu.Game.Screens.Menu
ButtonSystemState lastState = state; ButtonSystemState lastState = state;
state = value; state = value;
if (game != null)
game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All;
updateLogoState(lastState); updateLogoState(lastState);
Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}");
@ -205,11 +208,7 @@ namespace osu.Game.Screens.Menu
{ {
logoTracking = false; logoTracking = false;
if (game != null) game?.Toolbar.Hide();
{
game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All;
game.Toolbar.Hide();
}
logo.ClearTransforms(targetMember: nameof(Position)); logo.ClearTransforms(targetMember: nameof(Position));
logo.RelativePositionAxes = Axes.Both; logo.RelativePositionAxes = Axes.Both;
@ -243,11 +242,7 @@ namespace osu.Game.Screens.Menu
if (impact) if (impact)
logo.Impact(); logo.Impact();
if (game != null) game?.Toolbar.Show();
{
game.OverlayActivationMode.Value = OverlayActivation.All;
game.Toolbar.State = Visibility.Visible;
}
}, 200); }, 200);
break; break;
default: default:
@ -278,7 +273,7 @@ namespace osu.Game.Screens.Menu
if (logo != null) if (logo != null)
{ {
if (logoTracking && iconFacade.IsLoaded) if (logoTracking && logo.RelativePositionAxes == Axes.None && iconFacade.IsLoaded)
logo.Position = logoTrackingPosition; logo.Position = logoTrackingPosition;
iconFacade.Width = logo.SizeForFlow * 0.5f; iconFacade.Width = logo.SizeForFlow * 0.5f;

View File

@ -31,23 +31,30 @@ namespace osu.Game.Screens.Play.Break
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
leftGlowIcon = new GlowIcon new ParallaxContainer
{ {
Anchor = Anchor.Centre, ParallaxAmount = -0.01f,
Origin = Anchor.CentreRight, Children = new Drawable[]
X = -glow_icon_offscreen_offset, {
Icon = Graphics.FontAwesome.fa_chevron_right, leftGlowIcon = new GlowIcon
BlurSigma = new Vector2(glow_icon_blur_sigma), {
Size = new Vector2(glow_icon_size), Anchor = Anchor.Centre,
}, Origin = Anchor.CentreRight,
rightGlowIcon = new GlowIcon X = -glow_icon_offscreen_offset,
{ Icon = Graphics.FontAwesome.fa_chevron_right,
Anchor = Anchor.Centre, BlurSigma = new Vector2(glow_icon_blur_sigma),
Origin = Anchor.CentreLeft, Size = new Vector2(glow_icon_size),
X = glow_icon_offscreen_offset, },
Icon = Graphics.FontAwesome.fa_chevron_left, rightGlowIcon = new GlowIcon
BlurSigma = new Vector2(glow_icon_blur_sigma), {
Size = new Vector2(glow_icon_size), Anchor = Anchor.Centre,
Origin = Anchor.CentreLeft,
X = glow_icon_offscreen_offset,
Icon = Graphics.FontAwesome.fa_chevron_left,
BlurSigma = new Vector2(glow_icon_blur_sigma),
Size = new Vector2(glow_icon_size),
},
}
}, },
new ParallaxContainer new ParallaxContainer
{ {

View File

@ -63,6 +63,12 @@ namespace osu.Game.Screens.Play.HUD
}; };
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
mods.UnbindAll();
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();

View File

@ -124,7 +124,7 @@ namespace osu.Game.Screens.Play
if (!RulesetContainer.Objects.Any()) if (!RulesetContainer.Objects.Any())
{ {
Logger.Error(new InvalidOperationException("Beatmap contains no hit objects!"), "Beatmap contains no hit objects!"); Logger.Log("Beatmap contains no hit objects!", level: LogLevel.Error);
return; return;
} }
} }

View File

@ -64,32 +64,36 @@ namespace osu.Game.Screens.Select
public IEnumerable<BeatmapSetInfo> BeatmapSets public IEnumerable<BeatmapSetInfo> BeatmapSets
{ {
get { return beatmapSets.Select(g => g.BeatmapSet); } get => beatmapSets.Select(g => g.BeatmapSet);
set set => loadBeatmapSets(() => value);
}
public void LoadBeatmapSetsFromManager(BeatmapManager manager) => loadBeatmapSets(manager.GetAllUsableBeatmapSetsEnumerable);
private void loadBeatmapSets(Func<IEnumerable<BeatmapSetInfo>> beatmapSets)
{
CarouselRoot newRoot = new CarouselRoot(this);
Task.Run(() =>
{ {
CarouselRoot newRoot = new CarouselRoot(this); beatmapSets().Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild);
newRoot.Filter(activeCriteria);
Task.Run(() => // preload drawables as the ctor overhead is quite high currently.
var _ = newRoot.Drawables;
}).ContinueWith(_ => Schedule(() =>
{
root = newRoot;
scrollableContent.Clear(false);
itemsCache.Invalidate();
scrollPositionCache.Invalidate();
Schedule(() =>
{ {
value.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); BeatmapSetsChanged?.Invoke();
newRoot.Filter(activeCriteria); initialLoadComplete = true;
});
// preload drawables as the ctor overhead is quite high currently. }));
var _ = newRoot.Drawables;
}).ContinueWith(_ => Schedule(() =>
{
root = newRoot;
scrollableContent.Clear(false);
itemsCache.Invalidate();
scrollPositionCache.Invalidate();
Schedule(() =>
{
BeatmapSetsChanged?.Invoke();
initialLoadComplete = true;
});
}));
}
} }
private readonly List<float> yPositions = new List<float>(); private readonly List<float> yPositions = new List<float>();

View File

@ -41,7 +41,10 @@ namespace osu.Game.Screens.Select.Leaderboards
updateTexture(); updateTexture();
} }
private void updateTexture() => rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}"); private void updateTexture()
{
rankSprite.Texture = textures.Get($@"Grades/{Rank.GetDescription()}");
}
public void UpdateRank(ScoreRank newRank) public void UpdateRank(ScoreRank newRank)
{ {

View File

@ -221,7 +221,7 @@ namespace osu.Game.Screens.Select
sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty"); sampleChangeDifficulty = audio.Sample.Get(@"SongSelect/select-difficulty");
sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand"); sampleChangeBeatmap = audio.Sample.Get(@"SongSelect/select-expand");
Carousel.BeatmapSets = this.beatmaps.GetAllUsableBeatmapSetsEnumerable(); Carousel.LoadBeatmapSetsFromManager(this.beatmaps);
} }
public void Edit(BeatmapInfo beatmap) public void Edit(BeatmapInfo beatmap)
@ -460,6 +460,8 @@ namespace osu.Game.Screens.Select
{ {
base.Dispose(isDisposing); base.Dispose(isDisposing);
Ruleset.UnbindAll();
if (beatmaps != null) if (beatmaps != null)
{ {
beatmaps.ItemAdded -= onBeatmapSetAdded; beatmaps.ItemAdded -= onBeatmapSetAdded;

View File

@ -4,6 +4,7 @@
using System; using System;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Audio.Sample; using osu.Framework.Audio.Sample;
using osu.Framework.Graphics; using osu.Framework.Graphics;
@ -108,10 +109,12 @@ namespace osu.Game.Skinning
return path == null ? null : underlyingStore.GetStream(path); return path == null ? null : underlyingStore.GetStream(path);
} }
byte[] IResourceStore<byte[]>.Get(string name) byte[] IResourceStore<byte[]>.Get(string name) => GetAsync(name).Result;
public Task<byte[]> GetAsync(string name)
{ {
string path = getPathForFile(name); string path = getPathForFile(name);
return path == null ? null : underlyingStore.Get(path); return path == null ? Task.FromResult<byte[]>(null) : underlyingStore.GetAsync(path);
} }
#region IDisposable Support #region IDisposable Support

View File

@ -85,12 +85,10 @@ namespace osu.Game.Skinning
private void load(OsuConfigManager config) private void load(OsuConfigManager config)
{ {
beatmapSkins = config.GetBindable<bool>(OsuSetting.BeatmapSkins); beatmapSkins = config.GetBindable<bool>(OsuSetting.BeatmapSkins);
beatmapSkins.ValueChanged += val => onSourceChanged(); beatmapSkins.BindValueChanged(_ => onSourceChanged());
beatmapSkins.TriggerChange();
beatmapHitsounds = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds); beatmapHitsounds = config.GetBindable<bool>(OsuSetting.BeatmapHitsounds);
beatmapHitsounds.ValueChanged += val => onSourceChanged(); beatmapHitsounds.BindValueChanged(_ => onSourceChanged(), true);
beatmapHitsounds.TriggerChange();
} }
protected override void LoadComplete() protected override void LoadComplete()

View File

@ -52,5 +52,13 @@ namespace osu.Game.Skinning
protected virtual void SkinChanged(ISkinSource skin, bool allowFallback) protected virtual void SkinChanged(ISkinSource skin, bool allowFallback)
{ {
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
if (skin != null)
skin.SourceChanged -= onChange;
}
} }
} }

View File

@ -0,0 +1,89 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using osu.Framework.Logging;
using SharpRaven;
using SharpRaven.Data;
namespace osu.Game.Utils
{
/// <summary>
/// Report errors to sentry.
/// </summary>
public class RavenLogger : IDisposable
{
private readonly RavenClient raven = new RavenClient("https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255");
private readonly List<Task> tasks = new List<Task>();
private Exception lastException;
public RavenLogger(OsuGame game)
{
raven.Release = game.Version;
if (!game.IsDeployedBuild) return;
Logger.NewEntry += entry =>
{
if (entry.Level < LogLevel.Verbose) return;
var exception = entry.Exception;
if (exception != null)
{
// since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports.
if (lastException != null &&
lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace))
{
return;
}
lastException = exception;
queuePendingTask(raven.CaptureAsync(new SentryEvent(exception)));
}
else
raven.AddTrail(new Breadcrumb(entry.Target.ToString(), BreadcrumbType.Navigation) { Message = entry.Message });
};
}
private void queuePendingTask(Task<string> task)
{
lock (tasks) tasks.Add(task);
task.ContinueWith(_ =>
{
lock (tasks)
tasks.Remove(task);
});
}
#region Disposal
~RavenLogger()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private bool isDisposed;
protected virtual void Dispose(bool isDisposing)
{
if (isDisposed)
return;
isDisposed = true;
lock (tasks) Task.WaitAll(tasks.ToArray(), 5000);
}
#endregion
}
}

View File

@ -15,12 +15,13 @@
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Humanizer" Version="2.4.2" /> <PackageReference Include="Humanizer" Version="2.4.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.2" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="2.1.2" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" /> <PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="ppy.osu.Framework" Version="2018.806.0" /> <PackageReference Include="ppy.osu.Framework" Version="2018.901.0" />
<PackageReference Include="SharpCompress" Version="0.22.0" /> <PackageReference Include="SharpCompress" Version="0.22.0" />
<PackageReference Include="NUnit" Version="3.10.1" /> <PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="SharpRaven" Version="2.4.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -11,7 +11,7 @@
<ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj" /> <ProjectReference Include="..\osu-resources\osu.Game.Resources\osu.Game.Resources.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.1" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.2" />
<PackageReference Include="DeepEqual" Version="1.6.0" /> <PackageReference Include="DeepEqual" Version="1.6.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>