1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-21 16:07:24 +08:00

Merge branch 'master' into fix-test-beatmap-virtual-clock

This commit is contained in:
Dean Herbert 2020-10-29 17:51:15 +09:00
commit 7bef695072
417 changed files with 11173 additions and 3439 deletions

View File

@ -2,7 +2,7 @@
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/riderModule.iml" />
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.osu.Desktop/.idea/riderModule.iml" />
</modules>
</component>
</project>

View File

@ -34,6 +34,8 @@ If you are looking to install or test osu! without setting up a development envi
| [Windows (x64)](https://github.com/ppy/osu/releases/latest/download/install.exe) | [macOS 10.12+](https://github.com/ppy/osu/releases/latest/download/osu.app.zip) | [Linux (x64)](https://github.com/ppy/osu/releases/latest/download/osu.AppImage) | [iOS(iOS 10+)](https://osu.ppy.sh/home/testflight) | [Android (5+)](https://github.com/ppy/osu/releases/latest/download/sh.ppy.osulazer.apk)
| ------------- | ------------- | ------------- | ------------- | ------------- |
- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets.
- When running on Windows 7 or 8.1, **[additional prerequisites](https://docs.microsoft.com/en-us/dotnet/core/install/dependencies?tabs=netcore31&pivots=os-windows)** may be required to correctly run .NET Core applications if your operating system is not up-to-date with the latest service packs.
If your platform is not listed above, there is still a chance you can manually build it by following the instructions below.

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.904.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1004.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.1016.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.1019.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,34 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using Android.Content.PM;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game;
namespace osu.Android
{
public class GameplayScreenRotationLocker : Component
{
private Bindable<bool> localUserPlaying;
[Resolved]
private OsuGameActivity gameActivity { get; set; }
[BackgroundDependencyLoader]
private void load(OsuGame game)
{
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(updateLock, true);
}
private void updateLock(ValueChangedEvent<bool> userPlaying)
{
gameActivity.RunOnUiThread(() =>
{
gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser;
});
}
}
}

View File

@ -12,7 +12,7 @@ namespace osu.Android
[Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)]
public class OsuGameActivity : AndroidGameActivity
{
protected override Framework.Game CreateGame() => new OsuGameAndroid();
protected override Framework.Game CreateGame() => new OsuGameAndroid(this);
protected override void OnCreate(Bundle savedInstanceState)
{

View File

@ -4,6 +4,7 @@
using System;
using Android.App;
using Android.OS;
using osu.Framework.Allocation;
using osu.Game;
using osu.Game.Updater;
@ -11,6 +12,15 @@ namespace osu.Android
{
public class OsuGameAndroid : OsuGame
{
[Cached]
private readonly OsuGameActivity gameActivity;
public OsuGameAndroid(OsuGameActivity activity)
: base(null)
{
gameActivity = activity;
}
public override Version AssemblyVersion
{
get
@ -55,6 +65,12 @@ namespace osu.Android
}
}
protected override void LoadComplete()
{
base.LoadComplete();
LoadComponentAsync(new GameplayScreenRotationLocker(), Add);
}
protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager();
}
}

View File

@ -21,6 +21,7 @@
<AndroidLinkTool>r8</AndroidLinkTool>
</PropertyGroup>
<ItemGroup>
<Compile Include="GameplayScreenRotationLocker.cs" />
<Compile Include="OsuGameActivity.cs" />
<Compile Include="OsuGameAndroid.cs" />
</ItemGroup>
@ -53,4 +54,4 @@
<AndroidResource Include="Resources\drawable\lazer.png" />
</ItemGroup>
<Import Project="$(MSBuildExtensionsPath)\Xamarin\Android\Xamarin.Android.CSharp.targets" />
</Project>
</Project>

View File

@ -125,12 +125,14 @@ namespace osu.Desktop
{
base.SetHost(host);
var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico");
switch (host.Window)
{
// Legacy osuTK DesktopGameWindow
case DesktopGameWindow desktopGameWindow:
desktopGameWindow.CursorState |= CursorState.Hidden;
desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"));
desktopGameWindow.SetIconFromStream(iconStream);
desktopGameWindow.Title = Name;
desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames);
break;
@ -138,6 +140,7 @@ namespace osu.Desktop
// SDL2 DesktopWindow
case DesktopWindow desktopWindow:
desktopWindow.CursorState.Value |= CursorState.Hidden;
desktopWindow.SetIconFromStream(iconStream);
desktopWindow.Title = Name;
desktopWindow.DragDrop += f => fileDrop(new[] { f });
break;

View File

@ -29,6 +29,11 @@ namespace osu.Desktop.Updater
private static readonly Logger logger = Logger.GetLogger("updater");
/// <summary>
/// Whether an update has been downloaded but not yet applied.
/// </summary>
private bool updatePending;
[BackgroundDependencyLoader]
private void load(NotificationOverlay notification)
{
@ -37,9 +42,9 @@ namespace osu.Desktop.Updater
Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger));
}
protected override async Task PerformUpdateCheck() => await checkForUpdateAsync();
protected override async Task<bool> PerformUpdateCheck() => await checkForUpdateAsync();
private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
private async Task<bool> checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null)
{
// should we schedule a retry on completion of this check?
bool scheduleRecheck = true;
@ -49,9 +54,19 @@ namespace osu.Desktop.Updater
updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true);
var info = await updateManager.CheckForUpdate(!useDeltaPatching);
if (info.ReleasesToApply.Count == 0)
{
if (updatePending)
{
// the user may have dismissed the completion notice, so show it again.
notificationOverlay.Post(new UpdateCompleteNotification(this));
return true;
}
// no updates available. bail and retry later.
return;
return false;
}
if (notification == null)
{
@ -72,6 +87,7 @@ namespace osu.Desktop.Updater
await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f);
notification.State = ProgressNotificationState.Completed;
updatePending = true;
}
catch (Exception e)
{
@ -103,6 +119,8 @@ namespace osu.Desktop.Updater
Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30);
}
}
return true;
}
protected override void Dispose(bool isDisposing)
@ -111,10 +129,27 @@ namespace osu.Desktop.Updater
updateManager?.Dispose();
}
private class UpdateCompleteNotification : ProgressCompletionNotification
{
[Resolved]
private OsuGame game { get; set; }
public UpdateCompleteNotification(SquirrelUpdateManager updateManager)
{
Text = @"Update ready to install. Click to restart!";
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
};
}
}
private class UpdateProgressNotification : ProgressNotification
{
private readonly SquirrelUpdateManager updateManager;
private OsuGame game;
public UpdateProgressNotification(SquirrelUpdateManager updateManager)
{
@ -123,23 +158,12 @@ namespace osu.Desktop.Updater
protected override Notification CreateCompletionNotification()
{
return new ProgressCompletionNotification
{
Text = @"Update ready to install. Click to restart!",
Activated = () =>
{
updateManager.PrepareUpdateAsync()
.ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit()));
return true;
}
};
return new UpdateCompleteNotification(updateManager);
}
[BackgroundDependencyLoader]
private void load(OsuColour colours, OsuGame game)
private void load(OsuColour colours)
{
this.game = game;
IconContent.AddRange(new Drawable[]
{
new Box

View File

@ -5,24 +5,24 @@ using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> allowScreenSuspension;
private Bindable<bool> disableWinKey;
private Bindable<bool> localUserPlaying;
private GameHost host;
[Resolved]
private GameHost host { get; set; }
[BackgroundDependencyLoader]
private void load(GameHost host, OsuConfigManager config)
private void load(OsuGame game, OsuConfigManager config)
{
this.host = host;
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
localUserPlaying = game.LocalUserPlaying.GetBoundCopy();
localUserPlaying.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
@ -30,7 +30,7 @@ namespace osu.Desktop.Windows
private void updateBlocking()
{
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
bool shouldDisable = disableWinKey.Value && localUserPlaying.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);

View File

@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests
Origin = Anchor.Centre,
Scale = new Vector2(4f),
}, skin);
});
AddStep("get trails container", () =>
{
trails = catcherArea.OfType<CatcherTrailDisplay>().Single();
catcherArea.MovableCatcher.SetHyperDashState(2);
});

View File

@ -141,11 +141,40 @@ namespace osu.Game.Rulesets.Catch
public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch };
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.LargeTickHit,
HitResult.SmallTickHit,
HitResult.LargeBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "large droplet";
case HitResult.SmallTickHit:
return "small droplet";
case HitResult.LargeBonus:
return "banana";
}
return base.GetDisplayNameForHitResult(result);
}
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap);
public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score);
public int LegacyID => 2;

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@ -25,8 +24,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty
private int tinyTicksMissed;
private int misses;
public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
: base(ruleset, beatmap, score)
public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects
Volume = s.Volume
}).ToList();
int nodeIndex = 0;
SliderEventDescriptor? lastEvent = null;
foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken))
@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects
case SliderEventType.Repeat:
AddNested(new Fruit
{
Samples = Samples,
Samples = this.GetNodeSamples(nodeIndex++),
StartTime = e.Time,
X = X + Path.PositionAt(e.PathProgress).X,
});
@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects
public double Duration
{
get => this.SpanCount() * Path.Distance / Velocity;
set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed.
}
public double EndTime => StartTime + Duration;

View File

@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning
{
public class CatchLegacySkinTransformer : LegacySkinTransformer
{
/// <summary>
/// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
/// </summary>
private bool providesComboCounter => this.HasFont(GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score");
public CatchLegacySkinTransformer(ISkinSource source)
: base(source)
{
@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning
public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (component is HUDSkinComponent hudComponent)
{
switch (hudComponent.Component)
{
case HUDSkinComponents.ComboCounter:
// catch may provide its own combo counter; hide the default.
return providesComboCounter ? Drawable.Empty() : null;
}
}
if (!(component is CatchSkinComponent catchSkinComponent))
return null;
@ -55,11 +70,9 @@ namespace osu.Game.Rulesets.Catch.Skinning
this.GetAnimation("fruit-ryuuta", true, true, true);
case CatchSkinComponents.CatchComboCounter:
var comboFont = GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default.
if (this.HasFont(comboFont))
return new LegacyComboCounter(Source);
if (providesComboCounter)
return new LegacyCatchComboCounter(Source);
break;
}

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning
/// <summary>
/// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter.
/// </summary>
public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter
public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter
{
private readonly LegacyRollingCounter counter;
private readonly LegacyRollingCounter explosion;
public LegacyComboCounter(ISkin skin)
public LegacyCatchComboCounter(ISkin skin)
{
var fontName = skin.GetConfig<LegacySetting, string>(LegacySetting.ComboPrefix)?.Value ?? "score";
var fontOverlap = skin.GetConfig<LegacySetting, float>(LegacySetting.ComboOverlap)?.Value ?? -2f;

View File

@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI
}
};
trailsTarget.Add(trails = new CatcherTrailDisplay(this));
trails = new CatcherTrailDisplay(this);
updateCatcher();
}
protected override void LoadComplete()
{
base.LoadComplete();
// don't add in above load as we may potentially modify a parent in an unsafe manner.
trailsTarget.Add(trails);
}
/// <summary>
/// Creates proxied content to be displayed beneath hitobjects.
/// </summary>

View File

@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests
RandomZ = snapshot.RandomZ;
}
public override void PostProcess()
{
base.PostProcess();
Objects.Sort();
}
public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ;
public override bool Equals(ConvertMapping<ConvertValue> other) => base.Equals(other) && Equals(other as ManiaConvertMapping);
}
public struct ConvertValue : IEquatable<ConvertValue>
public struct ConvertValue : IEquatable<ConvertValue>, IComparable<ConvertValue>
{
/// <summary>
/// A sane value to account for osu!stable using ints everwhere.
@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests
=> Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience)
&& Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience)
&& Column == other.Column;
public int CompareTo(ConvertValue other)
{
var result = StartTime.CompareTo(other.StartTime);
if (result != 0)
return result;
return Column.CompareTo(other.Column);
}
}
}

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests
{
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3683365342338796d, "diffcalc-test")]
[TestCase(2.3449735700206298d, "diffcalc-test")]
public void Test(double expected, string name)
=> base.Test(expected, name);

View File

@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
/// </summary>
public int TotalColumns => Stages.Sum(g => g.Columns);
/// <summary>
/// The total number of columns that were present in this <see cref="ManiaBeatmap"/> before any user adjustments.
/// </summary>
public readonly int OriginalTotalColumns;
/// <summary>
/// Creates a new <see cref="ManiaBeatmap"/>.
/// </summary>
/// <param name="defaultStage">The initial stages.</param>
public ManiaBeatmap(StageDefinition defaultStage)
/// <param name="originalTotalColumns">The total number of columns present before any user adjustments. Defaults to the total columns in <paramref name="defaultStage"/>.</param>
public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null)
{
Stages.Add(defaultStage);
OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns;
}
public override IEnumerable<BeatmapStatistic> GetStatistics()

View File

@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
public bool Dual;
public readonly bool IsForCurrentRuleset;
private readonly int originalTargetColumns;
// Internal for testing purposes
internal FastRandom Random { get; private set; }
@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
else
TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7));
}
originalTargetColumns = TargetColumns;
}
public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition);
@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
protected override Beatmap<ManiaHitObject> CreateBeatmap()
{
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns });
beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns);
if (Dual)
beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns });
@ -116,7 +120,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
prevNoteTimes.RemoveAt(0);
prevNoteTimes.Add(newNoteTime);
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
if (prevNoteTimes.Count >= 2)
density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count;
}
private double lastTime;
@ -180,7 +185,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps
case IHasDuration endTimeData:
{
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap);
conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap);
recordNote(endTimeData.EndTime, new Vector2(256, 192));
computeDensity(endTimeData.EndTime);

View File

@ -3,8 +3,8 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Utils;
using osu.Game.Audio;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Mania.MathUtils;
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Formats;
namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
private const float osu_base_scoring_distance = 100;
public readonly double EndTime;
public readonly double SegmentDuration;
public readonly int StartTime;
public readonly int EndTime;
public readonly int SegmentDuration;
public readonly int SpanCount;
private PatternType convertType;
@ -41,20 +43,26 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
var distanceData = hitObject as IHasDistance;
var repeatsData = hitObject as IHasRepeats;
SpanCount = repeatsData?.SpanCount() ?? 1;
Debug.Assert(distanceData != null);
TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime);
DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime);
// The true distance, accounting for any repeats
double distance = (distanceData?.Distance ?? 0) * SpanCount;
// The velocity of the osu! hit object - calculated as the velocity of a slider
double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength;
// The duration of the osu! hit object
double osuDuration = distance / osuVelocity;
double beatLength;
#pragma warning disable 618
if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint)
#pragma warning restore 618
beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier;
else
beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier;
EndTime = hitObject.StartTime + osuDuration;
SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount;
SpanCount = repeatsData?.SpanCount() ?? 1;
StartTime = (int)Math.Round(hitObject.StartTime);
// This matches stable's calculation.
EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier);
SegmentDuration = (EndTime - StartTime) / SpanCount;
}
public override IEnumerable<Pattern> Generate()
@ -76,7 +84,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
foreach (var obj in originalPattern.HitObjects)
{
if (!Precision.AlmostEquals(EndTime, obj.GetEndTime()))
if (EndTime != (int)Math.Round(obj.GetEndTime()))
intermediatePattern.Add(obj);
else
endTimePattern.Add(obj);
@ -91,35 +99,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
if (TotalColumns == 1)
{
var pattern = new Pattern();
addToPattern(pattern, 0, HitObject.StartTime, EndTime);
addToPattern(pattern, 0, StartTime, EndTime);
return pattern;
}
if (SpanCount > 1)
{
if (SegmentDuration <= 90)
return generateRandomHoldNotes(HitObject.StartTime, 1);
return generateRandomHoldNotes(StartTime, 1);
if (SegmentDuration <= 120)
{
convertType |= PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SpanCount + 1);
return generateRandomNotes(StartTime, SpanCount + 1);
}
if (SegmentDuration <= 160)
return generateStair(HitObject.StartTime);
return generateStair(StartTime);
if (SegmentDuration <= 200 && ConversionDifficulty > 3)
return generateRandomMultipleNotes(HitObject.StartTime);
return generateRandomMultipleNotes(StartTime);
double duration = EndTime - HitObject.StartTime;
double duration = EndTime - StartTime;
if (duration >= 4000)
return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0);
return generateNRandomNotes(StartTime, 0.23, 0, 0);
if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart)
return generateTiledHoldNotes(HitObject.StartTime);
return generateTiledHoldNotes(StartTime);
return generateHoldAndNormalNotes(HitObject.StartTime);
return generateHoldAndNormalNotes(StartTime);
}
if (SegmentDuration <= 110)
@ -128,37 +136,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
convertType |= PatternType.ForceNotStack;
else
convertType &= ~PatternType.ForceNotStack;
return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2);
return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2);
}
if (ConversionDifficulty > 6.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(StartTime, 0.78, 0.3, 0);
return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03);
return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03);
}
if (ConversionDifficulty > 4)
{
if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(StartTime, 0.43, 0.08, 0);
return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0);
return generateNRandomNotes(StartTime, 0.56, 0.18, 0);
}
if (ConversionDifficulty > 2.5)
{
if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0);
return generateNRandomNotes(StartTime, 0.3, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0);
return generateNRandomNotes(StartTime, 0.37, 0.08, 0);
}
if (convertType.HasFlag(PatternType.LowProbability))
return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0);
return generateNRandomNotes(StartTime, 0.17, 0, 0);
return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0);
return generateNRandomNotes(StartTime, 0.27, 0, 0);
}
/// <summary>
@ -167,7 +175,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="startTime">Start time of each hold note.</param>
/// <param name="noteCount">Number of hold notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomHoldNotes(double startTime, int noteCount)
private Pattern generateRandomHoldNotes(int startTime, int noteCount)
{
// - - - -
// ■ - ■ ■
@ -202,7 +210,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="startTime">The start time.</param>
/// <param name="noteCount">The number of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomNotes(double startTime, int noteCount)
private Pattern generateRandomNotes(int startTime, int noteCount)
{
// - - - -
// x - - -
@ -234,7 +242,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateStair(double startTime)
private Pattern generateStair(int startTime)
{
// - - - -
// x - - -
@ -286,7 +294,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateRandomMultipleNotes(double startTime)
private Pattern generateRandomMultipleNotes(int startTime)
{
// - - - -
// x - - -
@ -329,7 +337,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="p3">The probability required for 3 hold notes to be generated.</param>
/// <param name="p4">The probability required for 4 hold notes to be generated.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4)
private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4)
{
// - - - -
// ■ - ■ ■
@ -366,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH;
bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample);
canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample);
if (canGenerateTwoNotes)
p2 = 1;
@ -379,7 +387,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The first hold note start time.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateTiledHoldNotes(double startTime)
private Pattern generateTiledHoldNotes(int startTime)
{
// - - - -
// ■ ■ ■ ■
@ -394,6 +402,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
int columnRepeat = Math.Min(SpanCount, TotalColumns);
// Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable).
int endTime = startTime + SegmentDuration * SpanCount;
int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true);
if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns)
nextColumn = FindAvailableColumn(nextColumn, PreviousPattern);
@ -401,7 +412,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i < columnRepeat; i++)
{
nextColumn = FindAvailableColumn(nextColumn, pattern);
addToPattern(pattern, nextColumn, startTime, EndTime);
addToPattern(pattern, nextColumn, startTime, endTime);
startTime += SegmentDuration;
}
@ -413,7 +424,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="startTime">The start time of notes.</param>
/// <returns>The <see cref="Pattern"/> containing the hit objects.</returns>
private Pattern generateHoldAndNormalNotes(double startTime)
private Pattern generateHoldAndNormalNotes(int startTime)
{
// - - - -
// ■ x x -
@ -448,7 +459,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
for (int i = 0; i <= SpanCount; i++)
{
if (!(ignoreHead && startTime == HitObject.StartTime))
if (!(ignoreHead && startTime == StartTime))
{
for (int j = 0; j < noteCount; j++)
{
@ -471,19 +482,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// </summary>
/// <param name="time">The time to retrieve the sample info list from.</param>
/// <returns></returns>
private IList<HitSampleInfo> sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
private IList<HitSampleInfo> sampleInfoListAt(int time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples;
/// <summary>
/// Retrieves the list of node samples that occur at time greater than or equal to <paramref name="time"/>.
/// </summary>
/// <param name="time">The time to retrieve node samples at.</param>
private List<IList<HitSampleInfo>> nodeSamplesAt(double time)
private List<IList<HitSampleInfo>> nodeSamplesAt(int time)
{
if (!(HitObject is IHasPathWithRepeats curveData))
return null;
// mathematically speaking this should be a whole number always, but floating-point arithmetic is not so kind
var index = (int)Math.Round(SegmentDuration == 0 ? 0 : (time - HitObject.StartTime) / SegmentDuration, MidpointRounding.AwayFromZero);
var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration;
// avoid slicing the list & creating copies, if at all possible.
return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList();
@ -496,7 +506,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
/// <param name="column">The column to add the note to.</param>
/// <param name="startTime">The start time of the note.</param>
/// <param name="endTime">The end time of the note (set to <paramref name="startTime"/> for a non-hold note).</param>
private void addToPattern(Pattern pattern, int column, double startTime, double endTime)
private void addToPattern(Pattern pattern, int column, int startTime, int endTime)
{
ManiaHitObject newObject;

View File

@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
{
internal class EndTimeObjectPatternGenerator : PatternGenerator
{
private readonly double endTime;
private readonly int endTime;
private readonly PatternType convertType;
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, new Pattern(), originalBeatmap)
public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap)
: base(random, hitObject, beatmap, previousPattern, originalBeatmap)
{
endTime = (HitObject as IHasDuration)?.EndTime ?? 0;
endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0);
convertType = PreviousPattern.ColumnWithObjects == TotalColumns
? PatternType.None
: PatternType.ForceNotStack;
}
public override IEnumerable<Pattern> Generate()
@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
break;
case 8:
addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold);
addToPattern(pattern, getRandomColumn(), generateHold);
break;
default:
if (TotalColumns > 0)
addToPattern(pattern, GetRandomColumn(), generateHold);
addToPattern(pattern, getRandomColumn(0), generateHold);
break;
}
return pattern;
}
private int getRandomColumn(int? lowerBound = null)
{
if ((convertType & PatternType.ForceNotStack) > 0)
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern);
return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound);
}
/// <summary>
/// Constructs and adds a note to a pattern.
/// </summary>

View File

@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 4:
centreProbability = 0;
p2 = Math.Min(p2 * 2, 0.2);
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
// so it needs to be converted to from a probability and then back after the multiplication.
p2 = 1 - Math.Max((1 - p2) * 2, 0.8);
p3 = 0;
break;
@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy
case 6:
centreProbability = 0;
p2 = Math.Min(p2 * 2, 0.5);
p3 = Math.Min(p3 * 2, 0.15);
// Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x).
// But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer),
// so it needs to be converted to from a probability and then back after the multiplication.
p2 = 1 - Math.Max((1 - p2) * 2, 0.5);
p3 = 1 - Math.Max((1 - p3) * 2, 0.85);
break;
}
// The stable values were allowed to exceed 1, which indicate <0% probability.
// These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception.
p2 = Math.Clamp(p2, 0, 1);
p3 = Math.Clamp(p3, 0, 1);
double centreVal = Random.NextDouble();
int noteCount = GetRandomNoteCount(p2, p3);

View File

@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
public class ManiaDifficultyAttributes : DifficultyAttributes
{
public double GreatHitWindow;
public double ScoreMultiplier;
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Game.Beatmaps;
@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Beatmaps;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Mania.Difficulty.Skills;
using osu.Game.Rulesets.Mania.MathUtils;
using osu.Game.Rulesets.Mania.Mods;
using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Mania.Difficulty
@ -23,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private const double star_scaling_factor = 0.018;
private readonly bool isForCurrentRuleset;
private readonly double originalOverallDifficulty;
public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap)
: base(ruleset, beatmap)
{
isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo);
originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty;
}
protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate)
@ -40,64 +45,33 @@ namespace osu.Game.Rulesets.Mania.Difficulty
return new ManiaDifficultyAttributes
{
StarRating = difficultyValue(skills) * star_scaling_factor,
StarRating = skills[0].DifficultyValue() * star_scaling_factor,
Mods = mods,
// Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future
GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate,
GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate),
ScoreMultiplier = getScoreMultiplier(beatmap, mods),
MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1),
Skills = skills
};
}
private double difficultyValue(Skill[] skills)
{
// Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section
var overall = skills.OfType<Overall>().Single();
var aggregatePeaks = new List<double>(Enumerable.Repeat(0.0, overall.StrainPeaks.Count));
foreach (var individual in skills.OfType<Individual>())
{
for (int i = 0; i < individual.StrainPeaks.Count; i++)
{
double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i];
if (aggregate > aggregatePeaks[i])
aggregatePeaks[i] = aggregate;
}
}
aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain.
double difficulty = 0;
double weight = 1;
// Difficulty is the weighted sum of the highest strains from every section.
foreach (double strain in aggregatePeaks)
{
difficulty += strain * weight;
weight *= 0.9;
}
return difficulty;
}
protected override IEnumerable<DifficultyHitObject> CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate)
{
for (int i = 1; i < beatmap.HitObjects.Count; i++)
yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate);
var sortedObjects = beatmap.HitObjects.ToArray();
LegacySortHelper<HitObject>.Sort(sortedObjects, Comparer<HitObject>.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime)));
for (int i = 1; i < sortedObjects.Length; i++)
yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate);
}
protected override Skill[] CreateSkills(IBeatmap beatmap)
// Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required.
protected override IEnumerable<DifficultyHitObject> SortObjects(IEnumerable<DifficultyHitObject> input) => input;
protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[]
{
int columnCount = ((ManiaBeatmap)beatmap).TotalColumns;
var skills = new List<Skill> { new Overall(columnCount) };
for (int i = 0; i < columnCount; i++)
skills.Add(new Individual(i, columnCount));
return skills.ToArray();
}
new Strain(((ManiaBeatmap)beatmap).TotalColumns)
};
protected override Mod[] DifficultyAdjustmentMods
{
@ -122,12 +96,73 @@ namespace osu.Game.Rulesets.Mania.Difficulty
new ManiaModKey3(),
new ManiaModKey4(),
new ManiaModKey5(),
new MultiMod(new ManiaModKey5(), new ManiaModDualStages()),
new ManiaModKey6(),
new MultiMod(new ManiaModKey6(), new ManiaModDualStages()),
new ManiaModKey7(),
new MultiMod(new ManiaModKey7(), new ManiaModDualStages()),
new ManiaModKey8(),
new MultiMod(new ManiaModKey8(), new ManiaModDualStages()),
new ManiaModKey9(),
new MultiMod(new ManiaModKey9(), new ManiaModDualStages()),
}).ToArray();
}
}
private int getHitWindow300(Mod[] mods)
{
if (isForCurrentRuleset)
{
double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty));
return applyModAdjustments(34 + 3 * od, mods);
}
if (Math.Round(originalOverallDifficulty) > 4)
return applyModAdjustments(34, mods);
return applyModAdjustments(47, mods);
static int applyModAdjustments(double value, Mod[] mods)
{
if (mods.Any(m => m is ManiaModHardRock))
value /= 1.4;
else if (mods.Any(m => m is ManiaModEasy))
value *= 1.4;
if (mods.Any(m => m is ManiaModDoubleTime))
value *= 1.5;
else if (mods.Any(m => m is ManiaModHalfTime))
value *= 0.75;
return (int)value;
}
}
private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods)
{
double scoreMultiplier = 1;
foreach (var m in mods)
{
switch (m)
{
case ManiaModNoFail _:
case ManiaModEasy _:
case ManiaModHalfTime _:
scoreMultiplier *= 0.5;
break;
}
}
var maniaBeatmap = (ManiaBeatmap)beatmap;
int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns;
if (diff > 0)
scoreMultiplier *= 0.9;
else if (diff < 0)
scoreMultiplier *= 0.9 + 0.04 * diff;
return scoreMultiplier;
}
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@ -29,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty
private int countMeh;
private int countMiss;
public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
: base(ruleset, beatmap, score)
public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
}

View File

@ -1,47 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Linq;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Individual : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.125;
private readonly double[] holdEndTimes;
private readonly int column;
public Individual(int column, int columnCount)
{
this.column = column;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
try
{
if (maniaCurrent.BaseObject.Column != column)
return 0;
// We give a slight bonus if something is held meanwhile
return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2;
}
finally
{
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
}
}
}
}

View File

@ -1,56 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Overall : Skill
{
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 0.3;
private readonly double[] holdEndTimes;
private readonly int columnCount;
public Overall(int columnCount)
{
this.columnCount = columnCount;
holdEndTimes = new double[columnCount];
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
double holdFactor = 1.0; // Factor in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
for (int i = 0; i < columnCount; i++)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i])
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending.
// Releasing multiple notes at the same time is just as easy as releasing one
if (endTime == holdEndTimes[i])
holdAddition = 0;
// We give a slight bonus if something is held meanwhile
if (holdEndTimes[i] > endTime)
holdFactor = 1.25;
}
holdEndTimes[maniaCurrent.BaseObject.Column] = endTime;
return (1 + holdAddition) * holdFactor;
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Utils;
using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mania.Difficulty.Preprocessing;
using osu.Game.Rulesets.Objects;
namespace osu.Game.Rulesets.Mania.Difficulty.Skills
{
public class Strain : Skill
{
private const double individual_decay_base = 0.125;
private const double overall_decay_base = 0.30;
protected override double SkillMultiplier => 1;
protected override double StrainDecayBase => 1;
private readonly double[] holdEndTimes;
private readonly double[] individualStrains;
private double individualStrain;
private double overallStrain;
public Strain(int totalColumns)
{
holdEndTimes = new double[totalColumns];
individualStrains = new double[totalColumns];
overallStrain = 1;
}
protected override double StrainValueOf(DifficultyHitObject current)
{
var maniaCurrent = (ManiaDifficultyHitObject)current;
var endTime = maniaCurrent.BaseObject.GetEndTime();
var column = maniaCurrent.BaseObject.Column;
double holdFactor = 1.0; // Factor to all additional strains in case something else is held
double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly
// Fill up the holdEndTimes array
for (int i = 0; i < holdEndTimes.Length; ++i)
{
// If there is at least one other overlapping end or note, then we get an addition, buuuuuut...
if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1))
holdAddition = 1.0;
// ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1
if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1))
holdAddition = 0;
// We give a slight bonus to everything if something is held meanwhile
if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1))
holdFactor = 1.25;
// Decay individual strains
individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base);
}
holdEndTimes[column] = endTime;
// Increase individual strain in own column
individualStrains[column] += 2.0 * holdFactor;
individualStrain = individualStrains[column];
overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor;
return individualStrain + overallStrain - CurrentStrain;
}
protected override double GetPeakStrain(double offset)
=> applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base)
+ applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base);
private double applyDecay(double value, double deltaTime, double decayBase)
=> value * Math.Pow(decayBase, deltaTime / 1000);
}
}

View File

@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit
int minColumn = int.MaxValue;
int maxColumn = int.MinValue;
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
{
if (obj.Column < minColumn)
minColumn = obj.Column;
@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit
columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn);
foreach (var obj in SelectedHitObjects.OfType<ManiaHitObject>())
foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType<ManiaHitObject>())
obj.Column += columnDelta;
}
}

View File

@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania
public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score);
public const string SHORT_NAME = "mania";
@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania
return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast<int>().OrderByDescending(i => i).First(v => variant >= v);
}
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Perfect,
HitResult.Great,
HitResult.Good,
HitResult.Ok,
HitResult.Meh,
HitResult.LargeTickHit,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "hold tick";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[]
{
new StatisticRow

View File

@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania
new SettingsEnumDropdown<ManiaScrollingDirection>
{
LabelText = "Scrolling direction",
Bindable = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
Current = config.GetBindable<ManiaScrollingDirection>(ManiaRulesetSetting.ScrollDirection)
},
new SettingsSlider<double, TimeSlider>
{
LabelText = "Scroll speed",
Bindable = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
Current = config.GetBindable<double>(ManiaRulesetSetting.ScrollTime),
KeyboardStep = 5
},
};

View File

@ -0,0 +1,165 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
namespace osu.Game.Rulesets.Mania.MathUtils
{
/// <summary>
/// Provides access to .NET4.0 unstable sorting methods.
/// </summary>
/// <remarks>
/// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs
/// Copyright (c) Microsoft Corporation. All rights reserved.
/// </remarks>
internal static class LegacySortHelper<T>
{
private const int quick_sort_depth_threshold = 32;
public static void Sort(T[] keys, IComparer<T> comparer)
{
if (keys == null)
throw new ArgumentNullException(nameof(keys));
if (keys.Length == 0)
return;
comparer ??= Comparer<T>.Default;
depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold);
}
private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer<T> comparer, int depthLimit)
{
do
{
if (depthLimit == 0)
{
heapsort(keys, left, right, comparer);
return;
}
int i = left;
int j = right;
// pre-sort the low, middle (pivot), and high values in place.
// this improves performance in the face of already sorted data, or
// data that is made up of multiple sorted runs appended together.
int middle = i + ((j - i) >> 1);
swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point
swapIfGreater(keys, comparer, i, j); // swap the low with the high
swapIfGreater(keys, comparer, middle, j); // swap the middle with the high
T x = keys[middle];
do
{
while (comparer.Compare(keys[i], x) < 0) i++;
while (comparer.Compare(x, keys[j]) < 0) j--;
Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?");
if (i > j) break;
if (i < j)
{
T key = keys[i];
keys[i] = keys[j];
keys[j] = key;
}
i++;
j--;
} while (i <= j);
// The next iteration of the while loop is to "recursively" sort the larger half of the array and the
// following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so
// both sorts see the new value.
depthLimit--;
if (j - left <= right - i)
{
if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit);
left = i;
}
else
{
if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit);
right = j;
}
} while (left < right);
}
private static void heapsort(T[] keys, int lo, int hi, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(hi > lo);
Contract.Requires(hi < keys.Length);
int n = hi - lo + 1;
for (int i = n / 2; i >= 1; i = i - 1)
{
downHeap(keys, i, n, lo, comparer);
}
for (int i = n; i > 1; i = i - 1)
{
swap(keys, lo, lo + i - 1);
downHeap(keys, 1, i - 1, lo, comparer);
}
}
private static void downHeap(T[] keys, int i, int n, int lo, IComparer<T> comparer)
{
Contract.Requires(keys != null);
Contract.Requires(comparer != null);
Contract.Requires(lo >= 0);
Contract.Requires(lo < keys.Length);
T d = keys[lo + i - 1];
while (i <= n / 2)
{
var child = 2 * i;
if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0)
{
child++;
}
if (!(comparer.Compare(d, keys[lo + child - 1]) < 0))
break;
keys[lo + i - 1] = keys[lo + child - 1];
i = child;
}
keys[lo + i - 1] = d;
}
private static void swap(T[] a, int i, int j)
{
if (i != j)
{
T t = a[i];
a[i] = a[j];
a[j] = t;
}
}
private static void swapIfGreater(T[] keys, IComparer<T> comparer, int a, int b)
{
if (a != b)
{
if (comparer.Compare(keys[a], keys[b]) > 0)
{
T key = keys[a];
keys[a] = keys[b];
keys[b] = key;
}
}
}
}
}

View File

@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
typeof(ManiaModKey7),
typeof(ManiaModKey8),
typeof(ManiaModKey9),
typeof(ManiaModKey10),
}.Except(new[] { GetType() }).ToArray();
}
}

View File

@ -10,7 +10,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
"Samples": ["drum-hitnormal"]
"Samples": ["-hitnormal"]
}, {
"StartTime": 1875.0,
"EndTime": 2750.0,
@ -19,7 +19,7 @@
["soft-hitnormal"],
["drum-hitnormal"]
],
"Samples": ["drum-hitnormal"]
"Samples": ["-hitnormal"]
}]
}, {
"StartTime": 3750.0,

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.UI;
using osu.Game.Tests.Visual;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests
{
public class TestPlayfieldBorder : OsuTestScene
{
public TestPlayfieldBorder()
{
Bindable<PlayfieldBorderStyle> playfieldBorderStyle = new Bindable<PlayfieldBorderStyle>();
AddStep("add drawables", () =>
{
Child = new Container
{
Size = new Vector2(400, 300),
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new Drawable[]
{
new PlayfieldBorder
{
PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle }
}
}
};
});
AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None);
AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners);
AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full);
}
}
}

View File

@ -3,6 +3,7 @@
using osu.Game.Configuration;
using osu.Game.Rulesets.Configuration;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.Configuration
{
@ -19,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
Set(OsuRulesetSetting.SnakingInSliders, true);
Set(OsuRulesetSetting.SnakingOutSliders, true);
Set(OsuRulesetSetting.ShowCursorTrail, true);
Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None);
}
}
@ -26,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Configuration
{
SnakingInSliders,
SnakingOutSliders,
ShowCursorTrail
ShowCursorTrail,
PlayfieldBorderStyle,
}
}

View File

@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
public double SpeedStrain;
public double ApproachRate;
public double OverallDifficulty;
public int HitCircleCount;
}
}

View File

@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
// 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);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
return new OsuDifficultyAttributes
{
StarRating = starRating,
@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5,
OverallDifficulty = (80 - hitWindowGreat) / 6,
MaxCombo = maxCombo,
HitCircleCount = hitCirclesCount,
Skills = skills
};
}

View File

@ -5,11 +5,9 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring;
@ -19,9 +17,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes;
private readonly int countHitCircles;
private readonly int beatmapMaxCombo;
private Mod[] mods;
private double accuracy;
@ -31,14 +26,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty
private int countMeh;
private int countMiss;
public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
: base(ruleset, beatmap, score)
public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle);
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);
}
public override double Calculate(Dictionary<string, double> categoryRatings = null)
@ -81,7 +71,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
categoryRatings.Add("Accuracy", accuracyValue);
categoryRatings.Add("OD", Attributes.OverallDifficulty);
categoryRatings.Add("AR", Attributes.ApproachRate);
categoryRatings.Add("Max Combo", beatmapMaxCombo);
categoryRatings.Add("Max Combo", Attributes.MaxCombo);
}
return totalValue;
@ -106,8 +96,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
aimValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
if (Attributes.MaxCombo > 0)
aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
@ -154,8 +144,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty
speedValue *= Math.Pow(0.97, countMiss);
// Combo scaling
if (beatmapMaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0);
if (Attributes.MaxCombo > 0)
speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0);
double approachRateFactor = 1.0;
if (Attributes.ApproachRate > 10.33)
@ -178,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
{
// This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window
double betterAccuracyPercentage;
int amountHitObjectsWithAccuracy = countHitCircles;
int amountHitObjectsWithAccuracy = Attributes.HitCircleCount;
if (amountHitObjectsWithAccuracy > 0)
betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6);

View File

@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles
InternalChild = circlePiece = new HitCirclePiece();
}
protected override void LoadComplete()
{
base.LoadComplete();
BeginPlacement();
}
protected override void Update()
{
base.Update();

View File

@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
OriginPosition = body.PathOffset;
}
public void RecyclePath() => body.RecyclePath();
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos);
}
}

View File

@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
{
public class SliderSelectionBlueprint : OsuSelectionBlueprint<Slider>
{
protected readonly SliderBodyPiece BodyPiece;
protected readonly SliderCircleSelectionBlueprint HeadBlueprint;
protected readonly SliderCircleSelectionBlueprint TailBlueprint;
protected readonly PathControlPointVisualiser ControlPointVisualiser;
protected SliderBodyPiece BodyPiece { get; private set; }
protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; }
protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; }
protected PathControlPointVisualiser ControlPointVisualiser { get; private set; }
private readonly DrawableSlider slider;
[Resolved(CanBeNull = true)]
private HitObjectComposer composer { get; set; }
@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
public SliderSelectionBlueprint(DrawableSlider slider)
: base(slider)
{
var sliderObject = (Slider)slider.HitObject;
this.slider = slider;
}
[BackgroundDependencyLoader]
private void load()
{
InternalChildren = new Drawable[]
{
BodyPiece = new SliderBodyPiece(),
HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start),
TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End),
ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true)
{
RemoveControlPointsRequested = removeControlPoints
}
};
}
@ -66,13 +68,35 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
pathVersion = HitObject.Path.Version.GetBoundCopy();
pathVersion.BindValueChanged(_ => updatePath());
BodyPiece.UpdateFrom(HitObject);
}
protected override void Update()
{
base.Update();
BodyPiece.UpdateFrom(HitObject);
if (IsSelected)
BodyPiece.UpdateFrom(HitObject);
}
protected override void OnSelected()
{
AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true)
{
RemoveControlPointsRequested = removeControlPoints
});
base.OnSelected();
}
protected override void OnDeselected()
{
base.OnDeselected();
// throw away frame buffers on deselection.
ControlPointVisualiser?.Expire();
BodyPiece.RecyclePath();
}
private Vector2 rightClickPosition;
@ -182,7 +206,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
private void updatePath()
{
HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance;
editorBeatmap?.UpdateHitObject(HitObject);
editorBeatmap?.Update(HitObject);
}
public override MenuItem[] ContextMenuItems => new MenuItem[]

View File

@ -56,7 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit
[BackgroundDependencyLoader]
private void load()
{
LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both });
LayerBelowRuleset.AddRange(new Drawable[]
{
new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners }
},
distanceSnapGridContainer = new Container
{
RelativeSizeAxes = Axes.Both
}
});
selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy();
selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid();

View File

@ -1,7 +1,14 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Primitives;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Screens.Edit.Compose.Components;
using osuTK;
@ -10,40 +17,268 @@ namespace osu.Game.Rulesets.Osu.Edit
{
public class OsuSelectionHandler : SelectionHandler
{
public override bool HandleMovement(MoveSelectionEvent moveEvent)
protected override void OnSelectionChanged()
{
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
base.OnSelectionChanged();
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider);
SelectionBox.CanRotate = canOperate;
SelectionBox.CanScaleX = canOperate;
SelectionBox.CanScaleY = canOperate;
SelectionBox.CanReverse = canOperate;
}
protected override void OnOperationEnded()
{
base.OnOperationEnded();
referenceOrigin = null;
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) =>
moveSelection(moveEvent.InstantDelta);
/// <summary>
/// During a transform, the initial origin is stored so it can be used throughout the operation.
/// </summary>
private Vector2? referenceOrigin;
public override bool HandleReverse()
{
var hitObjects = selectedMovableObjects;
double endTime = hitObjects.Max(h => h.GetEndTime());
double startTime = hitObjects.Min(h => h.StartTime);
bool moreThanOneObject = hitObjects.Length > 1;
foreach (var h in hitObjects)
{
if (h is Spinner)
if (moreThanOneObject)
h.StartTime = endTime - (h.GetEndTime() - startTime);
if (h is Slider slider)
{
// Spinners don't support position adjustments
continue;
var points = slider.Path.ControlPoints.ToArray();
Vector2 endPos = points.Last().Position.Value;
slider.Path.ControlPoints.Clear();
slider.Position += endPos;
PathType? lastType = null;
for (var i = 0; i < points.Length; i++)
{
var p = points[i];
p.Position.Value -= endPos;
// propagate types forwards to last null type
if (i == points.Length - 1)
p.Type.Value = lastType;
else if (p.Type.Value != null)
{
var newType = p.Type.Value;
p.Type.Value = lastType;
lastType = newType;
}
slider.Path.ControlPoints.Insert(0, p);
}
}
// Stacking is not considered
minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta));
}
if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight)
return false;
foreach (var h in SelectedHitObjects.OfType<OsuHitObject>())
{
if (h is Spinner)
{
// Spinners don't support position adjustments
continue;
}
h.Position += moveEvent.InstantDelta;
}
return true;
}
public override bool HandleFlip(Direction direction)
{
var hitObjects = selectedMovableObjects;
var selectedObjectsQuad = getSurroundingQuad(hitObjects);
var centre = selectedObjectsQuad.Centre;
foreach (var h in hitObjects)
{
var pos = h.Position;
switch (direction)
{
case Direction.Horizontal:
pos.X = centre.X - (pos.X - centre.X);
break;
case Direction.Vertical:
pos.Y = centre.Y - (pos.Y - centre.Y);
break;
}
h.Position = pos;
if (h is Slider slider)
{
foreach (var point in slider.Path.ControlPoints)
{
point.Position.Value = new Vector2(
(direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X,
(direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y
);
}
}
}
return true;
}
public override bool HandleScale(Vector2 scale, Anchor reference)
{
adjustScaleFromAnchor(ref scale, reference);
var hitObjects = selectedMovableObjects;
// for the time being, allow resizing of slider paths only if the slider is
// the only hit object selected. with a group selection, it's likely the user
// is not looking to change the duration of the slider but expand the whole pattern.
if (hitObjects.Length == 1 && hitObjects.First() is Slider slider)
{
Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value));
Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height);
foreach (var point in slider.Path.ControlPoints)
point.Position.Value *= pathRelativeDeltaScale;
}
else
{
// move the selection before scaling if dragging from top or left anchors.
if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false;
if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false;
Quad quad = getSurroundingQuad(hitObjects);
foreach (var h in hitObjects)
{
h.Position = new Vector2(
quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X),
quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y)
);
}
}
return true;
}
private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference)
{
// cancel out scale in axes we don't care about (based on which drag handle was used).
if ((reference & Anchor.x1) > 0) scale.X = 0;
if ((reference & Anchor.y1) > 0) scale.Y = 0;
// reverse the scale direction if dragging from top or left.
if ((reference & Anchor.x0) > 0) scale.X = -scale.X;
if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y;
}
public override bool HandleRotation(float delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
referenceOrigin ??= quad.Centre;
foreach (var h in hitObjects)
{
h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta);
if (h is IHasPath path)
{
foreach (var point in path.Path.ControlPoints)
point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta);
}
}
// this isn't always the case but let's be lenient for now.
return true;
}
private bool moveSelection(Vector2 delta)
{
var hitObjects = selectedMovableObjects;
Quad quad = getSurroundingQuad(hitObjects);
if (quad.TopLeft.X + delta.X < 0 ||
quad.TopLeft.Y + delta.Y < 0 ||
quad.BottomRight.X + delta.X > DrawWidth ||
quad.BottomRight.Y + delta.Y > DrawHeight)
return false;
foreach (var h in hitObjects)
h.Position += delta;
return true;
}
/// <summary>
/// Returns a gamefield-space quad surrounding the provided hit objects.
/// </summary>
/// <param name="hitObjects">The hit objects to calculate a quad for.</param>
private Quad getSurroundingQuad(OsuHitObject[] hitObjects) =>
getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition }));
/// <summary>
/// Returns a gamefield-space quad surrounding the provided points.
/// </summary>
/// <param name="points">The points to calculate a quad for.</param>
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
{
if (!EditorBeatmap.SelectedHitObjects.Any())
return new Quad();
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue);
// Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted
foreach (var p in points)
{
minPosition = Vector2.ComponentMin(minPosition, p);
maxPosition = Vector2.ComponentMax(maxPosition, p);
}
Vector2 size = maxPosition - minPosition;
return new Quad(minPosition.X, minPosition.Y, size.X, size.Y);
}
/// <summary>
/// All osu! hitobjects which can be moved/rotated/scaled.
/// </summary>
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
.OfType<OsuHitObject>()
.Where(h => !(h is Spinner))
.ToArray();
/// <summary>
/// Rotate a point around an arbitrary origin.
/// </summary>
/// <param name="point">The point.</param>
/// <param name="origin">The centre origin to rotate around.</param>
/// <param name="angle">The angle to rotate (in degrees).</param>
private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle)
{
angle = -angle;
point.X -= origin.X;
point.Y -= origin.Y;
Vector2 ret;
ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle));
ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle));
ret.X += origin.X;
ret.Y += origin.Y;
return ret;
}
}
}

View File

@ -11,7 +11,6 @@ using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private OsuInputManager inputManager;
private GameplayClock gameplayClock;
private IFrameStableClock gameplayClock;
private List<OsuReplayFrame> replayFrames;

View File

@ -39,7 +39,14 @@ namespace osu.Game.Rulesets.Osu.Mods
base.ApplyToDrawableHitObjects(drawables);
}
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
private double lastSliderHeadFadeOutStartTime;
private double lastSliderHeadFadeOutDuration;
protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true);
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false);
private void applyState(DrawableHitObject drawable, bool increaseVisibility)
{
if (!(drawable is DrawableOsuHitObject d))
return;
@ -54,15 +61,52 @@ namespace osu.Game.Rulesets.Osu.Mods
switch (drawable)
{
case DrawableSliderTail sliderTail:
// use stored values from head circle to achieve same fade sequence.
fadeOutDuration = lastSliderHeadFadeOutDuration;
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
sliderTail.FadeOut(fadeOutDuration);
break;
case DrawableSliderRepeat sliderRepeat:
// use stored values from head circle to achieve same fade sequence.
fadeOutDuration = lastSliderHeadFadeOutDuration;
fadeOutStartTime = lastSliderHeadFadeOutStartTime;
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
// only apply to circle piece reverse arrow is not affected by hidden.
sliderRepeat.CirclePiece.FadeOut(fadeOutDuration);
break;
case DrawableHitCircle circle:
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
if (circle is DrawableSliderHead)
{
lastSliderHeadFadeOutDuration = fadeOutDuration;
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
}
Drawable fadeTarget = circle;
if (increaseVisibility)
{
// only fade the circle piece (not the approach circle) for the increased visibility object.
fadeTarget = circle.CirclePiece;
}
else
{
// we don't want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.ApproachCircle.Hide();
}
// fade out immediately after fade in.
using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true))
circle.FadeOut(fadeOutDuration);
fadeTarget.FadeOut(fadeOutDuration);
break;
case DrawableSlider slider:

View File

@ -6,6 +6,7 @@ using System.Linq;
using osu.Framework.Bindables;
using System.Collections.Generic;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
@ -38,20 +39,25 @@ namespace osu.Game.Rulesets.Osu.Mods
protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state)
{
if (!(drawable is DrawableOsuHitObject drawableOsu))
if (!(drawable is DrawableOsuHitObject))
return;
var h = drawableOsu.HitObject;
//todo: expose and hide spinner background somehow
switch (drawable)
{
case DrawableHitCircle circle:
// we only want to see the approach circle
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
circle.CirclePiece.Hide();
applyCirclePieceState(circle, circle.CirclePiece);
break;
case DrawableSliderTail sliderTail:
applyCirclePieceState(sliderTail);
break;
case DrawableSliderRepeat sliderRepeat:
// show only the repeat arrow
applyCirclePieceState(sliderRepeat, sliderRepeat.CirclePiece);
break;
case DrawableSlider slider:
@ -61,6 +67,13 @@ namespace osu.Game.Rulesets.Osu.Mods
}
}
private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null)
{
var h = hitObject.HitObject;
using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
(hitCircle ?? hitObject).Hide();
}
private void applySliderState(DrawableSlider slider)
{
((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0);

View File

@ -51,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
Ball = new SliderBall(s, this)
@ -62,7 +63,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Alpha = 0
},
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
};
}

View File

@ -6,9 +6,11 @@ using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Utils;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
@ -22,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer;
public readonly Drawable CirclePiece;
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
@ -34,7 +38,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre;
InternalChild = scaleContainer = new ReverseArrowPiece();
InternalChild = scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Children = new[]
{
// no default for this; only visible in legacy skins.
CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()),
arrow = new ReverseArrowPiece(),
}
};
}
private readonly IBindable<float> scaleBindable = new BindableFloat();
@ -85,6 +100,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private bool hasRotation;
private readonly ReverseArrowPiece arrow;
public void UpdateSnakingPosition(Vector2 start, Vector2 end)
{
// When the repeat is hit, the arrow should fade out on spot rather than following the slider
@ -114,18 +131,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
}
float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X));
while (Math.Abs(aimRotation - Rotation) > 180)
aimRotation += aimRotation < Rotation ? 360 : -360;
while (Math.Abs(aimRotation - arrow.Rotation) > 180)
aimRotation += aimRotation < arrow.Rotation ? 360 : -360;
if (!hasRotation)
{
Rotation = aimRotation;
arrow.Rotation = aimRotation;
hasRotation = true;
}
else
{
// If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly).
Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint);
arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint);
}
}
}

View File

@ -1,15 +1,20 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Diagnostics;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Skinning;
using osuTK;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking
public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking
{
private readonly Slider slider;
private readonly SliderTailCircle tailCircle;
/// <summary>
/// The judgement text is provided by the <see cref="DrawableSlider"/>.
@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
public bool Tracking { get; set; }
private readonly IBindable<Vector2> positionBindable = new Bindable<Vector2>();
private readonly IBindable<int> pathVersion = new Bindable<int>();
private readonly IBindable<float> scaleBindable = new BindableFloat();
public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle)
: base(hitCircle)
private readonly SkinnableDrawable circlePiece;
private readonly Container scaleContainer;
public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle)
: base(tailCircle)
{
this.slider = slider;
this.tailCircle = tailCircle;
Origin = Anchor.Centre;
RelativeSizeAxes = Axes.Both;
FillMode = FillMode.Fit;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
AlwaysPresent = true;
InternalChildren = new Drawable[]
{
scaleContainer = new Container
{
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Children = new Drawable[]
{
// no default for this; only visible in legacy skins.
circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty())
}
},
};
}
positionBindable.BindTo(hitCircle.PositionBindable);
pathVersion.BindTo(slider.Path.Version);
[BackgroundDependencyLoader]
private void load()
{
scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true);
scaleBindable.BindTo(HitObject.ScaleBindable);
}
positionBindable.BindValueChanged(_ => updatePosition());
pathVersion.BindValueChanged(_ => updatePosition(), true);
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
// TODO: This has no drawable content. Support for skins should be added.
circlePiece.FadeInFromZero(HitObject.TimeFadeIn);
}
protected override void UpdateStateTransforms(ArmedState state)
{
base.UpdateStateTransforms(state);
Debug.Assert(HitObject.HitWindows != null);
switch (state)
{
case ArmedState.Idle:
this.Delay(HitObject.TimePreempt).FadeOut(500);
Expire(true);
break;
case ArmedState.Miss:
this.FadeOut(100);
break;
case ArmedState.Hit:
// todo: temporary / arbitrary
this.Delay(800).FadeOut();
break;
}
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
@ -48,6 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult);
}
private void updatePosition() => Position = HitObject.Position - slider.Position;
public void UpdateSnakingPosition(Vector2 start, Vector2 end) =>
Position = tailCircle.RepeatIndex % 2 == 0 ? end : start;
}
}

View File

@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
foreach (var tick in ticks.Where(t => !t.IsHit))
foreach (var tick in ticks.Where(t => !t.Result.HasResult))
tick.TriggerResult(false);
ApplyResult(r =>
@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
while (wholeSpins != spins)
{
var tick = ticks.FirstOrDefault(t => !t.IsHit);
var tick = ticks.FirstOrDefault(t => !t.Result.HasResult);
// tick may be null if we've hit the spin limit.
if (tick != null)

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Origin = Anchor.Centre;
Masking = true;
BorderThickness = 10;
BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other.
BorderColour = Color4.White;
Child = new Box

View File

@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects
Velocity = scoringDistance / timingPoint.BeatLength;
TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier;
// The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to.
// For now, the samples are attached to and played by the slider itself at the correct end time.
Samples = this.GetNodeSamples(repeatCount + 1);
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
@ -176,6 +180,7 @@ namespace osu.Game.Rulesets.Osu.Objects
// if this is to change, we should revisit this.
AddNested(TailCircle = new SliderTailCircle(this)
{
RepeatIndex = e.SpanIndex,
StartTime = e.Time,
Position = EndPosition,
StackHeight = StackHeight
@ -183,10 +188,9 @@ namespace osu.Game.Rulesets.Osu.Objects
break;
case SliderEventType.Repeat:
AddNested(new SliderRepeat
AddNested(new SliderRepeat(this)
{
RepeatIndex = e.SpanIndex,
SpanDuration = SpanDuration,
StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration,
Position = Position + Path.PositionAt(e.PathProgress),
StackHeight = StackHeight,
@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects
tick.Samples = sampleList;
foreach (var repeat in NestedHitObjects.OfType<SliderRepeat>())
repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1);
repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1);
if (HeadCircle != null)
HeadCircle.Samples = getNodeSamples(0);
HeadCircle.Samples = this.GetNodeSamples(0);
}
private IList<HitSampleInfo> getNodeSamples(int nodeIndex) =>
nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples;
public override Judgement CreateJudgement() => new OsuIgnoreJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;

View File

@ -0,0 +1,50 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
/// <summary>
/// A hit circle which is at the end of a slider path (either repeat or final tail).
/// </summary>
public abstract class SliderEndCircle : HitCircle
{
private readonly Slider slider;
protected SliderEndCircle(Slider slider)
{
this.slider = slider;
}
public int RepeatIndex { get; set; }
public double SpanDuration => slider.SpanDuration;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
if (RepeatIndex > 0)
{
// Repeat points after the first span should appear behind the still-visible one.
TimeFadeIn = 0;
// The next end circle should appear exactly after the previous circle (on the same end) is hit.
TimePreempt = SpanDuration * 2;
}
else
{
// taken from osu-stable
const float first_end_circle_preempt_adjust = 2 / 3f;
// The first end circle should fade in with the slider.
TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust;
}
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
}
}

View File

@ -1,35 +1,19 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SliderRepeat : OsuHitObject
public class SliderRepeat : SliderEndCircle
{
public int RepeatIndex { get; set; }
public double SpanDuration { get; set; }
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
public SliderRepeat(Slider slider)
: base(slider)
{
base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
// Out preempt should be one span early to give the user ample warning.
TimePreempt += SpanDuration;
// We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders
// we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time.
if (RepeatIndex > 0)
TimePreempt = Math.Min(SpanDuration * 2, TimePreempt);
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderRepeatJudgement();
public class SliderRepeatJudgement : OsuJudgement

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Judgements;
@ -13,18 +12,13 @@ namespace osu.Game.Rulesets.Osu.Objects
/// Note that this should not be used for timing correctness.
/// See <see cref="SliderEventType.LegacyLastTick"/> usage in <see cref="Slider"/> for more information.
/// </summary>
public class SliderTailCircle : SliderCircle
public class SliderTailCircle : SliderEndCircle
{
private readonly IBindable<int> pathVersion = new Bindable<int>();
public SliderTailCircle(Slider slider)
: base(slider)
{
pathVersion.BindTo(slider.Path.Version);
pathVersion.BindValueChanged(_ => Position = slider.EndPosition);
}
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public override Judgement CreateJudgement() => new SliderTailJudgement();
public class SliderTailJudgement : OsuJudgement

View File

@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score);
public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this);
@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu
public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo);
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.Ok,
HitResult.Meh,
HitResult.LargeTickHit,
HitResult.SmallTickHit,
HitResult.SmallBonus,
HitResult.LargeBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.LargeTickHit:
return "slider tick";
case HitResult.SmallTickHit:
return "slider end";
case HitResult.SmallBonus:
return "spinner spin";
case HitResult.LargeBonus:
return "spinner bonus";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList();

View File

@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu
ReverseArrow,
HitCircleText,
SliderHeadHitCircle,
SliderTailHitCircle,
SliderFollowCircle,
SliderBall,
SliderBody,

View File

@ -1,9 +1,12 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Input.Events;
using osu.Game.Configuration;
using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Skinning;
@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
private bool disjointTrail;
private double lastTrailTime;
private IBindable<float> cursorSize;
public LegacyCursorTrail()
{
@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
}
[BackgroundDependencyLoader]
private void load(ISkinSource skin)
private void load(ISkinSource skin, OsuConfigManager config)
{
Texture = skin.GetTexture("cursortrail");
disjointTrail = skin.GetTexture("cursormiddle") == null;
@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning
// stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation.
Texture.ScaleAdjust *= 1.6f;
}
cursorSize = config.GetBindable<float>(OsuSetting.GameplayCursorSize).GetBoundCopy();
}
protected override double FadeDuration => disjointTrail ? 150 : 500;
protected override bool InterpolateMovements => !disjointTrail;
protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1);
protected override bool OnMouseMove(MouseMoveEvent e)
{
if (!disjointTrail)

View File

@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
public class LegacyMainCirclePiece : CompositeDrawable
{
private readonly string priorityLookup;
private readonly bool hasNumber;
public LegacyMainCirclePiece(string priorityLookup = null)
public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true)
{
this.priorityLookup = priorityLookup;
this.hasNumber = hasNumber;
Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2);
}
@ -47,6 +49,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject;
bool allowFallback = false;
// attempt lookup using priority specification
Texture baseTexture = getTextureWithFallback(string.Empty);
// if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup.
if (baseTexture == null)
{
allowFallback = true;
baseTexture = getTextureWithFallback(string.Empty);
}
// at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it.
// the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist.
// expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin).
Texture overlayTexture = getTextureWithFallback("overlay");
InternalChildren = new Drawable[]
{
circleSprites = new Container<Sprite>
@ -58,19 +77,23 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
hitCircleSprite = new Sprite
{
Texture = getTextureWithFallback(string.Empty),
Texture = baseTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
hitCircleOverlay = new Sprite
{
Texture = getTextureWithFallback("overlay"),
Texture = overlayTexture,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
}
}
},
hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
};
if (hasNumber)
{
AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText
{
Font = OsuFont.Numeric.With(size: 40),
UseFullGlyphHeight = false,
@ -78,8 +101,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
},
};
});
}
bool overlayAboveNumber = skin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true;
@ -95,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Skinning
Texture tex = null;
if (!string.IsNullOrEmpty(priorityLookup))
{
tex = skin.GetTexture($"{priorityLookup}{name}");
if (!allowFallback)
return tex;
}
return tex ?? skin.GetTexture($"hitcircle{name}");
}
}
@ -107,7 +135,8 @@ namespace osu.Game.Rulesets.Osu.Skinning
state.BindValueChanged(updateState, true);
accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true);
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
if (hasNumber)
indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true);
}
private void updateState(ValueChangedEvent<ArmedState> state)
@ -120,16 +149,19 @@ namespace osu.Game.Rulesets.Osu.Skinning
circleSprites.FadeOut(legacy_fade_duration, Easing.Out);
circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
else
if (hasNumber)
{
// old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
var legacyVersion = skin.GetConfig<LegacySetting, decimal>(LegacySetting.Version)?.Value;
if (legacyVersion >= 2.0m)
// legacy skins of version 2.0 and newer only apply very short fade out to the number piece.
hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out);
else
{
// old skins scale and fade it normally along other pieces.
hitCircleText.FadeOut(legacy_fade_duration, Easing.Out);
hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out);
}
}
break;

View File

@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning
return null;
case OsuSkinComponents.SliderTailHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderendcircle", false);
return null;
case OsuSkinComponents.SliderHeadHitCircle:
if (hasHitCircle.Value)
return new LegacyMainCirclePiece("sliderstartcircle");

View File

@ -119,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
/// </summary>
protected virtual bool InterpolateMovements => true;
protected virtual float IntervalMultiplier => 1.0f;
private Vector2? lastPosition;
private readonly InputResampler resampler = new InputResampler();
@ -147,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
float distance = diff.Length;
Vector2 direction = diff / distance;
float interval = partSize.X / 2.5f;
float interval = partSize.X / 2.5f * IntervalMultiplier;
for (float d = interval; d < distance; d += interval)
{

View File

@ -17,12 +17,16 @@ using osu.Game.Rulesets.Osu.UI.Cursor;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Skinning;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Osu.Configuration;
using osuTK;
namespace osu.Game.Rulesets.Osu.UI
{
public class OsuPlayfield : Playfield
{
private readonly PlayfieldBorder playfieldBorder;
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
@ -33,12 +37,19 @@ namespace osu.Game.Rulesets.Osu.UI
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
private readonly Bindable<bool> playfieldBorderStyle = new BindableBool();
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
public OsuPlayfield()
{
InternalChildren = new Drawable[]
{
playfieldBorder = new PlayfieldBorder
{
RelativeSizeAxes = Axes.Both,
Depth = 3
},
spinnerProxies = new ProxyContainer
{
RelativeSizeAxes = Axes.Both
@ -76,6 +87,12 @@ namespace osu.Game.Rulesets.Osu.UI
AddRangeInternal(poolDictionary.Values);
}
[BackgroundDependencyLoader(true)]
private void load(OsuRulesetConfigManager config)
{
config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle);
}
public override void Add(DrawableHitObject h)
{
h.OnNewResult += onNewResult;

View File

@ -5,6 +5,7 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics;
using osu.Game.Overlays.Settings;
using osu.Game.Rulesets.Osu.Configuration;
using osu.Game.Rulesets.UI;
namespace osu.Game.Rulesets.Osu.UI
{
@ -27,17 +28,22 @@ namespace osu.Game.Rulesets.Osu.UI
new SettingsCheckbox
{
LabelText = "Snaking in sliders",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingInSliders)
},
new SettingsCheckbox
{
LabelText = "Snaking out sliders",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
Current = config.GetBindable<bool>(OsuRulesetSetting.SnakingOutSliders)
},
new SettingsCheckbox
{
LabelText = "Cursor trail",
Bindable = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
Current = config.GetBindable<bool>(OsuRulesetSetting.ShowCursorTrail)
},
new SettingsEnumDropdown<PlayfieldBorderStyle>
{
LabelText = "Playfield border style",
Current = config.GetBindable<PlayfieldBorderStyle>(OsuRulesetSetting.PlayfieldBorderStyle),
},
};
}

View File

@ -65,7 +65,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() && !(first is Swell))
if (x.Skip(1).Any() && first.CanBeStrong)
first.IsStrong = true;
return first;
}).ToList();

View File

@ -5,7 +5,6 @@ using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Extensions;
using osu.Game.Beatmaps;
using osu.Game.Rulesets.Difficulty;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
@ -24,8 +23,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
private int countMeh;
private int countMiss;
public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score)
: base(ruleset, beatmap, score)
public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score)
: base(ruleset, attributes, score)
{
}

View File

@ -52,32 +52,32 @@ namespace osu.Game.Rulesets.Taiko.Edit
public void SetStrongState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
{
if (h.IsStrong != state)
{
h.IsStrong = state;
EditorBeatmap.UpdateHitObject(h);
EditorBeatmap.Update(h);
}
}
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
public void SetRimState(bool state)
{
var hits = SelectedHitObjects.OfType<Hit>();
var hits = EditorBeatmap.SelectedHitObjects.OfType<Hit>();
ChangeHandler.BeginChange();
EditorBeatmap.BeginChange();
foreach (var h in hits)
h.Type = state ? HitType.Rim : HitType.Centre;
ChangeHandler.EndChange();
EditorBeatmap.EndChange();
}
protected override IEnumerable<MenuItem> GetContextMenuItemsForSelection(IEnumerable<SelectionBlueprint> selection)
@ -89,12 +89,14 @@ namespace osu.Game.Rulesets.Taiko.Edit
yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } };
}
public override bool HandleMovement(MoveSelectionEvent moveEvent) => true;
protected override void UpdateTernaryStates()
{
base.UpdateTernaryStates();
selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<Hit>(), h => h.Type == HitType.Rim);
selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType<TaikoHitObject>(), h => h.IsStrong);
}
}
}

View File

@ -158,7 +158,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables
{
base.LoadSamples();
isStrong.Value = getStrongSamples().Any();
if (HitObject.CanBeStrong)
isStrong.Value = getStrongSamples().Any();
}
private void updateSamplesFromStrong()

View File

@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects
set => Duration = value - StartTime;
}
public override bool CanBeStrong => false;
public double Duration { get; set; }
/// <summary>

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Threading;
using osu.Framework.Bindables;
using osu.Game.Rulesets.Judgements;
@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects
public readonly Bindable<bool> IsStrongBindable = new BindableBool();
/// <summary>
/// Whether this <see cref="TaikoHitObject"/> can be made a "strong" (large) hit.
/// </summary>
public virtual bool CanBeStrong => true;
/// <summary>
/// Whether this HitObject is a "strong" type.
/// Strong hit objects give more points for hitting the hit object with both keys.
@ -37,7 +43,13 @@ namespace osu.Game.Rulesets.Taiko.Objects
public bool IsStrong
{
get => IsStrongBindable.Value;
set => IsStrongBindable.Value = value;
set
{
if (value && !CanBeStrong)
throw new InvalidOperationException($"Object of type {GetType()} cannot be strong");
IsStrongBindable.Value = value;
}
}
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)

View File

@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Audio;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Taiko.UI;
using osu.Game.Skinning;
@ -14,13 +15,29 @@ namespace osu.Game.Rulesets.Taiko.Skinning
{
public class TaikoLegacySkinTransformer : LegacySkinTransformer
{
private Lazy<bool> hasExplosion;
public TaikoLegacySkinTransformer(ISkinSource source)
: base(source)
{
Source.SourceChanged += sourceChanged;
sourceChanged();
}
private void sourceChanged()
{
hasExplosion = new Lazy<bool>(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null);
}
public override Drawable GetDrawableComponent(ISkinComponent component)
{
if (component is GameplaySkinComponent<HitResult>)
{
// if a taiko skin is providing explosion sprites, hide the judgements completely
if (hasExplosion.Value)
return Drawable.Empty();
}
if (!(component is TaikoSkinComponent taikoComponent))
return null;
@ -87,10 +104,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning
var hitName = getHitName(taikoComponent.Component);
var hitSprite = this.GetAnimation(hitName, true, false);
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
if (hitSprite != null)
{
var strongHitSprite = this.GetAnimation($"{hitName}k", true, false);
return new LegacyHitExplosion(hitSprite, strongHitSprite);
}
return null;

View File

@ -153,12 +153,39 @@ namespace osu.Game.Rulesets.Taiko
public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap);
public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score);
public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score);
public int LegacyID => 1;
public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame();
protected override IEnumerable<HitResult> GetValidHitResults()
{
return new[]
{
HitResult.Great,
HitResult.Ok,
HitResult.SmallTickHit,
HitResult.SmallBonus,
};
}
public override string GetDisplayNameForHitResult(HitResult result)
{
switch (result)
{
case HitResult.SmallTickHit:
return "drum tick";
case HitResult.SmallBonus:
return "strong bonus";
}
return base.GetDisplayNameForHitResult(result);
}
public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap)
{
var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList();

View File

@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI
target = centreHit;
back = centre;
if (gameplayClock?.IsSeeking != true)
drumSample.Centre?.Play();
drumSample.Centre?.Play();
}
else if (action == RimAction)
{
target = rimHit;
back = rim;
if (gameplayClock?.IsSeeking != true)
drumSample.Rim?.Play();
drumSample.Rim?.Play();
}
if (target != null)

View File

@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps
Assert.That(key1, Is.EqualTo(key2));
}
[TestCase(1.3, DifficultyRating.Easy)]
[TestCase(1.993, DifficultyRating.Easy)]
[TestCase(1.998, DifficultyRating.Normal)]
[TestCase(2.4, DifficultyRating.Normal)]
[TestCase(2.693, DifficultyRating.Normal)]
[TestCase(2.698, DifficultyRating.Hard)]
[TestCase(3.5, DifficultyRating.Hard)]
[TestCase(3.993, DifficultyRating.Hard)]
[TestCase(3.997, DifficultyRating.Insane)]
[TestCase(5.0, DifficultyRating.Insane)]
[TestCase(5.292, DifficultyRating.Insane)]
[TestCase(5.297, DifficultyRating.Expert)]
[TestCase(6.2, DifficultyRating.Expert)]
[TestCase(6.493, DifficultyRating.Expert)]
[TestCase(6.498, DifficultyRating.ExpertPlus)]
[TestCase(8.3, DifficultyRating.ExpertPlus)]
public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket)
{
var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating);
Assert.AreEqual(expectedBracket, actualBracket);
}
}
}

View File

@ -651,5 +651,63 @@ namespace osu.Game.Tests.Beatmaps.Formats
Assert.IsInstanceOf<LegacyDifficultyCalculatorBeatmapDecoder>(decoder);
}
}
[Test]
public void TestMultiSegmentSliders()
{
var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false };
using (var resStream = TestResources.OpenResource("multi-segment-slider.osu"))
using (var stream = new LineBufferedReader(resStream))
{
var decoded = decoder.Decode(stream);
// Multi-segment
var first = ((IHasPath)decoded.HitObjects[0]).Path;
Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15)));
Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132)));
Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null));
Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107)));
Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null));
// Single-segment
var second = ((IHasPath)decoded.HitObjects[1]).Path;
Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve));
Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244)));
Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3)));
Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null));
// Implicit multi-segment
var third = ((IHasPath)decoded.HitObjects[2]).Path;
Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero));
Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192)));
Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192)));
Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0)));
Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier));
Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192)));
Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192)));
Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null));
Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0)));
Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null));
}
}
}
}

View File

@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats
{
private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore();
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu"));
private static IEnumerable<string> allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal));
[TestCaseSource(nameof(allBeatmaps))]
public void TestEncodeDecodeStability(string name)

View File

@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing
{
HitObjects =
{
new HitCircle { StartTime = 1000 }
new HitCircle { StartTime = 1000, NewCombo = true }
}
};
@ -56,7 +56,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 3000 },
});
@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
@ -100,7 +100,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
@ -109,7 +109,7 @@ namespace osu.Game.Tests.Editing
{
HitObjects =
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 500, NewCombo = true },
(OsuHitObject)current.HitObjects[1],
(OsuHitObject)current.HitObjects[2],
}
@ -123,7 +123,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
@ -146,7 +146,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new OsuHitObject[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new Slider
{
StartTime = 2000,
@ -188,7 +188,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1000, NewCombo = true },
new HitCircle { StartTime = 2000 },
new HitCircle { StartTime = 3000 },
});
@ -197,7 +197,7 @@ namespace osu.Game.Tests.Editing
{
HitObjects =
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 500, NewCombo = true },
(OsuHitObject)current.HitObjects[0],
new HitCircle { StartTime = 1500 },
(OsuHitObject)current.HitObjects[1],
@ -216,7 +216,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 500, NewCombo = true },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
@ -226,6 +226,9 @@ namespace osu.Game.Tests.Editing
new HitCircle { StartTime = 3500 },
});
var patchedFirst = (HitCircle)current.HitObjects[1];
patchedFirst.NewCombo = true;
var patch = new OsuBeatmap
{
HitObjects =
@ -244,7 +247,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 500, NewCombo = true },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
@ -277,7 +280,7 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500 },
new HitCircle { StartTime = 500, NewCombo = true },
new HitCircle { StartTime = 1000 },
new HitCircle { StartTime = 1500 },
new HitCircle { StartTime = 2000 },
@ -291,7 +294,7 @@ namespace osu.Game.Tests.Editing
{
HitObjects =
{
new HitCircle { StartTime = 750 },
new HitCircle { StartTime = 750, NewCombo = true },
(OsuHitObject)current.HitObjects[1],
(OsuHitObject)current.HitObjects[4],
(OsuHitObject)current.HitObjects[5],
@ -309,20 +312,20 @@ namespace osu.Game.Tests.Editing
{
current.AddRange(new[]
{
new HitCircle { StartTime = 500, Position = new Vector2(50) },
new HitCircle { StartTime = 500, Position = new Vector2(100) },
new HitCircle { StartTime = 500, Position = new Vector2(150) },
new HitCircle { StartTime = 500, Position = new Vector2(200) },
new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true },
});
var patch = new OsuBeatmap
{
HitObjects =
{
new HitCircle { StartTime = 500, Position = new Vector2(150) },
new HitCircle { StartTime = 500, Position = new Vector2(100) },
new HitCircle { StartTime = 500, Position = new Vector2(50) },
new HitCircle { StartTime = 500, Position = new Vector2(200) },
new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true },
new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true },
}
};

View File

@ -0,0 +1,100 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using NUnit.Framework;
using osu.Game.Screens.Edit;
namespace osu.Game.Tests.Editing
{
[TestFixture]
public class TransactionalCommitComponentTest
{
private TestHandler handler;
[SetUp]
public void SetUp()
{
handler = new TestHandler();
}
[Test]
public void TestCommitTransaction()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestSaveOutsideOfTransactionTriggersUpdates()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(2));
}
[Test]
public void TestEventsFire()
{
int transactionBegan = 0;
int transactionEnded = 0;
int stateSaved = 0;
handler.TransactionBegan += () => transactionBegan++;
handler.TransactionEnded += () => transactionEnded++;
handler.SaveStateTriggered += () => stateSaved++;
handler.BeginChange();
Assert.That(transactionBegan, Is.EqualTo(1));
handler.EndChange();
Assert.That(transactionEnded, Is.EqualTo(1));
Assert.That(stateSaved, Is.EqualTo(0));
handler.SaveState();
Assert.That(stateSaved, Is.EqualTo(1));
}
[Test]
public void TestSaveDuringTransactionDoesntTriggerUpdate()
{
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.BeginChange();
handler.SaveState();
Assert.That(handler.StateUpdateCount, Is.EqualTo(0));
handler.EndChange();
Assert.That(handler.StateUpdateCount, Is.EqualTo(1));
}
[Test]
public void TestEndWithoutBeginThrows()
{
handler.BeginChange();
handler.EndChange();
Assert.That(() => handler.EndChange(), Throws.TypeOf<InvalidOperationException>());
}
private class TestHandler : TransactionalCommitComponent
{
public int StateUpdateCount { get; private set; }
protected override void UpdateState()
{
StateUpdateCount++;
}
}
}
}

View File

@ -111,6 +111,7 @@ namespace osu.Game.Tests.NonVisual
var osu = LoadOsuIntoHost(host);
var storage = osu.Dependencies.Get<Storage>();
var osuStorage = storage as MigratableStorage;
// Store the current storage's path. We'll need to refer to this for assertions in the original directory after the migration completes.
string originalDirectory = storage.GetFullPath(".");
@ -137,13 +138,15 @@ namespace osu.Game.Tests.NonVisual
Assert.That(!Directory.Exists(Path.Combine(originalDirectory, "test-nested", "cache")));
Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache")));
foreach (var file in OsuStorage.IGNORE_FILES)
Assert.That(osuStorage, Is.Not.Null);
foreach (var file in osuStorage.IgnoreFiles)
{
Assert.That(File.Exists(Path.Combine(originalDirectory, file)));
Assert.That(storage.Exists(file), Is.False);
}
foreach (var dir in OsuStorage.IGNORE_DIRECTORIES)
foreach (var dir in osuStorage.IgnoreDirectories)
{
Assert.That(Directory.Exists(Path.Combine(originalDirectory, dir)));
Assert.That(storage.ExistsDirectory(dir), Is.False);

View File

@ -94,6 +94,52 @@ namespace osu.Game.Tests.NonVisual
Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA);
}
[Test]
public void TestMultiModFlattening()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(4, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(combinations[3] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC);
}
[Test]
public void TestIncompatibleThroughMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA);
}
[Test]
public void TestIncompatibleWithSameInstanceViaMultiMod()
{
var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations();
Assert.AreEqual(3, combinations.Length);
Assert.IsTrue(combinations[0] is ModNoMod);
Assert.IsTrue(combinations[1] is ModA);
Assert.IsTrue(combinations[2] is MultiMod);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA);
Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB);
}
private class ModA : Mod
{
public override string Name => nameof(ModA);
@ -112,6 +158,13 @@ namespace osu.Game.Tests.NonVisual
public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) };
}
private class ModC : Mod
{
public override string Name => nameof(ModC);
public override string Acronym => nameof(ModC);
public override double ScoreMultiplier => 1;
}
private class ModIncompatibleWithA : Mod
{
public override string Name => $"Incompatible With {nameof(ModA)}";

View File

@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
[TestCase("202010", true)]
[TestCase("20201010", false)]
[TestCase("153", true)]
[TestCase("1535", false)]
public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered)
{
var beatmap = getExampleBeatmap();
beatmap.OnlineBeatmapID = 20201010;
beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 };
var criteria = new FilterCriteria { SearchText = query };
var carouselItem = new CarouselBeatmap(beatmap);
carouselItem.Filter(criteria);
Assert.AreEqual(filtered, carouselItem.Filtered.Value);
}
}
}

View File

@ -0,0 +1,11 @@
osu file format v128
[HitObjects]
// Multi-segment
63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0:
// Single-segment
63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0:
// Implicit multi-segment
32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 865 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

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