1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-14 23:22:55 +08:00

Merge branch 'master' into changelog-overlay

This commit is contained in:
Jarosław Zgierski 2018-10-18 20:26:56 +02:00 committed by GitHub
commit 7cc5128353
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
356 changed files with 4479 additions and 2681 deletions

View File

@ -24,7 +24,9 @@ Clone the repository including submodules
Build and run
- Using Visual Studio 2017, Rider or Visual Studio Code (configurations are included)
- From command line using `dotnet run --project osu.Desktop`
- From command line using `dotnet run --project osu.Desktop`. When building for non-development purposes, add `-c Release` to gain higher performance.
Note: If you run from command line under linux, you will need to prefix the output folder to your `LD_LIBRARY_PATH`. See `.vscode/launch.json` for an example
If you run into issues building you may need to restore nuget packages (commonly via `dotnet restore`). Visual Studio Code users must run `Restore` task from debug tab before attempt to build.

View File

@ -10,8 +10,8 @@ before_build:
- cmd: nuget restore -verbosity quiet
build_script:
- ps: iex ((New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/appveyor/secure-file/master/install.ps1'))
- appveyor DownloadFile https://puu.sh/A6g5K/4d08705438.enc # signing certificate
- cmd: appveyor-tools\secure-file -decrypt 4d08705438.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx
- appveyor DownloadFile https://www.dropbox.com/s/f7hv069mr5tqi2j/deanherbert.pfx.enc?dl=1 # signing certificate
- cmd: appveyor-tools\secure-file -decrypt deanherbert.pfx.enc -secret %decode_secret% -out %HOMEPATH%\deanherbert.pfx
- appveyor DownloadFile https://puu.sh/A6g75/fdc6f19b04.enc # deploy configuration
- cd osu-deploy
- nuget restore -verbosity quiet

View File

@ -27,9 +27,6 @@ namespace osu.Desktop.Overlays
private NotificationOverlay notificationOverlay;
private GameHost host;
public override bool HandleKeyboardInput => false;
public override bool HandleMouseInput => false;
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification, OsuColour colours, TextureStore textures, OsuGameBase game, OsuConfigManager config, GameHost host)
{

View File

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

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Catch.Objects;
@ -38,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests
beatmap.HitObjects.Add(new JuiceStream
{
X = 0.5f - width / 2,
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(width * CatchPlayfield.BASE_WIDTH, 0)

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -74,42 +74,42 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
private void initialiseHyperDash(List<CatchHitObject> objects)
{
// todo: add difficulty adjust.
double halfCatcherWidth = CatcherArea.CATCHER_SIZE * (objects.FirstOrDefault()?.Scale ?? 1) / CatchPlayfield.BASE_WIDTH / 2;
List<CatchHitObject> objectWithDroplets = new List<CatchHitObject>();
foreach (var currentObject in objects)
{
if (currentObject is Fruit)
objectWithDroplets.Add(currentObject);
if (currentObject is JuiceStream)
foreach (var currentJuiceElement in currentObject.NestedHitObjects)
if (!(currentJuiceElement is TinyDroplet))
objectWithDroplets.Add((CatchHitObject)currentJuiceElement);
}
objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime));
double halfCatcherWidth = CatcherArea.GetCatcherSize(Beatmap.BeatmapInfo.BaseDifficulty) / 2;
int lastDirection = 0;
double lastExcess = halfCatcherWidth;
int objCount = objects.Count;
for (int i = 0; i < objCount - 1; i++)
for (int i = 0; i < objectWithDroplets.Count - 1; i++)
{
CatchHitObject currentObject = objects[i];
// not needed?
// if (currentObject is TinyDroplet) continue;
CatchHitObject nextObject = objects[i + 1];
// while (nextObject is TinyDroplet)
// {
// if (++i == objCount - 1) break;
// nextObject = objects[i + 1];
// }
CatchHitObject currentObject = objectWithDroplets[i];
CatchHitObject nextObject = objectWithDroplets[i + 1];
int thisDirection = nextObject.X > currentObject.X ? 1 : -1;
double timeToNext = nextObject.StartTime - ((currentObject as IHasEndTime)?.EndTime ?? currentObject.StartTime) - 4;
double timeToNext = nextObject.StartTime - currentObject.StartTime;
double distanceToNext = Math.Abs(nextObject.X - currentObject.X) - (lastDirection == thisDirection ? lastExcess : halfCatcherWidth);
if (timeToNext * CatcherArea.Catcher.BASE_SPEED < distanceToNext)
float distanceToHyper = (float)(timeToNext * CatcherArea.Catcher.BASE_SPEED - distanceToNext);
if (distanceToHyper < 0)
{
currentObject.HyperDashTarget = nextObject;
lastExcess = halfCatcherWidth;
}
else
{
//currentObject.DistanceToHyperDash = timeToNext - distanceToNext;
lastExcess = MathHelper.Clamp(timeToNext - distanceToNext, 0, halfCatcherWidth);
currentObject.DistanceToHyperDash = distanceToHyper;
lastExcess = MathHelper.Clamp(distanceToHyper, 0, halfCatcherWidth);
}
lastDirection = thisDirection;

View File

@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public int IndexInBeatmap { get; set; }
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4);
public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(ComboIndex % 4);
public virtual bool NewCombo { get; set; }
@ -27,7 +27,9 @@ namespace osu.Game.Rulesets.Catch.Objects
public int ComboIndex { get; set; }
/// <summary>
/// The distance for a fruit to to next hyper if it's not a hyper.
/// Difference between the distance to the next object
/// and the distance that would have triggered a hyper dash.
/// A value close to 0 indicates a difficult jump (for difficulty calculation).
/// </summary>
public float DistanceToHyperDash { get; set; }

View File

@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public SliderCurve Curve { get; } = new SliderCurve();
public List<Vector2> ControlPoints
public Vector2[] ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }

View File

@ -5,11 +5,13 @@ using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Configuration;
using osu.Game.Rulesets.Catch.Objects;
using osu.Game.Rulesets.Catch.Objects.Drawable;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -17,13 +19,13 @@ namespace osu.Game.Rulesets.Catch.UI
{
public const float BASE_WIDTH = 512;
protected override Container<Drawable> Content => content;
private readonly Container<Drawable> content;
private readonly CatcherArea catcherArea;
protected override bool UserScrollSpeedAdjustment => false;
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Constant;
public CatchPlayfield(BeatmapDifficulty difficulty, Func<CatchHitObject, DrawableHitObject<CatchHitObject>> getVisualRepresentation)
: base(BASE_WIDTH)
{
Direction.Value = ScrollingDirection.Down;
@ -32,27 +34,29 @@ namespace osu.Game.Rulesets.Catch.UI
Anchor = Anchor.TopCentre;
Origin = Anchor.TopCentre;
base.Content.Anchor = Anchor.BottomLeft;
base.Content.Origin = Anchor.BottomLeft;
Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate
base.Content.AddRange(new Drawable[]
InternalChild = new PlayfieldAdjustmentContainer
{
explodingFruitContainer = new Container
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
content = new Container<Drawable>
{
RelativeSizeAxes = Axes.Both,
},
});
explodingFruitContainer = new Container
{
RelativeSizeAxes = Axes.Both,
},
catcherArea = new CatcherArea(difficulty)
{
GetVisualRepresentation = getVisualRepresentation,
ExplodingFruitTarget = explodingFruitContainer,
Anchor = Anchor.BottomLeft,
Origin = Anchor.TopLeft,
},
HitObjectContainer
}
};
VisibleTimeRange.Value = BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450);
}
public bool CheckIfWeCanCatch(CatchHitObject obj) => catcherArea.AttemptCatch(obj);

View File

@ -13,7 +13,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Catch.UI
public override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo);
protected override Vector2 PlayfieldArea => new Vector2(0.86f); // matches stable's vertical offset for catcher plate
protected override DrawableHitObject<CatchHitObject> GetVisualRepresentation(CatchHitObject h)
{
switch (h)

View File

@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Catch.UI
{
public class CatcherArea : Container
{
public const float CATCHER_SIZE = 84;
public const float CATCHER_SIZE = 100;
protected readonly Catcher MovableCatcher;
@ -107,6 +107,11 @@ namespace osu.Game.Rulesets.Catch.UI
public bool AttemptCatch(CatchHitObject obj) => MovableCatcher.AttemptCatch(obj);
public static float GetCatcherSize(BeatmapDifficulty difficulty)
{
return CATCHER_SIZE / CatchPlayfield.BASE_WIDTH * (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5);
}
public class Catcher : Container, IKeyBindingHandler<CatchAction>
{
/// <summary>
@ -407,9 +412,7 @@ namespace osu.Game.Rulesets.Catch.UI
/// </summary>
public void Explode()
{
var fruit = caughtFruit.ToArray();
foreach (var f in fruit)
foreach (var f in caughtFruit.ToArray())
Explode(f);
}
@ -422,15 +425,15 @@ namespace osu.Game.Rulesets.Catch.UI
fruit.Anchor = Anchor.TopLeft;
fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget);
caughtFruit.Remove(fruit);
if (!caughtFruit.Remove(fruit))
// we may have already been removed by a previous operation (due to the weird OnLoadComplete scheduling).
// this avoids a crash on potentially attempting to Add a fruit to ExplodingFruitTarget twice.
return;
ExplodingFruitTarget.Add(fruit);
}
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine)
.Then()
.MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToY(fruit.Y - 50, 250, Easing.OutSine).Then().MoveToY(fruit.Y + 50, 500, Easing.InSine);
fruit.MoveToX(fruit.X + originalX * 6, 1000);
fruit.FadeOut(750);

View File

@ -0,0 +1,42 @@
// 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;
using OpenTK;
namespace osu.Game.Rulesets.Catch.UI
{
public class PlayfieldAdjustmentContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public PlayfieldAdjustmentContainer()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// A <see cref="Container"/> which scales its content relative to a target width.
/// </summary>
private class ScalingContainer : Container
{
protected override void Update()
{
base.Update();
Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.BASE_WIDTH);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
}
}

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
TargetColumns = (int)Math.Max(1, roundedCircleSize);
else
{
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count();
float percentSliderOrSpinner = (float)beatmap.HitObjects.Count(h => h is IHasEndTime) / beatmap.HitObjects.Count;
if (percentSliderOrSpinner < 0.2)
TargetColumns = 7;
else if (percentSliderOrSpinner < 0.3 || roundedCircleSize >= 5)

View File

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

View File

@ -39,34 +39,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
addToPattern(pattern, 0, generateHold);
break;
case 8:
addToPattern(pattern, getNextRandomColumn(RandomStart), generateHold);
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
break;
default:
if (TotalColumns > 0)
addToPattern(pattern, getNextRandomColumn(0), generateHold);
addToPattern(pattern, GetRandomColumn(), generateHold);
break;
}
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);
RunWhile(() => PreviousPattern.ColumnHasObject(nextColumn), () =>
{
nextColumn = Random.Next(start, TotalColumns);
});
return nextColumn;
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>

View File

@ -25,9 +25,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
PatternType lastStair, IBeatmap 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;
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);
for (int i = 0; i < noteCount; i++)
{
RunWhile(() => pattern.ColumnHasObject(nextColumn) || PreviousPattern.ColumnHasObject(nextColumn) && !allowStacking, () =>
{
if (convertType.HasFlag(PatternType.Gathered))
{
nextColumn++;
if (nextColumn == TotalColumns)
nextColumn = RandomStart;
}
else
nextColumn = Random.Next(RandomStart, TotalColumns);
});
nextColumn = allowStacking
? FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: pattern)
: FindAvailableColumn(nextColumn, nextColumn: getNextColumn, patterns: new[] { pattern, PreviousPattern });
addToPattern(pattern, nextColumn);
}
return pattern;
int getNextColumn(int last)
{
if (convertType.HasFlag(PatternType.Gathered))
{
last++;
if (last == TotalColumns)
last = RandomStart;
}
else
last = GetRandomColumn();
return last;
}
}
/// <summary>
@ -295,13 +297,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int noteCount = getRandomNoteCountMirrored(centreProbability, p2, p3, out addToCentre);
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++)
{
RunWhile(() => pattern.ColumnHasObject(nextColumn), () =>
{
nextColumn = Random.Next(RandomStart, columnLimit);
});
nextColumn = FindAvailableColumn(nextColumn, upperBound: columnLimit, patterns: pattern);
// Add normal note
addToPattern(pattern, nextColumn);

View File

@ -3,6 +3,7 @@
using System;
using System.Linq;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Objects;
@ -90,6 +91,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
}
private double? conversionDifficulty;
/// <summary>
/// A difficulty factor used for various conversion methods from osu!stable.
/// </summary>
@ -110,11 +112,88 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
drainTime = 10000;
BeatmapDifficulty difficulty = OriginalBeatmap.BeatmapInfo.BaseDifficulty;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count() / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = ((difficulty.DrainRate + MathHelper.Clamp(difficulty.ApproachRate, 4, 7)) / 1.5 + (double)OriginalBeatmap.HitObjects.Count / drainTime * 9f) / 38f * 5f / 1.15;
conversionDifficulty = Math.Min(conversionDifficulty.Value, 12);
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

@ -3,9 +3,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using JetBrains.Annotations;
using osu.Framework.Logging;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
@ -15,14 +12,6 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
/// </summary>
internal abstract class PatternGenerator
{
/// <summary>
/// An arbitrary maximum amount of iterations to perform in <see cref="RunWhile"/>.
/// The specific value is not super important - enough such that no false-positives occur.
///
/// /b/933228 requires at least 23 iterations.
/// </summary>
private const int max_rng_iterations = 30;
/// <summary>
/// The last pattern.
/// </summary>
@ -53,44 +42,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns
TotalColumns = Beatmap.TotalColumns;
}
protected void RunWhile([InstantHandle] Func<bool> condition, Action action)
{
int iterations = 0;
while (condition())
{
if (iterations++ >= max_rng_iterations)
{
// log an error but don't throw. we want to continue execution.
Logger.Error(new ExceededAllowedIterationsException(new StackTrace(0)),
"Conversion encountered errors. The beatmap may not be correctly converted.");
return;
}
action();
}
}
/// <summary>
/// Generates the patterns for <see cref="HitObject"/>, each filled with hit objects.
/// </summary>
/// <returns>The <see cref="Pattern"/>s containing the hit objects.</returns>
public abstract IEnumerable<Pattern> Generate();
/// <summary>
/// Denotes when a single conversion operation is in an infinitely looping state.
/// </summary>
public class ExceededAllowedIterationsException : Exception
{
private readonly string stackTrace;
public ExceededAllowedIterationsException(StackTrace stackTrace)
{
this.stackTrace = stackTrace.ToString();
}
public override string StackTrace => stackTrace;
public override string ToString() => $"{GetType().Name}: {Message}\r\n{StackTrace}";
}
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Configuration
{
base.InitialiseDefaults();
Set(ManiaSetting.ScrollTime, 1500.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollTime, 2250.0, 50.0, 10000.0, 50.0);
Set(ManiaSetting.ScrollDirection, ManiaScrollingDirection.Down);
}

View File

@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Mania.Edit.Layers.Selection.Overlays
}
// Todo: This is temporary, since the note masks don't do anything special yet. In the future they will handle input.
public override bool HandleMouseInput => false;
public override bool HandlePositionalInput => false;
}
}
}

View File

@ -20,8 +20,7 @@ namespace osu.Game.Rulesets.Mania.Edit
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Size = Vector2.One
};
protected override Vector2 PlayfieldArea => Vector2.One;
}
}

View File

@ -111,6 +111,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2;
}
protected void BeginHold()
{
holdStartTime = Time.Current;
bodyPiece.Hitting = true;
}
protected void EndHold()
{
holdStartTime = null;
bodyPiece.Hitting = false;
}
public bool OnPressed(ManiaAction action)
{
// Make sure the action happened within the body of the hold note
@ -123,8 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// The user has pressed during the body of the hold note, after the head note and its hit windows have passed
// and within the limited range of the above if-statement. This state will be managed by the head note if the
// user has pressed during the hit windows of the head note.
holdStartTime = Time.Current;
BeginHold();
return true;
}
@ -137,7 +148,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value)
return false;
holdStartTime = null;
EndHold();
// If the key has been released too early, the user should not receive full score for the release
if (!Tail.IsHit)
@ -170,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
// The head note also handles early hits before the body, but we want accurate early hits to count as the body being held
// The body doesn't handle these early early hits, so we have to explicitly set the holding state here
holdNote.holdStartTime = Time.Current;
holdNote.BeginHold();
return true;
}

View File

@ -32,6 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
background = new Box { RelativeSizeAxes = Axes.Both },
foreground = new BufferedContainer
{
Blending = BlendingMode.Additive,
RelativeSizeAxes = Axes.Both,
CacheDrawnFrameBuffer = true,
Children = new Drawable[]
@ -73,6 +74,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
private Color4 accentColour;
public Color4 AccentColour
{
get { return accentColour; }
@ -86,6 +88,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
public bool Hitting
{
get { return hitting; }
set
{
hitting = value;
updateAccentColour();
}
}
private Cached subtractionCache = new Cached();
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
@ -118,13 +130,26 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces
}
}
private bool hitting;
private void updateAccentColour()
{
if (!IsLoaded)
return;
foreground.Colour = AccentColour.Opacity(0.9f);
background.Colour = AccentColour.Opacity(0.6f);
foreground.Colour = AccentColour.Opacity(0.5f);
background.Colour = AccentColour.Opacity(0.7f);
const float animation_length = 50;
foreground.ClearTransforms(false, nameof(foreground.Colour));
if (hitting)
{
// wait for the next sync point
double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2);
using (foreground.BeginDelayedSequence(synchronisedOffset))
foreground.FadeColour(AccentColour.Lighten(0.2f), animation_length).Then().FadeColour(foreground.Colour, animation_length).Loop();
}
subtractionCache.Invalidate();
}

View File

@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Mania.UI
internal readonly Container TopLevelContainer;
private readonly Container explosionContainer;
protected override Container<Drawable> Content => hitObjectArea;
public Column()
{
RelativeSizeAxes = Axes.Y;
@ -54,7 +52,10 @@ namespace osu.Game.Rulesets.Mania.UI
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
hitObjectArea = new ColumnHitObjectArea { RelativeSizeAxes = Axes.Both },
hitObjectArea = new ColumnHitObjectArea(HitObjectContainer)
{
RelativeSizeAxes = Axes.Both,
},
explosionContainer = new Container
{
Name = "Hit explosions",

View File

@ -8,28 +8,24 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Mania.UI.Components
{
public class ColumnHitObjectArea : Container, IHasAccentColour
public class ColumnHitObjectArea : CompositeDrawable, IHasAccentColour
{
private const float hit_target_height = 10;
private const float hit_target_bar_height = 2;
private Container<Drawable> content;
protected override Container<Drawable> Content => content;
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private Container hitTargetLine;
private readonly Container hitTargetLine;
private readonly Drawable hitTargetBar;
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
public ColumnHitObjectArea(HitObjectContainer hitObjectContainer)
{
Drawable hitTargetBar;
InternalChildren = new[]
{
hitTargetBar = new Box
@ -45,13 +41,13 @@ namespace osu.Game.Rulesets.Mania.UI.Components
Masking = true,
Child = new Box { RelativeSizeAxes = Axes.Both }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
},
hitObjectContainer
};
}
[BackgroundDependencyLoader]
private void load(IScrollingInfo scrollingInfo)
{
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(direction =>
{

View File

@ -30,10 +30,11 @@ namespace osu.Game.Rulesets.Mania.UI
if (Result.IsHit)
{
this.ScaleTo(0.8f);
this.ScaleTo(1, 250, Easing.OutElastic);
JudgementBody.ScaleTo(0.8f);
JudgementBody.ScaleTo(1, 250, Easing.OutElastic);
this.Delay(50).FadeOut(200).ScaleTo(0.75f, 250);
JudgementBody.Delay(50).ScaleTo(0.75f, 250);
this.Delay(50).FadeOut(200);
}
Expire();

View File

@ -10,6 +10,7 @@ using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Configuration;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -25,12 +26,14 @@ namespace osu.Game.Rulesets.Mania.UI
if (stageDefinitions.Count <= 0)
throw new ArgumentException("Can't have zero or fewer stages.");
Size = new Vector2(1, 0.8f);
GridContainer playfieldGrid;
InternalChild = playfieldGrid = new GridContainer
AddInternal(playfieldGrid = new GridContainer
{
RelativeSizeAxes = Axes.Both,
Content = new[] { new Drawable[stageDefinitions.Count] }
};
});
var normalColumnAction = ManiaAction.Key1;
var specialColumnAction = ManiaAction.Special1;

View File

@ -24,7 +24,6 @@ using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using OpenTK;
namespace osu.Game.Rulesets.Mania.UI
{
@ -110,8 +109,6 @@ namespace osu.Game.Rulesets.Mania.UI
}
}
protected override Vector2 PlayfieldArea => new Vector2(1, 0.8f);
protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay);
}
}

View File

@ -7,7 +7,7 @@ using osu.Game.Rulesets.UI.Scrolling;
namespace osu.Game.Rulesets.Mania.UI
{
public class ManiaScrollingPlayfield : ScrollingPlayfield
public abstract class ManiaScrollingPlayfield : ScrollingPlayfield
{
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();

View File

@ -30,8 +30,7 @@ namespace osu.Game.Rulesets.Mania.UI
public IReadOnlyList<Column> Columns => columnFlow.Children;
private readonly FillFlowContainer<Column> columnFlow;
protected override Container<Drawable> Content => barLineContainer;
private readonly Container<Drawable> barLineContainer;
private readonly Container barLineContainer;
public Container<DrawableManiaJudgement> Judgements => judgements;
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
@ -105,6 +104,7 @@ namespace osu.Game.Rulesets.Mania.UI
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
RelativeSizeAxes = Axes.Y,
Child = HitObjectContainer
}
},
judgements = new JudgementContainer<DrawableManiaJudgement>

View File

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

@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(239, 176),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(154, 28),
@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-(distance / 2), 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(distance, 0),
@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Osu.Tests
{
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(200, 200),
@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Bezier,
StartTime = Time.Current + 1000,
Position = new Vector2(-200, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(150, 75),
@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Tests
CurveType = CurveType.Linear,
StartTime = Time.Current + 1000,
Position = new Vector2(0, 0),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(-200, 0),
@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Osu.Tests
StartTime = Time.Current + 1000,
Position = new Vector2(-100, 0),
CurveType = CurveType.Catmull,
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(50, -50),

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
{
public class OsuBeatmapProcessor : BeatmapProcessor
{
private const int stack_distance = 3;
public OsuBeatmapProcessor(IBeatmap beatmap)
: base(beatmap)
{
@ -18,17 +20,21 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
public override void PostProcess()
{
base.PostProcess();
applyStacking((Beatmap<OsuHitObject>)Beatmap);
var osuBeatmap = (Beatmap<OsuHitObject>)Beatmap;
// Reset stacking
foreach (var h in osuBeatmap.HitObjects)
h.StackHeight = 0;
if (Beatmap.BeatmapInfo.BeatmapVersion >= 6)
applyStacking(osuBeatmap);
else
applyStackingOld(osuBeatmap);
}
private void applyStacking(Beatmap<OsuHitObject> beatmap)
{
const int stack_distance = 3;
// Reset stacking
for (int i = 0; i <= beatmap.HitObjects.Count - 1; i++)
beatmap.HitObjects[i].StackHeight = 0;
// Extend the end index to include objects they are stacked on
int extendedEndIndex = beatmap.HitObjects.Count - 1;
for (int i = beatmap.HitObjects.Count - 1; i >= 0; i--)
@ -167,5 +173,40 @@ namespace osu.Game.Rulesets.Osu.Beatmaps
}
}
}
private void applyStackingOld(Beatmap<OsuHitObject> beatmap)
{
for (int i = 0; i < beatmap.HitObjects.Count; i++)
{
OsuHitObject currHitObject = beatmap.HitObjects[i];
if (currHitObject.StackHeight != 0 && !(currHitObject is Slider))
continue;
double startTime = (currHitObject as IHasEndTime)?.EndTime ?? currHitObject.StartTime;
int sliderStack = 0;
for (int j = i + 1; j < beatmap.HitObjects.Count; j++)
{
double stackThreshold = beatmap.HitObjects[i].TimePreempt * beatmap.BeatmapInfo.StackLeniency;
if (beatmap.HitObjects[j].StartTime - stackThreshold > startTime)
break;
if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.Position) < stack_distance)
{
currHitObject.StackHeight++;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
}
else if (Vector2Extensions.Distance(beatmap.HitObjects[j].Position, currHitObject.EndPosition) < stack_distance)
{
//Case for sliders - bump notes down and right, rather than up and left.
sliderStack++;
beatmap.HitObjects[j].StackHeight -= sliderStack;
startTime = (beatmap.HitObjects[j] as IHasEndTime)?.EndTime ?? beatmap.HitObjects[i].StartTime;
}
}
}
}
}
}

View File

@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double sectionLength = section_length * timeRate;
// The first object doesn't generate a strain, so we begin with an incremented section end
double currentSectionEnd = 2 * sectionLength;
double currentSectionEnd = Math.Ceiling(beatmap.HitObjects.First().StartTime / sectionLength) * sectionLength;
foreach (OsuDifficultyHitObject h in difficultyBeatmap)
{
@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double hitWindowGreat = (int)(beatmap.HitObjects.First().HitWindows.Great / 2) / timeRate;
double preempt = (int)BeatmapDifficulty.DifficultyRange(beatmap.BeatmapInfo.BaseDifficulty.ApproachRate, 1800, 1200, 450) / timeRate;
int maxCombo = beatmap.HitObjects.Count();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
beatmapMaxCombo = Beatmap.HitObjects.Count();
beatmapMaxCombo = Beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above)
beatmapMaxCombo += Beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
}

View File

@ -3,6 +3,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Rulesets.Osu.Objects;
namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
@ -23,8 +24,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
{
// Sort OsuHitObjects by StartTime - they are not correctly ordered in some cases.
// This should probably happen before the objects reach the difficulty calculator.
objects.Sort((a, b) => a.StartTime.CompareTo(b.StartTime));
difficultyObjects = createDifficultyObjectEnumerator(objects, timeRate);
difficultyObjects = createDifficultyObjectEnumerator(objects.OrderBy(h => h.StartTime).ToList(), timeRate);
}
/// <summary>

View File

@ -21,15 +21,25 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
public OsuHitObject BaseObject { get; }
/// <summary>
/// Normalized distance from the <see cref="OsuHitObject.StackedPosition"/> of the previous <see cref="OsuDifficultyHitObject"/>.
/// Normalized distance from the end position of the previous <see cref="OsuDifficultyHitObject"/> to the start position of this <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double Distance { get; private set; }
public double JumpDistance { get; private set; }
/// <summary>
/// Normalized distance between the start and end position of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double TravelDistance { get; private set; }
/// <summary>
/// Milliseconds elapsed since the StartTime of the previous <see cref="OsuDifficultyHitObject"/>.
/// </summary>
public double DeltaTime { get; private set; }
/// <summary>
/// Milliseconds elapsed since the start time of the previous <see cref="OsuDifficultyHitObject"/>, with a minimum of 50ms.
/// </summary>
public double StrainTime { get; private set; }
private readonly OsuHitObject lastObject;
private readonly double timeRate;
@ -51,31 +61,37 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
private void setDistances()
{
// We will scale distances by this factor, so we can assume a uniform CircleSize among beatmaps.
double scalingFactor = normalized_radius / BaseObject.Radius;
float scalingFactor = normalized_radius / (float)BaseObject.Radius;
if (BaseObject.Radius < 30)
{
double smallCircleBonus = Math.Min(30 - BaseObject.Radius, 5) / 50;
float smallCircleBonus = Math.Min(30 - (float)BaseObject.Radius, 5) / 50;
scalingFactor *= 1 + smallCircleBonus;
}
Vector2 lastCursorPosition = lastObject.StackedPosition;
float lastTravelDistance = 0;
var lastSlider = lastObject as Slider;
if (lastSlider != null)
{
computeSliderCursorPosition(lastSlider);
lastCursorPosition = lastSlider.LazyEndPosition ?? lastCursorPosition;
lastTravelDistance = lastSlider.LazyTravelDistance;
}
Distance = (lastTravelDistance + (BaseObject.StackedPosition - lastCursorPosition).Length) * scalingFactor;
// Don't need to jump to reach spinners
if (!(BaseObject is Spinner))
JumpDistance = (BaseObject.StackedPosition * scalingFactor - lastCursorPosition * scalingFactor).Length;
// Todo: BUG!!! Last slider's travel distance is considered ONLY IF we ourselves are also a slider!
if (BaseObject is Slider)
TravelDistance = (lastSlider?.LazyTravelDistance ?? 0) * scalingFactor;
}
private void setTimingValues()
{
// Every timing inverval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure.
DeltaTime = Math.Max(50, (BaseObject.StartTime - lastObject.StartTime) / timeRate);
DeltaTime = (BaseObject.StartTime - lastObject.StartTime) / timeRate;
// Every strain interval is hard capped at the equivalent of 375 BPM streaming speed as a safety measure
StrainTime = Math.Max(50, DeltaTime);
}
private void computeSliderCursorPosition(Slider slider)
@ -87,8 +103,14 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Preprocessing
float approxFollowCircleRadius = (float)(slider.Radius * 3);
var computeVertex = new Action<double>(t =>
{
double progress = ((int)t - (int)slider.StartTime) / (float)(int)slider.SpanDuration;
if (progress % 2 > 1)
progress = 1 - progress % 1;
else
progress = progress % 1;
// ReSharper disable once PossibleInvalidOperationException (bugged in current r# version)
var diff = slider.StackedPositionAt(t) - slider.LazyEndPosition.Value;
var diff = slider.StackedPosition + slider.Curve.PositionAt(progress) - slider.LazyEndPosition.Value;
float dist = diff.Length;
if (dist > approxFollowCircleRadius)

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double SkillMultiplier => 26.25;
protected override double StrainDecayBase => 0.15;
protected override double StrainValueOf(OsuDifficultyHitObject current) => Math.Pow(current.Distance, 0.99) / current.DeltaTime;
protected override double StrainValueOf(OsuDifficultyHitObject current)
=> (Math.Pow(current.TravelDistance, 0.99) + Math.Pow(current.JumpDistance, 0.99)) / current.StrainTime;
}
}

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
protected override double StrainValueOf(OsuDifficultyHitObject current)
{
double distance = current.Distance;
double distance = current.TravelDistance + current.JumpDistance;
double speedValue;
if (distance > single_spacing_threshold)
@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills
else
speedValue = 0.95;
return speedValue / current.DeltaTime;
return speedValue / current.StrainTime;
}
}
}

View File

@ -56,6 +56,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
}
// Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input.
public override bool HandleMouseInput => false;
public override bool HandlePositionalInput => false;
}
}

View File

@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Layers.Selection.Overlays
body.UpdateProgress(0);
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => body.ReceiveMouseInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
public override Vector2 SelectionPoint => ToScreenSpace(OriginPosition);
public override Quad SelectionQuad => body.PathDrawQuad;

View File

@ -4,6 +4,7 @@
using osu.Framework.Graphics.Cursor;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Edit
@ -15,8 +16,8 @@ namespace osu.Game.Rulesets.Osu.Edit
{
}
protected override Vector2 PlayfieldArea => Vector2.One;
protected override CursorContainer CreateCursor() => null;
protected override Playfield CreatePlayfield() => new OsuPlayfield { Size = Vector2.One };
}
}

View File

@ -3,6 +3,7 @@
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Edit;
using osu.Game.Rulesets.Edit.Tools;
@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Edit
new HitObjectCompositionTool<Spinner>()
};
protected override ScalableContainer CreateLayerContainer() => new ScalableContainer(OsuPlayfield.BASE_SIZE.X) { RelativeSizeAxes = Axes.Both };
protected override Container CreateLayerContainer() => new PlayfieldAdjustmentContainer { RelativeSizeAxes = Axes.Both };
public override HitObjectMask CreateMaskFor(DrawableHitObject hitObject)
{

View File

@ -1,7 +1,6 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Game.Rulesets.Mods;
@ -33,8 +32,9 @@ namespace osu.Game.Rulesets.Osu.Mods
slider.NestedHitObjects.OfType<SliderTick>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
slider.NestedHitObjects.OfType<RepeatPoint>().ForEach(h => h.Position = new Vector2(h.Position.X, OsuPlayfield.BASE_SIZE.Y - h.Position.Y));
var newControlPoints = new List<Vector2>();
slider.ControlPoints.ForEach(c => newControlPoints.Add(new Vector2(c.X, -c.Y)));
var newControlPoints = new Vector2[slider.ControlPoints.Length];
for (int i = 0; i < slider.ControlPoints.Length; i++)
newControlPoints[i] = new Vector2(slider.ControlPoints[i].X, -slider.ControlPoints[i].Y);
slider.ControlPoints = newControlPoints;
slider.Curve?.Calculate(); // Recalculate the slider curve

View File

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Input.States;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
@ -28,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Mods
const float relax_leniency = 3;
foreach (var drawable in playfield.HitObjects.AliveObjects)
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
if (!(drawable is DrawableOsuHitObject osuHit))
continue;
@ -77,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.Mods
wasLeft = !wasLeft;
}
osuInputManager.HandleCustomInput(new InputState(), state);
state.Apply(osuInputManager.CurrentState, osuInputManager);
}
public void ApplyToRulesetContainer(RulesetContainer<OsuHitObject> rulesetContainer)

View File

@ -0,0 +1,53 @@
// 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 osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModTransform : Mod, IApplicableToDrawableHitObjects
{
public override string Name => "Transform";
public override string ShortenedName => "TR";
public override FontAwesome Icon => FontAwesome.fa_arrows;
public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle) };
private float theta;
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var drawable in drawables)
{
var hitObject = (OsuHitObject) drawable.HitObject;
float appearDistance = (float)(hitObject.TimePreempt - hitObject.TimeFadeIn) / 2;
Vector2 originalPosition = drawable.Position;
Vector2 appearOffset = new Vector2((float)Math.Cos(theta), (float)Math.Sin(theta)) * appearDistance;
//the - 1 and + 1 prevents the hit objects to appear in the wrong position.
double appearTime = hitObject.StartTime - hitObject.TimePreempt - 1;
double moveDuration = hitObject.TimePreempt + 1;
using (drawable.BeginAbsoluteSequence(appearTime, true))
{
drawable
.MoveToOffset(appearOffset)
.MoveTo(originalPosition, moveDuration, Easing.InOutSine);
}
theta += (float) hitObject.TimeFadeIn / 1000;
}
}
}
}

View File

@ -0,0 +1,72 @@
// 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 osu.Framework.Graphics;
using osu.Game.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModWiggle : Mod, IApplicableToDrawableHitObjects
{
public override string Name => "Wiggle";
public override string ShortenedName => "WG";
public override FontAwesome Icon => FontAwesome.fa_certificate;
public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles
public void ApplyToDrawableHitObjects(IEnumerable<DrawableHitObject> drawables)
{
foreach (var drawable in drawables)
drawable.ApplyCustomUpdateState += drawableOnApplyCustomUpdateState;
}
private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state)
{
var osuObject = (OsuHitObject)drawable.HitObject;
Vector2 origin = drawable.Position;
// Wiggle the repeat points with the slider instead of independently.
// Also fixes an issue with repeat points being positioned incorrectly.
if (osuObject is RepeatPoint)
return;
Random objRand = new Random((int)osuObject.StartTime);
// Wiggle all objects during TimePreempt
int amountWiggles = (int)osuObject.TimePreempt / wiggle_duration;
void wiggle()
{
float nextAngle = (float)(objRand.NextDouble() * 2 * Math.PI);
float nextDist = (float)(objRand.NextDouble() * wiggle_strength);
drawable.MoveTo(new Vector2((float)(nextDist * Math.Cos(nextAngle) + origin.X), (float)(nextDist * Math.Sin(nextAngle) + origin.Y)), wiggle_duration);
}
for (int i = 0; i < amountWiggles; i++)
using (drawable.BeginAbsoluteSequence(osuObject.StartTime - osuObject.TimePreempt + i * wiggle_duration, true))
wiggle();
// Keep wiggling sliders and spinners for their duration
if (!(osuObject is IHasEndTime endTime))
return;
amountWiggles = (int)(endTime.Duration / wiggle_duration);
for (int i = 0; i < amountWiggles; i++)
using (drawable.BeginAbsoluteSequence(osuObject.StartTime + i * wiggle_duration, true))
wiggle();
}
}
}

View File

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

View File

@ -10,19 +10,29 @@ using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
using OpenTK.Graphics;
using osu.Game.Graphics.Containers;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableOsuHitObject : DrawableHitObject<OsuHitObject>
{
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 && Clock?.CurrentTime >= HitObject.StartTime - HitObject.TimePreempt;
private readonly ShakeContainer shakeContainer;
protected DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
base.AddInternal(shakeContainer = new ShakeContainer { RelativeSizeAxes = Axes.Both });
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)
{
double transformTime = HitObject.StartTime - HitObject.TimePreempt;
@ -68,6 +78,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private OsuInputManager osuActionInputManager;
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);
}
}

View File

@ -44,14 +44,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
},
ticks = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatPoints = new Container<DrawableRepeatPoint> { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s)
Ball = new SliderBall(s, this)
{
BypassAutoSizeAxes = Axes.Both,
Scale = new Vector2(s.Scale),
AlwaysPresent = true,
Alpha = 0
},
HeadCircle = new DrawableSliderHead(s, s.HeadCircle),
HeadCircle = new DrawableSliderHead(s, s.HeadCircle)
{
OnShake = Shake
},
TailCircle = new DrawableSliderTail(s, s.TailCircle)
};
@ -181,6 +184,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public Drawable ProxiedLayer => HeadCircle.ApproachCircle;
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => Body.ReceiveMouseInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Body.ReceivePositionalInputAt(screenSpacePos);
}
}

View File

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

View File

@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class CirclePiece : Container, IKeyBindingHandler<OsuAction>
{
// IsHovered is used
public override bool HandlePositionalInput => true;
public Func<bool> Hit;
public CirclePiece()

View File

@ -5,11 +5,11 @@ using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Objects.Types;
using OpenTK.Graphics;
using osu.Game.Skinning;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
@ -36,9 +36,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider;
public readonly Drawable FollowCircle;
private Drawable drawableBall;
private readonly DrawableSlider drawableSlider;
public SliderBall(Slider slider)
public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{
this.drawableSlider = drawableSlider;
this.slider = slider;
Masking = true;
AutoSizeAxes = Axes.Both;
@ -100,24 +102,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
private InputState lastState;
private Vector2? lastScreenSpaceMousePosition;
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
protected override bool OnMouseDown(MouseDownEvent e)
{
lastState = state;
return base.OnMouseDown(state, args);
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseDown(e);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
protected override bool OnMouseUp(MouseUpEvent e)
{
lastState = state;
return base.OnMouseUp(state, args);
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseUp(e);
}
protected override bool OnMouseMove(InputState state)
protected override bool OnMouseMove(MouseMoveEvent e)
{
lastState = state;
return base.OnMouseMove(state);
lastScreenSpaceMousePosition = e.ScreenSpaceMousePosition;
return base.OnMouseMove(e);
}
public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null)
@ -151,11 +153,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
if (Time.Current < slider.EndTime)
{
// Make sure to use the base version of ReceiveMouseInputAt so that we correctly check the position.
// Make sure to use the base version of ReceivePositionalInputAt so that we correctly check the position.
Tracking = canCurrentlyTrack
&& lastState != null
&& ReceiveMouseInputAt(lastState.Mouse.NativeState.Position)
&& ((Parent as DrawableSlider)?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
&& lastScreenSpaceMousePosition.HasValue
&& ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value)
&& (drawableSlider?.OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false);
}
}

View File

@ -7,26 +7,24 @@ using osu.Framework.Allocation;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.OpenGL.Textures;
using osu.Framework.Graphics.Lines;
using osu.Framework.Graphics.Textures;
using OpenTK;
using OpenTK.Graphics.ES30;
using OpenTK.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Game.Rulesets.Objects.Types;
using OpenTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SliderBody : Container, ISliderProgress
{
private readonly Path path;
private readonly SliderPath path;
private readonly BufferedContainer container;
public float PathWidth
{
get { return path.PathWidth; }
set { path.PathWidth = value; }
get => path.PathWidth;
set => path.PathWidth = value;
}
/// <summary>
@ -42,49 +40,44 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
public double? SnakedStart { get; private set; }
public double? SnakedEnd { get; private set; }
private Color4 accentColour = Color4.White;
/// <summary>
/// Used to colour the path.
/// </summary>
public Color4 AccentColour
{
get { return accentColour; }
get => path.AccentColour;
set
{
if (accentColour == value)
if (path.AccentColour == value)
return;
accentColour = value;
path.AccentColour = value;
if (LoadState >= LoadState.Ready)
reloadTexture();
container.ForceRedraw();
}
}
private Color4 borderColour = Color4.White;
/// <summary>
/// Used to colour the path border.
/// </summary>
public new Color4 BorderColour
{
get { return borderColour; }
get => path.BorderColour;
set
{
if (borderColour == value)
if (path.BorderColour == value)
return;
borderColour = value;
path.BorderColour = value;
if (LoadState >= LoadState.Ready)
reloadTexture();
container.ForceRedraw();
}
}
public Quad PathDrawQuad => container.ScreenSpaceDrawQuad;
private int textureWidth => (int)PathWidth * 2;
private Vector2 topLeftOffset;
private readonly Slider slider;
public SliderBody(Slider s)
{
slider = s;
@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
CacheDrawnFrameBuffer = true,
Children = new Drawable[]
{
path = new Path
path = new SliderPath
{
Blending = BlendingMode.None,
},
@ -108,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
container.Attach(RenderbufferInternalFormat.DepthComponent16);
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => path.ReceiveMouseInputAt(screenSpacePos);
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => path.ReceivePositionalInputAt(screenSpacePos);
public void SetRange(double p0, double p1)
{
@ -130,53 +123,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
[BackgroundDependencyLoader]
private void load()
{
reloadTexture();
computeSize();
}
private void reloadTexture()
{
var texture = new Texture(textureWidth, 1);
//initialise background
var raw = new RawTexture(textureWidth, 1);
var bytes = raw.Data;
const float aa_portion = 0.02f;
const float border_portion = 0.128f;
const float gradient_portion = 1 - border_portion;
const float opacity_at_centre = 0.3f;
const float opacity_at_edge = 0.8f;
for (int i = 0; i < textureWidth; i++)
{
float progress = (float)i / (textureWidth - 1);
if (progress <= border_portion)
{
bytes[i * 4] = (byte)(BorderColour.R * 255);
bytes[i * 4 + 1] = (byte)(BorderColour.G * 255);
bytes[i * 4 + 2] = (byte)(BorderColour.B * 255);
bytes[i * 4 + 3] = (byte)(Math.Min(progress / aa_portion, 1) * (BorderColour.A * 255));
}
else
{
progress -= border_portion;
bytes[i * 4] = (byte)(AccentColour.R * 255);
bytes[i * 4 + 1] = (byte)(AccentColour.G * 255);
bytes[i * 4 + 2] = (byte)(AccentColour.B * 255);
bytes[i * 4 + 3] = (byte)((opacity_at_edge - (opacity_at_edge - opacity_at_centre) * progress / gradient_portion) * (AccentColour.A * 255));
}
}
texture.SetData(new TextureUpload(raw));
path.Texture = texture;
container.ForceRedraw();
}
private void computeSize()
{
// Generate the entire curve
@ -229,5 +178,53 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
SetRange(start, end);
}
private class SliderPath : SmoothPath
{
private const float border_portion = 0.128f;
private const float gradient_portion = 1 - border_portion;
private const float opacity_at_centre = 0.3f;
private const float opacity_at_edge = 0.8f;
private Color4 borderColour = Color4.White;
public Color4 BorderColour
{
get => borderColour;
set
{
if (borderColour == value)
return;
borderColour = value;
InvalidateTexture();
}
}
private Color4 accentColour = Color4.White;
public Color4 AccentColour
{
get => accentColour;
set
{
if (accentColour == value)
return;
accentColour = value;
InvalidateTexture();
}
}
protected override Color4 ColourAt(float position)
{
if (position <= border_portion)
return BorderColour;
position -= border_portion;
return new Color4(AccentColour.R, AccentColour.G, AccentColour.B, (opacity_at_edge - (opacity_at_edge - opacity_at_centre) * position / gradient_portion) * AccentColour.A);
}
}
}
}

View File

@ -11,9 +11,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
public class SpinnerBackground : CircularContainer, IHasAccentColour
{
public override bool HandleKeyboardInput => false;
public override bool HandleMouseInput => false;
protected Box Disc;
public Color4 AccentColour

View File

@ -4,7 +4,7 @@
using System;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.States;
using osu.Framework.Input.Events;
using osu.Game.Graphics;
using OpenTK;
using OpenTK.Graphics;
@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
};
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
private bool tracking;
public bool Tracking
@ -68,10 +68,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}
}
protected override bool OnMouseMove(InputState state)
protected override bool OnMouseMove(MouseMoveEvent e)
{
mousePosition = Parent.ToLocalSpace(state.Mouse.NativeState.Position);
return base.OnMouseMove(state);
mousePosition = Parent.ToLocalSpace(e.ScreenSpaceMousePosition);
return base.OnMouseMove(e);
}
private Vector2 mousePosition;

View File

@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects
public SliderCurve Curve { get; } = new SliderCurve();
public List<Vector2> ControlPoints
public Vector2[] ControlPoints
{
get { return Curve.ControlPoints; }
set { Curve.ControlPoints = value; }

View File

@ -4,8 +4,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.EventArgs;
using osu.Framework.Input.States;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu
@ -36,22 +35,20 @@ namespace osu.Game.Rulesets.Osu
{
}
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);
protected override bool Handle(UIEvent e)
{
if (!AllowUserPresses) return false;
return base.Handle(e);
}
}
}
public enum OsuAction
{
[Description("Left Button")]
[Description("Left button")]
LeftButton,
[Description("Right Button")]
[Description("Right button")]
RightButton
}
}

View File

@ -117,6 +117,11 @@ namespace osu.Game.Rulesets.Osu
new OsuModRelax(),
new OsuModAutopilot(),
};
case ModType.Fun:
return new Mod[] {
new OsuModTransform(),
new OsuModWiggle(),
};
default:
return new Mod[] { };
}

View File

@ -12,7 +12,7 @@ using osu.Framework.Graphics.Primitives;
using osu.Framework.Graphics.Shaders;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input;
using osu.Framework.Input.States;
using osu.Framework.Input.Events;
using osu.Framework.Timing;
using OpenTK;
using OpenTK.Graphics;
@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
}
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => true;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true;
[BackgroundDependencyLoader]
private void load(ShaderManager shaders, TextureStore textures)
@ -117,15 +117,15 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
timeOffset = Time.Current;
}
protected override bool OnMouseMove(InputState state)
protected override bool OnMouseMove(MouseMoveEvent e)
{
Vector2 pos = state.Mouse.NativeState.Position;
Vector2 pos = e.ScreenSpaceMousePosition;
if (lastPosition == null)
{
lastPosition = pos;
resampler.AddPosition(lastPosition.Value);
return base.OnMouseMove(state);
return base.OnMouseMove(e);
}
foreach (Vector2 pos2 in resampler.AddPosition(pos))
@ -147,7 +147,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
}
}
return base.OnMouseMove(state);
return base.OnMouseMove(e);
}
private void addPosition(Vector2 pos)
@ -216,7 +216,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
Texture.DrawQuad(
new Quad(pos.X - Size.X / 2, pos.Y - Size.Y / 2, Size.X, Size.Y),
DrawInfo.Colour,
DrawColourInfo.Colour,
null,
v => Shared.VertexBuffer.Vertices[end++] = new TexturedTrailVertex
{

View File

@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
return false;
}
public override bool HandleMouseInput => true; // OverlayContainer will set this false when we go hidden, but we always want to receive input.
public override bool HandlePositionalInput => true; // OverlayContainer will set this false when we go hidden, but we always want to receive input.
protected override void PopIn()
{

View File

@ -23,29 +23,35 @@ namespace osu.Game.Rulesets.Osu.UI
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
public OsuPlayfield()
: base(BASE_SIZE.X)
{
Anchor = Anchor.Centre;
Origin = Anchor.Centre;
AddRange(new Drawable[]
Size = new Vector2(0.75f);
InternalChild = new PlayfieldAdjustmentContainer
{
connectionLayer = new FollowPointRenderer
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
approachCircles = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
});
connectionLayer = new FollowPointRenderer
{
RelativeSizeAxes = Axes.Both,
Depth = 2,
},
judgementLayer = new JudgementContainer<DrawableOsuJudgement>
{
RelativeSizeAxes = Axes.Both,
Depth = 1,
},
HitObjectContainer,
approachCircles = new Container
{
RelativeSizeAxes = Axes.Both,
Depth = -1,
},
}
};
}
public override void Add(DrawableHitObject h)
@ -61,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.UI
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)
@ -72,7 +78,8 @@ namespace osu.Game.Rulesets.Osu.UI
DrawableOsuJudgement explosion = new DrawableOsuJudgement(result, judgedObject)
{
Origin = Anchor.Centre,
Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition
Position = ((OsuHitObject)judgedObject.HitObject).StackedEndPosition,
Scale = new Vector2(((OsuHitObject)judgedObject.HitObject).Scale * 1.65f)
};
judgementLayer.Add(explosion);

View File

@ -4,7 +4,6 @@
using System.Linq;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Input;
using OpenTK;
using osu.Game.Beatmaps;
using osu.Game.Input.Handlers;
using osu.Game.Rulesets.Objects.Drawables;
@ -58,12 +57,6 @@ namespace osu.Game.Rulesets.Osu.UI
}
}
protected override Vector2 GetAspectAdjustedSize()
{
var aspectSize = DrawSize.X * 0.75f < DrawSize.Y ? new Vector2(DrawSize.X, DrawSize.X * 0.75f) : new Vector2(DrawSize.Y * 4f / 3f, DrawSize.Y);
return new Vector2(aspectSize.X / DrawSize.X, aspectSize.Y / DrawSize.Y);
}
protected override CursorContainer CreateCursor() => new GameplayCursor();
}
}

View File

@ -0,0 +1,42 @@
// 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;
using OpenTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class PlayfieldAdjustmentContainer : Container
{
protected override Container<Drawable> Content => content;
private readonly Container content;
public PlayfieldAdjustmentContainer()
{
InternalChild = new Container
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
FillAspectRatio = 4f / 3,
Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both }
};
}
/// <summary>
/// A <see cref="Container"/> which scales its content relative to a target width.
/// </summary>
private class ScalingContainer : Container
{
protected override void Update()
{
base.Update();
Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X);
Size = Vector2.Divide(Vector2.One, Scale);
}
}
}
}

View File

@ -2,9 +2,10 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps
converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x =>
{
TaikoHitObject first = x.First();
if (x.Skip(1).Any())
if (x.Skip(1).Any() && !(first is Swell))
first.IsStrong = true;
return first;
}).ToList();

View File

@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
private bool validActionPressed;
private bool pressHandledThisFrame;
protected DrawableHit(Hit hit)
: base(hit)
{
@ -51,6 +53,9 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
public override bool OnPressed(TaikoAction action)
{
if (pressHandledThisFrame)
return true;
if (Judged)
return false;
@ -62,6 +67,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
if (IsHit)
HitAction = action;
// Regardless of whether we've hit or not, any secondary key presses in the same frame should be discarded
// E.g. hitting a non-strong centre as a strong should not fall through and perform a hit on the next note
pressHandledThisFrame = true;
return result;
}
@ -76,6 +85,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.Update();
// The input manager processes all input prior to us updating, so this is the perfect time
// for us to remove the extra press blocking, before input is handled in the next frame
pressHandledThisFrame = false;
Size = BaseSize * Parent.RelativeChildSize;
}

View File

@ -17,13 +17,13 @@ namespace osu.Game.Rulesets.Taiko
public enum TaikoAction
{
[Description("Left (Rim)")]
[Description("Left (rim)")]
LeftRim,
[Description("Left (Centre)")]
[Description("Left (centre)")]
LeftCentre,
[Description("Right (Centre)")]
[Description("Right (centre)")]
RightCentre,
[Description("Right (Rim)")]
[Description("Right (rim)")]
RightRim
}
}

View File

@ -31,10 +31,10 @@ namespace osu.Game.Rulesets.Taiko.UI
switch (Result.Type)
{
case HitResult.Good:
Colour = colours.GreenLight;
JudgementBody.Colour = colours.GreenLight;
break;
case HitResult.Great:
Colour = colours.BlueLight;
JudgementBody.Colour = colours.BlueLight;
break;
}
}

View File

@ -0,0 +1,22 @@
// 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.Containers;
using OpenTK;
namespace osu.Game.Rulesets.Taiko.UI
{
public class PlayfieldAdjustmentContainer : Container
{
private const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
private const float default_aspect = 16f / 9f;
protected override void Update()
{
base.Update();
float aspectAdjust = MathHelper.Clamp(Parent.ChildSize.X / Parent.ChildSize.Y, 0.4f, 4) / default_aspect;
Size = new Vector2(1, default_relative_height * aspectAdjust);
}
}
}

View File

@ -1,23 +1,24 @@
// 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.Allocation;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Shapes;
using osu.Game.Rulesets.Taiko.Objects;
using OpenTK;
using OpenTK.Graphics;
using osu.Game.Rulesets.Taiko.Judgements;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Extensions.Color4Extensions;
using System.Linq;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Configuration;
using osu.Game.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.UI.Scrolling;
using osu.Game.Rulesets.Taiko.Objects;
using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Judgements;
using OpenTK;
using OpenTK.Graphics;
namespace osu.Game.Rulesets.Taiko.UI
{
@ -40,13 +41,12 @@ namespace osu.Game.Rulesets.Taiko.UI
protected override bool UserScrollSpeedAdjustment => false;
protected override SpeedChangeVisualisationMethod VisualisationMethod => SpeedChangeVisualisationMethod.Overlapping;
private readonly Container<HitExplosion> hitExplosionContainer;
private readonly Container<KiaiHitExplosion> kiaiExplosionContainer;
private readonly JudgementContainer<DrawableTaikoJudgement> judgementContainer;
protected override Container<Drawable> Content => content;
private readonly Container content;
private readonly Container topLevelHitContainer;
private readonly Container barlineContainer;
@ -61,140 +61,147 @@ namespace osu.Game.Rulesets.Taiko.UI
{
Direction.Value = ScrollingDirection.Left;
AddRangeInternal(new Drawable[]
InternalChild = new PlayfieldAdjustmentContainer
{
backgroundContainer = new Container
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Name = "Transparent playfield background",
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
backgroundContainer = new Container
{
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
},
Children = new Drawable[]
{
background = new Box
Name = "Transparent playfield background",
RelativeSizeAxes = Axes.Both,
Masking = true,
EdgeEffect = new EdgeEffectParameters
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f
Type = EdgeEffectType.Shadow,
Colour = Color4.Black.Opacity(0.2f),
Radius = 5,
},
}
},
new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = left_area_size },
Children = new Drawable[]
{
new Container
Children = new Drawable[]
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Children = new Drawable[]
background = new Box
{
hitExplosionContainer = new Container<HitExplosion>
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Blending = BlendingMode.Additive,
},
new HitTarget
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
}
}
},
barlineContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
},
content = new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true
},
kiaiExplosionContainer = new Container<KiaiHitExplosion>
{
Name = "Kiai hit explosions",
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
{
Name = "Judgements",
RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
}
},
overlayBackgroundContainer = new Container
{
Name = "Left overlay",
RelativeSizeAxes = Axes.Y,
Size = new Vector2(left_area_size, 1),
Children = new Drawable[]
{
overlayBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new InputDrum(controlPoints)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Scale = new Vector2(0.9f),
Margin = new MarginPadding { Right = 20 }
},
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
}
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
RelativeSizeAxes = Axes.Both,
Alpha = 0.6f
},
}
},
new Container
{
Name = "Right area",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = left_area_size },
Children = new Drawable[]
{
new Container
{
Name = "Masked elements before hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Children = new Drawable[]
{
hitExplosionContainer = new Container<HitExplosion>
{
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Blending = BlendingMode.Additive,
},
new HitTarget
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit
}
}
},
barlineContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }
},
new Container
{
Name = "Hit objects",
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = HIT_TARGET_OFFSET },
Masking = true,
Child = HitObjectContainer
},
kiaiExplosionContainer = new Container<KiaiHitExplosion>
{
Name = "Kiai hit explosions",
RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
judgementContainer = new JudgementContainer<DrawableTaikoJudgement>
{
Name = "Judgements",
RelativeSizeAxes = Axes.Y,
Margin = new MarginPadding { Left = HIT_TARGET_OFFSET },
Blending = BlendingMode.Additive
},
}
},
overlayBackgroundContainer = new Container
{
Name = "Left overlay",
RelativeSizeAxes = Axes.Y,
Size = new Vector2(left_area_size, 1),
Children = new Drawable[]
{
overlayBackground = new Box
{
RelativeSizeAxes = Axes.Both,
},
new InputDrum(controlPoints)
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Scale = new Vector2(0.9f),
Margin = new MarginPadding { Right = 20 }
},
new Box
{
Anchor = Anchor.TopRight,
RelativeSizeAxes = Axes.Y,
Width = 10,
Colour = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)),
},
}
},
new Container
{
Name = "Border",
RelativeSizeAxes = Axes.Both,
Masking = true,
MaskingSmoothness = 0,
BorderThickness = 2,
AlwaysPresent = true,
Children = new[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0,
AlwaysPresent = true
}
}
},
topLevelHitContainer = new Container
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
}
},
topLevelHitContainer = new Container
{
Name = "Top level hit objects",
RelativeSizeAxes = Axes.Both,
}
});
};
VisibleTimeRange.Value = 6000;
VisibleTimeRange.Value = 7000;
}
[BackgroundDependencyLoader]

View File

@ -2,7 +2,6 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
@ -13,7 +12,6 @@ using osu.Game.Rulesets.Taiko.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Rulesets.Taiko.Replays;
using OpenTK;
using System.Linq;
using osu.Framework.Input;
using osu.Game.Input.Handlers;
@ -74,27 +72,11 @@ namespace osu.Game.Rulesets.Taiko.UI
}
}
protected override Vector2 GetAspectAdjustedSize()
{
const float default_relative_height = TaikoPlayfield.DEFAULT_HEIGHT / 768;
const float default_aspect = 16f / 9f;
float aspectAdjust = MathHelper.Clamp(DrawWidth / DrawHeight, 0.4f, 4) / default_aspect;
return new Vector2(1, default_relative_height * aspectAdjust);
}
protected override Vector2 PlayfieldArea => Vector2.One;
public override ScoreProcessor CreateScoreProcessor() => new TaikoScoreProcessor(this);
public override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo)
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft
};
protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo);
protected override DrawableHitObject<TaikoHitObject> GetVisualRepresentation(TaikoHitObject h)
{

View File

@ -165,7 +165,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
}
[Test]
public void TestDecodeBeatmapColors()
public void TestDecodeBeatmapColours()
{
var decoder = new LegacySkinDecoder();
using (var resStream = Resource.OpenResource("Soleily - Renatus (Gamu) [Insane].osu"))
@ -181,6 +181,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
new Color4(128, 255, 128, 255),
new Color4(255, 187, 255, 255),
new Color4(255, 177, 140, 255),
new Color4(100, 100, 100, 100),
};
Assert.AreEqual(expectedColors.Length, comboColors.Count);
for (int i = 0; i < expectedColors.Length; i++)

View File

@ -101,6 +101,7 @@ Combo3 : 128,255,255
Combo4 : 128,255,128
Combo5 : 255,187,255
Combo6 : 255,177,140
Combo7 : 100,100,100,100
[HitObjects]
192,168,956,6,0,P|184:128|200:80,1,90,4|0,1:2|0:0,0:0:0:0:

View File

@ -7,7 +7,7 @@ using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.States;
using osu.Framework.Input.Events;
using osu.Framework.MathUtils;
using osu.Game.Graphics.Cursor;
using osu.Game.Graphics.Sprites;
@ -184,7 +184,7 @@ namespace osu.Game.Tests.Visual
/// </summary>
/// <param name="cursorContainer">The cursor to check.</param>
private bool checkAtMouse(CursorContainer cursorContainer)
=> Precision.AlmostEquals(InputManager.CurrentState.Mouse.NativeState.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition));
=> Precision.AlmostEquals(InputManager.CurrentState.Mouse.Position, cursorContainer.ToScreenSpace(cursorContainer.ActiveCursor.DrawPosition));
private class CustomCursorBox : Container, IProvideCursor
{
@ -193,7 +193,7 @@ namespace osu.Game.Tests.Visual
public CursorContainer Cursor { get; }
public bool ProvidingUserCursor { get; }
public override bool ReceiveMouseInputAt(Vector2 screenSpacePos) => base.ReceiveMouseInputAt(screenSpacePos) || SmoothTransition && !ProvidingUserCursor;
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || SmoothTransition && !ProvidingUserCursor;
private readonly Box background;
@ -224,16 +224,16 @@ namespace osu.Game.Tests.Visual
};
}
protected override bool OnHover(InputState state)
protected override bool OnHover(HoverEvent e)
{
background.FadeTo(0.4f, 250, Easing.OutQuint);
return false;
}
protected override void OnHoverLost(InputState state)
protected override void OnHoverLost(HoverLostEvent e)
{
background.FadeTo(0.1f, 250);
base.OnHoverLost(state);
base.OnHoverLost(e);
}
}

View File

@ -8,14 +8,14 @@ using System.Linq;
using OpenTK.Input;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers;
using osu.Framework.Input.EventArgs;
using osu.Framework.Logging;
using osu.Game.Screens.Play;
using OpenTK;
namespace osu.Game.Tests.Visual
{
[Description("player pause/fail screens")]
public class TestCaseGameplayMenuOverlay : OsuTestCase
public class TestCaseGameplayMenuOverlay : ManualInputManagerTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[] { typeof(FailOverlay), typeof(PauseContainer) };
@ -73,12 +73,18 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Hover first button", () => failOverlay.Buttons.First().TriggerOnMouseMove(null));
AddStep("Hover first button", () => InputManager.MoveMouseTo(failOverlay.Buttons.First()));
AddStep("Hide overlay", () => failOverlay.Hide());
AddAssert("Overlay state is reset", () => !failOverlay.Buttons.Any(b => b.Selected));
}
private void press(Key key)
{
InputManager.PressKey(key);
InputManager.ReleaseKey(key);
}
/// <summary>
/// Tests that pressing enter after an overlay shows doesn't trigger an event because a selection hasn't occurred.
/// </summary>
@ -86,7 +92,7 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => pauseOverlay.Show());
AddStep("Press enter", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Enter }));
AddStep("Press enter", () => press(Key.Enter));
AddAssert("Overlay still open", () => pauseOverlay.State == Visibility.Visible);
AddStep("Hide overlay", () => pauseOverlay.Hide());
@ -99,7 +105,7 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => pauseOverlay.Show());
AddStep("Up arrow", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Up }));
AddStep("Up arrow", () => press(Key.Up));
AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected);
AddStep("Hide overlay", () => pauseOverlay.Hide());
@ -112,7 +118,7 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => pauseOverlay.Show());
AddStep("Down arrow", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Down arrow", () => press(Key.Down));
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected);
AddStep("Hide overlay", () => pauseOverlay.Hide());
@ -125,11 +131,11 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Up arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Up }));
AddStep("Up arrow", () => press(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
AddStep("Up arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Up }));
AddStep("Up arrow", () => press(Key.Up));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
AddStep("Up arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Up }));
AddStep("Up arrow", () => press(Key.Up));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
AddStep("Hide overlay", () => failOverlay.Hide());
@ -142,11 +148,11 @@ namespace osu.Game.Tests.Visual
{
AddStep("Show overlay", () => failOverlay.Show());
AddStep("Down arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Down arrow", () => press(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
AddStep("Down arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Down arrow", () => press(Key.Down));
AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected);
AddStep("Down arrow", () => failOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Down arrow", () => press(Key.Down));
AddAssert("First button selected", () => failOverlay.Buttons.First().Selected);
AddStep("Hide overlay", () => failOverlay.Hide());
@ -161,8 +167,8 @@ namespace osu.Game.Tests.Visual
var secondButton = pauseOverlay.Buttons.Skip(1).First();
AddStep("Down arrow", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Hover second button", () => secondButton.TriggerOnMouseMove(null));
AddStep("Down arrow", () => press(Key.Down));
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
AddAssert("First button not selected", () => !pauseOverlay.Buttons.First().Selected);
AddAssert("Second button selected", () => secondButton.Selected);
@ -174,12 +180,16 @@ namespace osu.Game.Tests.Visual
/// </summary>
private void testKeySelectionAfterMouseSelection()
{
AddStep("Show overlay", () => pauseOverlay.Show());
AddStep("Show overlay", () =>
{
pauseOverlay.Show();
InputManager.MoveMouseTo(Vector2.Zero);
});
var secondButton = pauseOverlay.Buttons.Skip(1).First();
AddStep("Hover second button", () => secondButton.TriggerOnMouseMove(null));
AddStep("Up arrow", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Up }));
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
AddStep("Up arrow", () => press(Key.Up));
AddAssert("Second button not selected", () => !secondButton.Selected);
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected);
@ -195,9 +205,9 @@ namespace osu.Game.Tests.Visual
var secondButton = pauseOverlay.Buttons.Skip(1).First();
AddStep("Hover second button", () => secondButton.TriggerOnMouseMove(null));
AddStep("Unhover second button", () => secondButton.TriggerOnHoverLost(null));
AddStep("Down arrow", () => pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down }));
AddStep("Hover second button", () => InputManager.MoveMouseTo(secondButton));
AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero));
AddStep("Down arrow", () => press(Key.Down));
AddAssert("First button selected", () => pauseOverlay.Buttons.First().Selected); // Initial state condition
AddStep("Hide overlay", () => pauseOverlay.Hide());
@ -218,7 +228,7 @@ namespace osu.Game.Tests.Visual
var lastAction = pauseOverlay.OnRetry;
pauseOverlay.OnRetry = () => triggered = true;
retryButton.TriggerOnClick();
retryButton.Click();
pauseOverlay.OnRetry = lastAction;
});
@ -235,23 +245,28 @@ namespace osu.Game.Tests.Visual
AddStep("Select second button", () =>
{
pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down });
pauseOverlay.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Down });
press(Key.Down);
press(Key.Down);
});
var retryButton = pauseOverlay.Buttons.Skip(1).First();
bool triggered = false;
Action lastAction = null;
AddStep("Press enter", () =>
{
var lastAction = pauseOverlay.OnRetry;
lastAction = pauseOverlay.OnRetry;
pauseOverlay.OnRetry = () => triggered = true;
retryButton.TriggerOnKeyDown(null, new KeyDownEventArgs { Key = Key.Enter });
pauseOverlay.OnRetry = lastAction;
press(Key.Enter);
});
AddAssert("Action was triggered", () => triggered);
AddAssert("Action was triggered", () =>
{
if (lastAction != null)
{
pauseOverlay.OnRetry = lastAction;
lastAction = null;
}
return triggered;
});
AddAssert("Overlay is closed", () => pauseOverlay.State == Visibility.Hidden);
}
}

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual
new Slider
{
Position = new Vector2(128, 256),
ControlPoints = new List<Vector2>
ControlPoints = new[]
{
Vector2.Zero,
new Vector2(216, 0),

View File

@ -1,32 +1,45 @@
// 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.Linq;
using NUnit.Framework;
using osu.Framework.Graphics;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
using osu.Game.Screens.Play;
using OpenTK.Input;
namespace osu.Game.Tests.Visual
{
[TestFixture]
public class TestCaseKeyCounter : OsuTestCase
public class TestCaseKeyCounter : ManualInputManagerTestCase
{
public override IReadOnlyList<Type> RequiredTypes => new[]
{
typeof(KeyCounterKeyboard),
typeof(KeyCounterMouse),
typeof(KeyCounterCollection)
};
public TestCaseKeyCounter()
{
KeyCounterKeyboard rewindTestKeyCounterKeyboard;
KeyCounterCollection kc = new KeyCounterCollection
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new KeyCounter[]
{
new KeyCounterKeyboard(Key.Z),
rewindTestKeyCounterKeyboard = new KeyCounterKeyboard(Key.X),
new KeyCounterKeyboard(Key.X),
new KeyCounterMouse(MouseButton.Left),
new KeyCounterMouse(MouseButton.Right),
},
};
AddStep("Add random", () =>
{
Key key = (Key)((int)Key.A + RNG.Next(26));
@ -34,7 +47,57 @@ namespace osu.Game.Tests.Visual
});
AddSliderStep("Fade time", 0, 200, 50, v => kc.FadeTime = v);
Key testKey = ((KeyCounterKeyboard)kc.Children.First()).Key;
double time1 = 0;
AddStep($"Press {testKey} key", () =>
{
InputManager.PressKey(testKey);
InputManager.ReleaseKey(testKey);
});
AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 1);
AddStep($"Press {testKey} key", () =>
{
InputManager.PressKey(testKey);
InputManager.ReleaseKey(testKey);
time1 = Clock.CurrentTime;
});
AddAssert($"Check {testKey} counter after keypress", () => rewindTestKeyCounterKeyboard.CountPresses == 2);
IFrameBasedClock oldClock = null;
AddStep($"Rewind {testKey} counter once", () =>
{
oldClock = rewindTestKeyCounterKeyboard.Clock;
rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(time1 - 10));
});
AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 1);
AddStep($"Rewind {testKey} counter to zero", () => rewindTestKeyCounterKeyboard.Clock = new FramedOffsetClock(new FixedClock(0)));
AddAssert($"Check {testKey} counter after rewind", () => rewindTestKeyCounterKeyboard.CountPresses == 0);
AddStep("Restore clock", () => rewindTestKeyCounterKeyboard.Clock = oldClock);
Add(kc);
}
private class FixedClock : IClock
{
private readonly double time;
public FixedClock(double time)
{
this.time = time;
}
public double CurrentTime => time;
public double Rate => 1;
public bool IsRunning => false;
}
}
}

View File

@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual
[BackgroundDependencyLoader]
private void load()
{
AddInternal(trackManager);
Add(trackManager);
}
[Test]
@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual
TestTrackOwner owner = 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("attempt stop", () => trackManager.StopAnyPlaying(this));
AddAssert("not stopped", () => track.IsRunning);
@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual
{
var track = getTrack();
AddInternal(track);
Add(track);
return track;
}

View File

@ -121,14 +121,21 @@ namespace osu.Game.Tests.Visual
Direction = direction;
Padding = new MarginPadding(2);
Content.Masking = true;
AddInternal(new Box
InternalChildren = new Drawable[]
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
Depth = float.MaxValue
});
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.5f,
},
new Container
{
RelativeSizeAxes = Axes.Both,
Masking = true,
Child = HitObjectContainer
}
};
}
}

View File

@ -2,9 +2,11 @@
<Import Project="..\osu.TestProject.props" />
<ItemGroup Label="Package References">
<PackageReference Include="Appveyor.TestLogger" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="NUnit" Version="3.10.1" />
<PackageReference Include="DeepEqual" Version="1.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
<PackageReference Include="NUnit" Version="3.11.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.10.0" />
<PackageReference Update="Microsoft.EntityFrameworkCore.Sqlite" Version="2.1.4" />
</ItemGroup>
<PropertyGroup Label="Project">
<OutputType>WinExe</OutputType>

View File

@ -1,7 +1,7 @@
<!-- Contains required properties for osu!framework projects. -->
<Project>
<PropertyGroup Label="C#">
<LangVersion>7</LangVersion>
<LangVersion>7.2</LangVersion>
</PropertyGroup>
<PropertyGroup>
<ApplicationManifest>..\app.manifest</ApplicationManifest>

View File

@ -48,7 +48,7 @@ namespace osu.Game.Beatmaps
[JsonConverter(typeof(TypedListConverter<HitObject>))]
public List<T> HitObjects = new List<T>();
IEnumerable<HitObject> IBeatmap.HitObjects => HitObjects;
IReadOnlyList<HitObject> IBeatmap.HitObjects => HitObjects;
public virtual IEnumerable<BeatmapStatistic> GetStatistics() => Enumerable.Empty<BeatmapStatistic>();

View File

@ -55,39 +55,40 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapInfo = original.BeatmapInfo;
beatmap.ControlPointInfo = original.ControlPointInfo;
beatmap.HitObjects = original.HitObjects.SelectMany(h => convert(h, original)).ToList();
beatmap.HitObjects = convertHitObjects(original.HitObjects, original);
beatmap.Breaks = original.Breaks;
return beatmap;
}
/// <summary>
/// Converts a hit object.
/// </summary>
/// <param name="original">The hit object to convert.</param>
/// <param name="beatmap">The un-converted Beatmap.</param>
/// <returns>The converted hit object.</returns>
private IEnumerable<T> convert(HitObject original, IBeatmap beatmap)
private List<T> convertHitObjects(IReadOnlyList<HitObject> hitObjects, IBeatmap beatmap)
{
// Check if the hitobject is already the converted type
T tObject = original as T;
if (tObject != null)
{
yield return tObject;
yield break;
}
var result = new List<T>(hitObjects.Count);
var converted = ConvertHitObject(original, beatmap).ToList();
ObjectConverted?.Invoke(original, converted);
// Convert the hit object
foreach (var obj in converted)
foreach (var obj in hitObjects)
{
if (obj == null)
if (obj is T tObj)
{
result.Add(tObj);
continue;
}
yield return obj;
var converted = ConvertHitObject(obj, beatmap);
if (ObjectConverted != null)
{
converted = converted.ToList();
ObjectConverted.Invoke(obj, converted);
}
foreach (var c in converted)
{
if (c != null)
result.Add(c);
}
}
return result;
}
/// <summary>

View File

@ -27,13 +27,15 @@ namespace osu.Game.Beatmaps
[JsonProperty("id")]
public int? OnlineBeatmapID
{
get { return onlineBeatmapID; }
set { onlineBeatmapID = value > 0 ? value : null; }
get => onlineBeatmapID;
set => onlineBeatmapID = value > 0 ? value : null;
}
[JsonIgnore]
public int BeatmapSetInfoID { get; set; }
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
[Required]
public BeatmapSetInfo BeatmapSet { get; set; }
@ -82,7 +84,7 @@ namespace osu.Game.Beatmaps
[JsonIgnore]
public string StoredBookmarks
{
get { return string.Join(",", Bookmarks); }
get => string.Join(",", Bookmarks);
set
{
if (string.IsNullOrEmpty(value))
@ -93,8 +95,7 @@ namespace osu.Game.Beatmaps
Bookmarks = value.Split(',').Select(v =>
{
int val;
bool result = int.TryParse(v, out val);
bool result = int.TryParse(v, out int val);
return new { result, val };
}).Where(p => p.result).Select(p => p.val).ToArray();
}

View File

@ -1,4 +1,4 @@
// 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
using System;
@ -11,6 +11,7 @@ using Microsoft.EntityFrameworkCore;
using osu.Framework.Audio;
using osu.Framework.Audio.Track;
using osu.Framework.Extensions;
using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics.Textures;
using osu.Framework.Logging;
using osu.Framework.Platform;
@ -62,6 +63,8 @@ namespace osu.Game.Beatmaps
public override string[] HandledExtensions => new[] { ".osz" };
protected override string ImportFromStablePath => "Songs";
private readonly RulesetStore rulesets;
private readonly BeatmapStore beatmaps;
@ -72,11 +75,6 @@ namespace osu.Game.Beatmaps
private readonly List<DownloadBeatmapSetRequest> currentDownloads = new List<DownloadBeatmapSetRequest>();
/// <summary>
/// Set a storage with access to an osu-stable install for import purposes.
/// </summary>
public Func<Storage> GetStableStorage { private get; set; }
public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, APIAccess api, AudioManager audioManager, IIpcHost importHost = null)
: base(storage, contextFactory, new BeatmapStore(contextFactory), importHost)
{
@ -103,6 +101,11 @@ namespace osu.Game.Beatmaps
b.BeatmapSet = beatmapSet;
}
validateOnlineIds(beatmapSet.Beatmaps);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
fetchAndPopulateOnlineValues(b, beatmapSet.Beatmaps);
// check if a set already exists with the same online id, delete if it does.
if (beatmapSet.OnlineBeatmapSetID != null)
{
@ -114,11 +117,6 @@ namespace osu.Game.Beatmaps
Logger.Log($"Found existing beatmap set with same OnlineBeatmapSetID ({beatmapSet.OnlineBeatmapSetID}). It has been purged.", LoggingTarget.Database);
}
}
validateOnlineIds(beatmapSet.Beatmaps);
foreach (BeatmapInfo b in beatmapSet.Beatmaps)
fetchAndPopulateOnlineIDs(b, beatmapSet.Beatmaps);
}
private void validateOnlineIds(List<BeatmapInfo> beatmaps)
@ -195,7 +193,7 @@ namespace osu.Game.Beatmaps
downloadNotification.CompletionClickAction = () =>
{
PresentBeatmap?.Invoke(importedBeatmap);
PresentCompletedImport(importedBeatmap.Yield());
return true;
};
downloadNotification.State = ProgressNotificationState.Completed;
@ -231,6 +229,12 @@ namespace osu.Game.Beatmaps
BeatmapDownloadBegan?.Invoke(request);
}
protected override void PresentCompletedImport(IEnumerable<BeatmapSetInfo> imported)
{
base.PresentCompletedImport(imported);
PresentBeatmap?.Invoke(imported.LastOrDefault());
}
/// <summary>
/// Get an existing download request if it exists.
/// </summary>
@ -311,27 +315,6 @@ namespace osu.Game.Beatmaps
/// <returns>Results from the provided query.</returns>
public IQueryable<BeatmapInfo> QueryBeatmaps(Expression<Func<BeatmapInfo, bool>> query) => beatmaps.Beatmaps.AsNoTracking().Where(query);
/// <summary>
/// Denotes whether an osu-stable installation is present to perform automated imports from.
/// </summary>
public bool StableInstallationAvailable => GetStableStorage?.Invoke() != null;
/// <summary>
/// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future.
/// </summary>
public async Task ImportFromStable()
{
var stable = GetStableStorage?.Invoke();
if (stable == null)
{
Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error);
return;
}
await Task.Factory.StartNew(() => Import(stable.GetDirectories("Songs").Select(f => stable.GetFullPath(f)).ToArray()), TaskCreationOptions.LongRunning);
}
/// <summary>
/// Create a SHA-2 hash from the provided archive based on contained beatmap (.osu) file content.
/// </summary>
@ -350,7 +333,11 @@ namespace osu.Game.Beatmaps
{
// let's make sure there are actually .osu files to import.
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;
using (var stream = new StreamReader(reader.GetStream(mapName)))
@ -401,21 +388,22 @@ namespace osu.Game.Beatmaps
}
/// <summary>
/// Query the API to populate mising OnlineBeatmapID / OnlineBeatmapSetID properties.
/// Query the API to populate missing values like OnlineBeatmapID / OnlineBeatmapSetID or (Rank-)Status.
/// </summary>
/// <param name="beatmap">The beatmap to populate.</param>
/// <param name="otherBeatmaps">The other beatmaps contained within this set.</param>
/// <param name="force">Whether to re-query if the provided beatmap already has populated values.</param>
/// <returns>True if population was successful.</returns>
private bool fetchAndPopulateOnlineIDs(BeatmapInfo beatmap, IEnumerable<BeatmapInfo> otherBeatmaps, bool force = false)
private bool fetchAndPopulateOnlineValues(BeatmapInfo beatmap, IEnumerable<BeatmapInfo> otherBeatmaps, bool force = false)
{
if (api?.State != APIState.Online)
return false;
if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null)
if (!force && beatmap.OnlineBeatmapID != null && beatmap.BeatmapSet.OnlineBeatmapSetID != null
&& beatmap.Status != BeatmapSetOnlineStatus.None && beatmap.BeatmapSet.Status != BeatmapSetOnlineStatus.None)
return true;
Logger.Log("Attempting online lookup for IDs...", LoggingTarget.Database);
Logger.Log("Attempting online lookup for the missing values...", LoggingTarget.Database);
try
{
@ -427,6 +415,9 @@ namespace osu.Game.Beatmaps
Logger.Log($"Successfully mapped to {res.OnlineBeatmapSetID} / {res.OnlineBeatmapID}.", LoggingTarget.Database);
beatmap.Status = res.Status;
beatmap.BeatmapSet.Status = res.BeatmapSet.Status;
if (otherBeatmaps.Any(b => b.OnlineBeatmapID == res.OnlineBeatmapID))
{
Logger.Log("Another beatmap in the same set already mapped to this ID. We'll skip adding it this time.", LoggingTarget.Database);
@ -435,6 +426,7 @@ namespace osu.Game.Beatmaps
beatmap.BeatmapSet.OnlineBeatmapSetID = res.OnlineBeatmapSetID;
beatmap.OnlineBeatmapID = res.OnlineBeatmapID;
return true;
}
catch (Exception e)

View File

@ -10,7 +10,6 @@ using osu.Framework.Graphics.Textures;
using osu.Framework.IO.Stores;
using osu.Framework.Logging;
using osu.Game.Beatmaps.Formats;
using osu.Game.Graphics.Textures;
using osu.Game.Skinning;
using osu.Game.Storyboards;
@ -45,6 +44,10 @@ namespace osu.Game.Beatmaps
private string getPathForFile(string filename) => BeatmapSetInfo.Files.First(f => string.Equals(f.Filename, filename, StringComparison.InvariantCultureIgnoreCase)).FileInfo.StoragePath;
private LargeTextureStore textureStore;
protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes.
protected override Texture GetBackground()
{
if (Metadata?.BackgroundFile == null)
@ -52,7 +55,7 @@ namespace osu.Game.Beatmaps
try
{
return new LargeTextureStore(new RawTextureLoaderStore(store)).Get(getPathForFile(Metadata.BackgroundFile));
return (textureStore ?? (textureStore = new LargeTextureStore(new TextureLoaderStore(store)))).Get(getPathForFile(Metadata.BackgroundFile));
}
catch
{
@ -73,6 +76,14 @@ namespace osu.Game.Beatmaps
}
}
public override void TransferTo(WorkingBeatmap other)
{
base.TransferTo(other);
if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo))
owb.textureStore = textureStore;
}
protected override Waveform GetWaveform()
{
try

View File

@ -2,6 +2,7 @@
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Linq;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
namespace osu.Game.Beatmaps
@ -44,6 +45,25 @@ namespace osu.Game.Beatmaps
public virtual void PostProcess()
{
void updateNestedCombo(HitObject obj, int comboIndex, int indexInCurrentCombo)
{
if (obj is IHasComboInformation objectComboInfo)
{
objectComboInfo.ComboIndex = comboIndex;
objectComboInfo.IndexInCurrentCombo = indexInCurrentCombo;
foreach (var nestedObject in obj.NestedHitObjects)
updateNestedCombo(nestedObject, comboIndex, indexInCurrentCombo);
}
}
foreach (var hitObject in Beatmap.HitObjects)
{
if (hitObject is IHasComboInformation objectComboInfo)
{
foreach (var nested in hitObject.NestedHitObjects)
updateNestedCombo(nested, objectComboInfo.ComboIndex, objectComboInfo.IndexInCurrentCombo);
}
}
}
}
}

View File

@ -17,10 +17,12 @@ namespace osu.Game.Beatmaps
public int? OnlineBeatmapSetID
{
get { return onlineBeatmapSetID; }
set { onlineBeatmapSetID = value > 0 ? value : null; }
get => onlineBeatmapSetID;
set => onlineBeatmapSetID = value > 0 ? value : null;
}
public BeatmapSetOnlineStatus Status { get; set; } = BeatmapSetOnlineStatus.None;
public BeatmapMetadata Metadata { get; set; }
public List<BeatmapInfo> Beatmaps { get; set; }

View File

@ -55,14 +55,14 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary>
/// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns>
public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.FirstOrDefault());
public SampleControlPoint SamplePointAt(double time) => binarySearch(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null);
/// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns>
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.FirstOrDefault());
public TimingControlPoint TimingPointAt(double time) => binarySearch(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null);
/// <summary>
/// Finds the maximum BPM represented by any timing control point.
@ -104,17 +104,26 @@ namespace osu.Game.Beatmaps.ControlPoints
if (time < list[0].Time)
return prePoint ?? new T();
int index = list.BinarySearch(new T { Time = time });
if (time >= list[list.Count - 1].Time)
return list[list.Count - 1];
// Check if we've found an exact match (t == time)
if (index >= 0)
return list[index];
int l = 0;
int r = list.Count - 2;
index = ~index;
while (l <= r)
{
int pivot = l + ((r - l) >> 1);
// BinarySearch will return the index of the first element _greater_ than the search
// This is the inactive point - the active point is the one before it (index - 1)
return list[index - 1];
if (list[pivot].Time < time)
l = pivot + 1;
else if (list[pivot].Time > time)
r = pivot - 1;
else
return list[pivot];
}
// l will be the first control point with Time > time, but we want the one before it
return list[l - 1];
}
}
}

View File

@ -16,7 +16,7 @@ namespace osu.Game.Beatmaps.ControlPoints
/// <summary>
/// The beat length at this control point.
/// </summary>
public double BeatLength
public virtual double BeatLength
{
get => beatLength;
set => beatLength = MathHelper.Clamp(value, 6, 60000);

View File

@ -1,7 +1,6 @@
// 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 osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
@ -14,20 +13,35 @@ namespace osu.Game.Beatmaps.Drawables
{
private readonly OsuSpriteText statusText;
private BeatmapSetOnlineStatus status = BeatmapSetOnlineStatus.None;
private BeatmapSetOnlineStatus status;
public BeatmapSetOnlineStatus Status
{
get { return status; }
get => status;
set
{
if (value == status) return;
if (status == value)
return;
status = value;
statusText.Text = Enum.GetName(typeof(BeatmapSetOnlineStatus), Status)?.ToUpperInvariant();
Alpha = value == BeatmapSetOnlineStatus.None ? 0 : 1;
statusText.Text = value.ToString().ToUpperInvariant();
}
}
public BeatmapSetOnlineStatusPill(float textSize, MarginPadding textPadding)
public float TextSize
{
get => statusText.TextSize;
set => statusText.TextSize = value;
}
public MarginPadding TextPadding
{
get => statusText.Padding;
set => statusText.Padding = value;
}
public BeatmapSetOnlineStatusPill()
{
AutoSizeAxes = Axes.Both;
Masking = true;
@ -45,10 +59,10 @@ namespace osu.Game.Beatmaps.Drawables
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = @"Exo2.0-Bold",
TextSize = textSize,
Padding = textPadding,
},
};
Status = BeatmapSetOnlineStatus.None;
}
}
}

View File

@ -55,11 +55,11 @@ namespace osu.Game.Beatmaps.Formats
} while (line != null && line.Length == 0);
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)
throw new IOException(@"Unknown file format");
throw new IOException($@"Unknown file format ({line})");
return (Decoder<T>)decoder.Invoke(line);
}

View File

@ -5,6 +5,7 @@ using System;
using System.Globalization;
using System.IO;
using System.Linq;
using osu.Framework.IO.File;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects.Legacy;
using osu.Game.Beatmaps.ControlPoints;
@ -58,7 +59,7 @@ namespace osu.Game.Beatmaps.Formats
hitObject.ApplyDefaults(this.beatmap.ControlPointInfo, this.beatmap.BeatmapInfo.BaseDifficulty);
}
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(" ") || line.StartsWith("_");
protected override bool ShouldSkipLine(string line) => base.ShouldSkipLine(line) || line.StartsWith(" ", StringComparison.Ordinal) || line.StartsWith("_", StringComparison.Ordinal);
protected override void ParseLine(Beatmap beatmap, Section section, string line)
{
@ -100,7 +101,7 @@ namespace osu.Game.Beatmaps.Formats
switch (pair.Key)
{
case @"AudioFilename":
metadata.AudioFile = pair.Value;
metadata.AudioFile = FileSafety.PathStandardise(pair.Value);
break;
case @"AudioLeadIn":
beatmap.BeatmapInfo.AudioLeadIn = int.Parse(pair.Value);
@ -256,7 +257,7 @@ namespace osu.Game.Beatmaps.Formats
{
case EventType.Background:
string filename = split[2].Trim('"');
beatmap.BeatmapInfo.Metadata.BackgroundFile = filename;
beatmap.BeatmapInfo.Metadata.BackgroundFile = FileSafety.PathStandardise(filename);
break;
case EventType.Break:
var breakEvent = new BreakPeriod
@ -318,12 +319,12 @@ namespace osu.Game.Beatmaps.Formats
if (timingChange)
{
handleTimingControlPoint(new TimingControlPoint
{
Time = time,
BeatLength = beatLength,
TimeSignature = timeSignature
});
var controlPoint = CreateTimingControlPoint();
controlPoint.Time = time;
controlPoint.BeatLength = beatLength;
controlPoint.TimeSignature = timeSignature;
handleTimingControlPoint(controlPoint);
}
handleDifficultyControlPoint(new DifficultyControlPoint
@ -408,11 +409,8 @@ namespace osu.Game.Beatmaps.Formats
parser = new Rulesets.Objects.Legacy.Osu.ConvertHitObjectParser(getOffsetTime(), FormatVersion);
var obj = parser.Parse(line);
if (obj != null)
{
beatmap.HitObjects.Add(obj);
}
}
private int getOffsetTime(int time) => time + (ApplyOffsets ? offset : 0);
@ -421,6 +419,8 @@ namespace osu.Game.Beatmaps.Formats
private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0);
protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint();
[Flags]
internal enum EffectFlags
{

View File

@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps.Formats
protected string StripComments(string line)
{
var index = line.IndexOf("//", StringComparison.Ordinal);
var index = line.AsSpan().IndexOf("//".AsSpan());
if (index > 0)
return line.Substring(0, index);
return line;
@ -85,13 +85,19 @@ namespace osu.Game.Beatmaps.Formats
string[] split = pair.Value.Split(',');
if (split.Length != 3)
throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B): {pair.Value}");
if (split.Length != 3 && split.Length != 4)
throw new InvalidOperationException($@"Color specified in incorrect format (should be R,G,B or R,G,B,A): {pair.Value}");
if (!byte.TryParse(split[0], out var r) || !byte.TryParse(split[1], out var g) || !byte.TryParse(split[2], out var b))
Color4 colour;
try
{
colour = new Color4(byte.Parse(split[0]), byte.Parse(split[1]), byte.Parse(split[2]), split.Length == 4 ? byte.Parse(split[3]) : (byte)255);
}
catch (Exception e)
{
throw new InvalidOperationException(@"Color must be specified with 8-bit integer components");
Color4 colour = new Color4(r, g, b, 255);
}
if (isCombo)
{

View File

@ -0,0 +1,37 @@
// 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.Linq;
using osu.Game.Beatmaps.ControlPoints;
namespace osu.Game.Beatmaps.Formats
{
/// <summary>
/// A <see cref="LegacyBeatmapDecoder"/> built for difficulty calculation of legacy <see cref="Beatmap"/>s
/// <remarks>
/// To use this, the decoder must be registered by the application through <see cref="LegacyDifficultyCalculatorBeatmapDecoder.Register"/>.
/// Doing so will override any existing <see cref="Beatmap"/> decoders.
/// </remarks>
/// </summary>
public class LegacyDifficultyCalculatorBeatmapDecoder : LegacyBeatmapDecoder
{
public LegacyDifficultyCalculatorBeatmapDecoder(int version = LATEST_VERSION)
: base(version)
{
ApplyOffsets = false;
}
public new static void Register()
{
AddDecoder<Beatmap>(@"osu file format v", m => new LegacyDifficultyCalculatorBeatmapDecoder(int.Parse(m.Split('v').Last())));
}
protected override TimingControlPoint CreateTimingControlPoint()
=> new LegacyDifficultyCalculatorControlPoint();
private class LegacyDifficultyCalculatorControlPoint : TimingControlPoint
{
public override double BeatLength { get; set; } = 1000;
}
}
}

Some files were not shown because too many files have changed in this diff Show More