Merge branch 'master' into results-dynamic-pp-calc
@ -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>
|
@ -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>
|
||||
|
34
osu.Android/GameplayScreenRotationLocker.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
});
|
||||
|
@ -141,6 +141,35 @@ 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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
@ -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>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
||||
|
@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty
|
||||
public class ManiaDifficultyAttributes : DifficultyAttributes
|
||||
{
|
||||
public double GreatHitWindow;
|
||||
public double ScoreMultiplier;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
80
osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
165
osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods
|
||||
typeof(ManiaModKey7),
|
||||
typeof(ManiaModKey8),
|
||||
typeof(ManiaModKey9),
|
||||
typeof(ManiaModKey10),
|
||||
}.Except(new[] { GetType() }).ToArray();
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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[]
|
||||
|
@ -7,6 +7,7 @@ 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;
|
||||
@ -20,11 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
{
|
||||
base.OnSelectionChanged();
|
||||
|
||||
bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider);
|
||||
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()
|
||||
@ -41,6 +43,54 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// </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 (moreThanOneObject)
|
||||
h.StartTime = endTime - (h.GetEndTime() - startTime);
|
||||
|
||||
if (h is Slider slider)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public override bool HandleFlip(Direction direction)
|
||||
{
|
||||
var hitObjects = selectedMovableObjects;
|
||||
@ -182,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// <param name="points">The points to calculate a quad for.</param>
|
||||
private Quad getSurroundingQuad(IEnumerable<Vector2> points)
|
||||
{
|
||||
if (!SelectedHitObjects.Any())
|
||||
if (!EditorBeatmap.SelectedHitObjects.Any())
|
||||
return new Quad();
|
||||
|
||||
Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue);
|
||||
@ -203,10 +253,10 @@ namespace osu.Game.Rulesets.Osu.Edit
|
||||
/// <summary>
|
||||
/// All osu! hitobjects which can be moved/rotated/scaled.
|
||||
/// </summary>
|
||||
private OsuHitObject[] selectedMovableObjects => SelectedHitObjects
|
||||
.OfType<OsuHitObject>()
|
||||
.Where(h => !(h is Spinner))
|
||||
.ToArray();
|
||||
private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects
|
||||
.OfType<OsuHitObject>()
|
||||
.Where(h => !(h is Spinner))
|
||||
.ToArray();
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a point around an arbitrary origin.
|
||||
|
@ -42,7 +42,11 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
private double lastSliderHeadFadeOutStartTime;
|
||||
private double lastSliderHeadFadeOutDuration;
|
||||
|
||||
protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state)
|
||||
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;
|
||||
@ -86,14 +90,23 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
lastSliderHeadFadeOutStartTime = fadeOutStartTime;
|
||||
}
|
||||
|
||||
// we don't want to see the approach circle
|
||||
using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true))
|
||||
circle.ApproachCircle.Hide();
|
||||
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:
|
||||
|
@ -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)
|
||||
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
|
@ -159,6 +159,33 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
100
osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs
Normal 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++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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)}";
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
11
osu.Game.Tests/Resources/multi-segment-slider.osu
Normal 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
|
BIN
osu.Game.Tests/Resources/old-skin/score-0.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-1.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-2.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-3.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-4.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-5.png
Normal file
After Width: | Height: | Size: 3.0 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-6.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-7.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-8.png
Normal file
After Width: | Height: | Size: 3.6 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-9.png
Normal file
After Width: | Height: | Size: 3.5 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-comma.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
osu.Game.Tests/Resources/old-skin/score-dot.png
Normal file
After Width: | Height: | Size: 771 B |
BIN
osu.Game.Tests/Resources/old-skin/score-percent.png
Normal file
After Width: | Height: | Size: 4.8 KiB |
BIN
osu.Game.Tests/Resources/old-skin/score-x.png
Normal file
After Width: | Height: | Size: 2.5 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-bg.png
Normal file
After Width: | Height: | Size: 6.9 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png
Normal file
After Width: | Height: | Size: 465 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png
Normal file
After Width: | Height: | Size: 475 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png
Normal file
After Width: | Height: | Size: 466 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png
Normal file
After Width: | Height: | Size: 464 B |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-ki.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png
Normal file
After Width: | Height: | Size: 7.2 KiB |
BIN
osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png
Normal file
After Width: | Height: | Size: 9.1 KiB |
2
osu.Game.Tests/Resources/old-skin/skin.ini
Normal file
@ -0,0 +1,2 @@
|
||||
[General]
|
||||
Version: 1.0
|
@ -53,5 +53,263 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
|
||||
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Test to see that all <see cref="HitResult"/>s contribute to score portions in correct amounts.
|
||||
/// </summary>
|
||||
/// <param name="scoringMode">Scoring mode to test.</param>
|
||||
/// <param name="hitResult">The <see cref="HitResult"/> that will be applied to selected hit objects.</param>
|
||||
/// <param name="maxResult">The maximum <see cref="HitResult"/> achievable.</param>
|
||||
/// <param name="expectedScore">Expected score after all objects have been judged, rounded to the nearest integer.</param>
|
||||
/// <remarks>
|
||||
/// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo.
|
||||
/// <para>
|
||||
/// For standardised scoring, <paramref name="expectedScore"/> is calculated using the following formula:
|
||||
/// 1_000_000 * (((3 * <paramref name="hitResult"/>) / (4 * <paramref name="maxResult"/>)) * 30% + (bestCombo / maxCombo) * 70%)
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// For classic scoring, <paramref name="expectedScore"/> is calculated using the following formula:
|
||||
/// <paramref name="hitResult"/> / <paramref name="maxResult"/> * 936
|
||||
/// where 936 is simplified from:
|
||||
/// 75% * 4 * 300 * (1 + 1/25)
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0)
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0)
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points)
|
||||
[TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points)
|
||||
[TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points)
|
||||
public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore)
|
||||
{
|
||||
var minResult = new TestJudgement(hitResult).MinResult;
|
||||
|
||||
IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo())
|
||||
{
|
||||
HitObjects = new List<HitObject>(Enumerable.Repeat(new TestHitObject(maxResult), 4))
|
||||
};
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(fourObjectBeatmap);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement())
|
||||
{
|
||||
Type = i == 2 ? minResult : hitResult
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
}
|
||||
|
||||
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// This test uses a beatmap with four small ticks and one object with the <see cref="Judgement.MaxResult"/> of <see cref="HitResult.Ok"/>.
|
||||
/// Its goal is to ensure that with the <see cref="ScoringMode"/> of <see cref="ScoringMode.Standardised"/>,
|
||||
/// small ticks contribute to the accuracy portion, but not the combo portion.
|
||||
/// In contrast, <see cref="ScoringMode.Classic"/> does not have separate combo and accuracy portion (they are multiplied by each other).
|
||||
/// </remarks>
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
|
||||
[TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
|
||||
[TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25)
|
||||
public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore)
|
||||
{
|
||||
IEnumerable<HitObject> hitObjects = Enumerable
|
||||
.Repeat(new TestHitObject(HitResult.SmallTickHit), 4)
|
||||
.Append(new TestHitObject(HitResult.Ok));
|
||||
IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo())
|
||||
{
|
||||
HitObjects = hitObjects.ToList()
|
||||
};
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(fiveObjectBeatmap);
|
||||
|
||||
for (int i = 0; i < 4; i++)
|
||||
{
|
||||
var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement())
|
||||
{
|
||||
Type = i == 2 ? HitResult.SmallTickMiss : hitResult
|
||||
};
|
||||
scoreProcessor.ApplyResult(judgementResult);
|
||||
}
|
||||
|
||||
var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement())
|
||||
{
|
||||
Type = HitResult.Ok
|
||||
};
|
||||
scoreProcessor.ApplyResult(lastJudgementResult);
|
||||
|
||||
Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEmptyBeatmap(
|
||||
[Values(ScoringMode.Standardised, ScoringMode.Classic)]
|
||||
ScoringMode scoringMode)
|
||||
{
|
||||
scoreProcessor.Mode.Value = scoringMode;
|
||||
scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo()));
|
||||
|
||||
Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value));
|
||||
}
|
||||
|
||||
[TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)]
|
||||
[TestCase(HitResult.Meh, HitResult.Miss)]
|
||||
[TestCase(HitResult.Ok, HitResult.Miss)]
|
||||
[TestCase(HitResult.Good, HitResult.Miss)]
|
||||
[TestCase(HitResult.Great, HitResult.Miss)]
|
||||
[TestCase(HitResult.Perfect, HitResult.Miss)]
|
||||
[TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)]
|
||||
[TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)]
|
||||
[TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)]
|
||||
[TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)]
|
||||
public void TestMinResults(HitResult hitResult, HitResult expectedMinResult)
|
||||
{
|
||||
Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult);
|
||||
}
|
||||
|
||||
[TestCase(HitResult.None, false)]
|
||||
[TestCase(HitResult.IgnoreMiss, false)]
|
||||
[TestCase(HitResult.IgnoreHit, false)]
|
||||
[TestCase(HitResult.Miss, true)]
|
||||
[TestCase(HitResult.Meh, true)]
|
||||
[TestCase(HitResult.Ok, true)]
|
||||
[TestCase(HitResult.Good, true)]
|
||||
[TestCase(HitResult.Great, true)]
|
||||
[TestCase(HitResult.Perfect, true)]
|
||||
[TestCase(HitResult.SmallTickMiss, false)]
|
||||
[TestCase(HitResult.SmallTickHit, false)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, false)]
|
||||
[TestCase(HitResult.LargeBonus, false)]
|
||||
public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue)
|
||||
{
|
||||
Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo());
|
||||
}
|
||||
|
||||
[TestCase(HitResult.None, false)]
|
||||
[TestCase(HitResult.IgnoreMiss, false)]
|
||||
[TestCase(HitResult.IgnoreHit, false)]
|
||||
[TestCase(HitResult.Miss, true)]
|
||||
[TestCase(HitResult.Meh, true)]
|
||||
[TestCase(HitResult.Ok, true)]
|
||||
[TestCase(HitResult.Good, true)]
|
||||
[TestCase(HitResult.Great, true)]
|
||||
[TestCase(HitResult.Perfect, true)]
|
||||
[TestCase(HitResult.SmallTickMiss, true)]
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, false)]
|
||||
[TestCase(HitResult.LargeBonus, false)]
|
||||
public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue)
|
||||
{
|
||||
Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy());
|
||||
}
|
||||
|
||||
[TestCase(HitResult.None, false)]
|
||||
[TestCase(HitResult.IgnoreMiss, false)]
|
||||
[TestCase(HitResult.IgnoreHit, false)]
|
||||
[TestCase(HitResult.Miss, false)]
|
||||
[TestCase(HitResult.Meh, false)]
|
||||
[TestCase(HitResult.Ok, false)]
|
||||
[TestCase(HitResult.Good, false)]
|
||||
[TestCase(HitResult.Great, false)]
|
||||
[TestCase(HitResult.Perfect, false)]
|
||||
[TestCase(HitResult.SmallTickMiss, false)]
|
||||
[TestCase(HitResult.SmallTickHit, false)]
|
||||
[TestCase(HitResult.LargeTickMiss, false)]
|
||||
[TestCase(HitResult.LargeTickHit, false)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsBonus(HitResult hitResult, bool expectedReturnValue)
|
||||
{
|
||||
Assert.AreEqual(expectedReturnValue, hitResult.IsBonus());
|
||||
}
|
||||
|
||||
[TestCase(HitResult.None, false)]
|
||||
[TestCase(HitResult.IgnoreMiss, false)]
|
||||
[TestCase(HitResult.IgnoreHit, true)]
|
||||
[TestCase(HitResult.Miss, false)]
|
||||
[TestCase(HitResult.Meh, true)]
|
||||
[TestCase(HitResult.Ok, true)]
|
||||
[TestCase(HitResult.Good, true)]
|
||||
[TestCase(HitResult.Great, true)]
|
||||
[TestCase(HitResult.Perfect, true)]
|
||||
[TestCase(HitResult.SmallTickMiss, false)]
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, false)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsHit(HitResult hitResult, bool expectedReturnValue)
|
||||
{
|
||||
Assert.AreEqual(expectedReturnValue, hitResult.IsHit());
|
||||
}
|
||||
|
||||
[TestCase(HitResult.None, false)]
|
||||
[TestCase(HitResult.IgnoreMiss, false)]
|
||||
[TestCase(HitResult.IgnoreHit, false)]
|
||||
[TestCase(HitResult.Miss, true)]
|
||||
[TestCase(HitResult.Meh, true)]
|
||||
[TestCase(HitResult.Ok, true)]
|
||||
[TestCase(HitResult.Good, true)]
|
||||
[TestCase(HitResult.Great, true)]
|
||||
[TestCase(HitResult.Perfect, true)]
|
||||
[TestCase(HitResult.SmallTickMiss, true)]
|
||||
[TestCase(HitResult.SmallTickHit, true)]
|
||||
[TestCase(HitResult.LargeTickMiss, true)]
|
||||
[TestCase(HitResult.LargeTickHit, true)]
|
||||
[TestCase(HitResult.SmallBonus, true)]
|
||||
[TestCase(HitResult.LargeBonus, true)]
|
||||
public void TestIsScorable(HitResult hitResult, bool expectedReturnValue)
|
||||
{
|
||||
Assert.AreEqual(expectedReturnValue, hitResult.IsScorable());
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
public TestJudgement(HitResult maxResult)
|
||||
{
|
||||
MaxResult = maxResult;
|
||||
}
|
||||
}
|
||||
|
||||
private class TestHitObject : HitObject
|
||||
{
|
||||
private readonly HitResult maxResult;
|
||||
|
||||
public override Judgement CreateJudgement()
|
||||
{
|
||||
return new TestJudgement(maxResult);
|
||||
}
|
||||
|
||||
public TestHitObject(HitResult maxResult)
|
||||
{
|
||||
this.maxResult = maxResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -192,6 +192,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo
|
||||
{
|
||||
Ruleset = new OsuRuleset().RulesetInfo,
|
||||
User = new User { Username = "osu!" },
|
||||
Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo,
|
||||
Ruleset = Ruleset.Value,
|
||||
|
@ -5,7 +5,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Screens.Edit.Compose.Components;
|
||||
using osu.Game.Screens.Edit.Compose.Components.Timeline;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Editing
|
||||
@ -13,7 +12,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
[TestFixture]
|
||||
public class TestSceneTimelineTickDisplay : TimelineTestScene
|
||||
{
|
||||
public override Drawable CreateTestComponent() => new TimelineTickDisplay();
|
||||
public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline.
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -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.Threading;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
|
||||
AddUntilStep("wait for load", () => graph.Loaded.IsSet);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -98,12 +99,18 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
};
|
||||
});
|
||||
|
||||
AddUntilStep("wait for load", () => graph.ResampledWaveform != null);
|
||||
AddUntilStep("wait for load", () => graph.Loaded.IsSet);
|
||||
}
|
||||
|
||||
public class TestWaveformGraph : WaveformGraph
|
||||
{
|
||||
public new Waveform ResampledWaveform => base.ResampledWaveform;
|
||||
public readonly ManualResetEventSlim Loaded = new ManualResetEventSlim();
|
||||
|
||||
protected override void OnWaveformRegenerated(Waveform waveform)
|
||||
{
|
||||
base.OnWaveformRegenerated(waveform);
|
||||
Loaded.Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
47
osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs
Normal file
@ -0,0 +1,47 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneComboCounter : SkinnableTestScene
|
||||
{
|
||||
private IEnumerable<SkinnableComboCounter> comboCounters => CreatedDrawables.OfType<SkinnableComboCounter>();
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create combo counters", () => SetContents(() =>
|
||||
{
|
||||
var comboCounter = new SkinnableComboCounter();
|
||||
comboCounter.Current.Value = 1;
|
||||
return comboCounter;
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestComboCounterIncrementing()
|
||||
{
|
||||
AddRepeatStep("increase combo", () =>
|
||||
{
|
||||
foreach (var counter in comboCounters)
|
||||
counter.Current.Value++;
|
||||
}, 10);
|
||||
|
||||
AddStep("reset combo", () =>
|
||||
{
|
||||
foreach (var counter in comboCounters)
|
||||
counter.Current.Value = 0;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddStep("get variables", () =>
|
||||
{
|
||||
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First().GameplayClock;
|
||||
gameplayClock = Player.ChildrenOfType<FrameStabilityContainer>().First();
|
||||
slider = Player.ChildrenOfType<DrawableSlider>().OrderBy(s => s.HitObject.StartTime).First();
|
||||
samples = slider.ChildrenOfType<DrawableSample>().ToArray();
|
||||
});
|
||||
|
@ -2,23 +2,29 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneHUDOverlay : OsuManualInputManagerTestScene
|
||||
public class TestSceneHUDOverlay : SkinnableTestScene
|
||||
{
|
||||
private HUDOverlay hudOverlay;
|
||||
|
||||
private IEnumerable<HUDOverlay> hudOverlays => CreatedDrawables.OfType<HUDOverlay>();
|
||||
|
||||
// best way to check without exposing.
|
||||
private Drawable hideTarget => hudOverlay.KeyCounter;
|
||||
private FillFlowContainer<KeyCounter> keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType<FillFlowContainer<KeyCounter>>().First();
|
||||
@ -26,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
[Test]
|
||||
public void TestComboCounterIncrementing()
|
||||
{
|
||||
createNew();
|
||||
|
||||
AddRepeatStep("increase combo", () =>
|
||||
{
|
||||
foreach (var hud in hudOverlays)
|
||||
hud.ComboCounter.Current.Value++;
|
||||
}, 10);
|
||||
|
||||
AddStep("reset combo", () =>
|
||||
{
|
||||
foreach (var hud in hudOverlays)
|
||||
hud.ComboCounter.Current.Value = 0;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShownByDefault()
|
||||
{
|
||||
@ -45,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha);
|
||||
AddUntilStep("wait for load", () => hudOverlay.IsAlive);
|
||||
AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1);
|
||||
AddAssert("initial alpha was less than 1", () => initialAlpha < 1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -53,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
createNew();
|
||||
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
|
||||
|
||||
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
||||
AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent);
|
||||
@ -89,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddStep("set keycounter visible false", () =>
|
||||
{
|
||||
config.Set<bool>(OsuSetting.KeyOverlay, false);
|
||||
hudOverlay.KeyCounter.AlwaysVisible.Value = false;
|
||||
hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false);
|
||||
});
|
||||
|
||||
AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false);
|
||||
AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false));
|
||||
AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent);
|
||||
AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent);
|
||||
|
||||
AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true);
|
||||
AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true));
|
||||
AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent);
|
||||
AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent);
|
||||
|
||||
@ -107,13 +131,22 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("create overlay", () =>
|
||||
{
|
||||
Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
|
||||
SetContents(() =>
|
||||
{
|
||||
hudOverlay = new HUDOverlay(null, null, null, Array.Empty<Mod>());
|
||||
|
||||
// Add any key just to display the key counter visually.
|
||||
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
|
||||
// Add any key just to display the key counter visually.
|
||||
hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space));
|
||||
|
||||
action?.Invoke(hudOverlay);
|
||||
hudOverlay.ComboCounter.Current.Value = 1;
|
||||
|
||||
action?.Invoke(hudOverlay);
|
||||
|
||||
return hudOverlay;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
}
|
||||
}
|
||||
|
@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
private BarHitErrorMeter barMeter;
|
||||
private BarHitErrorMeter barMeter2;
|
||||
private BarHitErrorMeter barMeter3;
|
||||
private ColourHitErrorMeter colourMeter;
|
||||
private ColourHitErrorMeter colourMeter2;
|
||||
private ColourHitErrorMeter colourMeter3;
|
||||
private HitWindows hitWindows;
|
||||
|
||||
public TestSceneHitErrorMeter()
|
||||
@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Origin = Anchor.CentreLeft,
|
||||
});
|
||||
|
||||
Add(barMeter3 = new BarHitErrorMeter(hitWindows, true)
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Rotation = 270,
|
||||
});
|
||||
|
||||
Add(colourMeter = new ColourHitErrorMeter(hitWindows)
|
||||
{
|
||||
Anchor = Anchor.CentreRight,
|
||||
@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Origin = Anchor.CentreLeft,
|
||||
Margin = new MarginPadding { Left = 50 }
|
||||
});
|
||||
|
||||
Add(colourMeter3 = new ColourHitErrorMeter(hitWindows)
|
||||
{
|
||||
Anchor = Anchor.BottomCentre,
|
||||
Origin = Anchor.CentreLeft,
|
||||
Rotation = 270,
|
||||
Margin = new MarginPadding { Left = 50 }
|
||||
});
|
||||
}
|
||||
|
||||
private void newJudgement(double offset = 0)
|
||||
@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
barMeter.OnNewJudgement(judgement);
|
||||
barMeter2.OnNewJudgement(judgement);
|
||||
barMeter3.OnNewJudgement(judgement);
|
||||
colourMeter.OnNewJudgement(judgement);
|
||||
colourMeter2.OnNewJudgement(judgement);
|
||||
colourMeter3.OnNewJudgement(judgement);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
@ -23,33 +24,41 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Test]
|
||||
public void TestGameplayOverlayActivation()
|
||||
{
|
||||
AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
|
||||
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayOverlayActivationPaused()
|
||||
{
|
||||
AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
|
||||
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
|
||||
AddStep("pause gameplay", () => Player.Pause());
|
||||
AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
|
||||
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayOverlayActivationReplayLoaded()
|
||||
{
|
||||
AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
|
||||
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
|
||||
AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true);
|
||||
AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
|
||||
AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayOverlayActivationBreaks()
|
||||
{
|
||||
AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
|
||||
AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
|
||||
AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime));
|
||||
AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered);
|
||||
AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value);
|
||||
AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime));
|
||||
AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled);
|
||||
AddAssert("local user playing", () => Player.LocalUserPlaying.Value);
|
||||
}
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer();
|
||||
@ -57,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
protected class OverlayTestPlayer : TestPlayer
|
||||
{
|
||||
public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value;
|
||||
public new Bindable<bool> LocalUserPlaying => base.LocalUserPlaying;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,68 +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 NUnit.Framework;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
[TestFixture]
|
||||
public class TestSceneScoreCounter : OsuTestScene
|
||||
{
|
||||
public TestSceneScoreCounter()
|
||||
{
|
||||
int numerator = 0, denominator = 0;
|
||||
|
||||
ScoreCounter score = new ScoreCounter(7)
|
||||
{
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Margin = new MarginPadding(20),
|
||||
};
|
||||
Add(score);
|
||||
|
||||
ComboCounter comboCounter = new StandardComboCounter
|
||||
{
|
||||
Origin = Anchor.BottomLeft,
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Margin = new MarginPadding(10),
|
||||
};
|
||||
Add(comboCounter);
|
||||
|
||||
PercentageCounter accuracyCounter = new PercentageCounter
|
||||
{
|
||||
Origin = Anchor.TopRight,
|
||||
Anchor = Anchor.TopRight,
|
||||
Position = new Vector2(-20, 60),
|
||||
};
|
||||
Add(accuracyCounter);
|
||||
|
||||
AddStep(@"Reset all", delegate
|
||||
{
|
||||
score.Current.Value = 0;
|
||||
comboCounter.Current.Value = 0;
|
||||
numerator = denominator = 0;
|
||||
accuracyCounter.SetFraction(0, 0);
|
||||
});
|
||||
|
||||
AddStep(@"Hit! :D", delegate
|
||||
{
|
||||
score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0);
|
||||
comboCounter.Increment();
|
||||
numerator++;
|
||||
denominator++;
|
||||
accuracyCounter.SetFraction(numerator, denominator);
|
||||
});
|
||||
|
||||
AddStep(@"miss...", delegate
|
||||
{
|
||||
comboCounter.Current.Value = 0;
|
||||
denominator++;
|
||||
accuracyCounter.SetFraction(numerator, denominator);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene
|
||||
{
|
||||
private IEnumerable<SkinnableAccuracyCounter> accuracyCounters => CreatedDrawables.OfType<SkinnableAccuracyCounter>();
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create combo counters", () => SetContents(() =>
|
||||
{
|
||||
var accuracyCounter = new SkinnableAccuracyCounter();
|
||||
|
||||
accuracyCounter.Current.Value = 1;
|
||||
|
||||
return accuracyCounter;
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestChangingAccuracy()
|
||||
{
|
||||
AddStep(@"Reset all", delegate
|
||||
{
|
||||
foreach (var s in accuracyCounters)
|
||||
s.Current.Value = 1;
|
||||
});
|
||||
|
||||
AddStep(@"Hit! :D", delegate
|
||||
{
|
||||
foreach (var s in accuracyCounters)
|
||||
s.Current.Value -= 0.023f;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Screens.Play;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneSkinnableHealthDisplay : SkinnableTestScene
|
||||
{
|
||||
private IEnumerable<SkinnableHealthDisplay> healthDisplays => CreatedDrawables.OfType<SkinnableHealthDisplay>();
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create health displays", () =>
|
||||
{
|
||||
SetContents(() => new SkinnableHealthDisplay());
|
||||
});
|
||||
AddStep(@"Reset all", delegate
|
||||
{
|
||||
foreach (var s in healthDisplays)
|
||||
s.Current.Value = 1;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHealthDisplayIncrementing()
|
||||
{
|
||||
AddRepeatStep(@"decrease hp", delegate
|
||||
{
|
||||
foreach (var healthDisplay in healthDisplays)
|
||||
healthDisplay.Current.Value -= 0.08f;
|
||||
}, 10);
|
||||
|
||||
AddRepeatStep(@"increase hp without flash", delegate
|
||||
{
|
||||
foreach (var healthDisplay in healthDisplays)
|
||||
healthDisplay.Current.Value += 0.1f;
|
||||
}, 3);
|
||||
|
||||
AddRepeatStep(@"increase hp with flash", delegate
|
||||
{
|
||||
foreach (var healthDisplay in healthDisplays)
|
||||
{
|
||||
healthDisplay.Current.Value += 0.1f;
|
||||
healthDisplay.Flash(new JudgementResult(null, new OsuJudgement()));
|
||||
}
|
||||
}, 3);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,54 @@
|
||||
// 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.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneSkinnableScoreCounter : SkinnableTestScene
|
||||
{
|
||||
private IEnumerable<SkinnableScoreCounter> scoreCounters => CreatedDrawables.OfType<SkinnableScoreCounter>();
|
||||
|
||||
protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset();
|
||||
|
||||
[SetUpSteps]
|
||||
public void SetUpSteps()
|
||||
{
|
||||
AddStep("Create combo counters", () => SetContents(() =>
|
||||
{
|
||||
var comboCounter = new SkinnableScoreCounter();
|
||||
comboCounter.Current.Value = 1;
|
||||
return comboCounter;
|
||||
}));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestScoreCounterIncrementing()
|
||||
{
|
||||
AddStep(@"Reset all", delegate
|
||||
{
|
||||
foreach (var s in scoreCounters)
|
||||
s.Current.Value = 0;
|
||||
});
|
||||
|
||||
AddStep(@"Hit! :D", delegate
|
||||
{
|
||||
foreach (var s in scoreCounters)
|
||||
s.Current.Value += 300;
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestVeryLargeScore()
|
||||
{
|
||||
AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000));
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Skinning;
|
||||
@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
public class TestSceneSkinnableSound : OsuTestScene
|
||||
{
|
||||
[Cached(typeof(ISamplePlaybackDisabler))]
|
||||
private GameplayClock gameplayClock = new GameplayClock(new FramedClock());
|
||||
|
||||
private TestSkinSourceContainer skinSource;
|
||||
private PausableSkinnableSound skinnableSound;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
public void SetUpSteps()
|
||||
{
|
||||
gameplayClock.IsPaused.Value = false;
|
||||
|
||||
Children = new Drawable[]
|
||||
AddStep("setup hierarchy", () =>
|
||||
{
|
||||
skinSource = new TestSkinSourceContainer
|
||||
Children = new Drawable[]
|
||||
{
|
||||
Clock = gameplayClock,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
|
||||
},
|
||||
};
|
||||
});
|
||||
skinSource = new TestSkinSourceContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide"))
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestStoppedSoundDoesntResumeAfterPause()
|
||||
@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||
|
||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
||||
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||
|
||||
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||
|
||||
AddWaitStep("wait a bit", 5);
|
||||
AddAssert("sample not playing", () => !sample.Playing);
|
||||
@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddUntilStep("wait for sample to start playing", () => sample.Playing);
|
||||
|
||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
||||
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||
|
||||
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||
AddUntilStep("wait for sample to start playing", () => sample.Playing);
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddAssert("sample playing", () => sample.Playing);
|
||||
|
||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||
|
||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
||||
AddUntilStep("sample not playing", () => !sample.Playing);
|
||||
|
||||
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||
|
||||
AddAssert("sample not playing", () => !sample.Playing);
|
||||
AddAssert("sample not playing", () => !sample.Playing);
|
||||
@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
AddAssert("sample playing", () => sample.Playing);
|
||||
|
||||
AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true);
|
||||
AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true);
|
||||
AddUntilStep("wait for sample to stop playing", () => !sample.Playing);
|
||||
|
||||
AddStep("trigger skin change", () => skinSource.TriggerSourceChanged());
|
||||
@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
|
||||
AddAssert("new sample stopped", () => !sample.Playing);
|
||||
AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false);
|
||||
AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false);
|
||||
|
||||
AddWaitStep("wait a bit", 5);
|
||||
AddAssert("new sample not played", () => !sample.Playing);
|
||||
}
|
||||
|
||||
[Cached(typeof(ISkinSource))]
|
||||
private class TestSkinSourceContainer : Container, ISkinSource
|
||||
[Cached(typeof(ISamplePlaybackDisabler))]
|
||||
private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler
|
||||
{
|
||||
[Resolved]
|
||||
private ISkinSource source { get; set; }
|
||||
|
||||
public event Action SourceChanged;
|
||||
|
||||
public Bindable<bool> SamplePlaybackDisabled { get; } = new Bindable<bool>();
|
||||
|
||||
IBindable<bool> ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled;
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component);
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT);
|
||||
public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo);
|
||||
|
@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking
|
||||
createTest(CreateDistributedHitEvents());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAroundCentre()
|
||||
{
|
||||
createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestZeroTimeOffset()
|
||||
{
|
||||
|
@ -394,7 +394,7 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false));
|
||||
AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz");
|
||||
AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false));
|
||||
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!"));
|
||||
AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -18,10 +19,11 @@ using osu.Game.Rulesets.Difficulty;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
public class TestSceneModSettings : OsuTestScene
|
||||
public class TestSceneModSettings : OsuManualInputManagerTestScene
|
||||
{
|
||||
private TestModSelectOverlay modSelect;
|
||||
|
||||
@ -95,6 +97,41 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMultiModSettingsUnboundWhenCopied()
|
||||
{
|
||||
MultiMod original = null;
|
||||
MultiMod copy = null;
|
||||
|
||||
AddStep("create mods", () =>
|
||||
{
|
||||
original = new MultiMod(new OsuModDoubleTime());
|
||||
copy = (MultiMod)original.CreateCopy();
|
||||
});
|
||||
|
||||
AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2);
|
||||
|
||||
AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value));
|
||||
AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomisationMenuNoClickthrough()
|
||||
{
|
||||
createModSelect();
|
||||
openModSelect();
|
||||
|
||||
AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f));
|
||||
AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod));
|
||||
AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1);
|
||||
AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod)));
|
||||
AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered);
|
||||
AddStep("left click mod", () => InputManager.Click(MouseButton.Left));
|
||||
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
|
||||
AddStep("right click mod", () => InputManager.Click(MouseButton.Right));
|
||||
AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1);
|
||||
}
|
||||
|
||||
private void createModSelect()
|
||||
{
|
||||
AddStep("create mod select", () =>
|
||||
@ -121,9 +158,16 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded);
|
||||
|
||||
public ModButton GetModButton(Mod mod)
|
||||
{
|
||||
return ModSectionsContainer.ChildrenOfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
|
||||
}
|
||||
|
||||
public void SelectMod(Mod mod) =>
|
||||
ModSectionsContainer.Children.Single(s => s.ModType == mod.Type)
|
||||
.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1);
|
||||
GetModButton(mod).SelectNext(1);
|
||||
|
||||
public void SetModSettingsWidth(float newWidth) =>
|
||||
ModSettingsContainer.Width = newWidth;
|
||||
}
|
||||
|
||||
public class TestRulesetInfo : RulesetInfo
|
||||
|
@ -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.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Audio;
|
||||
@ -59,7 +60,7 @@ namespace osu.Game.Tests
|
||||
get
|
||||
{
|
||||
using (var reader = getZipReader())
|
||||
return reader.Filenames.First(f => f.EndsWith(".mp3"));
|
||||
return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal));
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,7 +74,7 @@ namespace osu.Game.Tests
|
||||
protected override Beatmap CreateBeatmap()
|
||||
{
|
||||
using (var reader = getZipReader())
|
||||
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu"))))
|
||||
using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal))))
|
||||
using (var beatmapReader = new LineBufferedReader(beatmapStream))
|
||||
return Decoder.GetDecoder<Beatmap>(beatmapReader).Decode(beatmapReader);
|
||||
}
|
||||
|
@ -0,0 +1,169 @@
|
||||
// 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.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.Tournament.Configuration;
|
||||
using osu.Game.Tests;
|
||||
|
||||
namespace osu.Game.Tournament.Tests.NonVisual
|
||||
{
|
||||
[TestFixture]
|
||||
public class CustomTourneyDirectoryTest
|
||||
{
|
||||
[Test]
|
||||
public void TestDefaultDirectory()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost())
|
||||
{
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default")));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomDirectory()
|
||||
{
|
||||
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file.
|
||||
{
|
||||
string osuDesktopStorage = basePath(nameof(TestCustomDirectory));
|
||||
const string custom_tournament = "custom";
|
||||
|
||||
// need access before the game has constructed its own storage yet.
|
||||
Storage storage = new DesktopStorage(osuDesktopStorage, host);
|
||||
// manual cleaning so we can prepare a config file.
|
||||
storage.DeleteDirectory(string.Empty);
|
||||
|
||||
using (var storageConfig = new TournamentStorageManager(storage))
|
||||
storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament);
|
||||
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
|
||||
storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament)));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMigration()
|
||||
{
|
||||
using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration.
|
||||
{
|
||||
string osuRoot = basePath(nameof(TestMigration));
|
||||
string configFile = Path.Combine(osuRoot, "tournament.ini");
|
||||
|
||||
if (File.Exists(configFile))
|
||||
File.Delete(configFile);
|
||||
|
||||
// Recreate the old setup that uses "tournament" as the base path.
|
||||
string oldPath = Path.Combine(osuRoot, "tournament");
|
||||
|
||||
string videosPath = Path.Combine(oldPath, "videos");
|
||||
string modsPath = Path.Combine(oldPath, "mods");
|
||||
string flagsPath = Path.Combine(oldPath, "flags");
|
||||
|
||||
Directory.CreateDirectory(videosPath);
|
||||
Directory.CreateDirectory(modsPath);
|
||||
Directory.CreateDirectory(flagsPath);
|
||||
|
||||
// Define testing files corresponding to the specific file migrations that are needed
|
||||
string bracketFile = Path.Combine(osuRoot, "bracket.json");
|
||||
|
||||
string drawingsConfig = Path.Combine(osuRoot, "drawings.ini");
|
||||
string drawingsFile = Path.Combine(osuRoot, "drawings.txt");
|
||||
string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt");
|
||||
|
||||
// Define sample files to test recursive copying
|
||||
string videoFile = Path.Combine(videosPath, "video.mp4");
|
||||
string modFile = Path.Combine(modsPath, "mod.png");
|
||||
string flagFile = Path.Combine(flagsPath, "flag.png");
|
||||
|
||||
File.WriteAllText(bracketFile, "{}");
|
||||
File.WriteAllText(drawingsConfig, "test");
|
||||
File.WriteAllText(drawingsFile, "test");
|
||||
File.WriteAllText(drawingsResult, "test");
|
||||
File.WriteAllText(videoFile, "test");
|
||||
File.WriteAllText(modFile, "test");
|
||||
File.WriteAllText(flagFile, "test");
|
||||
|
||||
try
|
||||
{
|
||||
var osu = loadOsu(host);
|
||||
|
||||
var storage = osu.Dependencies.Get<Storage>();
|
||||
|
||||
string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default");
|
||||
|
||||
videosPath = Path.Combine(migratedPath, "videos");
|
||||
modsPath = Path.Combine(migratedPath, "mods");
|
||||
flagsPath = Path.Combine(migratedPath, "flags");
|
||||
|
||||
videoFile = Path.Combine(videosPath, "video.mp4");
|
||||
modFile = Path.Combine(modsPath, "mod.png");
|
||||
flagFile = Path.Combine(flagsPath, "flag.png");
|
||||
|
||||
Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath));
|
||||
|
||||
Assert.True(storage.Exists("bracket.json"));
|
||||
Assert.True(storage.Exists("drawings.txt"));
|
||||
Assert.True(storage.Exists("drawings_results.txt"));
|
||||
|
||||
Assert.True(storage.Exists("drawings.ini"));
|
||||
|
||||
Assert.True(storage.Exists(videoFile));
|
||||
Assert.True(storage.Exists(modFile));
|
||||
Assert.True(storage.Exists(flagFile));
|
||||
}
|
||||
finally
|
||||
{
|
||||
host.Storage.Delete("tournament.ini");
|
||||
host.Storage.DeleteDirectory("tournaments");
|
||||
host.Exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private TournamentGameBase loadOsu(GameHost host)
|
||||
{
|
||||
var osu = new TournamentGameBase();
|
||||
Task.Run(() => host.Run(osu));
|
||||
waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time");
|
||||
return osu;
|
||||
}
|
||||
|
||||
private static void waitForOrAssert(Func<bool> result, string failureMessage, int timeout = 90000)
|
||||
{
|
||||
Task task = Task.Run(() =>
|
||||
{
|
||||
while (!result()) Thread.Sleep(200);
|
||||
});
|
||||
|
||||
Assert.IsTrue(task.Wait(timeout), failureMessage);
|
||||
}
|
||||
|
||||
private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance);
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Video;
|
||||
using osu.Framework.Timing;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Tournament.IO;
|
||||
|
||||
namespace osu.Game.Tournament.Components
|
||||
{
|
||||
@ -17,7 +18,6 @@ namespace osu.Game.Tournament.Components
|
||||
private readonly string filename;
|
||||
private readonly bool drawFallbackGradient;
|
||||
private Video video;
|
||||
|
||||
private ManualClock manualClock;
|
||||
|
||||
public TourneyVideo(string filename, bool drawFallbackGradient = false)
|
||||
@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TournamentStorage storage)
|
||||
private void load(TournamentVideoResourceStore storage)
|
||||
{
|
||||
var stream = storage.GetStream($@"videos/{filename}");
|
||||
var stream = storage.GetStream(filename);
|
||||
|
||||
if (stream != null)
|
||||
{
|
||||
|
@ -0,0 +1,23 @@
|
||||
// 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.Configuration;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Tournament.Configuration
|
||||
{
|
||||
public class TournamentStorageManager : IniConfigManager<StorageConfig>
|
||||
{
|
||||
protected override string Filename => "tournament.ini";
|
||||
|
||||
public TournamentStorageManager(Storage storage)
|
||||
: base(storage)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public enum StorageConfig
|
||||
{
|
||||
CurrentTournament,
|
||||
}
|
||||
}
|
72
osu.Game.Tournament/IO/TournamentStorage.cs
Normal file
@ -0,0 +1,72 @@
|
||||
// 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.Logging;
|
||||
using osu.Framework.Platform;
|
||||
using osu.Game.IO;
|
||||
using System.IO;
|
||||
using osu.Game.Tournament.Configuration;
|
||||
|
||||
namespace osu.Game.Tournament.IO
|
||||
{
|
||||
public class TournamentStorage : MigratableStorage
|
||||
{
|
||||
private const string default_tournament = "default";
|
||||
private readonly Storage storage;
|
||||
private readonly TournamentStorageManager storageConfig;
|
||||
|
||||
public TournamentStorage(Storage storage)
|
||||
: base(storage.GetStorageForDirectory("tournaments"), string.Empty)
|
||||
{
|
||||
this.storage = storage;
|
||||
|
||||
storageConfig = new TournamentStorageManager(storage);
|
||||
|
||||
if (storage.Exists("tournament.ini"))
|
||||
{
|
||||
ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get<string>(StorageConfig.CurrentTournament)));
|
||||
}
|
||||
else
|
||||
Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament));
|
||||
|
||||
Logger.Log("Using tournament storage: " + GetFullPath(string.Empty));
|
||||
}
|
||||
|
||||
public override void Migrate(Storage newStorage)
|
||||
{
|
||||
// this migration only happens once on moving to the per-tournament storage system.
|
||||
// listed files are those known at that point in time.
|
||||
// this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19)
|
||||
|
||||
var source = new DirectoryInfo(storage.GetFullPath("tournament"));
|
||||
var destination = new DirectoryInfo(newStorage.GetFullPath("."));
|
||||
|
||||
if (source.Exists)
|
||||
{
|
||||
Logger.Log("Migrating tournament assets to default tournament storage.");
|
||||
CopyRecursive(source, destination);
|
||||
DeleteRecursive(source);
|
||||
}
|
||||
|
||||
moveFileIfExists("bracket.json", destination);
|
||||
moveFileIfExists("drawings.txt", destination);
|
||||
moveFileIfExists("drawings_results.txt", destination);
|
||||
moveFileIfExists("drawings.ini", destination);
|
||||
|
||||
ChangeTargetStorage(newStorage);
|
||||
storageConfig.Set(StorageConfig.CurrentTournament, default_tournament);
|
||||
storageConfig.Save();
|
||||
}
|
||||
|
||||
private void moveFileIfExists(string file, DirectoryInfo destination)
|
||||
{
|
||||
if (!storage.Exists(file))
|
||||
return;
|
||||
|
||||
Logger.Log($"Migrating {file} to default tournament storage.");
|
||||
var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file));
|
||||
AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true));
|
||||
fileInfo.Delete();
|
||||
}
|
||||
}
|
||||
}
|
@ -4,12 +4,12 @@
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Tournament
|
||||
namespace osu.Game.Tournament.IO
|
||||
{
|
||||
internal class TournamentStorage : NamespacedResourceStore<byte[]>
|
||||
public class TournamentVideoResourceStore : NamespacedResourceStore<byte[]>
|
||||
{
|
||||
public TournamentStorage(Storage storage)
|
||||
: base(new StorageBackedResourceStore(storage), "tournament")
|
||||
public TournamentVideoResourceStore(Storage storage)
|
||||
: base(new StorageBackedResourceStore(storage), "videos")
|
||||
{
|
||||
AddExtension("m4v");
|
||||
AddExtension("avi");
|
@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings
|
||||
if (string.IsNullOrEmpty(line))
|
||||
continue;
|
||||
|
||||
if (line.ToUpperInvariant().StartsWith("GROUP"))
|
||||
if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal))
|
||||
continue;
|
||||
|
||||
// ReSharper disable once AccessToModifiedClosure
|
||||
|