mirror of
https://github.com/ppy/osu.git
synced 2026-05-18 04:19:53 +08:00
Compare commits
336 Commits
+1
-1
@@ -10,7 +10,7 @@
|
||||
<EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2023.1227.1" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2024.114.0" />
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<!-- Fody does not handle Android build well, and warns when unchanged.
|
||||
|
||||
+18
-6
@@ -30,12 +30,19 @@ namespace osu.Desktop
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
// Lazer and stable share the same executable name.
|
||||
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// run Squirrel first, as the app may exit after these run
|
||||
/*
|
||||
* WARNING: DO NOT PLACE **ANY** CODE ABOVE THE FOLLOWING BLOCK!
|
||||
*
|
||||
* Logic handling Squirrel MUST run before EVERYTHING if you do not want to break it.
|
||||
* To be more precise: Squirrel is internally using a rather... crude method to determine whether it is running under NUnit,
|
||||
* namely by checking loaded assemblies:
|
||||
* https://github.com/clowd/Clowd.Squirrel/blob/24427217482deeeb9f2cacac555525edfc7bd9ac/src/Squirrel/SimpleSplat/PlatformModeDetector.cs#L17-L32
|
||||
*
|
||||
* If it finds ANY assembly from the ones listed above - REGARDLESS of the reason why it is loaded -
|
||||
* the app will then do completely broken things like:
|
||||
* - not creating system shortcuts (as the logic is if'd out if "running tests")
|
||||
* - not exiting after the install / first-update / uninstall hooks are ran (as the `Environment.Exit()` calls are if'd out if "running tests")
|
||||
*/
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
var windowsVersion = Environment.OSVersion.Version;
|
||||
@@ -59,6 +66,11 @@ namespace osu.Desktop
|
||||
setupSquirrel();
|
||||
}
|
||||
|
||||
// NVIDIA profiles are based on the executable name of a process.
|
||||
// Lazer and stable share the same executable name.
|
||||
// Stable sets this setting to "Off", which may not be what we want, so let's force it back to the default "Auto" on startup.
|
||||
NVAPI.ThreadedOptimisations = NvThreadControlSetting.OGL_THREAD_CONTROL_DEFAULT;
|
||||
|
||||
// Back up the cwd before DesktopGameHost changes it
|
||||
string cwd = Environment.CurrentDirectory;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Scoring;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Scoring;
|
||||
@@ -20,20 +21,73 @@ namespace osu.Game.Rulesets.Catch.Scoring
|
||||
private const int combo_cap = 200;
|
||||
private const double combo_base = 4;
|
||||
|
||||
private double fruitTinyScale;
|
||||
|
||||
public CatchScoreProcessor()
|
||||
: base(new CatchRuleset())
|
||||
{
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
// large ticks are *purposefully* not counted to match stable
|
||||
int fruitTinyScaleDivisor = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) + MaximumResultCounts.GetValueOrDefault(HitResult.Great);
|
||||
fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
}
|
||||
|
||||
protected override double ComputeTotalScore(double comboProgress, double accuracyProgress, double bonusPortion)
|
||||
{
|
||||
return 600000 * comboProgress
|
||||
+ 400000 * Accuracy.Value * accuracyProgress
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)ScoreResultCounts.GetValueOrDefault(HitResult.SmallTickHit) / MaximumResultCounts.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
return comboPortion * comboProgress
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusPortion;
|
||||
}
|
||||
|
||||
public override int GetBaseScoreForResult(HitResult result)
|
||||
{
|
||||
switch (result)
|
||||
{
|
||||
// dirty hack to emulate accuracy on stable weighting every object equally in accuracy portion
|
||||
case HitResult.Great:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SmallTickHit:
|
||||
return 300;
|
||||
|
||||
case HitResult.LargeBonus:
|
||||
return 200;
|
||||
}
|
||||
|
||||
return base.GetBaseScoreForResult(result);
|
||||
}
|
||||
|
||||
protected override double GetComboScoreChange(JudgementResult result)
|
||||
=> GetBaseScoreForResult(result.Type) * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
{
|
||||
double baseIncrease = 0;
|
||||
|
||||
switch (result.Type)
|
||||
{
|
||||
case HitResult.Great:
|
||||
baseIncrease = 300;
|
||||
break;
|
||||
|
||||
case HitResult.LargeTickHit:
|
||||
baseIncrease = 100;
|
||||
break;
|
||||
}
|
||||
|
||||
return baseIncrease * Math.Min(Math.Max(0.5, Math.Log(result.ComboAfterJudgement, combo_base)), Math.Log(combo_cap, combo_base));
|
||||
}
|
||||
|
||||
public override ScoreRank RankFromAccuracy(double accuracy)
|
||||
{
|
||||
|
||||
@@ -1,22 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Mania.Beatmaps;
|
||||
using osu.Game.Rulesets.Mania.Objects;
|
||||
using osu.Game.Rulesets.Mania.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Mania.Scoring;
|
||||
using osu.Game.Rulesets.Mania.Skinning;
|
||||
using osu.Game.Rulesets.Mania.UI.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Rulesets.UI.Scrolling;
|
||||
using osu.Game.Skinning;
|
||||
@@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
private readonly ColumnFlow<Column> columnFlow;
|
||||
|
||||
private readonly JudgementContainer<DrawableManiaJudgement> judgements;
|
||||
private readonly DrawablePool<DrawableManiaJudgement> judgementPool;
|
||||
private readonly JudgementPooler<DrawableManiaJudgement> judgementPooler;
|
||||
|
||||
private readonly Drawable barLineContainer;
|
||||
|
||||
@@ -48,6 +49,8 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
private readonly int firstColumnIndex;
|
||||
|
||||
private ISkinSource currentSkin = null!;
|
||||
|
||||
public Stage(int firstColumnIndex, StageDefinition definition, ref ManiaAction normalColumnStartAction, ref ManiaAction specialColumnStartAction)
|
||||
{
|
||||
this.firstColumnIndex = firstColumnIndex;
|
||||
@@ -65,7 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
InternalChildren = new Drawable[]
|
||||
{
|
||||
judgementPool = new DrawablePool<DrawableManiaJudgement>(2),
|
||||
new Container
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
@@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
{
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
},
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground), _ => null)
|
||||
new SkinnableDrawable(new ManiaSkinComponentLookup(ManiaSkinComponents.StageForeground))
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
},
|
||||
@@ -137,11 +139,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
AddNested(column);
|
||||
}
|
||||
|
||||
var hitWindows = new ManiaHitWindows();
|
||||
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableManiaJudgement>(Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r))));
|
||||
|
||||
RegisterPool<BarLine, DrawableBarLine>(50, 200);
|
||||
}
|
||||
|
||||
private ISkinSource currentSkin;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
@@ -170,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
if (currentSkin != null)
|
||||
if (currentSkin.IsNotNull())
|
||||
currentSkin.SourceChanged -= onSkinChanged;
|
||||
}
|
||||
|
||||
@@ -196,13 +200,13 @@ namespace osu.Game.Rulesets.Mania.UI
|
||||
return;
|
||||
|
||||
judgements.Clear(false);
|
||||
judgements.Add(judgementPool.Get(j =>
|
||||
judgements.Add(judgementPooler.Get(result.Type, j =>
|
||||
{
|
||||
j.Apply(result, judgedObject);
|
||||
|
||||
j.Anchor = Anchor.Centre;
|
||||
j.Origin = Anchor.Centre;
|
||||
}));
|
||||
})!);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
// 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.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.Replays;
|
||||
using osu.Game.Rulesets.Replays;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
{
|
||||
@@ -21,5 +31,129 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
|
||||
[Test]
|
||||
public void TestComboBasedSize([Values] bool comboBasedSize) => CreateModTest(new ModTestData { Mod = new OsuModFlashlight { ComboBasedSize = { Value = comboBasedSize } }, PassCondition = () => true });
|
||||
|
||||
[Test]
|
||||
public void TestSliderDimsOnlyAfterStartTime()
|
||||
{
|
||||
bool sliderDimmedBeforeStartTime = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmedBeforeStartTime |=
|
||||
Player.GameplayClockContainer.CurrentTime < 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && !sliderDimmedBeforeStartTime;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new HitCircle { StartTime = 0, },
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
BeatmapInfo =
|
||||
{
|
||||
StackLeniency = 0,
|
||||
}
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(0, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(990, new Vector2()),
|
||||
new OsuReplayFrame(1000, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitEarly()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(990, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSliderDoesDimAfterStartTimeIfHitLate()
|
||||
{
|
||||
bool sliderDimmed = false;
|
||||
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new OsuModFlashlight(),
|
||||
PassCondition = () =>
|
||||
{
|
||||
sliderDimmed |=
|
||||
Player.GameplayClockContainer.CurrentTime >= 1000 && Player.ChildrenOfType<ModFlashlight<OsuHitObject>.Flashlight>().Single().FlashlightDim > 0;
|
||||
return Player.GameplayState.HasPassed && sliderDimmed;
|
||||
},
|
||||
Beatmap = new OsuBeatmap
|
||||
{
|
||||
HitObjects = new List<OsuHitObject>
|
||||
{
|
||||
new Slider
|
||||
{
|
||||
StartTime = 1000,
|
||||
Path = new SliderPath(new[]
|
||||
{
|
||||
new PathControlPoint(),
|
||||
new PathControlPoint(new Vector2(100))
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
ReplayFrames = new List<ReplayFrame>
|
||||
{
|
||||
new OsuReplayFrame(1100, new Vector2(), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2000, new Vector2(100), OsuAction.LeftButton),
|
||||
new OsuReplayFrame(2001, new Vector2(100)),
|
||||
},
|
||||
Autoplay = false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
public void TestUserAlreadyHasTouchDeviceActive()
|
||||
{
|
||||
loadPlayer();
|
||||
// it is presumed that a previous screen (i.e. song select) will set this up
|
||||
AddStep("set up touchscreen user", () =>
|
||||
{
|
||||
currentPlayer.Score.ScoreInfo.Mods = currentPlayer.Score.ScoreInfo.Mods.Append(new OsuModTouchDevice()).ToArray();
|
||||
@@ -69,6 +68,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods
|
||||
AddAssert("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchActivePriorToPlayerLoad()
|
||||
{
|
||||
AddStep("set touch input active", () => statics.SetValue(Static.TouchInputActive, true));
|
||||
loadPlayer();
|
||||
AddUntilStep("touch device mod activated", () => currentPlayer.Score.ScoreInfo.Mods, () => Has.One.InstanceOf<OsuModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchDuringBreak()
|
||||
{
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
@@ -62,13 +63,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
int difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
|
||||
scoreMultiplier = difficultyPeppyStars;
|
||||
scoreMultiplier = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
+2
-2
@@ -51,10 +51,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
base.LoadComplete();
|
||||
|
||||
hitObjectPosition = hitObject.PositionBindable.GetBoundCopy();
|
||||
hitObjectPosition.BindValueChanged(_ => updateConnectingPath());
|
||||
hitObjectPosition.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
pathVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => updateConnectingPath());
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(updateConnectingPath));
|
||||
|
||||
updateConnectingPath();
|
||||
}
|
||||
|
||||
@@ -4,20 +4,15 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
@@ -41,8 +36,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
public Action<DragEvent> DragInProgress;
|
||||
public Action DragEnded;
|
||||
|
||||
public List<PathControlPoint> PointsInSegment;
|
||||
|
||||
public readonly BindableBool IsSelected = new BindableBool();
|
||||
public readonly PathControlPoint ControlPoint;
|
||||
|
||||
@@ -56,27 +49,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
private IBindable<Vector2> hitObjectPosition;
|
||||
private IBindable<float> hitObjectScale;
|
||||
|
||||
[UsedImplicitly]
|
||||
private readonly IBindable<int> hitObjectVersion;
|
||||
|
||||
public PathControlPointPiece(T hitObject, PathControlPoint controlPoint)
|
||||
{
|
||||
this.hitObject = hitObject;
|
||||
ControlPoint = controlPoint;
|
||||
|
||||
// we don't want to run the path type update on construction as it may inadvertently change the hit object.
|
||||
cachePoints(hitObject);
|
||||
|
||||
hitObjectVersion = hitObject.Path.Version.GetBoundCopy();
|
||||
|
||||
// schedule ensure that updates are only applied after all operations from a single frame are applied.
|
||||
// this avoids inadvertently changing the hit object path type for batch operations.
|
||||
hitObjectVersion.BindValueChanged(_ => Scheduler.AddOnce(() =>
|
||||
{
|
||||
cachePoints(hitObject);
|
||||
updatePathType();
|
||||
}));
|
||||
|
||||
controlPoint.Changed += updateMarkerDisplay;
|
||||
|
||||
Origin = Anchor.Centre;
|
||||
@@ -214,28 +191,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
|
||||
protected override void OnDragEnd(DragEndEvent e) => DragEnded?.Invoke();
|
||||
|
||||
private void cachePoints(T hitObject) => PointsInSegment = hitObject.Path.PointsInSegment(ControlPoint);
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
private void updatePathType()
|
||||
{
|
||||
if (ControlPoint.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (PointsInSegment.Count > 3)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
|
||||
if (PointsInSegment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = PointsInSegment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
ControlPoint.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the state of the circular control point marker.
|
||||
/// </summary>
|
||||
|
||||
+54
-3
@@ -14,10 +14,12 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Bindings;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -76,6 +78,50 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
controlPoints.BindTo(hitObject.Path.ControlPoints);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Handles correction of invalid path types.
|
||||
/// </summary>
|
||||
public void EnsureValidPathTypes()
|
||||
{
|
||||
List<PathControlPoint> pointsInCurrentSegment = new List<PathControlPoint>();
|
||||
|
||||
foreach (var controlPoint in controlPoints)
|
||||
{
|
||||
if (controlPoint.Type != null)
|
||||
{
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
pointsInCurrentSegment.Clear();
|
||||
}
|
||||
|
||||
pointsInCurrentSegment.Add(controlPoint);
|
||||
}
|
||||
|
||||
ensureValidPathType(pointsInCurrentSegment);
|
||||
}
|
||||
|
||||
private void ensureValidPathType(IReadOnlyList<PathControlPoint> segment)
|
||||
{
|
||||
if (segment.Count == 0)
|
||||
return;
|
||||
|
||||
var first = segment[0];
|
||||
|
||||
if (first.Type != PathType.PERFECT_CURVE)
|
||||
return;
|
||||
|
||||
if (segment.Count > 3)
|
||||
first.Type = PathType.BEZIER;
|
||||
|
||||
if (segment.Count != 3)
|
||||
return;
|
||||
|
||||
ReadOnlySpan<Vector2> points = segment.Select(p => p.Position).ToArray();
|
||||
RectangleF boundingBox = PathApproximator.CircularArcBoundingBox(points);
|
||||
if (boundingBox.Width >= 640 || boundingBox.Height >= 480)
|
||||
first.Type = PathType.BEZIER;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the <see cref="PathControlPointPiece{T}"/> corresponding to the given <paramref name="pathControlPoint"/>,
|
||||
/// and deselects all other <see cref="PathControlPointPiece{T}"/>s.
|
||||
@@ -240,7 +286,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
/// <param name="type">The path type we want to assign to the given control point piece.</param>
|
||||
private void updatePathType(PathControlPointPiece<T> piece, PathType? type)
|
||||
{
|
||||
int indexInSegment = piece.PointsInSegment.IndexOf(piece.ControlPoint);
|
||||
var pointsInSegment = hitObject.Path.PointsInSegment(piece.ControlPoint);
|
||||
int indexInSegment = pointsInSegment.IndexOf(piece.ControlPoint);
|
||||
|
||||
if (type?.Type == SplineType.PerfectCurve)
|
||||
{
|
||||
@@ -249,8 +296,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// and one segment of the previous type.
|
||||
int thirdPointIndex = indexInSegment + 2;
|
||||
|
||||
if (piece.PointsInSegment.Count > thirdPointIndex + 1)
|
||||
piece.PointsInSegment[thirdPointIndex].Type = piece.PointsInSegment[0].Type;
|
||||
if (pointsInSegment.Count > thirdPointIndex + 1)
|
||||
pointsInSegment[thirdPointIndex].Type = pointsInSegment[0].Type;
|
||||
}
|
||||
|
||||
hitObject.Path.ExpectedDistance.Value = null;
|
||||
@@ -339,6 +386,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
// Maintain the path types in case they got defaulted to bezier at some point during the drag.
|
||||
for (int i = 0; i < hitObject.Path.ControlPoints.Count; i++)
|
||||
hitObject.Path.ControlPoints[i].Type = dragPathTypes[i];
|
||||
|
||||
EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
public void DragEnded() => changeHandler?.EndChange();
|
||||
@@ -412,6 +461,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
{
|
||||
foreach (var p in Pieces.Where(p => p.IsSelected.Value))
|
||||
updatePathType(p, type);
|
||||
|
||||
EnsureValidPathTypes();
|
||||
});
|
||||
|
||||
if (countOfState == totalCount)
|
||||
|
||||
@@ -267,6 +267,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
segmentStart.Type = PathType.BEZIER;
|
||||
break;
|
||||
}
|
||||
|
||||
controlPointVisualiser.EnsureValidPathTypes();
|
||||
}
|
||||
|
||||
private void updateCursor()
|
||||
|
||||
@@ -254,6 +254,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
// Move the control points from the insertion index onwards to make room for the insertion
|
||||
controlPoints.Insert(insertionIndex, pathControlPoint);
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
return pathControlPoint;
|
||||
@@ -275,6 +277,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders
|
||||
controlPoints.Remove(c);
|
||||
}
|
||||
|
||||
ControlPointVisualiser?.EnsureValidPathTypes();
|
||||
|
||||
// Snap the slider to the current beat divisor before checking length validity.
|
||||
HitObject.SnapTo(distanceSnapProvider);
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
|
||||
{
|
||||
if (drawable is DrawableSlider s)
|
||||
s.Tracking.ValueChanged += flashlight.OnSliderTrackingChange;
|
||||
s.OnUpdate += _ => flashlight.OnSliderTrackingChange(s);
|
||||
}
|
||||
|
||||
private partial class OsuFlashlight : Flashlight, IRequireHighFrequencyMousePosition
|
||||
@@ -66,10 +66,10 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
FlashlightSmoothness = 1.4f;
|
||||
}
|
||||
|
||||
public void OnSliderTrackingChange(ValueChangedEvent<bool> e)
|
||||
public void OnSliderTrackingChange(DrawableSlider e)
|
||||
{
|
||||
// If a slider is in a tracking state, a further dim should be applied to the (remaining) visible portion of the playfield.
|
||||
FlashlightDim = e.NewValue ? 0.8f : 0.0f;
|
||||
FlashlightDim = Time.Current >= e.HitObject.StartTime && e.Tracking.Value ? 0.8f : 0.0f;
|
||||
}
|
||||
|
||||
protected override bool OnMouseMove(MouseMoveEvent e)
|
||||
|
||||
@@ -11,6 +11,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Layout;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -66,6 +67,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
private Container<DrawableSliderRepeat> repeatContainer;
|
||||
private PausableSkinnableSound slidingSample;
|
||||
|
||||
private readonly LayoutValue drawSizeLayout;
|
||||
|
||||
public DrawableSlider()
|
||||
: this(null)
|
||||
{
|
||||
@@ -82,6 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
AlwaysPresent = true,
|
||||
Alpha = 0
|
||||
};
|
||||
AddLayout(drawSizeLayout = new LayoutValue(Invalidation.DrawSize | Invalidation.MiscGeometry));
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -240,27 +244,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
else if (slidingSample.IsPlaying)
|
||||
slidingSample.Stop();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildren()
|
||||
{
|
||||
base.UpdateAfterChildren();
|
||||
|
||||
// During slider path editing, the PlaySliderBody is scheduled to refresh once on Update.
|
||||
// It is crucial to perform the code below in UpdateAfterChildren. This ensures that the SliderBody has the opportunity
|
||||
// to update its Size and PathOffset beforehand, ensuring correct placement.
|
||||
|
||||
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
|
||||
|
||||
Ball.UpdateProgress(completionProgress);
|
||||
SliderBody?.UpdateProgress(HeadCircle.IsHit ? completionProgress : 0);
|
||||
|
||||
foreach (DrawableHitObject hitObject in NestedHitObjects)
|
||||
{
|
||||
if (hitObject is ITrackSnaking s)
|
||||
s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
}
|
||||
foreach (DrawableSliderRepeat repeat in repeatContainer)
|
||||
repeat.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
|
||||
|
||||
Size = SliderBody?.Size ?? Vector2.Zero;
|
||||
OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
|
||||
|
||||
if (DrawSize != Vector2.Zero)
|
||||
if (!drawSizeLayout.IsValid)
|
||||
{
|
||||
var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize);
|
||||
Vector2 pos = Vector2.Divide(OriginPosition, DrawSize);
|
||||
foreach (var obj in NestedHitObjects)
|
||||
obj.RelativeAnchorPosition = childAnchorPosition;
|
||||
Ball.RelativeAnchorPosition = childAnchorPosition;
|
||||
obj.RelativeAnchorPosition = pos;
|
||||
Ball.RelativeAnchorPosition = pos;
|
||||
|
||||
drawSizeLayout.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking
|
||||
public partial class DrawableSliderRepeat : DrawableOsuHitObject
|
||||
{
|
||||
public new SliderRepeat HitObject => (SliderRepeat)base.HitObject;
|
||||
|
||||
|
||||
@@ -1,15 +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 osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Objects.Drawables
|
||||
{
|
||||
/// <summary>
|
||||
/// A component which tracks the current end snaking position of a slider.
|
||||
/// </summary>
|
||||
public interface ITrackSnaking
|
||||
{
|
||||
void UpdateSnakingPosition(Vector2 start, Vector2 end);
|
||||
}
|
||||
}
|
||||
@@ -98,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
set
|
||||
{
|
||||
repeatCount = value;
|
||||
updateNestedPositions();
|
||||
endPositionCache.Invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +165,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
public Slider()
|
||||
{
|
||||
SamplesBindable.CollectionChanged += (_, _) => UpdateNestedSamples();
|
||||
Path.Version.ValueChanged += _ => updateNestedPositions();
|
||||
Path.Version.ValueChanged += _ => endPositionCache.Invalidate();
|
||||
}
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
|
||||
@@ -14,16 +14,16 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
/// </summary>
|
||||
public abstract class SliderEndCircle : HitCircle
|
||||
{
|
||||
private readonly Slider slider;
|
||||
protected readonly Slider Slider;
|
||||
|
||||
protected SliderEndCircle(Slider slider)
|
||||
{
|
||||
this.slider = slider;
|
||||
Slider = slider;
|
||||
}
|
||||
|
||||
public int RepeatIndex { get; set; }
|
||||
|
||||
public double SpanDuration => slider.SpanDuration;
|
||||
public double SpanDuration => Slider.SpanDuration;
|
||||
|
||||
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, IBeatmapDifficultyInfo difficulty)
|
||||
{
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects
|
||||
else
|
||||
{
|
||||
// The first end circle should fade in with the slider.
|
||||
TimePreempt += StartTime - slider.StartTime;
|
||||
TimePreempt += StartTime - Slider.StartTime;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Argon
|
||||
{
|
||||
public partial class ArgonCursor : OsuCursorSprite
|
||||
public partial class ArgonCursor : SkinnableCursor
|
||||
{
|
||||
public ArgonCursor()
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
ScaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true);
|
||||
|
||||
pathVersion = drawableSlider.PathVersion.GetBoundCopy();
|
||||
pathVersion.BindValueChanged(_ => Refresh());
|
||||
pathVersion.BindValueChanged(_ => Scheduler.AddOnce(Refresh));
|
||||
|
||||
AccentColourBindable = drawableObject.AccentColour.GetBoundCopy();
|
||||
AccentColourBindable.BindValueChanged(accent => AccentColour = GetBodyAccentColour(skin, accent.NewValue), true);
|
||||
|
||||
@@ -15,6 +15,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default
|
||||
{
|
||||
TriangleScale = 1.2f;
|
||||
HideAlphaDiscrepancies = false;
|
||||
ClampToDrawable = false;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
|
||||
@@ -9,8 +9,11 @@ using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
{
|
||||
public partial class LegacyCursor : OsuCursorSprite
|
||||
public partial class LegacyCursor : SkinnableCursor
|
||||
{
|
||||
private const float pressed_scale = 1.3f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
private readonly ISkin skin;
|
||||
private bool spin;
|
||||
|
||||
@@ -51,5 +54,16 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
if (spin)
|
||||
ExpandTarget.Spin(10000, RotationDirection.Clockwise);
|
||||
}
|
||||
|
||||
public override void Expand()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale)
|
||||
.ScaleTo(pressed_scale, 100, Easing.Out);
|
||||
}
|
||||
|
||||
public override void Contract()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale, 100, Easing.Out);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation;
|
||||
|
||||
float turnRatio = spinningMiddle.Texture != null ? 0.5f : 1;
|
||||
discTop.Rotation = DrawableSpinner.RotationTracker.Rotation * turnRatio;
|
||||
spinningMiddle.Rotation = DrawableSpinner.RotationTracker.Rotation;
|
||||
|
||||
discBottom.Rotation = discTop.Rotation / 3;
|
||||
|
||||
glow.Alpha = DrawableSpinner.Progress;
|
||||
|
||||
@@ -33,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
private void load(OsuRulesetConfigManager? rulesetConfig)
|
||||
{
|
||||
rulesetConfig?.BindWith(OsuRulesetSetting.ShowCursorRipples, showRipples);
|
||||
|
||||
AddInternal(ripplePool);
|
||||
}
|
||||
|
||||
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
|
||||
|
||||
@@ -24,15 +24,12 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public const float SIZE = 28;
|
||||
|
||||
private const float pressed_scale = 1.2f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
private bool cursorExpand;
|
||||
|
||||
private SkinnableDrawable cursorSprite;
|
||||
private Container cursorScaleContainer = null!;
|
||||
|
||||
private Drawable expandTarget => (cursorSprite.Drawable as OsuCursorSprite)?.ExpandTarget ?? cursorSprite;
|
||||
private SkinnableCursor skinnableCursor => (SkinnableCursor)cursorSprite.Drawable;
|
||||
|
||||
public IBindable<float> CursorScale => cursorScale;
|
||||
|
||||
@@ -57,17 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
InternalChild = cursorScaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
}
|
||||
};
|
||||
InternalChild = CreateCursorContent();
|
||||
|
||||
userCursorScale = config.GetBindable<float>(OsuSetting.GameplayCursorSize);
|
||||
userCursorScale.ValueChanged += _ => cursorScale.Value = CalculateCursorScale();
|
||||
@@ -84,6 +71,18 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
cursorScale.Value = CalculateCursorScale();
|
||||
}
|
||||
|
||||
protected virtual Drawable CreateCursorContent() => cursorScaleContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = cursorSprite = new SkinnableDrawable(new OsuSkinComponentLookup(OsuSkinComponents.Cursor), _ => new DefaultCursor(), confineMode: ConfineMode.NoScaling)
|
||||
{
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
},
|
||||
};
|
||||
|
||||
protected virtual float CalculateCursorScale()
|
||||
{
|
||||
float scale = userCursorScale.Value;
|
||||
@@ -106,10 +105,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
if (!cursorExpand) return;
|
||||
|
||||
expandTarget.ScaleTo(released_scale).ScaleTo(pressed_scale, 400, Easing.OutElasticHalf);
|
||||
skinnableCursor.Expand();
|
||||
}
|
||||
|
||||
public void Contract() => expandTarget.ScaleTo(released_scale, 400, Easing.OutQuad);
|
||||
public void Contract() => skinnableCursor.Contract();
|
||||
|
||||
/// <summary>
|
||||
/// Get the scale applicable to the ActiveCursor based on a beatmap's circle size.
|
||||
@@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
public static float GetScaleForCircleSize(float circleSize) =>
|
||||
1f - 0.7f * (1f + circleSize - BeatmapDifficulty.DEFAULT_DIFFICULTY) / BeatmapDifficulty.DEFAULT_DIFFICULTY;
|
||||
|
||||
private partial class DefaultCursor : OsuCursorSprite
|
||||
private partial class DefaultCursor : SkinnableCursor
|
||||
{
|
||||
public DefaultCursor()
|
||||
{
|
||||
|
||||
@@ -1,19 +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.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public abstract partial class OsuCursorSprite : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// The an optional piece of the cursor to expand when in a clicked state.
|
||||
/// If null, the whole cursor will be affected by expansion.
|
||||
/// </summary>
|
||||
public Drawable ExpandTarget { get; protected set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// 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.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.UI.Cursor
|
||||
{
|
||||
public abstract partial class SkinnableCursor : CompositeDrawable
|
||||
{
|
||||
private const float pressed_scale = 1.2f;
|
||||
private const float released_scale = 1f;
|
||||
|
||||
public virtual void Expand()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale)
|
||||
.ScaleTo(pressed_scale, 400, Easing.OutElasticHalf);
|
||||
}
|
||||
|
||||
public virtual void Contract()
|
||||
{
|
||||
ExpandTarget?.ScaleTo(released_scale, 400, Easing.OutQuad);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The an optional piece of the cursor to expand when in a clicked state.
|
||||
/// If null, the whole cursor will be affected by expansion.
|
||||
/// </summary>
|
||||
public Drawable? ExpandTarget { get; protected set; }
|
||||
}
|
||||
}
|
||||
@@ -4,13 +4,11 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
@@ -35,6 +33,8 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
private readonly ProxyContainer spinnerProxies;
|
||||
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
|
||||
|
||||
private readonly JudgementPooler<DrawableOsuJudgement> judgementPooler;
|
||||
|
||||
public SmokeContainer Smoke { get; }
|
||||
public FollowPointRenderer FollowPoints { get; }
|
||||
|
||||
@@ -42,8 +42,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer();
|
||||
|
||||
private readonly IDictionary<HitResult, DrawablePool<DrawableOsuJudgement>> poolDictionary = new Dictionary<HitResult, DrawablePool<DrawableOsuJudgement>>();
|
||||
|
||||
private readonly Container judgementAboveHitObjectLayer;
|
||||
|
||||
public OsuPlayfield()
|
||||
@@ -65,24 +63,15 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
|
||||
HitPolicy = new StartTimeOrderedHitPolicy();
|
||||
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r =>
|
||||
{
|
||||
switch (r)
|
||||
{
|
||||
case HitResult.Great:
|
||||
case HitResult.Ok:
|
||||
case HitResult.Meh:
|
||||
case HitResult.Miss:
|
||||
case HitResult.LargeTickMiss:
|
||||
case HitResult.IgnoreMiss:
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}))
|
||||
poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgementLoaded));
|
||||
|
||||
AddRangeInternal(poolDictionary.Values);
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableOsuJudgement>(new[]
|
||||
{
|
||||
HitResult.Great,
|
||||
HitResult.Ok,
|
||||
HitResult.Meh,
|
||||
HitResult.Miss,
|
||||
HitResult.LargeTickMiss,
|
||||
HitResult.IgnoreMiss,
|
||||
}, onJudgementLoaded));
|
||||
|
||||
NewResult += onNewResult;
|
||||
}
|
||||
@@ -182,10 +171,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!judgedObject.DisplayResult || !DisplayJudgements.Value)
|
||||
return;
|
||||
|
||||
if (!poolDictionary.TryGetValue(result.Type, out var pool))
|
||||
return;
|
||||
var explosion = judgementPooler.Get(result.Type, doj => doj.Apply(result, judgedObject));
|
||||
|
||||
DrawableOsuJudgement explosion = pool.Get(doj => doj.Apply(result, judgedObject));
|
||||
if (explosion == null)
|
||||
return;
|
||||
|
||||
judgementLayer.Add(explosion);
|
||||
|
||||
@@ -201,31 +190,6 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public void Add(Drawable proxy) => AddInternal(proxy);
|
||||
}
|
||||
|
||||
private partial class DrawableJudgementPool : DrawablePool<DrawableOsuJudgement>
|
||||
{
|
||||
private readonly HitResult result;
|
||||
private readonly Action<DrawableOsuJudgement> onLoaded;
|
||||
|
||||
public DrawableJudgementPool(HitResult result, Action<DrawableOsuJudgement> onLoaded)
|
||||
: base(20)
|
||||
{
|
||||
this.result = result;
|
||||
this.onLoaded = onLoaded;
|
||||
}
|
||||
|
||||
protected override DrawableOsuJudgement CreateNewDrawable()
|
||||
{
|
||||
var judgement = base.CreateNewDrawable();
|
||||
|
||||
// just a placeholder to initialise the correct drawable hierarchy for this pool.
|
||||
judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null);
|
||||
|
||||
onLoaded?.Invoke(judgement);
|
||||
|
||||
return judgement;
|
||||
}
|
||||
}
|
||||
|
||||
private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry
|
||||
{
|
||||
public OsuHitObjectLifetimeEntry(HitObject hitObject)
|
||||
|
||||
@@ -65,17 +65,24 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public override bool HandlePositionalInput => true;
|
||||
|
||||
public Action ResumeRequested;
|
||||
private Container scaleTransitionContainer;
|
||||
|
||||
public OsuClickToResumeCursor()
|
||||
{
|
||||
RelativePositionAxes = Axes.Both;
|
||||
}
|
||||
|
||||
protected override float CalculateCursorScale()
|
||||
protected override Container CreateCursorContent() => scaleTransitionContainer = new Container
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Origin = Anchor.Centre,
|
||||
Anchor = Anchor.Centre,
|
||||
Child = base.CreateCursorContent(),
|
||||
};
|
||||
|
||||
protected override float CalculateCursorScale() =>
|
||||
// Force minimum cursor size so it's easily clickable
|
||||
return Math.Max(1f, base.CalculateCursorScale());
|
||||
}
|
||||
Math.Max(1f, base.CalculateCursorScale());
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
@@ -98,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
if (!IsHovered)
|
||||
return false;
|
||||
|
||||
this.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||
scaleTransitionContainer.ScaleTo(2, TRANSITION_TIME, Easing.OutQuint);
|
||||
|
||||
ResumeRequested?.Invoke();
|
||||
return true;
|
||||
@@ -114,7 +121,10 @@ namespace osu.Game.Rulesets.Osu.UI
|
||||
public void Appear() => Schedule(() =>
|
||||
{
|
||||
updateColour();
|
||||
this.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
|
||||
|
||||
// importantly, we perform the scale transition on an underlying container rather than the whole cursor
|
||||
// to prevent attempts of abuse by the scale change in the cursor's hitbox (see: https://github.com/ppy/osu/issues/26477).
|
||||
scaleTransitionContainer.ScaleTo(4).Then().ScaleTo(1, 1000, Easing.OutQuint);
|
||||
});
|
||||
|
||||
private void updateColour()
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// 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.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko.Beatmaps;
|
||||
using osu.Game.Rulesets.Taiko.Judgements;
|
||||
using osu.Game.Rulesets.Taiko.Objects;
|
||||
using osu.Game.Rulesets.Taiko.Scoring;
|
||||
|
||||
namespace osu.Game.Rulesets.Taiko.Tests
|
||||
{
|
||||
[TestFixture]
|
||||
public class TaikoHealthProcessorTest
|
||||
{
|
||||
[Test]
|
||||
public void TestHitsOnlyGreat()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsAboveThreshold()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Great });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.GreaterThan(0.5));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsBelowThreshold()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new Hit(),
|
||||
new Hit { StartTime = 1000 },
|
||||
new Hit { StartTime = 2000 },
|
||||
new Hit { StartTime = 3000 },
|
||||
new Hit { StartTime = 4000 },
|
||||
}
|
||||
};
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[1], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[2], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[3], new TaikoJudgement()) { Type = HitResult.Ok });
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[4], new TaikoJudgement()) { Type = HitResult.Miss });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.LessThan(0.5));
|
||||
Assert.That(healthProcessor.HasFailed, Is.True);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDrumRollOnly()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSwellOnly()
|
||||
{
|
||||
var beatmap = new TaikoBeatmap
|
||||
{
|
||||
HitObjects =
|
||||
{
|
||||
new DrumRoll { Duration = 2000 }
|
||||
}
|
||||
};
|
||||
|
||||
foreach (var ho in beatmap.HitObjects)
|
||||
ho.ApplyDefaults(beatmap.ControlPointInfo, beatmap.Difficulty);
|
||||
|
||||
var healthProcessor = new TaikoHealthProcessor();
|
||||
healthProcessor.ApplyBeatmap(beatmap);
|
||||
|
||||
foreach (var nested in beatmap.HitObjects[0].NestedHitObjects)
|
||||
{
|
||||
var nestedJudgement = nested.CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(nested, nestedJudgement) { Type = nestedJudgement.MaxResult });
|
||||
}
|
||||
|
||||
var judgement = beatmap.HitObjects[0].CreateJudgement();
|
||||
healthProcessor.ApplyResult(new JudgementResult(beatmap.HitObjects[0], judgement) { Type = judgement.MaxResult });
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(healthProcessor.Health.Value, Is.EqualTo(1));
|
||||
Assert.That(healthProcessor.HasFailed, Is.False);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using System.Linq;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Legacy;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Scoring.Legacy;
|
||||
@@ -65,11 +66,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty
|
||||
drainLength = ((int)Math.Round(baseBeatmap.HitObjects[^1].StartTime) - (int)Math.Round(baseBeatmap.HitObjects[0].StartTime) - breakLength) / 1000;
|
||||
}
|
||||
|
||||
difficultyPeppyStars = (int)Math.Round(
|
||||
(baseBeatmap.Difficulty.DrainRate
|
||||
+ baseBeatmap.Difficulty.OverallDifficulty
|
||||
+ baseBeatmap.Difficulty.CircleSize
|
||||
+ Math.Clamp((float)objectCount / drainLength * 8, 0, 16)) / 38 * 5);
|
||||
difficultyPeppyStars = LegacyRulesetExtensions.CalculateDifficultyPeppyStars(baseBeatmap.Difficulty, objectCount, drainLength);
|
||||
|
||||
LegacyScoreAttributes attributes = new LegacyScoreAttributes();
|
||||
|
||||
|
||||
@@ -31,11 +31,39 @@ namespace osu.Game.Rulesets.Taiko.Scoring
|
||||
/// </summary>
|
||||
private double hpMissMultiplier;
|
||||
|
||||
/// <summary>
|
||||
/// Sum of all achievable health increases throughout the map.
|
||||
/// Used to determine if there are any objects that give health.
|
||||
/// If there are none, health will be forcibly pulled up to 1 to avoid cases of impassable maps.
|
||||
/// </summary>
|
||||
private double sumOfMaxHealthIncreases;
|
||||
|
||||
public TaikoHealthProcessor()
|
||||
: base(0.5)
|
||||
{
|
||||
}
|
||||
|
||||
protected override void ApplyResultInternal(JudgementResult result)
|
||||
{
|
||||
base.ApplyResultInternal(result);
|
||||
sumOfMaxHealthIncreases += result.Judgement.MaxHealthIncrease;
|
||||
}
|
||||
|
||||
protected override void RevertResultInternal(JudgementResult result)
|
||||
{
|
||||
base.RevertResultInternal(result);
|
||||
sumOfMaxHealthIncreases -= result.Judgement.MaxHealthIncrease;
|
||||
}
|
||||
|
||||
protected override void Reset(bool storeResults)
|
||||
{
|
||||
base.Reset(storeResults);
|
||||
|
||||
if (storeResults && sumOfMaxHealthIncreases == 0)
|
||||
Health.Value = 1;
|
||||
sumOfMaxHealthIncreases = 0;
|
||||
}
|
||||
|
||||
public override void ApplyBeatmap(IBeatmap beatmap)
|
||||
{
|
||||
base.ApplyBeatmap(beatmap);
|
||||
|
||||
@@ -69,9 +69,9 @@ namespace osu.Game.Rulesets.Taiko
|
||||
|
||||
public override IEnumerable<KeyBinding> GetDefaultKeyBindings(int variant = 0) => new[]
|
||||
{
|
||||
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.MouseRight, TaikoAction.LeftRim),
|
||||
new KeyBinding(InputKey.D, TaikoAction.LeftRim),
|
||||
new KeyBinding(InputKey.MouseLeft, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.F, TaikoAction.LeftCentre),
|
||||
new KeyBinding(InputKey.J, TaikoAction.RightCentre),
|
||||
new KeyBinding(InputKey.K, TaikoAction.RightRim),
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -10,7 +8,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Pooling;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
@@ -42,29 +39,29 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
public Container UnderlayElements { get; private set; } = null!;
|
||||
|
||||
private Container<HitExplosion> hitExplosionContainer;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer;
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer;
|
||||
private ScrollingHitObjectContainer drumRollHitContainer;
|
||||
internal Drawable HitTarget;
|
||||
private SkinnableDrawable mascot;
|
||||
private Container<HitExplosion> hitExplosionContainer = null!;
|
||||
private Container<KiaiHitExplosion> kiaiExplosionContainer = null!;
|
||||
private JudgementContainer<DrawableTaikoJudgement> judgementContainer = null!;
|
||||
private ScrollingHitObjectContainer drumRollHitContainer = null!;
|
||||
internal Drawable HitTarget = null!;
|
||||
private SkinnableDrawable mascot = null!;
|
||||
|
||||
private readonly IDictionary<HitResult, DrawablePool<DrawableTaikoJudgement>> judgementPools = new Dictionary<HitResult, DrawablePool<DrawableTaikoJudgement>>();
|
||||
private JudgementPooler<DrawableTaikoJudgement> judgementPooler = null!;
|
||||
private readonly IDictionary<HitResult, HitExplosionPool> explosionPools = new Dictionary<HitResult, HitExplosionPool>();
|
||||
|
||||
private ProxyContainer topLevelHitContainer;
|
||||
private InputDrum inputDrum;
|
||||
private Container rightArea;
|
||||
private ProxyContainer topLevelHitContainer = null!;
|
||||
private InputDrum inputDrum = null!;
|
||||
private Container rightArea = null!;
|
||||
|
||||
/// <remarks>
|
||||
/// <see cref="Playfield.AddNested"/> is purposefully not called on this to prevent i.e. being able to interact
|
||||
/// with bar lines in the editor.
|
||||
/// </remarks>
|
||||
private BarLinePlayfield barLinePlayfield;
|
||||
private BarLinePlayfield barLinePlayfield = null!;
|
||||
|
||||
private Container barLineContent;
|
||||
private Container hitObjectContent;
|
||||
private Container overlayContent;
|
||||
private Container barLineContent = null!;
|
||||
private Container hitObjectContent = null!;
|
||||
private Container overlayContent = null!;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colours)
|
||||
@@ -202,13 +199,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
|
||||
var hitWindows = new TaikoHitWindows();
|
||||
|
||||
foreach (var result in Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)))
|
||||
{
|
||||
judgementPools.Add(result, new DrawablePool<DrawableTaikoJudgement>(15));
|
||||
explosionPools.Add(result, new HitExplosionPool(result));
|
||||
}
|
||||
HitResult[] usableHitResults = Enum.GetValues<HitResult>().Where(r => hitWindows.IsHitResultAllowed(r)).ToArray();
|
||||
|
||||
AddRangeInternal(judgementPools.Values);
|
||||
AddInternal(judgementPooler = new JudgementPooler<DrawableTaikoJudgement>(usableHitResults));
|
||||
|
||||
foreach (var result in usableHitResults)
|
||||
explosionPools.Add(result, new HitExplosionPool(result));
|
||||
AddRangeInternal(explosionPools.Values);
|
||||
}
|
||||
|
||||
@@ -339,7 +335,12 @@ namespace osu.Game.Rulesets.Taiko.UI
|
||||
if (!result.Type.IsScorable())
|
||||
break;
|
||||
|
||||
judgementContainer.Add(judgementPools[result.Type].Get(j => j.Apply(result, judgedObject)));
|
||||
var judgement = judgementPooler.Get(result.Type, j => j.Apply(result, judgedObject));
|
||||
|
||||
if (judgement == null)
|
||||
return;
|
||||
|
||||
judgementContainer.Add(judgement);
|
||||
|
||||
var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre;
|
||||
addExplosion(judgedObject, result.Type, type);
|
||||
|
||||
@@ -3,14 +3,18 @@
|
||||
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Formats;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.IO.Legacy;
|
||||
using osu.Game.Replays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Catch;
|
||||
@@ -247,6 +251,123 @@ namespace osu.Game.Tests.Beatmaps.Formats
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfStableScorePreserved()
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
// local partial implementation of legacy score encoder
|
||||
// this is done half for readability, half because `LegacyScoreEncoder` forces `LATEST_VERSION`
|
||||
// and we want to emulate a stable score here
|
||||
using (var sw = new SerializationWriter(memoryStream, true))
|
||||
{
|
||||
sw.Write((byte)0); // ruleset id (osu!)
|
||||
sw.Write(20240116); // version (anything below `LegacyScoreEncoder.FIRST_LAZER_VERSION` is stable)
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||
sw.Write("username"); // irrelevant to this test
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||
sw.Write((ushort)198); // count300
|
||||
sw.Write((ushort)1); // count100
|
||||
sw.Write((ushort)0); // count50
|
||||
sw.Write((ushort)0); // countGeki
|
||||
sw.Write((ushort)0); // countKatu
|
||||
sw.Write((ushort)1); // countMiss
|
||||
sw.Write(12345678); // total score, irrelevant to this test
|
||||
sw.Write((ushort)1000); // max combo, irrelevant to this test
|
||||
sw.Write(false); // full combo, irrelevant to this test
|
||||
sw.Write((int)LegacyMods.Hidden); // mods
|
||||
sw.Write(string.Empty); // hp graph, irrelevant
|
||||
sw.Write(DateTime.Now); // date, irrelevant
|
||||
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
|
||||
sw.Write((long)1234); // legacy online ID, irrelevant
|
||||
}
|
||||
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 100) / (200 * 300)));
|
||||
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfLazerScorePreserved()
|
||||
{
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
var scoreInfo = TestResources.CreateTestScoreInfo(ruleset);
|
||||
scoreInfo.Mods = new Mod[] { new OsuModFlashlight() };
|
||||
scoreInfo.Statistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 199,
|
||||
[HitResult.Miss] = 1,
|
||||
[HitResult.LargeTickHit] = 1,
|
||||
};
|
||||
scoreInfo.MaximumStatistics = new Dictionary<HitResult, int>
|
||||
{
|
||||
[HitResult.Great] = 200,
|
||||
[HitResult.LargeTickHit] = 1,
|
||||
};
|
||||
|
||||
var beatmap = new TestBeatmap(ruleset);
|
||||
var score = new Score
|
||||
{
|
||||
ScoreInfo = scoreInfo,
|
||||
};
|
||||
|
||||
var decodedAfterEncode = encodeThenDecode(LegacyBeatmapDecoder.LATEST_VERSION, score, beatmap);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Accuracy, Is.EqualTo((double)(199 * 300 + 30) / (200 * 300 + 30)));
|
||||
Assert.That(decodedAfterEncode.ScoreInfo.Rank, Is.EqualTo(ScoreRank.SH));
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AccuracyAndRankOfLazerScoreWithoutLegacyReplaySoloScoreInfoUsesBestEffortFallbackToLegacy()
|
||||
{
|
||||
var memoryStream = new MemoryStream();
|
||||
|
||||
// local partial implementation of legacy score encoder
|
||||
// this is done half for readability, half because we want to emulate an old lazer score here
|
||||
// that does not have everything that `LegacyScoreEncoder` now writes to the replay
|
||||
using (var sw = new SerializationWriter(memoryStream, true))
|
||||
{
|
||||
sw.Write((byte)0); // ruleset id (osu!)
|
||||
sw.Write(LegacyScoreEncoder.FIRST_LAZER_VERSION); // version
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // beatmap hash, irrelevant to this test
|
||||
sw.Write("username"); // irrelevant to this test
|
||||
sw.Write(string.Empty.ComputeMD5Hash()); // score hash, irrelevant to this test
|
||||
sw.Write((ushort)198); // count300
|
||||
sw.Write((ushort)0); // count100
|
||||
sw.Write((ushort)1); // count50
|
||||
sw.Write((ushort)0); // countGeki
|
||||
sw.Write((ushort)0); // countKatu
|
||||
sw.Write((ushort)1); // countMiss
|
||||
sw.Write(12345678); // total score, irrelevant to this test
|
||||
sw.Write((ushort)1000); // max combo, irrelevant to this test
|
||||
sw.Write(false); // full combo, irrelevant to this test
|
||||
sw.Write((int)LegacyMods.Hidden); // mods
|
||||
sw.Write(string.Empty); // hp graph, irrelevant
|
||||
sw.Write(DateTime.Now); // date, irrelevant
|
||||
sw.Write(Array.Empty<byte>()); // replay data, irrelevant
|
||||
sw.Write((long)1234); // legacy online ID, irrelevant
|
||||
// importantly, no compressed `LegacyReplaySoloScoreInfo` here
|
||||
}
|
||||
|
||||
memoryStream.Seek(0, SeekOrigin.Begin);
|
||||
var decoded = new TestLegacyScoreDecoder().Parse(memoryStream);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.ScoreInfo.Accuracy, Is.EqualTo((double)(198 * 300 + 50) / (200 * 300)));
|
||||
Assert.That(decoded.ScoreInfo.Rank, Is.EqualTo(ScoreRank.A));
|
||||
});
|
||||
}
|
||||
|
||||
private static Score encodeThenDecode(int beatmapVersion, Score score, TestBeatmap beatmap)
|
||||
{
|
||||
var encodeStream = new MemoryStream();
|
||||
|
||||
@@ -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.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
@@ -182,9 +183,63 @@ namespace osu.Game.Tests.Database
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000002));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCustomRulesetScoreNotSubjectToUpgrades([Values] bool available)
|
||||
{
|
||||
RulesetInfo rulesetInfo = null!;
|
||||
ScoreInfo scoreInfo = null!;
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
|
||||
AddStep("Add unavailable ruleset", () => Realm.Write(r => r.Add(rulesetInfo = new RulesetInfo
|
||||
{
|
||||
ShortName = Guid.NewGuid().ToString(),
|
||||
Available = available
|
||||
})));
|
||||
|
||||
AddStep("Add score for unavailable ruleset", () => Realm.Write(r => r.Add(scoreInfo = new ScoreInfo(
|
||||
ruleset: rulesetInfo,
|
||||
beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000001
|
||||
})));
|
||||
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000001));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNonLegacyScoreNotSubjectToUpgrades()
|
||||
{
|
||||
ScoreInfo scoreInfo = null!;
|
||||
TestBackgroundDataStoreProcessor processor = null!;
|
||||
|
||||
AddStep("Add score which requires upgrade (and has beatmap)", () =>
|
||||
{
|
||||
Realm.Write(r =>
|
||||
{
|
||||
r.Add(scoreInfo = new ScoreInfo(ruleset: r.All<RulesetInfo>().First(), beatmap: r.All<BeatmapInfo>().First())
|
||||
{
|
||||
TotalScoreVersion = 30000005,
|
||||
LegacyTotalScore = 123456,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
AddStep("Run background processor", () => Add(processor = new TestBackgroundDataStoreProcessor()));
|
||||
AddUntilStep("Wait for completion", () => processor.Completed);
|
||||
|
||||
AddAssert("Score not marked as failed", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.BackgroundReprocessingFailed), () => Is.False);
|
||||
AddAssert("Score version not upgraded", () => Realm.Run(r => r.Find<ScoreInfo>(scoreInfo.ID)!.TotalScoreVersion), () => Is.EqualTo(30000005));
|
||||
}
|
||||
|
||||
public partial class TestBackgroundDataStoreProcessor : BackgroundDataStoreProcessor
|
||||
{
|
||||
protected override int TimeToSleepDuringGameplay => 10;
|
||||
|
||||
public bool Completed => ProcessingTask.IsCompleted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ using Moq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Utils;
|
||||
|
||||
namespace osu.Game.Tests.Mods
|
||||
@@ -310,6 +312,16 @@ namespace osu.Game.Tests.Mods
|
||||
Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestModBelongsToRuleset()
|
||||
{
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), Array.Empty<Mod>()));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime() }));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new OsuModAccuracyChallenge() }));
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new ModAccuracyChallenge() }), Is.False);
|
||||
Assert.That(ModUtils.CheckModsBelongToRuleset(new OsuRuleset(), new Mod[] { new OsuModDoubleTime(), new TaikoModFlashlight() }), Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFormatScoreMultiplier()
|
||||
{
|
||||
|
||||
@@ -127,8 +127,50 @@ namespace osu.Game.Tests.NonVisual.Skinning
|
||||
Assert.IsNull(texture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestDisallowHighResolutionSprites()
|
||||
{
|
||||
var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x");
|
||||
var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = false };
|
||||
|
||||
var texture = legacySkin.GetTexture("hitcircle");
|
||||
|
||||
Assert.IsNotNull(texture);
|
||||
Assert.That(texture.ScaleAdjust, Is.EqualTo(1));
|
||||
|
||||
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
|
||||
|
||||
Assert.IsNotNull(twoTimesTexture);
|
||||
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(1));
|
||||
|
||||
Assert.AreNotEqual(texture, twoTimesTexture);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestAllowHighResolutionSprites()
|
||||
{
|
||||
var textureStore = new TestTextureStore("hitcircle", "hitcircle@2x");
|
||||
var legacySkin = new TestLegacySkin(textureStore) { HighResolutionSprites = true };
|
||||
|
||||
var texture = legacySkin.GetTexture("hitcircle");
|
||||
|
||||
Assert.IsNotNull(texture);
|
||||
Assert.That(texture.ScaleAdjust, Is.EqualTo(2));
|
||||
|
||||
var twoTimesTexture = legacySkin.GetTexture("hitcircle@2x");
|
||||
|
||||
Assert.IsNotNull(twoTimesTexture);
|
||||
Assert.That(twoTimesTexture.ScaleAdjust, Is.EqualTo(2));
|
||||
|
||||
Assert.AreEqual(texture, twoTimesTexture);
|
||||
}
|
||||
|
||||
private class TestLegacySkin : LegacySkin
|
||||
{
|
||||
public bool HighResolutionSprites { get; set; } = true;
|
||||
|
||||
protected override bool AllowHighResolutionSprites => HighResolutionSprites;
|
||||
|
||||
public TestLegacySkin(IResourceStore<TextureUpload> textureStore)
|
||||
: base(new SkinInfo(), new TestResourceProvider(textureStore), null, string.Empty)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
[Mania]
|
||||
Keys: 4
|
||||
ColumnLineWidth: 3,,3,3,3
|
||||
@@ -18,10 +18,12 @@ using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.UI;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Scoring.Legacy;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@@ -385,6 +387,42 @@ namespace osu.Game.Tests.Rulesets.Scoring
|
||||
Assert.That(scoreProcessor.Accuracy.Value, Is.Not.EqualTo(1));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalGrades()
|
||||
{
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.S));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSilverGrades()
|
||||
{
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.X));
|
||||
|
||||
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSilverGradesModsAppliedFirst()
|
||||
{
|
||||
scoreProcessor.Mods.Value = new[] { new OsuModHidden() };
|
||||
scoreProcessor.ApplyBeatmap(new Beatmap());
|
||||
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.XH));
|
||||
|
||||
scoreProcessor.Accuracy.Value = 0.99f;
|
||||
Assert.That(scoreProcessor.Rank.Value, Is.EqualTo(ScoreRank.SH));
|
||||
}
|
||||
|
||||
private class TestJudgement : Judgement
|
||||
{
|
||||
public override HitResult MaxResult { get; }
|
||||
|
||||
@@ -114,5 +114,25 @@ namespace osu.Game.Tests.Skins
|
||||
Assert.That(configs[0].MinimumColumnWidth, Is.EqualTo(16));
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestParseArrayWithSomeEmptyElements()
|
||||
{
|
||||
var decoder = new LegacyManiaSkinDecoder();
|
||||
|
||||
using (var resStream = TestResources.OpenResource("mania-skin-broken-array.ini"))
|
||||
using (var stream = new LineBufferedReader(resStream))
|
||||
{
|
||||
var configs = decoder.Decode(stream);
|
||||
|
||||
Assert.That(configs.Count, Is.EqualTo(1));
|
||||
Assert.That(configs[0].ColumnLineWidth.Length, Is.EqualTo(5));
|
||||
Assert.That(configs[0].ColumnLineWidth[0], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[1], Is.EqualTo(0)); // malformed entry, should be parsed as zero
|
||||
Assert.That(configs[0].ColumnLineWidth[2], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[3], Is.EqualTo(3));
|
||||
Assert.That(configs[0].ColumnLineWidth[4], Is.EqualTo(3));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
|
||||
AddSliderStep("Triangle scale", 0f, 10f, 1f, s => triangles.TriangleScale = s);
|
||||
AddSliderStep("Seed", 0, 1000, 0, s => triangles.Reset(s));
|
||||
AddToggleStep("Masking", m => triangles.Masking = m);
|
||||
AddToggleStep("ClampToDrawable", c => triangles.ClampToDrawable = c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ namespace osu.Game.Tests.Visual.Background
|
||||
AddStep("White colour", () => box.Colour = triangles.Colour = maskedTriangles.Colour = Color4.White);
|
||||
AddStep("Vertical gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientVertical(Color4.White, Color4.Red));
|
||||
AddStep("Horizontal gradient", () => box.Colour = triangles.Colour = maskedTriangles.Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.Red));
|
||||
AddToggleStep("Masking", m => maskedTriangles.Masking = m);
|
||||
AddToggleStep("ClampToDrawable", c => maskedTriangles.ClampToDrawable = c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
#nullable disable
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@@ -177,6 +179,43 @@ namespace osu.Game.Tests.Visual.Editing
|
||||
AddUntilStep("Scrolled to end", () => timingScreen.ChildrenOfType<OsuScrollContainer>().First().IsScrolledToEnd());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestEditThenClickAwayAppliesChanges()
|
||||
{
|
||||
AddStep("Add two control points", () =>
|
||||
{
|
||||
editorBeatmap.ControlPointInfo.Clear();
|
||||
editorBeatmap.ControlPointInfo.Add(1000, new TimingControlPoint());
|
||||
editorBeatmap.ControlPointInfo.Add(2000, new TimingControlPoint());
|
||||
});
|
||||
|
||||
AddStep("Select second timing point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().Last());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddStep("Scroll to end", () => timingScreen.ChildrenOfType<ControlPointSettings>().Single().ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
AddStep("Modify time signature", () =>
|
||||
{
|
||||
var timeSignatureTextBox = Child.ChildrenOfType<LabelledTimeSignature.TimeSignatureBox>().Single().ChildrenOfType<TextBox>().Single();
|
||||
InputManager.MoveMouseTo(timeSignatureTextBox);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
|
||||
Debug.Assert(!timeSignatureTextBox.Current.Value.Equals("1", StringComparison.Ordinal));
|
||||
timeSignatureTextBox.Current.Value = "1";
|
||||
});
|
||||
|
||||
AddStep("Select first timing point", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(Child.ChildrenOfType<TimingRowAttribute>().First());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddAssert("Second timing point changed time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.Last().TimeSignature.Numerator == 1);
|
||||
AddAssert("First timing point preserved time signature", () => editorBeatmap.ControlPointInfo.TimingPoints.First().TimeSignature.Numerator == 4);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
Beatmap.Disabled = false;
|
||||
|
||||
@@ -7,6 +7,7 @@ using osu.Framework.Extensions.ObjectExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
@@ -159,5 +160,15 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
Type = HitResult.Perfect
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSimulateDrain()
|
||||
{
|
||||
ScheduledDelegate del = null!;
|
||||
|
||||
AddStep("simulate drain", () => del = Scheduler.AddDelayed(() => healthProcessor.Health.Value -= 0.00025f * Time.Elapsed, 0, true));
|
||||
AddUntilStep("wait until zero", () => healthProcessor.Health.Value == 0);
|
||||
AddStep("cancel drain", () => del.Cancel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private readonly Bindable<bool> showHealth = new Bindable<bool>();
|
||||
|
||||
private HealthProcessor healthProcessor;
|
||||
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
@@ -29,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
AddStep("create layer", () =>
|
||||
{
|
||||
Child = new HealthProcessorContainer(healthProcessor)
|
||||
Child = new HealthProcessorContainer(this.healthProcessor = healthProcessor)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Child = layer = new FailingLayer()
|
||||
@@ -50,12 +52,12 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddSliderStep("current health", 0.0, 1.0, 1.0, val =>
|
||||
{
|
||||
if (layer != null)
|
||||
layer.Current.Value = val;
|
||||
healthProcessor.Health.Value = val;
|
||||
});
|
||||
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer fade is visible", () => layer.ChildrenOfType<Container>().First().Alpha > 0.1f);
|
||||
AddStep("set health to 1", () => layer.Current.Value = 1f);
|
||||
AddStep("set health to 1", () => healthProcessor.Health.Value = 1f);
|
||||
AddUntilStep("layer fade is invisible", () => !layer.ChildrenOfType<Container>().First().IsPresent);
|
||||
}
|
||||
|
||||
@@ -65,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
create(new DrainingHealthProcessor(0));
|
||||
AddUntilStep("layer is visible", () => layer.IsPresent);
|
||||
AddStep("disable layer", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
@@ -74,7 +76,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
create(new AccumulatingHealthProcessor(1));
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddUntilStep("layer is not visible", () => !layer.IsPresent);
|
||||
}
|
||||
|
||||
@@ -82,7 +84,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
public void TestLayerVisibilityWithDrainingProcessor()
|
||||
{
|
||||
create(new DrainingHealthProcessor(0));
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
AddWaitStep("wait for potential fade", 10);
|
||||
AddAssert("layer is still visible", () => layer.IsPresent);
|
||||
}
|
||||
@@ -92,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
{
|
||||
create(new DrainingHealthProcessor(0));
|
||||
|
||||
AddStep("set health to 0.10", () => layer.Current.Value = 0.1);
|
||||
AddStep("set health to 0.10", () => healthProcessor.Health.Value = 0.1);
|
||||
|
||||
AddStep("don't show health", () => showHealth.Value = false);
|
||||
AddStep("disable FadePlayfieldWhenHealthLow", () => config.SetValue(OsuSetting.FadePlayfieldWhenHealthLow, false));
|
||||
|
||||
@@ -147,6 +147,16 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("Assert max judgement hidden", () => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().First().Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoDuplicates()
|
||||
{
|
||||
AddStep("create counter", () => Child = counterDisplay = new TestJudgementCounterDisplay());
|
||||
AddStep("Show all judgements", () => counterDisplay.Mode.Value = JudgementCounterDisplay.DisplayMode.All);
|
||||
AddAssert("Check no duplicates",
|
||||
() => counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Count(),
|
||||
() => Is.EqualTo(counterDisplay.CounterFlow.ChildrenOfType<JudgementCounter>().Select(c => c.ResultName.Text).Distinct().Count()));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCycleDisplayModes()
|
||||
{
|
||||
@@ -163,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private int hiddenCount()
|
||||
{
|
||||
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Type == HitResult.LargeTickHit);
|
||||
var num = counterDisplay.CounterFlow.Children.First(child => child.Result.Types.Contains(HitResult.LargeTickHit));
|
||||
return num.Result.ResultCount.Value;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Screens;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@@ -487,13 +486,8 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
}
|
||||
}
|
||||
|
||||
private class TestMod : Mod, IApplicableToScoreProcessor
|
||||
private class TestMod : OsuModDoubleTime, IApplicableToScoreProcessor
|
||||
{
|
||||
public override string Name => string.Empty;
|
||||
public override string Acronym => string.Empty;
|
||||
public override double ScoreMultiplier => 1;
|
||||
public override LocalisableString Description => string.Empty;
|
||||
|
||||
public bool Applied { get; private set; }
|
||||
|
||||
public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor)
|
||||
|
||||
@@ -15,13 +15,14 @@ using osu.Game.Online.Rooms;
|
||||
using osu.Game.Online.Solo;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mania;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Judgements;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Rulesets.Taiko.Mods;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
@@ -35,12 +36,19 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
|
||||
private Func<RulesetInfo, IBeatmap> createCustomBeatmap;
|
||||
private Func<Ruleset> createCustomRuleset;
|
||||
private Func<Mod[]> createCustomMods;
|
||||
|
||||
private DummyAPIAccess dummyAPI => (DummyAPIAccess)API;
|
||||
|
||||
protected override bool HasCustomSteps => true;
|
||||
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new FakeImportingPlayer(false);
|
||||
protected override TestPlayer CreatePlayer(Ruleset ruleset)
|
||||
{
|
||||
if (createCustomMods != null)
|
||||
SelectedMods.Value = SelectedMods.Value.Concat(createCustomMods()).ToList();
|
||||
|
||||
return new FakeImportingPlayer(false);
|
||||
}
|
||||
|
||||
protected new FakeImportingPlayer Player => (FakeImportingPlayer)base.Player;
|
||||
|
||||
@@ -278,13 +286,28 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null)
|
||||
[Test]
|
||||
public void TestNoSubmissionWithModsOfDifferentRuleset()
|
||||
{
|
||||
prepareTestAPI(true);
|
||||
|
||||
createPlayerTest(createRuleset: () => new OsuRuleset(), createMods: () => new Mod[] { new TaikoModHidden() });
|
||||
|
||||
AddUntilStep("wait for token request", () => Player.TokenCreationRequested);
|
||||
AddAssert("gameplay not loaded", () => Player.DrawableRuleset == null);
|
||||
|
||||
AddStep("exit", () => Player.Exit());
|
||||
AddAssert("ensure no submission", () => Player.SubmittedScore == null);
|
||||
}
|
||||
|
||||
private void createPlayerTest(bool allowFail = false, Func<RulesetInfo, IBeatmap> createBeatmap = null, Func<Ruleset> createRuleset = null, Func<Mod[]> createMods = null)
|
||||
{
|
||||
CreateTest(() => AddStep("set up requirements", () =>
|
||||
{
|
||||
this.allowFail = allowFail;
|
||||
createCustomBeatmap = createBeatmap;
|
||||
createCustomRuleset = createRuleset;
|
||||
createCustomMods = createMods;
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -360,11 +383,6 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
AllowImportCompletion = new SemaphoreSlim(1);
|
||||
}
|
||||
|
||||
protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new MasterGameplayClockContainer(beatmap, gameplayStart)
|
||||
{
|
||||
ShouldValidatePlaybackRate = false,
|
||||
};
|
||||
|
||||
protected override async Task ImportScore(Score score)
|
||||
{
|
||||
ScoreImportStarted = true;
|
||||
|
||||
@@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
[Cached(typeof(HealthProcessor))]
|
||||
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);
|
||||
|
||||
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 1f };
|
||||
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), Width = 600, UseRelativeSize = { Value = false } };
|
||||
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
|
||||
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };
|
||||
|
||||
@@ -35,6 +35,13 @@ namespace osu.Game.Tests.Visual.Gameplay
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
healthProcessor.Health.Value -= 0.0001f * Time.Elapsed;
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHealthDisplayIncrementing()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 NUnit.Framework;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Input.Bindings;
|
||||
using osu.Game.Screens.Menu;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Menus
|
||||
{
|
||||
public partial class TestSceneIntroMusicActionHandling : OsuGameTestScene
|
||||
{
|
||||
private GlobalActionContainer globalActionContainer => Game.ChildrenOfType<GlobalActionContainer>().First();
|
||||
|
||||
public override void SetUpSteps()
|
||||
{
|
||||
CreateNewGame();
|
||||
// we do not want to progress to main menu immediately, hence the override and lack of `ConfirmAtMainMenu()` call here.
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestPauseDuringIntro()
|
||||
{
|
||||
AddUntilStep("Wait for music", () => Game?.MusicController.IsPlaying == true);
|
||||
|
||||
// Check that pause doesn't work during intro sequence.
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddAssert("Still playing before menu", () => Game?.MusicController.IsPlaying == true);
|
||||
AddUntilStep("Wait for main menu", () => Game?.ScreenStack.CurrentScreen is MainMenu menu && menu.IsLoaded);
|
||||
|
||||
// Check that toggling after intro still works.
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddUntilStep("Music paused", () => Game?.MusicController.IsPlaying == false && Game?.MusicController.UserPauseRequested == true);
|
||||
AddStep("Toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay));
|
||||
AddUntilStep("Music resumed", () => Game?.MusicController.IsPlaying == true && Game?.MusicController.UserPauseRequested == false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osuTK;
|
||||
|
||||
@@ -29,7 +30,7 @@ namespace osu.Game.Tests.Visual.Mods
|
||||
public void TestMaximumAchievableAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
Mod = new OsuModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 }
|
||||
},
|
||||
@@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Mods
|
||||
public void TestStandardAccuracy() =>
|
||||
CreateModTest(new ModTestData
|
||||
{
|
||||
Mod = new ModAccuracyChallenge
|
||||
Mod = new OsuModAccuracyChallenge
|
||||
{
|
||||
MinimumAccuracy = { Value = 0.6 },
|
||||
AccuracyJudgeMode = { Value = ModAccuracyChallenge.AccuracyMode.Standard }
|
||||
|
||||
@@ -19,8 +19,10 @@ using osu.Game.Beatmaps.Drawables;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Cursor;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
@@ -302,6 +304,37 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectableMouseHandling()
|
||||
{
|
||||
bool resultsRequested = false;
|
||||
|
||||
AddStep("reset flag", () => resultsRequested = false);
|
||||
createPlaylist(p =>
|
||||
{
|
||||
p.AllowSelection = true;
|
||||
p.AllowShowingResults = true;
|
||||
p.RequestResults = _ => resultsRequested = true;
|
||||
});
|
||||
|
||||
AddStep("move mouse to first item title", () =>
|
||||
{
|
||||
var drawQuad = playlist.ChildrenOfType<LinkFlowContainer>().First().ScreenSpaceDrawQuad;
|
||||
var location = (drawQuad.TopLeft + drawQuad.BottomLeft) / 2 + new Vector2(drawQuad.Width * 0.2f, 0);
|
||||
InputManager.MoveMouseTo(location);
|
||||
});
|
||||
AddUntilStep("wait for text load", () => playlist.ChildrenOfType<DrawableLinkCompiler>().Any());
|
||||
AddAssert("first item title not hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.False);
|
||||
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("first item selected", () => playlist.ChildrenOfType<DrawableRoomPlaylistItem>().First().IsSelectedItem, () => Is.True);
|
||||
// implies being clickable.
|
||||
AddUntilStep("first item title hovered", () => playlist.ChildrenOfType<DrawableLinkCompiler>().First().IsHovered, () => Is.True);
|
||||
|
||||
AddStep("move mouse to second item results button", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<GrayButton>().ElementAt(5)));
|
||||
AddStep("click left mouse", () => InputManager.Click(MouseButton.Left));
|
||||
AddUntilStep("results requested", () => resultsRequested);
|
||||
}
|
||||
|
||||
private void moveToItem(int index, Vector2? offset = null)
|
||||
=> AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType<DrawableRoomPlaylistItem>().ElementAt(index), offset));
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Utils;
|
||||
using osuTK.Input;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Multiplayer
|
||||
@@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
public partial class TestSceneFreeModSelectOverlay : MultiplayerTestScene
|
||||
{
|
||||
private FreeModSelectOverlay freeModSelectOverlay;
|
||||
private FooterButtonFreeMods footerButtonFreeMods;
|
||||
private readonly Bindable<Dictionary<ModType, IReadOnlyList<Mod>>> availableMods = new Bindable<Dictionary<ModType, IReadOnlyList<Mod>>>();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
@@ -119,11 +122,46 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
AddAssert("select all button enabled", () => this.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestSelectAllViaFooterButtonThenDeselectFromOverlay()
|
||||
{
|
||||
createFreeModSelect();
|
||||
|
||||
AddAssert("overlay select all button enabled", () => freeModSelectOverlay.ChildrenOfType<SelectAllModsButton>().Single().Enabled.Value);
|
||||
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
|
||||
|
||||
AddStep("click footer select all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(footerButtonFreeMods);
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
|
||||
AddUntilStep("all mods selected", assertAllAvailableModsSelected);
|
||||
AddAssert("footer button displays all", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "all"));
|
||||
|
||||
AddStep("click deselect all button", () =>
|
||||
{
|
||||
InputManager.MoveMouseTo(this.ChildrenOfType<DeselectAllModsButton>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
});
|
||||
AddUntilStep("all mods deselected", () => !freeModSelectOverlay.SelectedMods.Value.Any());
|
||||
AddAssert("footer button displays off", () => footerButtonFreeMods.ChildrenOfType<IHasText>().Any(t => t.Text == "off"));
|
||||
}
|
||||
|
||||
private void createFreeModSelect()
|
||||
{
|
||||
AddStep("create free mod select screen", () => Child = freeModSelectOverlay = new FreeModSelectOverlay
|
||||
AddStep("create free mod select screen", () => Children = new Drawable[]
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
freeModSelectOverlay = new FreeModSelectOverlay
|
||||
{
|
||||
State = { Value = Visibility.Visible }
|
||||
},
|
||||
footerButtonFreeMods = new FooterButtonFreeMods(freeModSelectOverlay)
|
||||
{
|
||||
Anchor = Anchor.BottomRight,
|
||||
Origin = Anchor.BottomRight,
|
||||
Current = { BindTarget = freeModSelectOverlay.SelectedMods },
|
||||
},
|
||||
});
|
||||
AddUntilStep("all column content loaded",
|
||||
() => freeModSelectOverlay.ChildrenOfType<ModColumn>().Any()
|
||||
@@ -134,10 +172,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
{
|
||||
var allAvailableMods = availableMods.Value
|
||||
.Where(pair => pair.Key != ModType.System)
|
||||
.SelectMany(pair => pair.Value)
|
||||
.SelectMany(pair => ModUtils.FlattenMods(pair.Value))
|
||||
.Where(mod => mod.UserPlayable && mod.HasImplementation)
|
||||
.ToList();
|
||||
|
||||
if (freeModSelectOverlay.SelectedMods.Value.Count != allAvailableMods.Count)
|
||||
return false;
|
||||
|
||||
foreach (var availableMod in allAvailableMods)
|
||||
{
|
||||
if (freeModSelectOverlay.SelectedMods.Value.All(selectedMod => selectedMod.GetType() != availableMod.GetType()))
|
||||
|
||||
@@ -29,7 +29,9 @@ using osu.Game.Rulesets.Catch;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Osu.Mods;
|
||||
using osu.Game.Rulesets.Taiko;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay;
|
||||
using osu.Game.Screens.OnlinePlay.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
@@ -1009,6 +1011,43 @@ namespace osu.Game.Tests.Visual.Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGameplayStartsWhileInSongSelectWithDifferentRuleset()
|
||||
{
|
||||
createRoom(() => new Room
|
||||
{
|
||||
Name = { Value = "Test Room" },
|
||||
QueueMode = { Value = QueueMode.AllPlayers },
|
||||
Playlist =
|
||||
{
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 0)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new OsuRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
|
||||
},
|
||||
new PlaylistItem(beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First(b => b.Ruleset.OnlineID == 1)).BeatmapInfo)
|
||||
{
|
||||
RulesetID = new TaikoRuleset().RulesetInfo.OnlineID,
|
||||
AllowedMods = new[] { new APIMod { Acronym = "HD" } },
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
AddStep("select hidden", () => multiplayerClient.ChangeUserMods(new[] { new APIMod { Acronym = "HD" } }));
|
||||
AddStep("make user ready", () => multiplayerClient.ChangeState(MultiplayerUserState.Ready));
|
||||
AddStep("press edit on second item", () => this.ChildrenOfType<DrawableRoomPlaylistItem>().Single(i => i.Item.RulesetID == 1)
|
||||
.ChildrenOfType<DrawableRoomPlaylistItem.PlaylistEditButton>().Single().TriggerClick());
|
||||
|
||||
AddUntilStep("wait for song select", () => InputManager.ChildrenOfType<MultiplayerMatchSongSelect>().FirstOrDefault()?.BeatmapSetsLoaded == true);
|
||||
AddAssert("ruleset is taiko", () => Ruleset.Value.OnlineID == 1);
|
||||
|
||||
AddStep("start match", () => multiplayerClient.StartMatch().WaitSafely());
|
||||
|
||||
AddUntilStep("wait for loading", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.WaitingForLoad);
|
||||
AddUntilStep("wait for gameplay to start", () => multiplayerClient.ClientRoom?.State == MultiplayerRoomState.Playing);
|
||||
AddAssert("hidden is selected", () => SelectedMods.Value, () => Has.One.TypeOf(typeof(OsuModHidden)));
|
||||
}
|
||||
|
||||
private void enterGameplay()
|
||||
{
|
||||
pressReadyButton();
|
||||
|
||||
@@ -35,6 +35,7 @@ using osu.Game.Screens.OnlinePlay.Lounge;
|
||||
using osu.Game.Screens.OnlinePlay.Match.Components;
|
||||
using osu.Game.Screens.OnlinePlay.Playlists;
|
||||
using osu.Game.Screens.Play;
|
||||
using osu.Game.Screens.Play.HUD;
|
||||
using osu.Game.Screens.Ranking;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Carousel;
|
||||
@@ -834,6 +835,24 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddAssert("exit dialog is shown", () => Game.Dependencies.Get<IDialogOverlay>().CurrentDialog is ConfirmExitDialog);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestQuickSkinEditorDoesntNukeSkin()
|
||||
{
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddStep("open", () => InputManager.Key(Key.Space));
|
||||
AddStep("skin", () => InputManager.Key(Key.E));
|
||||
AddStep("editor", () => InputManager.Key(Key.S));
|
||||
AddStep("and close immediately", () => InputManager.Key(Key.Escape));
|
||||
|
||||
AddStep("open again", () => InputManager.Key(Key.S));
|
||||
|
||||
Player player = null;
|
||||
|
||||
AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null);
|
||||
AddUntilStep("wait for gameplay still has health bar", () => player.ChildrenOfType<ArgonHealthDisplay>().Any());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestTouchScreenDetectionAtSongSelect()
|
||||
{
|
||||
@@ -938,6 +957,35 @@ namespace osu.Game.Tests.Visual.Navigation
|
||||
AddUntilStep("touch device mod still active", () => Game.SelectedMods.Value, () => Has.One.InstanceOf<ModTouchDevice>());
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExitSongSelectAndImmediatelyClickLogo()
|
||||
{
|
||||
Screens.Select.SongSelect songSelect = null;
|
||||
PushAndConfirm(() => songSelect = new TestPlaySongSelect());
|
||||
AddUntilStep("wait for song select", () => songSelect.BeatmapSetsLoaded);
|
||||
|
||||
AddStep("import beatmap", () => BeatmapImportHelper.LoadQuickOszIntoOsu(Game).WaitSafely());
|
||||
|
||||
AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault);
|
||||
|
||||
AddStep("press escape and then click logo immediately", () =>
|
||||
{
|
||||
InputManager.Key(Key.Escape);
|
||||
clickLogoWhenNotCurrent();
|
||||
});
|
||||
|
||||
void clickLogoWhenNotCurrent()
|
||||
{
|
||||
if (songSelect.IsCurrentScreen())
|
||||
Scheduler.AddOnce(clickLogoWhenNotCurrent);
|
||||
else
|
||||
{
|
||||
InputManager.MoveMouseTo(Game.ChildrenOfType<OsuLogo>().Single());
|
||||
InputManager.Click(MouseButton.Left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Func<Player> playToResults()
|
||||
{
|
||||
var player = playToCompletion();
|
||||
|
||||
@@ -268,6 +268,26 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("update not received", () => update == null);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestGlobalStatisticsUpdatedAfterRegistrationAddedAndScoreProcessed()
|
||||
{
|
||||
int userId = getUserId();
|
||||
long scoreId = getScoreId();
|
||||
setUpUser(userId);
|
||||
|
||||
var ruleset = new OsuRuleset().RulesetInfo;
|
||||
|
||||
SoloStatisticsUpdate? update = null;
|
||||
registerForUpdates(scoreId, ruleset, receivedUpdate => update = receivedUpdate);
|
||||
|
||||
feignScoreProcessing(userId, ruleset, 5_000_000);
|
||||
|
||||
AddStep("signal score processed", () => ((ISpectatorClient)spectatorClient).UserScoreProcessed(userId, scoreId));
|
||||
AddUntilStep("update received", () => update != null);
|
||||
AddAssert("local user values are correct", () => dummyAPI.LocalUser.Value.Statistics.TotalScore, () => Is.EqualTo(5_000_000));
|
||||
AddAssert("statistics values are correct", () => dummyAPI.Statistics.Value!.TotalScore, () => Is.EqualTo(5_000_000));
|
||||
}
|
||||
|
||||
private int nextUserId = 2000;
|
||||
private long nextScoreId = 50000;
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Scoring;
|
||||
@@ -29,6 +31,9 @@ namespace osu.Game.Tests.Visual.Online
|
||||
private UserGridPanel boundPanel1;
|
||||
private TestUserListPanel boundPanel2;
|
||||
|
||||
[Cached]
|
||||
private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple);
|
||||
|
||||
[Resolved]
|
||||
private IRulesetStore rulesetStore { get; set; }
|
||||
|
||||
@@ -85,8 +90,25 @@ namespace osu.Game.Tests.Visual.Online
|
||||
CoverUrl = @"https://assets.ppy.sh/user-profile-covers/8195163/4a8e2ad5a02a2642b631438cfa6c6bd7e2f9db289be881cb27df18331f64144c.jpeg",
|
||||
IsOnline = false,
|
||||
LastVisit = DateTimeOffset.Now
|
||||
})
|
||||
},
|
||||
}),
|
||||
new UserRankPanel(new APIUser
|
||||
{
|
||||
Username = @"flyte",
|
||||
Id = 3103765,
|
||||
CountryCode = CountryCode.JP,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg",
|
||||
Statistics = new UserStatistics { GlobalRank = 12345, CountryRank = 1234 }
|
||||
}) { Width = 300 },
|
||||
new UserRankPanel(new APIUser
|
||||
{
|
||||
Username = @"peppy",
|
||||
Id = 2,
|
||||
Colour = "99EB47",
|
||||
CountryCode = CountryCode.AU,
|
||||
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
||||
Statistics = new UserStatistics { GlobalRank = null, CountryRank = null }
|
||||
}) { Width = 300 }
|
||||
}
|
||||
};
|
||||
|
||||
boundPanel1.Status.BindTo(status);
|
||||
@@ -136,6 +158,23 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddAssert("visit message is not visible", () => !boundPanel2.LastVisitMessage.IsPresent);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserStatisticsChange()
|
||||
{
|
||||
AddStep("update statistics", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics
|
||||
{
|
||||
GlobalRank = RNG.Next(100000),
|
||||
CountryRank = RNG.Next(100000)
|
||||
});
|
||||
});
|
||||
AddStep("set statistics to empty", () =>
|
||||
{
|
||||
API.UpdateStatistics(new UserStatistics());
|
||||
});
|
||||
}
|
||||
|
||||
private UserActivity soloGameStatusForRuleset(int rulesetId) => new UserActivity.InSoloGame(new BeatmapInfo(), rulesetStore.GetRuleset(rulesetId)!);
|
||||
|
||||
private ScoreInfo createScore(string name) => new ScoreInfo(new TestBeatmap(Ruleset.Value).BeatmapInfo)
|
||||
|
||||
@@ -24,8 +24,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
[Cached]
|
||||
private readonly Bindable<APIWikiPage> wikiPageData = new Bindable<APIWikiPage>(new APIWikiPage
|
||||
{
|
||||
Title = "Main Page",
|
||||
Path = "Main_Page",
|
||||
Title = "Main page",
|
||||
Path = WikiOverlay.INDEX_PATH,
|
||||
});
|
||||
|
||||
private TestHeader header;
|
||||
|
||||
@@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online
|
||||
};
|
||||
}
|
||||
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_Page
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_page
|
||||
private const string main_page_markdown =
|
||||
"---\nlayout: main_page\n---\n\n<!-- Do not add any empty lines inside this div. -->\n\n<div class=\"wiki-main-page__blurb\">\nWelcome to the osu! wiki, a project containing a wide range of osu! related information.\n</div>\n\n<div class=\"wiki-main-page__panels\">\n<div class=\"wiki-main-page-panel wiki-main-page-panel--full\">\n\n# Getting started\n\n[Welcome](/wiki/Welcome) • [Installation](/wiki/Installation) • [Registration](/wiki/Registration) • [Help Centre](/wiki/Help_Centre) • [FAQ](/wiki/FAQ)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Game client\n\n[Interface](/wiki/Interface) • [Options](/wiki/Options) • [Visual settings](/wiki/Visual_Settings) • [Shortcut key reference](/wiki/Shortcut_key_reference) • [Configuration file](/wiki/osu!_Program_Files/User_Configuration_File) • [Program files](/wiki/osu!_Program_Files)\n\n[File formats](/wiki/osu!_File_Formats): [.osz](/wiki/osu!_File_Formats/Osz_(file_format)) • [.osk](/wiki/osu!_File_Formats/Osk_(file_format)) • [.osr](/wiki/osu!_File_Formats/Osr_(file_format)) • [.osu](/wiki/osu!_File_Formats/Osu_(file_format)) • [.osb](/wiki/osu!_File_Formats/Osb_(file_format)) • [.db](/wiki/osu!_File_Formats/Db_(file_format))\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Gameplay\n\n[Game modes](/wiki/Game_mode): [osu!](/wiki/Game_mode/osu!) • [osu!taiko](/wiki/Game_mode/osu!taiko) • [osu!catch](/wiki/Game_mode/osu!catch) • [osu!mania](/wiki/Game_mode/osu!mania)\n\n[Beatmap](/wiki/Beatmap) • [Hit object](/wiki/Hit_object) • [Mods](/wiki/Game_modifier) • [Score](/wiki/Score) • [Replay](/wiki/Replay) • [Multi](/wiki/Multi)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# [Beatmap editor](/wiki/Beatmap_Editor)\n\nSections: [Compose](/wiki/Beatmap_Editor/Compose) • [Design](/wiki/Beatmap_Editor/Design) • [Timing](/wiki/Beatmap_Editor/Timing) • [Song setup](/wiki/Beatmap_Editor/Song_Setup)\n\nComponents: [AiMod](/wiki/Beatmap_Editor/AiMod) • [Beat snap divisor](/wiki/Beatmap_Editor/Beat_Snap_Divisor) • [Distance snap](/wiki/Beatmap_Editor/Distance_Snap) • [Menu](/wiki/Beatmap_Editor/Menu) • [SB load](/wiki/Beatmap_Editor/SB_Load) • [Timelines](/wiki/Beatmap_Editor/Timelines)\n\n[Beatmapping](/wiki/Beatmapping) • [Difficulty](/wiki/Beatmap/Difficulty) • [Mapping techniques](/wiki/Mapping_Techniques) • [Storyboarding](/wiki/Storyboarding)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Beatmap submission and ranking\n\n[Submission](/wiki/Submission) • [Modding](/wiki/Modding) • [Ranking procedure](/wiki/Beatmap_ranking_procedure) • [Mappers' Guild](/wiki/Mappers_Guild) • [Project Loved](/wiki/Project_Loved)\n\n[Ranking criteria](/wiki/Ranking_Criteria): [osu!](/wiki/Ranking_Criteria/osu!) • [osu!taiko](/wiki/Ranking_Criteria/osu!taiko) • [osu!catch](/wiki/Ranking_Criteria/osu!catch) • [osu!mania](/wiki/Ranking_Criteria/osu!mania)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# Community\n\n[Tournaments](/wiki/Tournaments) • [Skinning](/wiki/Skinning) • [Projects](/wiki/Projects) • [Guides](/wiki/Guides) • [osu!dev Discord server](/wiki/osu!dev_Discord_server) • [How you can help](/wiki/How_You_Can_Help!) • [Glossary](/wiki/Glossary)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# People\n\n[The Team](/wiki/People/The_Team): [Developers](/wiki/People/The_Team/Developers) • [Global Moderation Team](/wiki/People/The_Team/Global_Moderation_Team) • [Support Team](/wiki/People/The_Team/Support_Team) • [Nomination Assessment Team](/wiki/People/The_Team/Nomination_Assessment_Team) • [Beatmap Nominators](/wiki/People/The_Team/Beatmap_Nominators) • [osu! Alumni](/wiki/People/The_Team/osu!_Alumni) • [Project Loved Team](/wiki/People/The_Team/Project_Loved_Team)\n\nOrganisations: [osu! UCI](/wiki/Organisations/osu!_UCI)\n\n[Community Contributors](/wiki/People/Community_Contributors) • [Users with unique titles](/wiki/People/Users_with_unique_titles)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# For developers\n\n[API](/wiki/osu!api) • [Bot account](/wiki/Bot_account) • [Brand identity guidelines](/wiki/Brand_identity_guidelines)\n\n</div>\n<div class=\"wiki-main-page-panel\">\n\n# About the wiki\n\n[Sitemap](/wiki/Sitemap) • [Contribution guide](/wiki/osu!_wiki_Contribution_Guide) • [Article styling criteria](/wiki/Article_Styling_Criteria) • [News styling criteria](/wiki/News_Styling_Criteria)\n\n</div>\n</div>\n";
|
||||
}
|
||||
|
||||
@@ -69,8 +69,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
AddStep("set current path", () => markdownContainer.CurrentPath = $"{API.WebsiteRootUrl}/wiki/Article_styling_criteria/");
|
||||
|
||||
AddStep("set '/wiki/Main_Page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_Page)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_Page");
|
||||
AddStep("set '/wiki/Main_page''", () => markdownContainer.Text = "[wiki main page](/wiki/Main_page)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/Main_page");
|
||||
|
||||
AddStep("set '../FAQ''", () => markdownContainer.Text = "[FAQ](../FAQ)");
|
||||
AddAssert("check url", () => markdownContainer.Link.Url == $"{API.WebsiteRootUrl}/wiki/FAQ");
|
||||
|
||||
@@ -107,12 +107,12 @@ namespace osu.Game.Tests.Visual.Online
|
||||
};
|
||||
});
|
||||
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_Page
|
||||
// From https://osu.ppy.sh/api/v2/wiki/en/Main_page
|
||||
private APIWikiPage responseMainPage => new APIWikiPage
|
||||
{
|
||||
Title = "Main Page",
|
||||
Layout = "main_page",
|
||||
Path = "Main_Page",
|
||||
Title = "Main page",
|
||||
Layout = WikiOverlay.INDEX_PATH.ToLowerInvariant(), // custom classes are always lower snake.
|
||||
Path = WikiOverlay.INDEX_PATH,
|
||||
Locale = "en",
|
||||
Subtitle = null,
|
||||
Markdown =
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Online.Placeholders;
|
||||
using osu.Game.Online.Rooms;
|
||||
using osu.Game.Rulesets.Osu;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
@@ -49,8 +50,8 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
|
||||
// Previous test instances of the results screen may still exist at this point so wait for
|
||||
// those screens to be cleaned up by the base SetUpSteps before re-initialising test state.
|
||||
// The the screen also holds a leased Beatmap bindable so reassigning it must happen after
|
||||
// the screen as been exited.
|
||||
// The screen also holds a leased Beatmap bindable so reassigning it must happen after
|
||||
// the screen has been exited.
|
||||
AddStep("initialise user scores and beatmap", () =>
|
||||
{
|
||||
lowestScoreId = 1;
|
||||
@@ -63,8 +64,6 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
userScore.Statistics = new Dictionary<HitResult, int>();
|
||||
userScore.MaximumStatistics = new Dictionary<HitResult, int>();
|
||||
|
||||
bindHandler();
|
||||
|
||||
// Beatmap is required to be an actual beatmap so the scores can get their scores correctly
|
||||
// calculated for standardised scoring, else the tests that rely on ordering will fall over.
|
||||
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
||||
@@ -77,6 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
|
||||
AddAssert($"score panel position is {real_user_position}",
|
||||
@@ -86,7 +86,10 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[Test]
|
||||
public void TestShowNullUserScore()
|
||||
{
|
||||
AddStep("bind user score info handler", () => bindHandler());
|
||||
|
||||
createResults();
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
|
||||
}
|
||||
@@ -97,6 +100,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("bind user score info handler", () => bindHandler(true, userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
|
||||
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
|
||||
@@ -108,6 +112,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddStep("bind delayed handler", () => bindHandler(true));
|
||||
|
||||
createResults();
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
|
||||
}
|
||||
@@ -115,10 +120,11 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
[Test]
|
||||
public void TestFetchWhenScrolledToTheRight()
|
||||
{
|
||||
createResults();
|
||||
|
||||
AddStep("bind delayed handler", () => bindHandler(true));
|
||||
|
||||
createResults();
|
||||
waitForDisplay();
|
||||
|
||||
for (int i = 0; i < 2; i++)
|
||||
{
|
||||
int beforePanelCount = 0;
|
||||
@@ -134,12 +140,44 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNoMoreScoresToTheRight()
|
||||
{
|
||||
AddStep("bind delayed handler with scores", () => bindHandler(delayed: true));
|
||||
|
||||
createResults();
|
||||
waitForDisplay();
|
||||
|
||||
int beforePanelCount = 0;
|
||||
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
|
||||
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() >= beforePanelCount + scores_per_result);
|
||||
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
||||
|
||||
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
||||
AddStep("bind delayed handler with no scores", () => bindHandler(delayed: true, noScores: true));
|
||||
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
||||
|
||||
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
||||
waitForDisplay();
|
||||
|
||||
AddAssert("count not increased", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount);
|
||||
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
||||
AddAssert("no placeholders shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.Zero);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestFetchWhenScrolledToTheLeft()
|
||||
{
|
||||
AddStep("bind user score info handler", () => bindHandler(userScore: userScore));
|
||||
|
||||
createResults(() => userScore);
|
||||
waitForDisplay();
|
||||
|
||||
AddStep("bind delayed handler", () => bindHandler(true));
|
||||
|
||||
@@ -158,6 +196,15 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestShowWithNoScores()
|
||||
{
|
||||
AddStep("bind user score info handler", () => bindHandler(noScores: true));
|
||||
createResults();
|
||||
AddAssert("no scores visible", () => !resultsScreen.ScorePanelList.GetScorePanels().Any());
|
||||
AddAssert("placeholder shown", () => this.ChildrenOfType<MessagePlaceholder>().Count(), () => Is.EqualTo(1));
|
||||
}
|
||||
|
||||
private void createResults(Func<ScoreInfo> getScore = null)
|
||||
{
|
||||
AddStep("load results", () =>
|
||||
@@ -169,7 +216,6 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
});
|
||||
|
||||
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
|
||||
waitForDisplay();
|
||||
}
|
||||
|
||||
private void waitForDisplay()
|
||||
@@ -183,7 +229,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
AddWaitStep("wait for display", 5);
|
||||
}
|
||||
|
||||
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
|
||||
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false, bool noScores = false) => ((DummyAPIAccess)API).HandleRequest = request =>
|
||||
{
|
||||
// pre-check for requests we should be handling (as they are scheduled below).
|
||||
switch (request)
|
||||
@@ -219,7 +265,7 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
break;
|
||||
|
||||
case IndexPlaylistScoresRequest i:
|
||||
triggerSuccess(i, createIndexResponse(i));
|
||||
triggerSuccess(i, createIndexResponse(i, noScores));
|
||||
break;
|
||||
}
|
||||
}, delay);
|
||||
@@ -301,10 +347,12 @@ namespace osu.Game.Tests.Visual.Playlists
|
||||
return multiplayerUserScore;
|
||||
}
|
||||
|
||||
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
|
||||
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req, bool noScores = false)
|
||||
{
|
||||
var result = new IndexedMultiplayerScores();
|
||||
|
||||
if (noScores) return result;
|
||||
|
||||
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
||||
|
||||
for (int i = 1; i <= scores_per_result; i++)
|
||||
|
||||
@@ -40,8 +40,15 @@ namespace osu.Game.Tests.Visual.Settings
|
||||
AddStep("change value from default", () => textBox.Current.Value = "non-default");
|
||||
AddUntilStep("restore button shown", () => revertToDefaultButton.Alpha > 0);
|
||||
|
||||
AddStep("disable setting", () => textBox.Current.Disabled = true);
|
||||
AddUntilStep("restore button still shown", () => revertToDefaultButton.Alpha > 0);
|
||||
|
||||
AddStep("enable setting", () => textBox.Current.Disabled = false);
|
||||
AddStep("restore default", () => textBox.Current.SetDefault());
|
||||
AddUntilStep("restore button hidden", () => revertToDefaultButton.Alpha == 0);
|
||||
|
||||
AddStep("disable setting", () => textBox.Current.Disabled = true);
|
||||
AddUntilStep("restore button still hidden", () => revertToDefaultButton.Alpha == 0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -76,6 +76,20 @@ namespace osu.Game.Tests.Visual.SongSelect
|
||||
assertCollectionDropdownContains("2");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionsCleared()
|
||||
{
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "1"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "2"))));
|
||||
AddStep("add collection", () => writeAndRefresh(r => r.Add(new BeatmapCollection(name: "3"))));
|
||||
|
||||
AddAssert("check count 5", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(5));
|
||||
|
||||
AddStep("delete all collections", () => writeAndRefresh(r => r.RemoveAll<BeatmapCollection>()));
|
||||
|
||||
AddAssert("check count 2", () => control.ChildrenOfType<CollectionDropdown>().Single().ChildrenOfType<Menu.DrawableMenuItem>().Count(), () => Is.EqualTo(2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestCollectionRemovedFromDropdown()
|
||||
{
|
||||
|
||||
@@ -542,10 +542,23 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("hidden selected", () => getPanelForMod(typeof(OsuModHidden)).Active.Value);
|
||||
AddAssert("all text selected in textbox", () =>
|
||||
{
|
||||
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
|
||||
return textBox.SelectedText == textBox.Text;
|
||||
});
|
||||
|
||||
AddStep("press enter again", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("hidden deselected", () => !getPanelForMod(typeof(OsuModHidden)).Active.Value);
|
||||
|
||||
AddStep("apply search matching nothing", () => modSelectOverlay.SearchTerm = "ZZZ");
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("all text not selected in textbox", () =>
|
||||
{
|
||||
var textBox = modSelectOverlay.ChildrenOfType<SearchTextBox>().Single();
|
||||
return textBox.SelectedText != textBox.Text;
|
||||
});
|
||||
|
||||
AddStep("clear search", () => modSelectOverlay.SearchTerm = string.Empty);
|
||||
AddStep("press enter", () => InputManager.Key(Key.Enter));
|
||||
AddAssert("mod select hidden", () => modSelectOverlay.State.Value == Visibility.Hidden);
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
{
|
||||
VolumeMeter meter;
|
||||
MuteButton mute;
|
||||
Add(meter = new VolumeMeter("MASTER", 125, Color4.Blue) { Position = new Vector2(10) });
|
||||
Add(meter = new VolumeMeter("MASTER", 125, Color4.Green) { Position = new Vector2(10) });
|
||||
AddSliderStep("master volume", 0, 10, 0, i => meter.Bindable.Value = i * 0.1);
|
||||
|
||||
Add(new VolumeMeter("BIG", 250, Color4.Red)
|
||||
@@ -22,6 +22,15 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(10),
|
||||
Margin = new MarginPadding { Left = 250 },
|
||||
});
|
||||
|
||||
Add(new VolumeMeter("SML", 125, Color4.Blue)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Position = new Vector2(10),
|
||||
Margin = new MarginPadding { Right = 500 },
|
||||
});
|
||||
|
||||
Add(mute = new MuteButton
|
||||
|
||||
@@ -12,6 +12,7 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.Legacy;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Models;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Screens.Menu;
|
||||
using osuTK;
|
||||
@@ -101,11 +102,25 @@ namespace osu.Game.Tournament.Components
|
||||
|
||||
private void refreshContent()
|
||||
{
|
||||
if (beatmap == null)
|
||||
beatmap ??= new BeatmapInfo
|
||||
{
|
||||
flow.Clear();
|
||||
return;
|
||||
}
|
||||
Metadata = new BeatmapMetadata
|
||||
{
|
||||
Artist = "unknown",
|
||||
Title = "no beatmap selected",
|
||||
Author = new RealmUser { Username = "unknown" },
|
||||
},
|
||||
DifficultyName = "unknown",
|
||||
BeatmapSet = new BeatmapSetInfo(),
|
||||
StarRating = 0,
|
||||
Difficulty = new BeatmapDifficulty
|
||||
{
|
||||
CircleSize = 0,
|
||||
DrainRate = 0,
|
||||
OverallDifficulty = 0,
|
||||
ApproachRate = 0,
|
||||
},
|
||||
};
|
||||
|
||||
double bpm = beatmap.BPM;
|
||||
double length = beatmap.Length;
|
||||
|
||||
@@ -194,7 +194,7 @@ namespace osu.Game.Tournament.Components
|
||||
|
||||
// Use DelayedLoadWrapper to avoid content unloading when switching away to another screen.
|
||||
protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func<Drawable> createContentFunc, double timeBeforeLoad)
|
||||
=> new DelayedLoadWrapper(createContentFunc, timeBeforeLoad);
|
||||
=> new DelayedLoadWrapper(createContentFunc(), timeBeforeLoad);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Extensions;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Notifications;
|
||||
@@ -28,6 +29,8 @@ namespace osu.Game
|
||||
/// </summary>
|
||||
public partial class BackgroundDataStoreProcessor : Component
|
||||
{
|
||||
protected Task ProcessingTask { get; private set; } = null!;
|
||||
|
||||
[Resolved]
|
||||
private RulesetStore rulesetStore { get; set; } = null!;
|
||||
|
||||
@@ -61,7 +64,7 @@ namespace osu.Game
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
Task.Factory.StartNew(() =>
|
||||
ProcessingTask = Task.Factory.StartNew(() =>
|
||||
{
|
||||
Logger.Log("Beginning background data store processing..");
|
||||
|
||||
@@ -314,10 +317,17 @@ namespace osu.Game
|
||||
{
|
||||
Logger.Log("Querying for scores that need total score conversion...");
|
||||
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed && s.BeatmapInfo != null
|
||||
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.AsEnumerable().Select(s => s.ID)));
|
||||
HashSet<Guid> scoreIds = realmAccess.Run(r => new HashSet<Guid>(
|
||||
r.All<ScoreInfo>()
|
||||
.Where(s => !s.BackgroundReprocessingFailed
|
||||
&& s.BeatmapInfo != null
|
||||
&& s.IsLegacyScore
|
||||
&& s.TotalScoreVersion < LegacyScoreEncoder.LATEST_VERSION)
|
||||
.AsEnumerable()
|
||||
// must be done after materialisation, as realm doesn't want to support
|
||||
// nested property predicates
|
||||
.Where(s => s.Ruleset.IsLegacyRuleset())
|
||||
.Select(s => s.ID)));
|
||||
|
||||
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
|
||||
|
||||
|
||||
@@ -92,7 +92,6 @@ namespace osu.Game.Beatmaps.Drawables
|
||||
EdgeEffect = new EdgeEffectParameters
|
||||
{
|
||||
Colour = Color4.Black.Opacity(0.06f),
|
||||
|
||||
Type = EdgeEffectType.Shadow,
|
||||
Radius = 3,
|
||||
},
|
||||
|
||||
@@ -74,7 +74,7 @@ namespace osu.Game.Collections
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (int i in changes.DeletedIndices)
|
||||
foreach (int i in changes.DeletedIndices.OrderByDescending(i => i))
|
||||
filters.RemoveAt(i + 1);
|
||||
|
||||
foreach (int i in changes.InsertedIndices)
|
||||
|
||||
@@ -17,6 +17,7 @@ using osu.Game.Localisation;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.Mods.Input;
|
||||
using osu.Game.Rulesets.Scoring;
|
||||
using osu.Game.Screens.OnlinePlay.Lounge.Components;
|
||||
using osu.Game.Screens.Select;
|
||||
using osu.Game.Screens.Select.Filter;
|
||||
using osu.Game.Skinning;
|
||||
@@ -191,6 +192,8 @@ namespace osu.Game.Configuration
|
||||
SetDefault(OsuSetting.EditorLimitedDistanceSnap, false);
|
||||
SetDefault(OsuSetting.EditorShowSpeedChanges, false);
|
||||
|
||||
SetDefault(OsuSetting.MultiplayerRoomFilter, RoomPermissionsFilter.All);
|
||||
|
||||
SetDefault(OsuSetting.LastProcessedMetadataId, -1);
|
||||
|
||||
SetDefault(OsuSetting.ComboColourNormalisationAmount, 0.2f, 0f, 1f, 0.01f);
|
||||
@@ -423,5 +426,6 @@ namespace osu.Game.Configuration
|
||||
TouchDisableGameplayTaps,
|
||||
ModSelectTextSearchStartsActive,
|
||||
UserOnlineStatus,
|
||||
MultiplayerRoomFilter
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using osu.Framework.Extensions;
|
||||
using osu.Framework.IO.Stores;
|
||||
using osu.Framework.Logging;
|
||||
@@ -98,15 +98,11 @@ namespace osu.Game.Database
|
||||
// can potentially be run asynchronously, although we will need to consider operation order for disk deletion vs realm removal.
|
||||
realm.Write(r =>
|
||||
{
|
||||
// TODO: consider using a realm native query to avoid iterating all files (https://github.com/realm/realm-dotnet/issues/2659#issuecomment-927823707)
|
||||
var files = r.All<RealmFile>().ToList();
|
||||
|
||||
foreach (var file in files)
|
||||
foreach (var file in r.All<RealmFile>().Filter(@$"{nameof(RealmFile.Usages)}.@count = 0"))
|
||||
{
|
||||
totalFiles++;
|
||||
|
||||
if (file.BacklinksCount > 0)
|
||||
continue;
|
||||
Debug.Assert(file.BacklinksCount == 0);
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -311,13 +311,22 @@ namespace osu.Game.Database
|
||||
long maximumLegacyBonusScore = attributes.BonusScore;
|
||||
|
||||
double legacyAccScore = maximumLegacyAccuracyScore * score.Accuracy;
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
// Note that `maximumLegacyComboScore + maximumLegacyBonusScore` can actually be 0
|
||||
// when playing a beatmap with no bonus objects, with mods that have a 0.0x multiplier on stable (relax/autopilot).
|
||||
// In such cases, just assume 0.
|
||||
double comboProportion = maximumLegacyComboScore + maximumLegacyBonusScore > 0
|
||||
? Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore)
|
||||
: 0;
|
||||
|
||||
double comboProportion;
|
||||
|
||||
if (maximumLegacyComboScore + maximumLegacyBonusScore > 0)
|
||||
{
|
||||
// We can not separate the ComboScore from the BonusScore, so we keep the bonus in the ratio.
|
||||
comboProportion = Math.Max((double)score.LegacyTotalScore - legacyAccScore, 0) / (maximumLegacyComboScore + maximumLegacyBonusScore);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Two possible causes:
|
||||
// the beatmap has no bonus objects *AND*
|
||||
// either the active mods have a zero mod multiplier, in which case assume 0,
|
||||
// or the *beatmap* has a zero `difficultyPeppyStars` (or just no combo-giving objects), in which case assume 1.
|
||||
comboProportion = legacyModMultiplier == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
// We assume the bonus proportion only makes up the rest of the score that exceeds maximumLegacyBaseScore.
|
||||
long maximumLegacyBaseScore = maximumLegacyAccuracyScore + maximumLegacyComboScore;
|
||||
@@ -437,16 +446,42 @@ namespace osu.Game.Database
|
||||
break;
|
||||
|
||||
case 2:
|
||||
// compare logic in `CatchScoreProcessor`.
|
||||
|
||||
// this could technically be slightly incorrect in the case of stable scores.
|
||||
// because large droplet misses are counted as full misses in stable scores,
|
||||
// `score.MaximumStatistics.GetValueOrDefault(Great)` will be equal to the count of fruits *and* large droplets
|
||||
// rather than just fruits (which was the intent).
|
||||
// this is not fixable without introducing an extra legacy score attribute dedicated for catch,
|
||||
// and this is a ballpark conversion process anyway, so attempt to trudge on.
|
||||
int fruitTinyScaleDivisor = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) + score.MaximumStatistics.GetValueOrDefault(HitResult.Great);
|
||||
double fruitTinyScale = fruitTinyScaleDivisor == 0
|
||||
? 0
|
||||
: (double)score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) / fruitTinyScaleDivisor;
|
||||
|
||||
const int max_tiny_droplets_portion = 400000;
|
||||
|
||||
double comboPortion = 1000000 - max_tiny_droplets_portion + max_tiny_droplets_portion * (1 - fruitTinyScale);
|
||||
double dropletsPortion = max_tiny_droplets_portion * fruitTinyScale;
|
||||
double dropletsHit = score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit) == 0
|
||||
? 0
|
||||
: (double)score.Statistics.GetValueOrDefault(HitResult.SmallTickHit) / score.MaximumStatistics.GetValueOrDefault(HitResult.SmallTickHit);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
600000 * comboProportion
|
||||
+ 400000 * score.Accuracy
|
||||
comboPortion * estimateComboProportionForCatch(attributes.MaxCombo, score.MaxCombo, score.Statistics.GetValueOrDefault(HitResult.Miss))
|
||||
+ dropletsPortion * dropletsHit
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
case 3:
|
||||
// in the mania case accuracy actually changes between score V1 and score V2 / standardised
|
||||
// (PERFECT weighting changes from 300 to 305),
|
||||
// so for better accuracy recompute accuracy locally based on hit statistics and use that instead,
|
||||
double scoreV2Accuracy = ComputeAccuracy(score);
|
||||
|
||||
convertedTotalScore = (long)Math.Round((
|
||||
850000 * comboProportion
|
||||
+ 150000 * Math.Pow(score.Accuracy, 2 + 2 * score.Accuracy)
|
||||
+ 150000 * Math.Pow(scoreV2Accuracy, 2 + 2 * scoreV2Accuracy)
|
||||
+ bonusProportion) * modMultiplier);
|
||||
break;
|
||||
|
||||
@@ -461,6 +496,94 @@ namespace osu.Game.Database
|
||||
return convertedTotalScore;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// For catch, the general method of calculating the combo proportion used for other rulesets is generally useless.
|
||||
/// This is because in stable score V1, catch has quadratic score progression,
|
||||
/// while in stable score V2, score progression is logarithmic up to 200 combo and then linear.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This means that applying the naive rescale method to scores with lots of short combos (think 10x 100-long combos on a 1000-object map)
|
||||
/// by linearly rescaling the combo portion as given by score V1 leads to horribly underestimating it.
|
||||
/// Therefore this method attempts to counteract this by calculating the best case estimate for the combo proportion that takes all of the above into account.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// The general idea is that aside from the <paramref name="scoreMaxCombo"/> which the player is known to have hit,
|
||||
/// the remaining misses are evenly distributed across the rest of the objects that give combo.
|
||||
/// This is therefore a worst-case estimate.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
private static double estimateComboProportionForCatch(int beatmapMaxCombo, int scoreMaxCombo, int scoreMissCount)
|
||||
{
|
||||
if (beatmapMaxCombo == 0)
|
||||
return 1;
|
||||
|
||||
if (scoreMaxCombo == 0)
|
||||
return 0;
|
||||
|
||||
if (beatmapMaxCombo == scoreMaxCombo)
|
||||
return 1;
|
||||
|
||||
double estimatedBestCaseTotal = estimateBestCaseComboTotal(beatmapMaxCombo);
|
||||
|
||||
int remainingCombo = beatmapMaxCombo - (scoreMaxCombo + scoreMissCount);
|
||||
double totalDroppedScore = 0;
|
||||
|
||||
int assumedLengthOfRemainingCombos = (int)Math.Floor((double)remainingCombo / scoreMissCount);
|
||||
|
||||
if (assumedLengthOfRemainingCombos > 0)
|
||||
{
|
||||
int assumedCombosCount = (int)Math.Floor((double)remainingCombo / assumedLengthOfRemainingCombos);
|
||||
totalDroppedScore += assumedCombosCount * estimateDroppedComboScoreAfterMiss(assumedLengthOfRemainingCombos);
|
||||
|
||||
remainingCombo -= assumedCombosCount * assumedLengthOfRemainingCombos;
|
||||
|
||||
if (remainingCombo > 0)
|
||||
totalDroppedScore += estimateDroppedComboScoreAfterMiss(remainingCombo);
|
||||
}
|
||||
else
|
||||
{
|
||||
// there are so many misses that attempting to evenly divide remaining combo results in 0 length per combo,
|
||||
// i.e. all remaining judgements are combo breaks.
|
||||
// in that case, presume every single remaining object is a miss and did not give any combo score.
|
||||
totalDroppedScore = estimatedBestCaseTotal - estimateBestCaseComboTotal(scoreMaxCombo);
|
||||
}
|
||||
|
||||
return estimatedBestCaseTotal == 0
|
||||
? 1
|
||||
: 1 - Math.Clamp(totalDroppedScore / estimatedBestCaseTotal, 0, 1);
|
||||
|
||||
double estimateBestCaseComboTotal(int maxCombo)
|
||||
{
|
||||
if (maxCombo == 0)
|
||||
return 1;
|
||||
|
||||
double estimatedTotal = 0.5 * Math.Min(maxCombo, 2);
|
||||
|
||||
if (maxCombo <= 2)
|
||||
return estimatedTotal;
|
||||
|
||||
// int_2^x log_4(t) dt
|
||||
estimatedTotal += (Math.Min(maxCombo, 200) * (Math.Log(Math.Min(maxCombo, 200)) - 1) + 2 - Math.Log(4)) / Math.Log(4);
|
||||
|
||||
if (maxCombo <= 200)
|
||||
return estimatedTotal;
|
||||
|
||||
estimatedTotal += (maxCombo - 200) * Math.Log(200) / Math.Log(4);
|
||||
return estimatedTotal;
|
||||
}
|
||||
|
||||
double estimateDroppedComboScoreAfterMiss(int lengthOfComboAfterMiss)
|
||||
{
|
||||
if (lengthOfComboAfterMiss >= 200)
|
||||
lengthOfComboAfterMiss = 200;
|
||||
|
||||
// int_0^x (log_4(200) - log_4(t)) dt
|
||||
// note that this is an pessimistic estimate, i.e. it may subtract too much if the miss happened before reaching 200 combo
|
||||
return lengthOfComboAfterMiss * (1 + Math.Log(200) - Math.Log(lengthOfComboAfterMiss)) / Math.Log(4);
|
||||
}
|
||||
}
|
||||
|
||||
public static double ComputeAccuracy(ScoreInfo scoreInfo)
|
||||
{
|
||||
Ruleset ruleset = scoreInfo.Ruleset.CreateInstance();
|
||||
|
||||
@@ -59,7 +59,8 @@ namespace osu.Game.Extensions
|
||||
/// <returns>A short relative string representing the input time.</returns>
|
||||
public static string ToShortRelativeTime(this DateTimeOffset time, TimeSpan lowerCutoff)
|
||||
{
|
||||
if (time == default)
|
||||
// covers all `DateTimeOffset` instances with the date portion of 0001-01-01.
|
||||
if (time.Date == default)
|
||||
return "-";
|
||||
|
||||
var now = DateTime.Now;
|
||||
|
||||
@@ -15,7 +15,6 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Framework.Bindables;
|
||||
|
||||
@@ -79,9 +78,9 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s
|
||||
/// shape is drawn to the screen.
|
||||
/// shape is drawn to the screen. Default is true.
|
||||
/// </summary>
|
||||
public bool Masking { get; set; }
|
||||
public bool ClampToDrawable { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Whether we should drop-off alpha values of triangles more quickly to improve
|
||||
@@ -258,13 +257,12 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
private IShader shader;
|
||||
private Texture texture;
|
||||
private bool masking;
|
||||
private bool clamp;
|
||||
|
||||
private readonly List<TriangleParticle> parts = new List<TriangleParticle>();
|
||||
private readonly Vector2 triangleSize = new Vector2(1f, equilateral_triangle_ratio) * triangle_size;
|
||||
|
||||
private Vector2 size;
|
||||
private IVertexBatch<TexturedVertex2D> vertexBatch;
|
||||
|
||||
public TrianglesDrawNode(Triangles source)
|
||||
: base(source)
|
||||
@@ -278,7 +276,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
shader = Source.shader;
|
||||
texture = Source.texture;
|
||||
size = Source.DrawSize;
|
||||
masking = Source.Masking;
|
||||
clamp = Source.ClampToDrawable;
|
||||
|
||||
parts.Clear();
|
||||
parts.AddRange(Source.parts);
|
||||
@@ -290,12 +288,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
base.Draw(renderer);
|
||||
|
||||
if (Source.AimCount > 0 && (vertexBatch == null || vertexBatch.Size != Source.AimCount))
|
||||
{
|
||||
vertexBatch?.Dispose();
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
@@ -314,7 +306,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
|
||||
|
||||
Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
|
||||
var drawQuad = new Quad(
|
||||
Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix),
|
||||
@@ -333,7 +325,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
triangleQuad.Height
|
||||
) / relativeSize;
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
renderer.DrawQuad(texture, drawQuad, colourInfo, new RectangleF(0, 0, 1, 1), textureCoords: textureCoords);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
@@ -356,7 +348,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Graphics.Rendering;
|
||||
using osu.Framework.Graphics.Rendering.Vertices;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
|
||||
@@ -35,9 +34,9 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
|
||||
/// <summary>
|
||||
/// If enabled, only the portion of triangles that falls within this <see cref="Drawable"/>'s
|
||||
/// shape is drawn to the screen.
|
||||
/// shape is drawn to the screen. Default is true.
|
||||
/// </summary>
|
||||
public bool Masking { get; set; }
|
||||
public bool ClampToDrawable { get; set; } = true;
|
||||
|
||||
private readonly BindableFloat spawnRatio = new BindableFloat(1f);
|
||||
|
||||
@@ -194,9 +193,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
private Vector2 size;
|
||||
private float thickness;
|
||||
private float texelSize;
|
||||
private bool masking;
|
||||
|
||||
private IVertexBatch<TexturedVertex2D>? vertexBatch;
|
||||
private bool clamp;
|
||||
|
||||
public TrianglesDrawNode(TrianglesV2 source)
|
||||
: base(source)
|
||||
@@ -211,7 +208,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
texture = Source.texture;
|
||||
size = Source.DrawSize;
|
||||
thickness = Source.Thickness;
|
||||
masking = Source.Masking;
|
||||
clamp = Source.ClampToDrawable;
|
||||
|
||||
Quad triangleQuad = new Quad(
|
||||
Vector2Extensions.Transform(Vector2.Zero, DrawInfo.Matrix),
|
||||
@@ -235,12 +232,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
if (Source.AimCount == 0 || thickness == 0)
|
||||
return;
|
||||
|
||||
if (vertexBatch == null || vertexBatch.Size != Source.AimCount)
|
||||
{
|
||||
vertexBatch?.Dispose();
|
||||
vertexBatch = renderer.CreateQuadBatch<TexturedVertex2D>(Source.AimCount, 1);
|
||||
}
|
||||
|
||||
borderDataBuffer ??= renderer.CreateUniformBuffer<TriangleBorderData>();
|
||||
borderDataBuffer.Data = borderDataBuffer.Data with
|
||||
{
|
||||
@@ -257,7 +248,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
Vector2 topLeft = particle.Position - new Vector2(relativeSize.X * 0.5f, 0f);
|
||||
|
||||
Quad triangleQuad = masking ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
Quad triangleQuad = clamp ? clampToDrawable(topLeft, relativeSize) : new Quad(topLeft.X, topLeft.Y, relativeSize.X, relativeSize.Y);
|
||||
|
||||
var drawQuad = new Quad(
|
||||
Vector2Extensions.Transform(triangleQuad.TopLeft * size, DrawInfo.Matrix),
|
||||
@@ -273,7 +264,7 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
triangleQuad.Height
|
||||
) / relativeSize;
|
||||
|
||||
renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), vertexBatch.AddAction, textureCoords: textureCoords);
|
||||
renderer.DrawQuad(texture, drawQuad, DrawColourInfo.Colour.Interpolate(triangleQuad), new RectangleF(0, 0, 1, 1), textureCoords: textureCoords);
|
||||
}
|
||||
|
||||
shader.Unbind();
|
||||
@@ -296,7 +287,6 @@ namespace osu.Game.Graphics.Backgrounds
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
vertexBatch?.Dispose();
|
||||
borderDataBuffer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using System;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
@@ -17,21 +16,9 @@ namespace osu.Game.Graphics.Containers
|
||||
public Drawable Icon
|
||||
{
|
||||
get => InternalChild;
|
||||
|
||||
set => InternalChild = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines an edge effect of this <see cref="Container"/>.
|
||||
/// Edge effects are e.g. glow or a shadow.
|
||||
/// Only has an effect when <see cref="CompositeDrawable.Masking"/> is true.
|
||||
/// </summary>
|
||||
public new EdgeEffectParameters EdgeEffect
|
||||
{
|
||||
get => base.EdgeEffect;
|
||||
set => base.EdgeEffect = value;
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
@@ -49,10 +36,5 @@ namespace osu.Game.Graphics.Containers
|
||||
InternalChild.Origin = Anchor.Centre;
|
||||
}
|
||||
}
|
||||
|
||||
public ConstrainedIconContainer()
|
||||
{
|
||||
Masking = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,7 @@ namespace osu.Game.Graphics
|
||||
|
||||
case HitResult.SmallTickHit:
|
||||
case HitResult.LargeTickHit:
|
||||
case HitResult.SliderTailHit:
|
||||
case HitResult.Great:
|
||||
return Blue;
|
||||
|
||||
|
||||
@@ -5,95 +5,90 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Colour;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Primitives;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Graphics.Sprites
|
||||
{
|
||||
public partial class GlowingSpriteText : Container, IHasText
|
||||
public partial class GlowingSpriteText : BufferedContainer, IHasText
|
||||
{
|
||||
private readonly OsuSpriteText spriteText, blurredText;
|
||||
private const float blur_sigma = 3f;
|
||||
|
||||
// Inflate draw quad to prevent glow from trimming at the edges.
|
||||
// Padding won't suffice since it will affect text position in cases when it's not centered.
|
||||
protected override Quad ComputeScreenSpaceDrawQuad() => base.ComputeScreenSpaceDrawQuad().AABBFloat.Inflate(Blur.KernelSize(blur_sigma));
|
||||
|
||||
private readonly OsuSpriteText text;
|
||||
|
||||
public LocalisableString Text
|
||||
{
|
||||
get => spriteText.Text;
|
||||
set => blurredText.Text = spriteText.Text = value;
|
||||
get => text.Text;
|
||||
set => text.Text = value;
|
||||
}
|
||||
|
||||
public FontUsage Font
|
||||
{
|
||||
get => spriteText.Font;
|
||||
set => blurredText.Font = spriteText.Font = value.With(fixedWidth: true);
|
||||
get => text.Font;
|
||||
set => text.Font = value.With(fixedWidth: true);
|
||||
}
|
||||
|
||||
public Vector2 TextSize
|
||||
{
|
||||
get => spriteText.Size;
|
||||
set => blurredText.Size = spriteText.Size = value;
|
||||
get => text.Size;
|
||||
set => text.Size = value;
|
||||
}
|
||||
|
||||
public ColourInfo TextColour
|
||||
{
|
||||
get => spriteText.Colour;
|
||||
set => spriteText.Colour = value;
|
||||
get => text.Colour;
|
||||
set => text.Colour = value;
|
||||
}
|
||||
|
||||
public ColourInfo GlowColour
|
||||
{
|
||||
get => blurredText.Colour;
|
||||
set => blurredText.Colour = value;
|
||||
get => EffectColour;
|
||||
set
|
||||
{
|
||||
EffectColour = value;
|
||||
BackgroundColour = value.MultiplyAlpha(0f);
|
||||
}
|
||||
}
|
||||
|
||||
public Vector2 Spacing
|
||||
{
|
||||
get => spriteText.Spacing;
|
||||
set => spriteText.Spacing = blurredText.Spacing = value;
|
||||
get => text.Spacing;
|
||||
set => text.Spacing = value;
|
||||
}
|
||||
|
||||
public bool UseFullGlyphHeight
|
||||
{
|
||||
get => spriteText.UseFullGlyphHeight;
|
||||
set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value;
|
||||
get => text.UseFullGlyphHeight;
|
||||
set => text.UseFullGlyphHeight = value;
|
||||
}
|
||||
|
||||
public Bindable<string> Current
|
||||
{
|
||||
get => spriteText.Current;
|
||||
set => spriteText.Current = value;
|
||||
get => text.Current;
|
||||
set => text.Current = value;
|
||||
}
|
||||
|
||||
public GlowingSpriteText()
|
||||
: base(cachedFrameBuffer: true)
|
||||
{
|
||||
AutoSizeAxes = Axes.Both;
|
||||
|
||||
Children = new Drawable[]
|
||||
BlurSigma = new Vector2(blur_sigma);
|
||||
RedrawOnScale = false;
|
||||
DrawOriginal = true;
|
||||
EffectBlending = BlendingParameters.Additive;
|
||||
EffectPlacement = EffectPlacement.InFront;
|
||||
Child = text = new OsuSpriteText
|
||||
{
|
||||
new BufferedContainer(cachedFrameBuffer: true)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
BlurSigma = new Vector2(4),
|
||||
RedrawOnScale = false,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Blending = BlendingParameters.Additive,
|
||||
Size = new Vector2(3f),
|
||||
Children = new[]
|
||||
{
|
||||
blurredText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
},
|
||||
},
|
||||
},
|
||||
spriteText = new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
},
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Shadow = false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
TriangleScale = 4,
|
||||
ColourDark = OsuColour.Gray(0.88f),
|
||||
Shear = new Vector2(-0.2f, 0),
|
||||
ClampToDrawable = false
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// </summary>
|
||||
public partial class PercentageCounter : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 750;
|
||||
protected override double RollingDuration => 375;
|
||||
|
||||
private float epsilon => 1e-10f;
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
/// <summary>
|
||||
/// Easing for the counter rollover animation.
|
||||
/// </summary>
|
||||
protected virtual Easing RollingEasing => Easing.OutQuint;
|
||||
protected virtual Easing RollingEasing => Easing.OutQuad;
|
||||
|
||||
private T displayedCount;
|
||||
|
||||
|
||||
@@ -48,6 +48,8 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
public void KillFocus() => textBox.KillFocus();
|
||||
|
||||
public bool SelectAll() => textBox.SelectAll();
|
||||
|
||||
public ShearedSearchTextBox()
|
||||
{
|
||||
Height = 42;
|
||||
|
||||
@@ -22,6 +22,7 @@ namespace osu.Game.Input
|
||||
{
|
||||
private Bindable<ConfineMouseMode> frameworkConfineMode;
|
||||
private Bindable<WindowMode> frameworkWindowMode;
|
||||
private Bindable<bool> frameworkMinimiseOnFocusLossInFullscreen;
|
||||
|
||||
private Bindable<OsuConfineMouseMode> osuConfineMode;
|
||||
private IBindable<bool> localUserPlaying;
|
||||
@@ -31,7 +32,9 @@ namespace osu.Game.Input
|
||||
{
|
||||
frameworkConfineMode = frameworkConfigManager.GetBindable<ConfineMouseMode>(FrameworkSetting.ConfineMouseMode);
|
||||
frameworkWindowMode = frameworkConfigManager.GetBindable<WindowMode>(FrameworkSetting.WindowMode);
|
||||
frameworkMinimiseOnFocusLossInFullscreen = frameworkConfigManager.GetBindable<bool>(FrameworkSetting.MinimiseOnFocusLossInFullscreen);
|
||||
frameworkWindowMode.BindValueChanged(_ => updateConfineMode());
|
||||
frameworkMinimiseOnFocusLossInFullscreen.BindValueChanged(_ => updateConfineMode());
|
||||
|
||||
osuConfineMode = osuConfigManager.GetBindable<OsuConfineMouseMode>(OsuSetting.ConfineMouseMode);
|
||||
localUserPlaying = localUserInfo.IsPlaying.GetBoundCopy();
|
||||
@@ -46,7 +49,8 @@ namespace osu.Game.Input
|
||||
if (frameworkConfineMode.Disabled)
|
||||
return;
|
||||
|
||||
if (frameworkWindowMode.Value == WindowMode.Fullscreen)
|
||||
// override confine mode only when clicking outside the window minimises it.
|
||||
if (frameworkWindowMode.Value == WindowMode.Fullscreen && frameworkMinimiseOnFocusLossInFullscreen.Value)
|
||||
{
|
||||
frameworkConfineMode.Value = ConfineMouseMode.Fullscreen;
|
||||
return;
|
||||
|
||||
@@ -152,9 +152,13 @@ namespace osu.Game.Localisation
|
||||
/// <summary>
|
||||
/// "In order to change the renderer, the game will close. Please open it again."
|
||||
/// </summary>
|
||||
public static LocalisableString ChangeRendererConfirmation =>
|
||||
new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
|
||||
public static LocalisableString ChangeRendererConfirmation => new TranslatableString(getKey(@"change_renderer_configuration"), @"In order to change the renderer, the game will close. Please open it again.");
|
||||
|
||||
private static string getKey(string key) => $"{prefix}:{key}";
|
||||
/// <summary>
|
||||
/// "Minimise osu! when switching to another app"
|
||||
/// </summary>
|
||||
public static LocalisableString MinimiseOnFocusLoss => new TranslatableString(getKey(@"minimise_on_focus_loss"), @"Minimise osu! when switching to another app");
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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.Localisation;
|
||||
|
||||
namespace osu.Game.Localisation
|
||||
{
|
||||
public static class PlayerSettingsOverlayStrings
|
||||
{
|
||||
private const string prefix = @"osu.Game.Resources.Localisation.PlaybackSettings";
|
||||
|
||||
/// <summary>
|
||||
/// "Seek backward {0} seconds"
|
||||
/// </summary>
|
||||
public static LocalisableString SeekBackwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_backward_seconds"), @"Seek backward {0} seconds", arg0);
|
||||
|
||||
/// <summary>
|
||||
/// "Seek forward {0} seconds"
|
||||
/// </summary>
|
||||
public static LocalisableString SeekForwardSeconds(double arg0) => new TranslatableString(getKey(@"seek_forward_seconds"), @"Seek forward {0} seconds", arg0);
|
||||
|
||||
private static string getKey(string key) => $@"{prefix}:{key}";
|
||||
}
|
||||
}
|
||||
@@ -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.Linq;
|
||||
using osu.Game.IO;
|
||||
using Realms;
|
||||
|
||||
@@ -11,5 +12,8 @@ namespace osu.Game.Models
|
||||
{
|
||||
[PrimaryKey]
|
||||
public string Hash { get; set; } = string.Empty;
|
||||
|
||||
[Backlink(nameof(RealmNamedFileUsage.File))]
|
||||
public IQueryable<RealmNamedFileUsage> Usages { get; } = null!;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ namespace osu.Game.Online.API
|
||||
public IBindable<APIUser> LocalUser => localUser;
|
||||
public IBindableList<APIUser> Friends => friends;
|
||||
public IBindable<UserActivity> Activity => activity;
|
||||
public IBindable<UserStatistics> Statistics => statistics;
|
||||
|
||||
public Language Language => game.CurrentLanguage.Value;
|
||||
|
||||
@@ -65,6 +66,8 @@ namespace osu.Game.Online.API
|
||||
private Bindable<UserStatus?> configStatus { get; } = new Bindable<UserStatus?>();
|
||||
private Bindable<UserStatus?> localUserStatus { get; } = new Bindable<UserStatus?>();
|
||||
|
||||
private Bindable<UserStatistics> statistics { get; } = new Bindable<UserStatistics>();
|
||||
|
||||
protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password));
|
||||
|
||||
private readonly CancellationTokenSource cancellationToken = new CancellationTokenSource();
|
||||
@@ -517,9 +520,21 @@ namespace osu.Game.Online.API
|
||||
flushQueue();
|
||||
}
|
||||
|
||||
public void UpdateStatistics(UserStatistics newStatistics)
|
||||
{
|
||||
statistics.Value = newStatistics;
|
||||
|
||||
if (IsLoggedIn)
|
||||
localUser.Value.Statistics = newStatistics;
|
||||
}
|
||||
|
||||
private static APIUser createGuestUser() => new GuestUser();
|
||||
|
||||
private void setLocalUser(APIUser user) => Scheduler.Add(() => localUser.Value = user, false);
|
||||
private void setLocalUser(APIUser user) => Scheduler.Add(() =>
|
||||
{
|
||||
localUser.Value = user;
|
||||
statistics.Value = user.Statistics;
|
||||
}, false);
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
|
||||
@@ -28,6 +28,8 @@ namespace osu.Game.Online.API
|
||||
|
||||
public Bindable<UserActivity> Activity { get; } = new Bindable<UserActivity>();
|
||||
|
||||
public Bindable<UserStatistics?> Statistics { get; } = new Bindable<UserStatistics?>();
|
||||
|
||||
public Language Language => Language.en;
|
||||
|
||||
public string AccessToken => "token";
|
||||
@@ -115,6 +117,12 @@ namespace osu.Game.Online.API
|
||||
Id = DUMMY_USER_ID,
|
||||
};
|
||||
|
||||
Statistics.Value = new UserStatistics
|
||||
{
|
||||
GlobalRank = 1,
|
||||
CountryRank = 1
|
||||
};
|
||||
|
||||
state.Value = APIState.Online;
|
||||
}
|
||||
|
||||
@@ -126,6 +134,14 @@ namespace osu.Game.Online.API
|
||||
LocalUser.Value = new GuestUser();
|
||||
}
|
||||
|
||||
public void UpdateStatistics(UserStatistics newStatistics)
|
||||
{
|
||||
Statistics.Value = newStatistics;
|
||||
|
||||
if (IsLoggedIn)
|
||||
LocalUser.Value.Statistics = newStatistics;
|
||||
}
|
||||
|
||||
public IHubClientConnector? GetHubConnector(string clientName, string endpoint, bool preferMessagePack) => null;
|
||||
|
||||
public NotificationsClientConnector GetNotificationsConnector() => new PollingNotificationsClientConnector(this);
|
||||
@@ -141,6 +157,7 @@ namespace osu.Game.Online.API
|
||||
IBindable<APIUser> IAPIProvider.LocalUser => LocalUser;
|
||||
IBindableList<APIUser> IAPIProvider.Friends => Friends;
|
||||
IBindable<UserActivity> IAPIProvider.Activity => Activity;
|
||||
IBindable<UserStatistics?> IAPIProvider.Statistics => Statistics;
|
||||
|
||||
/// <summary>
|
||||
/// During the next simulated login, the process will fail immediately.
|
||||
|
||||
@@ -28,6 +28,11 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
IBindable<UserActivity> Activity { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The current user's online statistics.
|
||||
/// </summary>
|
||||
IBindable<UserStatistics?> Statistics { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The language supplied by this provider to API requests.
|
||||
/// </summary>
|
||||
@@ -111,6 +116,11 @@ namespace osu.Game.Online.API
|
||||
/// </summary>
|
||||
void Logout();
|
||||
|
||||
/// <summary>
|
||||
/// Sets Statistics bindable.
|
||||
/// </summary>
|
||||
void UpdateStatistics(UserStatistics newStatistics);
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="IHubClientConnector"/>. May be null if not supported.
|
||||
/// </summary>
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace osu.Game.Online.API.Requests.Responses
|
||||
[JsonProperty("latest_build")]
|
||||
public APIChangelogBuild LatestBuild { get; set; }
|
||||
|
||||
[JsonProperty("user_count")]
|
||||
public int UserCount { get; set; }
|
||||
|
||||
public bool Equals(APIUpdateStream other) => Id == other?.Id;
|
||||
|
||||
internal static readonly Dictionary<string, Color4> KNOWN_STREAMS = new Dictionary<string, Color4>
|
||||
|
||||
@@ -152,6 +152,15 @@ namespace osu.Game.Online.Leaderboards
|
||||
/// </summary>
|
||||
public void RefetchScores() => Scheduler.AddOnce(refetchScores);
|
||||
|
||||
/// <summary>
|
||||
/// Clear all scores from the display.
|
||||
/// </summary>
|
||||
public void ClearScores()
|
||||
{
|
||||
cancelPendingWork();
|
||||
SetScores(null);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Call when a retrieval or display failure happened to show a relevant message to the user.
|
||||
/// </summary>
|
||||
@@ -220,9 +229,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Debug.Assert(ThreadSafety.IsUpdateThread);
|
||||
|
||||
cancelPendingWork();
|
||||
|
||||
SetScores(null);
|
||||
ClearScores();
|
||||
setState(LeaderboardState.Retrieving);
|
||||
|
||||
currentFetchCancellationSource = new CancellationTokenSource();
|
||||
|
||||
@@ -164,7 +164,8 @@ namespace osu.Game.Online.Leaderboards
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.BottomLeft,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Height = 28,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Spacing = new Vector2(10f, 0f),
|
||||
Children = new Drawable[]
|
||||
@@ -357,14 +358,12 @@ namespace osu.Game.Online.Leaderboards
|
||||
},
|
||||
},
|
||||
},
|
||||
new GlowingSpriteText
|
||||
new OsuSpriteText
|
||||
{
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Origin = Anchor.CentreLeft,
|
||||
TextColour = Color4.White,
|
||||
GlowColour = Color4Extensions.FromHex(@"83ccfa"),
|
||||
Text = statistic.Value,
|
||||
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold),
|
||||
Font = OsuFont.GetFont(size: 17, weight: FontWeight.Bold, fixedWidth: true)
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -127,6 +127,8 @@ namespace osu.Game.Online.Solo
|
||||
{
|
||||
string rulesetName = callback.Score.Ruleset.ShortName;
|
||||
|
||||
api.UpdateStatistics(updatedStatistics);
|
||||
|
||||
if (latestStatistics == null)
|
||||
return;
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Changelog
|
||||
|
||||
protected override LocalisableString AdditionalText => Value.LatestBuild.DisplayVersion;
|
||||
|
||||
protected override LocalisableString InfoText => Value.LatestBuild.Users > 0 ? $"{"user".ToQuantity(Value.LatestBuild.Users, "N0")} online" : null;
|
||||
protected override LocalisableString InfoText => Value.UserCount > 0 ? $"{"user".ToQuantity(Value.UserCount, "N0")} online" : null;
|
||||
|
||||
protected override Color4 GetBarColour(OsuColour colours) => Value.Colour;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,6 @@ namespace osu.Game.Overlays.Login
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; } = null!;
|
||||
|
||||
private UserGridPanel panel = null!;
|
||||
private UserDropdown dropdown = null!;
|
||||
|
||||
/// <summary>
|
||||
@@ -39,6 +38,7 @@ namespace osu.Game.Overlays.Login
|
||||
public Action? RequestHide;
|
||||
|
||||
private readonly IBindable<APIState> apiState = new Bindable<APIState>();
|
||||
private readonly Bindable<UserStatus?> userStatus = new Bindable<UserStatus?>();
|
||||
|
||||
[Resolved]
|
||||
private IAPIProvider api { get; set; } = null!;
|
||||
@@ -131,7 +131,7 @@ namespace osu.Game.Overlays.Login
|
||||
Text = LoginPanelStrings.SignedIn,
|
||||
Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold),
|
||||
},
|
||||
panel = new UserGridPanel(api.LocalUser.Value)
|
||||
new UserRankPanel(api.LocalUser.Value)
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Action = RequestHide
|
||||
@@ -140,10 +140,8 @@ namespace osu.Game.Overlays.Login
|
||||
},
|
||||
};
|
||||
|
||||
panel.Status.BindTo(api.LocalUser.Value.Status);
|
||||
panel.Activity.BindTo(api.LocalUser.Value.Activity);
|
||||
|
||||
panel.Status.BindValueChanged(_ => updateDropdownCurrent(), true);
|
||||
userStatus.BindTo(api.LocalUser.Value.Status);
|
||||
userStatus.BindValueChanged(e => updateDropdownCurrent(e.NewValue), true);
|
||||
|
||||
dropdown.Current.BindValueChanged(action =>
|
||||
{
|
||||
@@ -176,9 +174,9 @@ namespace osu.Game.Overlays.Login
|
||||
ScheduleAfterChildren(() => GetContainingInputManager()?.ChangeFocus(form));
|
||||
});
|
||||
|
||||
private void updateDropdownCurrent()
|
||||
private void updateDropdownCurrent(UserStatus? status)
|
||||
{
|
||||
switch (panel.Status.Value)
|
||||
switch (status)
|
||||
{
|
||||
case UserStatus.Online:
|
||||
dropdown.Current.Value = UserAction.Online;
|
||||
|
||||
@@ -196,7 +196,7 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
private partial class BPMDisplay : RollingCounter<double>
|
||||
{
|
||||
protected override double RollingDuration => 500;
|
||||
protected override double RollingDuration => 250;
|
||||
|
||||
protected override LocalisableString FormatCount(double count) => count.ToLocalisableString("0 BPM");
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user