mirror of
https://github.com/ppy/osu.git
synced 2024-12-14 07:42:57 +08:00
Merge branch 'master' into fix-beatmap-import-fk-failure
This commit is contained in:
commit
697b07acb1
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@ -15,6 +15,7 @@ jobs:
|
||||
- { prettyname: macOS, fullname: macos-latest }
|
||||
- { prettyname: Linux, fullname: ubuntu-latest }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 60
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
1
.github/workflows/report-nunit.yml
vendored
1
.github/workflows/report-nunit.yml
vendored
@ -21,6 +21,7 @@ jobs:
|
||||
- { prettyname: macOS }
|
||||
- { prettyname: Linux }
|
||||
threadingMode: ['SingleThread', 'MultiThreaded']
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Annotate CI run with test results
|
||||
uses: dorny/test-reporter@v1.4.2
|
||||
|
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||
<data-source source="LOCAL" name="osu-client-sqlite" uuid="1aa4b9be-cd8d-47ae-8186-30a13cd724a5">
|
||||
<driver-ref>sqlite.xerial</driver-ref>
|
||||
<synchronize>true</synchronize>
|
||||
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
|
||||
<jdbc-url>jdbc:sqlite:$USER_HOME$/.local/share/osu/client.db</jdbc-url>
|
||||
<driver-properties>
|
||||
<property name="enable_load_extension" value="true" />
|
||||
</driver-properties>
|
||||
</data-source>
|
||||
</component>
|
||||
</project>
|
@ -1,3 +1,3 @@
|
||||
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
|
||||
<Realm />
|
||||
<Realm DisableAnalytics="true" />
|
||||
</Weavers>
|
@ -52,7 +52,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="ppy.osu.Game.Resources" Version="2021.618.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.622.0" />
|
||||
<PackageReference Include="ppy.osu.Framework.Android" Version="2021.628.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup Label="Transitive Dependencies">
|
||||
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->
|
||||
|
@ -6,8 +6,8 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Utils;
|
||||
@ -31,10 +31,23 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
[Resolved]
|
||||
private OsuConfigManager config { get; set; }
|
||||
|
||||
private Container<CaughtObject> droppedObjectContainer;
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
private readonly Container trailContainer;
|
||||
|
||||
private TestCatcher catcher;
|
||||
|
||||
public TestSceneCatcher()
|
||||
{
|
||||
Add(trailContainer = new Container
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Depth = -1
|
||||
});
|
||||
Add(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => Schedule(() =>
|
||||
{
|
||||
@ -43,20 +56,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
CircleSize = 0,
|
||||
};
|
||||
|
||||
var trailContainer = new Container();
|
||||
droppedObjectContainer = new Container<CaughtObject>();
|
||||
catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty);
|
||||
if (catcher != null)
|
||||
Remove(catcher);
|
||||
|
||||
Child = new Container
|
||||
Add(catcher = new TestCatcher(trailContainer, difficulty)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
trailContainer,
|
||||
droppedObjectContainer,
|
||||
catcher
|
||||
}
|
||||
};
|
||||
Anchor = Anchor.Centre
|
||||
});
|
||||
});
|
||||
|
||||
[Test]
|
||||
@ -293,8 +299,8 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
public IEnumerable<CaughtObject> CaughtObjects => this.ChildrenOfType<CaughtObject>();
|
||||
|
||||
public TestCatcher(Container trailsTarget, Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty)
|
||||
: base(trailsTarget, droppedObjectTarget, difficulty)
|
||||
public TestCatcher(Container trailsTarget, BeatmapDifficulty difficulty)
|
||||
: base(trailsTarget, difficulty)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Framework.Utils;
|
||||
@ -97,18 +96,12 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
SetContents(_ =>
|
||||
{
|
||||
var droppedObjectContainer = new Container<CaughtObject>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both
|
||||
};
|
||||
|
||||
return new CatchInputManager(catchRuleset)
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
new TestCatcherArea(droppedObjectContainer, beatmapDifficulty)
|
||||
new TestCatcherArea(beatmapDifficulty)
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@ -126,9 +119,13 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
private class TestCatcherArea : CatcherArea
|
||||
{
|
||||
public TestCatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty beatmapDifficulty)
|
||||
: base(droppedObjectContainer, beatmapDifficulty)
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
public TestCatcherArea(BeatmapDifficulty beatmapDifficulty)
|
||||
: base(beatmapDifficulty)
|
||||
{
|
||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
|
||||
public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1);
|
||||
|
@ -118,11 +118,10 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
|
||||
AddStep("create hyper-dashing catcher", () =>
|
||||
{
|
||||
Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container<CaughtObject>())
|
||||
Child = setupSkinHierarchy(catcherArea = new TestCatcherArea
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
Scale = new Vector2(4f),
|
||||
Origin = Anchor.Centre
|
||||
}, skin);
|
||||
});
|
||||
|
||||
@ -206,5 +205,18 @@ namespace osu.Game.Rulesets.Catch.Tests
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private class TestCatcherArea : CatcherArea
|
||||
{
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
public TestCatcherArea()
|
||||
{
|
||||
Scale = new Vector2(4f);
|
||||
|
||||
AddInternal(droppedObjectContainer = new DroppedObjectContainer());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,6 @@ using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.MathUtils;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.UI;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
@ -17,6 +16,8 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
{
|
||||
public const int RNG_SEED = 1337;
|
||||
|
||||
public bool HardRockOffsets { get; set; }
|
||||
|
||||
public CatchBeatmapProcessor(IBeatmap beatmap)
|
||||
: base(beatmap)
|
||||
{
|
||||
@ -43,11 +44,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyPositionOffsets(IBeatmap beatmap, params Mod[] mods)
|
||||
public void ApplyPositionOffsets(IBeatmap beatmap)
|
||||
{
|
||||
var rng = new FastRandom(RNG_SEED);
|
||||
|
||||
bool shouldApplyHardRockOffset = mods.Any(m => m is ModHardRock);
|
||||
float? lastPosition = null;
|
||||
double lastStartTime = 0;
|
||||
|
||||
@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps
|
||||
switch (obj)
|
||||
{
|
||||
case Fruit fruit:
|
||||
if (shouldApplyHardRockOffset)
|
||||
if (HardRockOffsets)
|
||||
applyHardRockOffset(fruit, ref lastPosition, ref lastStartTime, rng);
|
||||
break;
|
||||
|
||||
|
@ -5,11 +5,12 @@ using System.Linq;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust
|
||||
public class CatchModDifficultyAdjust : ModDifficultyAdjust, IApplicableToBeatmapProcessor
|
||||
{
|
||||
[SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)]
|
||||
public BindableNumber<float> CircleSize { get; } = new BindableFloatWithLimitExtension
|
||||
@ -31,6 +32,9 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
Value = 5,
|
||||
};
|
||||
|
||||
[SettingSource("Spicy Patterns", "Adjust the patterns as if Hard Rock is enabled.")]
|
||||
public BindableBool HardRockOffsets { get; } = new BindableBool();
|
||||
|
||||
protected override void ApplyLimits(bool extended)
|
||||
{
|
||||
base.ApplyLimits(extended);
|
||||
@ -45,12 +49,14 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
string circleSize = CircleSize.IsDefault ? string.Empty : $"CS {CircleSize.Value:N1}";
|
||||
string approachRate = ApproachRate.IsDefault ? string.Empty : $"AR {ApproachRate.Value:N1}";
|
||||
string spicyPatterns = HardRockOffsets.IsDefault ? string.Empty : "Spicy patterns";
|
||||
|
||||
return string.Join(", ", new[]
|
||||
{
|
||||
circleSize,
|
||||
base.SettingDescription,
|
||||
approachRate
|
||||
approachRate,
|
||||
spicyPatterns,
|
||||
}.Where(s => !string.IsNullOrEmpty(s)));
|
||||
}
|
||||
}
|
||||
@ -70,5 +76,11 @@ namespace osu.Game.Rulesets.Catch.Mods
|
||||
ApplySetting(CircleSize, cs => difficulty.CircleSize = cs);
|
||||
ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar);
|
||||
}
|
||||
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = HardRockOffsets.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,10 +7,14 @@ using osu.Game.Rulesets.Catch.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.Mods
|
||||
{
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmap
|
||||
public class CatchModHardRock : ModHardRock, IApplicableToBeatmapProcessor
|
||||
{
|
||||
public override double ScoreMultiplier => 1.12;
|
||||
|
||||
public void ApplyToBeatmap(IBeatmap beatmap) => CatchBeatmapProcessor.ApplyPositionOffsets(beatmap, this);
|
||||
public void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor)
|
||||
{
|
||||
var catchProcessor = (CatchBeatmapProcessor)beatmapProcessor;
|
||||
catchProcessor.HardRockOffsets = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Rulesets.Catch.Objects;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
@ -27,6 +26,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
public const float CENTER_X = WIDTH / 2;
|
||||
|
||||
[Cached]
|
||||
private readonly DroppedObjectContainer droppedObjectContainer;
|
||||
|
||||
internal readonly CatcherArea CatcherArea;
|
||||
|
||||
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) =>
|
||||
@ -35,12 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
public CatchPlayfield(BeatmapDifficulty difficulty)
|
||||
{
|
||||
var droppedObjectContainer = new Container<CaughtObject>
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
|
||||
CatcherArea = new CatcherArea(droppedObjectContainer, difficulty)
|
||||
CatcherArea = new CatcherArea(difficulty)
|
||||
{
|
||||
Anchor = Anchor.BottomLeft,
|
||||
Origin = Anchor.TopLeft,
|
||||
@ -48,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
droppedObjectContainer,
|
||||
droppedObjectContainer = new DroppedObjectContainer(),
|
||||
CatcherArea.MovableCatcher.CreateProxiedContent(),
|
||||
HitObjectContainer.CreateProxy(),
|
||||
// This ordering (`CatcherArea` before `HitObjectContainer`) is important to
|
||||
|
@ -79,7 +79,8 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// <summary>
|
||||
/// Contains objects dropped from the plate.
|
||||
/// </summary>
|
||||
private readonly Container<CaughtObject> droppedObjectTarget;
|
||||
[Resolved]
|
||||
private DroppedObjectContainer droppedObjectTarget { get; set; }
|
||||
|
||||
public CatcherAnimationState CurrentState
|
||||
{
|
||||
@ -134,10 +135,9 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
private readonly DrawablePool<CaughtBanana> caughtBananaPool;
|
||||
private readonly DrawablePool<CaughtDroplet> caughtDropletPool;
|
||||
|
||||
public Catcher([NotNull] Container trailsTarget, [NotNull] Container<CaughtObject> droppedObjectTarget, BeatmapDifficulty difficulty = null)
|
||||
public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
this.trailsTarget = trailsTarget;
|
||||
this.droppedObjectTarget = droppedObjectTarget;
|
||||
|
||||
Origin = Anchor.TopCentre;
|
||||
|
||||
|
@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
/// </summary>
|
||||
private int currentDirection;
|
||||
|
||||
public CatcherArea(Container<CaughtObject> droppedObjectContainer, BeatmapDifficulty difficulty = null)
|
||||
public CatcherArea(BeatmapDifficulty difficulty = null)
|
||||
{
|
||||
Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE);
|
||||
Children = new Drawable[]
|
||||
@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
Margin = new MarginPadding { Bottom = 350f },
|
||||
X = CatchPlayfield.CENTER_X
|
||||
},
|
||||
MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X },
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI
|
||||
protected override void FreeAfterUse()
|
||||
{
|
||||
ClearTransforms();
|
||||
Alpha = 1;
|
||||
base.FreeAfterUse();
|
||||
}
|
||||
}
|
||||
|
17
osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
Normal file
17
osu.Game.Rulesets.Catch/UI/DroppedObjectContainer.cs
Normal file
@ -0,0 +1,17 @@
|
||||
// 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;
|
||||
using osu.Game.Rulesets.Catch.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.Catch.UI
|
||||
{
|
||||
public class DroppedObjectContainer : Container<CaughtObject>
|
||||
{
|
||||
public DroppedObjectContainer()
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
using osu.Game.Rulesets.Mania.Configuration;
|
||||
@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Mania
|
||||
|
||||
private class TimeSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString("N0") + "ms";
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ 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.Edit;
|
||||
@ -283,6 +284,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components
|
||||
}
|
||||
}
|
||||
|
||||
public string TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
|
||||
public LocalisableString TooltipText => ControlPoint.Type.Value.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Beatmaps;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osu.Game.Rulesets.Osu.Utils;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Mods
|
||||
@ -23,15 +24,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
{
|
||||
public override string Description => "It never gets boring!";
|
||||
|
||||
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const float playfield_edge_ratio = 0.375f;
|
||||
|
||||
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
|
||||
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
|
||||
|
||||
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
private static readonly float playfield_diagonal = OsuPlayfield.BASE_SIZE.LengthFast;
|
||||
|
||||
private Random rng;
|
||||
@ -113,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
distanceToPrev * (float)Math.Sin(current.AngleRad)
|
||||
);
|
||||
|
||||
posRelativeToPrev = getRotatedVector(previous.EndPositionRandomised, posRelativeToPrev);
|
||||
posRelativeToPrev = OsuHitObjectGenerationUtils.RotateAwayFromEdge(previous.EndPositionRandomised, posRelativeToPrev);
|
||||
|
||||
current.AngleRad = (float)Math.Atan2(posRelativeToPrev.Y, posRelativeToPrev.X);
|
||||
|
||||
@ -185,73 +177,6 @@ namespace osu.Game.Rulesets.Osu.Mods
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the position of the current hit object relative to the previous one.
|
||||
/// </summary>
|
||||
/// <returns>The position of the current hit object relative to the previous one</returns>
|
||||
private Vector2 getRotatedVector(Vector2 prevPosChanged, Vector2 posRelativeToPrev)
|
||||
{
|
||||
var relativeRotationDistance = 0f;
|
||||
|
||||
if (prevPosChanged.X < playfield_middle.X)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_x - prevPosChanged.X) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevPosChanged.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
if (prevPosChanged.Y < playfield_middle.Y)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_y - prevPosChanged.Y) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevPosChanged.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
return rotateVectorTowardsVector(posRelativeToPrev, playfield_middle - prevPosChanged, relativeRotationDistance / 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates vector "initial" towards vector "destinantion"
|
||||
/// </summary>
|
||||
/// <param name="initial">Vector to rotate to "destination"</param>
|
||||
/// <param name="destination">Vector "initial" should be rotated to</param>
|
||||
/// <param name="relativeDistance">The angle the vector should be rotated relative to the difference between the angles of the the two vectors.</param>
|
||||
/// <returns>Resulting vector</returns>
|
||||
private Vector2 rotateVectorTowardsVector(Vector2 initial, Vector2 destination, float relativeDistance)
|
||||
{
|
||||
var initialAngleRad = Math.Atan2(initial.Y, initial.X);
|
||||
var destAngleRad = Math.Atan2(destination.Y, destination.X);
|
||||
|
||||
var diff = destAngleRad - initialAngleRad;
|
||||
|
||||
while (diff < -Math.PI) diff += 2 * Math.PI;
|
||||
|
||||
while (diff > Math.PI) diff -= 2 * Math.PI;
|
||||
|
||||
var finalAngleRad = initialAngleRad + relativeDistance * diff;
|
||||
|
||||
return new Vector2(
|
||||
initial.Length * (float)Math.Cos(finalAngleRad),
|
||||
initial.Length * (float)Math.Sin(finalAngleRad)
|
||||
);
|
||||
}
|
||||
|
||||
private class RandomObjectInfo
|
||||
{
|
||||
public float AngleRad { get; set; }
|
||||
|
104
osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
Normal file
104
osu.Game.Rulesets.Osu/Utils/OsuHitObjectGenerationUtils.cs
Normal file
@ -0,0 +1,104 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using osu.Game.Rulesets.Osu.UI;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Rulesets.Osu.Utils
|
||||
{
|
||||
public static class OsuHitObjectGenerationUtils
|
||||
{
|
||||
// The relative distance to the edge of the playfield before objects' positions should start to "turn around" and curve towards the middle.
|
||||
// The closer the hit objects draw to the border, the sharper the turn
|
||||
private const float playfield_edge_ratio = 0.375f;
|
||||
|
||||
private static readonly float border_distance_x = OsuPlayfield.BASE_SIZE.X * playfield_edge_ratio;
|
||||
private static readonly float border_distance_y = OsuPlayfield.BASE_SIZE.Y * playfield_edge_ratio;
|
||||
|
||||
private static readonly Vector2 playfield_middle = OsuPlayfield.BASE_SIZE / 2;
|
||||
|
||||
/// <summary>
|
||||
/// Rotate a hit object away from the playfield edge, while keeping a constant distance
|
||||
/// from the previous object.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The extent of rotation depends on the position of the hit object. Hit objects
|
||||
/// closer to the playfield edge will be rotated to a larger extent.
|
||||
/// </remarks>
|
||||
/// <param name="prevObjectPos">Position of the previous hit object.</param>
|
||||
/// <param name="posRelativeToPrev">Position of the hit object to be rotated, relative to the previous hit object.</param>
|
||||
/// <param name="rotationRatio">
|
||||
/// The extent of rotation.
|
||||
/// 0 means the hit object is never rotated.
|
||||
/// 1 means the hit object will be fully rotated towards playfield center when it is originally at playfield edge.
|
||||
/// </param>
|
||||
/// <returns>The new position of the hit object, relative to the previous one.</returns>
|
||||
public static Vector2 RotateAwayFromEdge(Vector2 prevObjectPos, Vector2 posRelativeToPrev, float rotationRatio = 0.5f)
|
||||
{
|
||||
var relativeRotationDistance = 0f;
|
||||
|
||||
if (prevObjectPos.X < playfield_middle.X)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_x - prevObjectPos.X) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevObjectPos.X - (OsuPlayfield.BASE_SIZE.X - border_distance_x)) / border_distance_x,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
if (prevObjectPos.Y < playfield_middle.Y)
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(border_distance_y - prevObjectPos.Y) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
relativeRotationDistance = Math.Max(
|
||||
(prevObjectPos.Y - (OsuPlayfield.BASE_SIZE.Y - border_distance_y)) / border_distance_y,
|
||||
relativeRotationDistance
|
||||
);
|
||||
}
|
||||
|
||||
return RotateVectorTowardsVector(
|
||||
posRelativeToPrev,
|
||||
playfield_middle - prevObjectPos,
|
||||
Math.Min(1, relativeRotationDistance * rotationRatio)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotates vector "initial" towards vector "destination".
|
||||
/// </summary>
|
||||
/// <param name="initial">The vector to be rotated.</param>
|
||||
/// <param name="destination">The vector that "initial" should be rotated towards.</param>
|
||||
/// <param name="rotationRatio">How much "initial" should be rotated. 0 means no rotation. 1 means "initial" is fully rotated to equal "destination".</param>
|
||||
/// <returns>The rotated vector.</returns>
|
||||
public static Vector2 RotateVectorTowardsVector(Vector2 initial, Vector2 destination, float rotationRatio)
|
||||
{
|
||||
var initialAngleRad = MathF.Atan2(initial.Y, initial.X);
|
||||
var destAngleRad = MathF.Atan2(destination.Y, destination.X);
|
||||
|
||||
var diff = destAngleRad - initialAngleRad;
|
||||
|
||||
while (diff < -MathF.PI) diff += 2 * MathF.PI;
|
||||
|
||||
while (diff > MathF.PI) diff -= 2 * MathF.PI;
|
||||
|
||||
var finalAngleRad = initialAngleRad + rotationRatio * diff;
|
||||
|
||||
return new Vector2(
|
||||
initial.Length * MathF.Cos(finalAngleRad),
|
||||
initial.Length * MathF.Sin(finalAngleRad)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -242,6 +242,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public async Task TestImportThenImportWithChangedFile()
|
||||
{
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest)))
|
||||
@ -344,6 +345,7 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Ignore("intentionally broken by import optimisations")]
|
||||
public async Task TestImportCorruptThenImport()
|
||||
{
|
||||
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
|
||||
@ -489,12 +491,11 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(true)]
|
||||
[TestCase(false)]
|
||||
public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set)
|
||||
[Test]
|
||||
public async Task TestImportThenDeleteThenImportWithOnlineIDsMissing()
|
||||
{
|
||||
// unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here.
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}"))
|
||||
using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}"))
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -502,10 +503,8 @@ namespace osu.Game.Tests.Beatmaps.IO
|
||||
|
||||
var imported = await LoadOszIntoOsu(osu);
|
||||
|
||||
if (set)
|
||||
imported.OnlineBeatmapSetID = 1234;
|
||||
else
|
||||
imported.Beatmaps.First().OnlineBeatmapID = 1234;
|
||||
foreach (var b in imported.Beatmaps)
|
||||
b.OnlineBeatmapID = null;
|
||||
|
||||
osu.Dependencies.Get<BeatmapManager>().Update(imported);
|
||||
|
||||
|
241
osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
Normal file
241
osu.Game.Tests/Editing/Checks/CheckFewHitsoundsTest.cs
Normal file
@ -0,0 +1,241 @@
|
||||
// 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.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckFewHitsoundsTest
|
||||
{
|
||||
private CheckFewHitsounds check;
|
||||
|
||||
private List<HitSampleInfo> notHitsounded;
|
||||
private List<HitSampleInfo> hitsounded;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckFewHitsounds();
|
||||
notHitsounded = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
hitsounded = new List<HitSampleInfo>
|
||||
{
|
||||
new HitSampleInfo(HitSampleInfo.HIT_NORMAL),
|
||||
new HitSampleInfo(HitSampleInfo.HIT_FINISH)
|
||||
};
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 16; ++i)
|
||||
{
|
||||
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
|
||||
if ((i + 1) % 2 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
if ((i + 1) % 3 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
|
||||
if ((i + 1) % 4 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestHitsoundedWithBreak()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 32; ++i)
|
||||
{
|
||||
var samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) };
|
||||
|
||||
if ((i + 1) % 2 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP));
|
||||
if ((i + 1) % 3 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_WHISTLE));
|
||||
if ((i + 1) % 4 == 0)
|
||||
samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH));
|
||||
// Leaves a gap in which no hitsounds exist or can be added, and so shouldn't be an issue.
|
||||
if (i > 8 && i < 24)
|
||||
continue;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLightlyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var samples = i % 8 == 0 ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
assertLongPeriodNegligible(hitObjects, count: 3);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestRarelyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 30; ++i)
|
||||
{
|
||||
var samples = (i == 0 || i == 15) ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
// Should prompt one warning between 1st and 16th, and another between 16th and 31st.
|
||||
assertLongPeriodWarning(hitObjects, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestExtremelyRarelyHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 80; ++i)
|
||||
{
|
||||
var samples = i == 40 ? hitsounded : notHitsounded;
|
||||
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = samples });
|
||||
}
|
||||
|
||||
// Should prompt one problem between 1st and 41st, and another between 41st and 81st.
|
||||
assertLongPeriodProblem(hitObjects, count: 2);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNotHitsounded()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
for (int i = 0; i < 20; ++i)
|
||||
hitObjects.Add(new HitCircle { StartTime = 1000 * i, Samples = notHitsounded });
|
||||
|
||||
assertNoHitsounds(hitObjects);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedObjectsHitsounded()
|
||||
{
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 16; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = hitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
|
||||
{
|
||||
Samples = hitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { nested });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNestedObjectsRarelyHitsounded()
|
||||
{
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 16; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 1000 * i, Samples = i == 0 ? hitsounded : notHitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 16000)
|
||||
{
|
||||
Samples = hitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
|
||||
assertLongPeriodWarning(new List<HitObject> { nested });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestConcurrentObjects()
|
||||
{
|
||||
var hitObjects = new List<HitObject>();
|
||||
|
||||
var ticks = new List<HitObject>();
|
||||
for (int i = 1; i < 10; ++i)
|
||||
ticks.Add(new SliderTick { StartTime = 5000 * i, Samples = hitsounded });
|
||||
|
||||
var nested = new MockNestableHitObject(ticks.ToList(), 0, 50000)
|
||||
{
|
||||
Samples = notHitsounded
|
||||
};
|
||||
nested.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty());
|
||||
hitObjects.Add(nested);
|
||||
|
||||
for (int i = 1; i <= 6; ++i)
|
||||
hitObjects.Add(new HitCircle { StartTime = 10000 * i, Samples = notHitsounded });
|
||||
|
||||
assertOk(hitObjects);
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLongPeriodProblem(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodProblem));
|
||||
}
|
||||
|
||||
private void assertLongPeriodWarning(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodWarning));
|
||||
}
|
||||
|
||||
private void assertLongPeriodNegligible(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckFewHitsounds.IssueTemplateLongPeriodNegligible));
|
||||
}
|
||||
|
||||
private void assertNoHitsounds(List<HitObject> hitObjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckFewHitsounds.IssueTemplateNoHitsounds));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject> { HitObjects = hitObjects };
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
}
|
289
osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
Normal file
289
osu.Game.Tests/Editing/Checks/CheckMutedObjectsTest.cs
Normal file
@ -0,0 +1,289 @@
|
||||
// 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.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Beatmaps.ControlPoints;
|
||||
using osu.Game.Rulesets.Edit;
|
||||
using osu.Game.Rulesets.Edit.Checks;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Osu.Objects;
|
||||
using osu.Game.Tests.Beatmaps;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
[TestFixture]
|
||||
public class CheckMutedObjectsTest
|
||||
{
|
||||
private CheckMutedObjects check;
|
||||
private ControlPointInfo cpi;
|
||||
|
||||
private const int volume_regular = 50;
|
||||
private const int volume_low = 15;
|
||||
private const int volume_muted = 5;
|
||||
|
||||
[SetUp]
|
||||
public void Setup()
|
||||
{
|
||||
check = new CheckMutedObjects();
|
||||
|
||||
cpi = new ControlPointInfo();
|
||||
cpi.Add(0, new SampleControlPoint { SampleVolume = volume_regular });
|
||||
cpi.Add(1000, new SampleControlPoint { SampleVolume = volume_low });
|
||||
cpi.Add(2000, new SampleControlPoint { SampleVolume = volume_muted });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 1000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertLowVolume(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSampleVolume()
|
||||
{
|
||||
// The sample volume should take precedence over the control point volume.
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestLowSampleVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_low) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertLowVolume(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolume()
|
||||
{
|
||||
var hitcircle = new HitCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
|
||||
};
|
||||
hitcircle.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { hitcircle });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestNormalSampleVolumeSlider()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick", volume: volume_muted) } // Should be fine.
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertOk(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolumeSliderHead()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) } // Applies to the tail.
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedSampleVolumeSliderTail()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_muted) } // Applies to the tail.
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderHead()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 2000,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 2250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 2000, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL, volume: volume_regular) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMuted(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestMutedControlPointVolumeSliderTail()
|
||||
{
|
||||
var sliderHead = new SliderHeadCircle
|
||||
{
|
||||
StartTime = 0,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
sliderHead.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
var sliderTick = new SliderTick
|
||||
{
|
||||
StartTime = 250,
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo("slidertick") }
|
||||
};
|
||||
sliderTick.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
// Ends after the 5% control point.
|
||||
var slider = new MockNestableHitObject(new List<HitObject> { sliderHead, sliderTick, }, startTime: 0, endTime: 2500)
|
||||
{
|
||||
Samples = new List<HitSampleInfo> { new HitSampleInfo(HitSampleInfo.HIT_NORMAL) }
|
||||
};
|
||||
slider.ApplyDefaults(cpi, new BeatmapDifficulty());
|
||||
|
||||
assertMutedPassive(new List<HitObject> { slider });
|
||||
}
|
||||
|
||||
private void assertOk(List<HitObject> hitObjects)
|
||||
{
|
||||
Assert.That(check.Run(getContext(hitObjects)), Is.Empty);
|
||||
}
|
||||
|
||||
private void assertLowVolume(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateLowVolumeActive));
|
||||
}
|
||||
|
||||
private void assertMuted(List<HitObject> hitObjects, int count = 1)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(count));
|
||||
Assert.That(issues.All(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedActive));
|
||||
}
|
||||
|
||||
private void assertMutedPassive(List<HitObject> hitObjects)
|
||||
{
|
||||
var issues = check.Run(getContext(hitObjects)).ToList();
|
||||
|
||||
Assert.That(issues, Has.Count.EqualTo(1));
|
||||
Assert.That(issues.Any(issue => issue.Template is CheckMutedObjects.IssueTemplateMutedPassive));
|
||||
}
|
||||
|
||||
private BeatmapVerifierContext getContext(List<HitObject> hitObjects)
|
||||
{
|
||||
var beatmap = new Beatmap<HitObject>
|
||||
{
|
||||
ControlPointInfo = cpi,
|
||||
HitObjects = hitObjects
|
||||
};
|
||||
|
||||
return new BeatmapVerifierContext(beatmap, new TestWorkingBeatmap(beatmap));
|
||||
}
|
||||
}
|
||||
}
|
36
osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
Normal file
36
osu.Game.Tests/Editing/Checks/MockNestableHitObject.cs
Normal file
@ -0,0 +1,36 @@
|
||||
// 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.Threading;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Tests.Editing.Checks
|
||||
{
|
||||
public sealed class MockNestableHitObject : HitObject, IHasDuration
|
||||
{
|
||||
private readonly IEnumerable<HitObject> toBeNested;
|
||||
|
||||
public MockNestableHitObject(IEnumerable<HitObject> toBeNested, double startTime, double endTime)
|
||||
{
|
||||
this.toBeNested = toBeNested;
|
||||
StartTime = startTime;
|
||||
EndTime = endTime;
|
||||
}
|
||||
|
||||
protected override void CreateNestedHitObjects(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var hitObject in toBeNested)
|
||||
AddNested(hitObject);
|
||||
}
|
||||
|
||||
public double EndTime { get; }
|
||||
|
||||
public double Duration
|
||||
{
|
||||
get => EndTime - StartTime;
|
||||
set => throw new System.NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
@ -90,6 +90,20 @@ namespace osu.Game.Tests.NonVisual.Filtering
|
||||
Assert.Less(filterCriteria.DrainRate.Min, 6.1f);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyOverallDifficultyQueries()
|
||||
{
|
||||
const string query = "od>4 easy od<8";
|
||||
var filterCriteria = new FilterCriteria();
|
||||
FilterQueryParser.ApplyQueries(filterCriteria, query);
|
||||
Assert.AreEqual("easy", filterCriteria.SearchText.Trim());
|
||||
Assert.AreEqual(1, filterCriteria.SearchTerms.Length);
|
||||
Assert.Greater(filterCriteria.OverallDifficulty.Min, 4.0);
|
||||
Assert.Less(filterCriteria.OverallDifficulty.Min, 4.1);
|
||||
Assert.Greater(filterCriteria.OverallDifficulty.Max, 7.9);
|
||||
Assert.Less(filterCriteria.OverallDifficulty.Max, 8.0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestApplyBPMQueries()
|
||||
{
|
||||
|
@ -0,0 +1,95 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using NUnit.Framework;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Audio.Sample;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.OpenGL.Textures;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Skinning;
|
||||
using osu.Game.Tests.Testing;
|
||||
using osu.Game.Tests.Visual;
|
||||
|
||||
namespace osu.Game.Tests.Rulesets
|
||||
{
|
||||
public class TestSceneRulesetSkinProvidingContainer : OsuTestScene
|
||||
{
|
||||
private SkinRequester requester;
|
||||
|
||||
protected override Ruleset CreateRuleset() => new TestSceneRulesetDependencies.TestRuleset();
|
||||
|
||||
[Cached(typeof(ISkinSource))]
|
||||
private readonly ISkinSource testSource = new TestSkinProvider();
|
||||
|
||||
[Test]
|
||||
public void TestEarlyAddedSkinRequester()
|
||||
{
|
||||
Texture textureOnLoad = null;
|
||||
|
||||
AddStep("setup provider", () =>
|
||||
{
|
||||
var rulesetSkinProvider = new RulesetSkinProvidingContainer(Ruleset.Value.CreateInstance(), Beatmap.Value.Beatmap, Beatmap.Value.Skin);
|
||||
|
||||
rulesetSkinProvider.Add(requester = new SkinRequester());
|
||||
|
||||
requester.OnLoadAsync += () => textureOnLoad = requester.GetTexture(TestSkinProvider.TEXTURE_NAME);
|
||||
|
||||
Child = rulesetSkinProvider;
|
||||
});
|
||||
|
||||
AddAssert("requester got correct initial texture", () => textureOnLoad != null);
|
||||
}
|
||||
|
||||
private class SkinRequester : Drawable, ISkin
|
||||
{
|
||||
private ISkinSource skin;
|
||||
|
||||
public event Action OnLoadAsync;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(ISkinSource skin)
|
||||
{
|
||||
this.skin = skin;
|
||||
|
||||
OnLoadAsync?.Invoke();
|
||||
}
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component);
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS = default, WrapMode wrapModeT = default) => skin.GetTexture(componentName);
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo);
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => skin.GetConfig<TLookup, TValue>(lookup);
|
||||
}
|
||||
|
||||
private class TestSkinProvider : ISkinSource
|
||||
{
|
||||
public const string TEXTURE_NAME = "some-texture";
|
||||
|
||||
public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException();
|
||||
|
||||
public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => componentName == TEXTURE_NAME ? Texture.WhitePixel : null;
|
||||
|
||||
public ISample GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException();
|
||||
|
||||
public IBindable<TValue> GetConfig<TLookup, TValue>(TLookup lookup) => throw new NotImplementedException();
|
||||
|
||||
public event Action SourceChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public ISkin FindProvider(Func<ISkin, bool> lookupFunction) => lookupFunction(this) ? this : null;
|
||||
|
||||
public IEnumerable<ISkin> AllSources => new[] { this };
|
||||
}
|
||||
}
|
||||
}
|
@ -52,7 +52,7 @@ namespace osu.Game.Tests.Testing
|
||||
Dependencies.Get<TestRulesetConfigManager>() != null);
|
||||
}
|
||||
|
||||
private class TestRuleset : Ruleset
|
||||
public class TestRuleset : Ruleset
|
||||
{
|
||||
public override string Description => string.Empty;
|
||||
public override string ShortName => string.Empty;
|
||||
|
@ -1,6 +1,7 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NUnit.Framework;
|
||||
@ -14,6 +15,8 @@ using osu.Game.Online.API.Requests.Responses;
|
||||
using osu.Game.Overlays;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Scoring;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Tests.Visual.Online
|
||||
{
|
||||
@ -23,6 +26,8 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
private BeatmapListingOverlay overlay;
|
||||
|
||||
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
@ -39,6 +44,16 @@ namespace osu.Game.Tests.Visual.Online
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
AddStep("initialize dummy", () =>
|
||||
{
|
||||
// non-supporter user
|
||||
((DummyAPIAccess)API).LocalUser.Value = new User
|
||||
{
|
||||
Username = "TestBot",
|
||||
Id = API.LocalUser.Value.Id + 1,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
@ -58,13 +73,164 @@ namespace osu.Game.Tests.Visual.Online
|
||||
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithoutResults()
|
||||
{
|
||||
AddStep("fetch for 0 beatmaps", () => fetchFor());
|
||||
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
|
||||
|
||||
// only Rank Achieved filter
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
// only Played filter
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
// both RankAchieved and Played filters
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
notFoundPlaceholderShown();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithoutResults()
|
||||
{
|
||||
AddStep("fetch for 0 beatmaps", () => fetchFor());
|
||||
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
|
||||
|
||||
// only Rank Achieved filter
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
// only Played filter
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
// both Rank Achieved and Played filters
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
notFoundPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
notFoundPlaceholderShown();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithoutSupporterUsesSupporterOnlyFiltersWithResults()
|
||||
{
|
||||
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
|
||||
AddStep("set dummy as non-supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = false);
|
||||
|
||||
// only Rank Achieved filter
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
noPlaceholderShown();
|
||||
|
||||
// only Played filter
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
noPlaceholderShown();
|
||||
|
||||
// both Rank Achieved and Played filters
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
supporterRequiredPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
noPlaceholderShown();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TestUserWithSupporterUsesSupporterOnlyFiltersWithResults()
|
||||
{
|
||||
AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet));
|
||||
AddStep("set dummy as supporter", () => ((DummyAPIAccess)API).LocalUser.Value.IsSupporter = true);
|
||||
|
||||
// only Rank Achieved filter
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
noPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
noPlaceholderShown();
|
||||
|
||||
// only Played filter
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
noPlaceholderShown();
|
||||
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
noPlaceholderShown();
|
||||
|
||||
// both Rank Achieved and Played filters
|
||||
setRankAchievedFilter(new[] { ScoreRank.XH });
|
||||
setPlayedFilter(SearchPlayed.Played);
|
||||
noPlaceholderShown();
|
||||
|
||||
setRankAchievedFilter(Array.Empty<ScoreRank>());
|
||||
setPlayedFilter(SearchPlayed.Any);
|
||||
noPlaceholderShown();
|
||||
}
|
||||
|
||||
private void fetchFor(params BeatmapSetInfo[] beatmaps)
|
||||
{
|
||||
setsForResponse.Clear();
|
||||
setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b)));
|
||||
|
||||
// trigger arbitrary change for fetching.
|
||||
overlay.ChildrenOfType<BeatmapListingSearchControl>().Single().Query.TriggerChange();
|
||||
searchControl.Query.TriggerChange();
|
||||
}
|
||||
|
||||
private void setRankAchievedFilter(ScoreRank[] ranks)
|
||||
{
|
||||
AddStep($"set Rank Achieved filter to [{string.Join(',', ranks)}]", () =>
|
||||
{
|
||||
searchControl.Ranks.Clear();
|
||||
searchControl.Ranks.AddRange(ranks);
|
||||
});
|
||||
}
|
||||
|
||||
private void setPlayedFilter(SearchPlayed played)
|
||||
{
|
||||
AddStep($"set Played filter to {played}", () => searchControl.Played.Value = played);
|
||||
}
|
||||
|
||||
private void supporterRequiredPlaceholderShown()
|
||||
{
|
||||
AddUntilStep("\"supporter required\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().SingleOrDefault()?.IsPresent == true);
|
||||
}
|
||||
|
||||
private void notFoundPlaceholderShown()
|
||||
{
|
||||
AddUntilStep("\"no maps found\" placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
|
||||
}
|
||||
|
||||
private void noPlaceholderShown()
|
||||
{
|
||||
AddUntilStep("no placeholder shown", () =>
|
||||
!overlay.ChildrenOfType<BeatmapListingOverlay.SupporterRequiredDrawable>().Any()
|
||||
&& !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any());
|
||||
}
|
||||
|
||||
private class TestAPIBeatmapSet : APIBeatmapSet
|
||||
|
@ -10,7 +10,6 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Testing;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Mods;
|
||||
using osu.Game.Rulesets;
|
||||
@ -107,7 +106,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
var conversionMods = osu.GetModsFor(ModType.Conversion);
|
||||
|
||||
var noFailMod = osu.GetModsFor(ModType.DifficultyReduction).FirstOrDefault(m => m is OsuModNoFail);
|
||||
var hiddenMod = harderMods.FirstOrDefault(m => m is OsuModHidden);
|
||||
|
||||
var doubleTimeMod = harderMods.OfType<MultiMod>().FirstOrDefault(m => m.Mods.Any(a => a is OsuModDoubleTime));
|
||||
|
||||
@ -120,8 +118,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
testMultiMod(doubleTimeMod);
|
||||
testIncompatibleMods(easy, hardRock);
|
||||
testDeselectAll(easierMods.Where(m => !(m is MultiMod)));
|
||||
testMultiplierTextColour(noFailMod, () => modSelect.LowMultiplierColour);
|
||||
testMultiplierTextColour(hiddenMod, () => modSelect.HighMultiplierColour);
|
||||
|
||||
testUnimplementedMod(targetMod);
|
||||
}
|
||||
@ -149,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
changeRuleset(0);
|
||||
|
||||
AddAssert("ensure mods still selected", () => modDisplay.Current.Value.Single(m => m is OsuModNoFail) != null);
|
||||
AddAssert("ensure mods still selected", () => modDisplay.Current.Value.SingleOrDefault(m => m is OsuModNoFail) != null);
|
||||
|
||||
changeRuleset(3);
|
||||
|
||||
@ -316,17 +312,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert("check for no selection", () => !modSelect.SelectedMods.Value.Any());
|
||||
}
|
||||
|
||||
private void testMultiplierTextColour(Mod mod, Func<Color4> getCorrectColour)
|
||||
{
|
||||
checkLabelColor(() => Color4.White);
|
||||
selectNext(mod);
|
||||
AddWaitStep("wait for changing colour", 1);
|
||||
checkLabelColor(getCorrectColour);
|
||||
selectPrevious(mod);
|
||||
AddWaitStep("wait for changing colour", 1);
|
||||
checkLabelColor(() => Color4.White);
|
||||
}
|
||||
|
||||
private void testModsWithSameBaseType(Mod modA, Mod modB)
|
||||
{
|
||||
selectNext(modA);
|
||||
@ -348,7 +333,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
AddAssert($"check {mod.Name} is selected", () =>
|
||||
{
|
||||
var button = modSelect.GetModButton(mod);
|
||||
return modSelect.SelectedMods.Value.Single(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
|
||||
return modSelect.SelectedMods.Value.SingleOrDefault(m => m.Name == mod.Name) != null && button.SelectedMod.GetType() == mod.GetType() && button.Selected;
|
||||
});
|
||||
}
|
||||
|
||||
@ -370,8 +355,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
});
|
||||
}
|
||||
|
||||
private void checkLabelColor(Func<Color4> getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour());
|
||||
|
||||
private void createDisplay(Func<TestModSelectOverlay> createOverlayFunc)
|
||||
{
|
||||
Children = new Drawable[]
|
||||
@ -408,7 +391,6 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
return section.ButtonsContainer.OfType<ModButton>().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType()));
|
||||
}
|
||||
|
||||
public new OsuSpriteText MultiplierLabel => base.MultiplierLabel;
|
||||
public new TriangleButton DeselectAllButton => base.DeselectAllButton;
|
||||
|
||||
public new Color4 LowMultiplierColour => base.LowMultiplierColour;
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osuTK;
|
||||
@ -59,7 +60,7 @@ namespace osu.Game.Tests.Visual.UserInterface
|
||||
|
||||
private class Icon : Container, IHasTooltip
|
||||
{
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public SpriteIcon SpriteIcon { get; }
|
||||
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; }
|
||||
|
||||
private readonly Bindable<string> beatmapId = new Bindable<string>();
|
||||
private readonly Bindable<int?> beatmapId = new Bindable<int?>();
|
||||
|
||||
private readonly Bindable<string> mods = new Bindable<string>();
|
||||
|
||||
@ -220,14 +220,12 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
beatmapId.Value = Model.ID.ToString();
|
||||
beatmapId.BindValueChanged(idString =>
|
||||
beatmapId.Value = Model.ID;
|
||||
beatmapId.BindValueChanged(id =>
|
||||
{
|
||||
int.TryParse(idString.NewValue, out var parsed);
|
||||
Model.ID = id.NewValue ?? 0;
|
||||
|
||||
Model.ID = parsed;
|
||||
|
||||
if (idString.NewValue != idString.OldValue)
|
||||
if (id.NewValue != id.OldValue)
|
||||
Model.BeatmapInfo = null;
|
||||
|
||||
if (Model.BeatmapInfo != null)
|
||||
|
@ -147,7 +147,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[Resolved]
|
||||
protected IAPIProvider API { get; private set; }
|
||||
|
||||
private readonly Bindable<string> beatmapId = new Bindable<string>();
|
||||
private readonly Bindable<int?> beatmapId = new Bindable<int?>();
|
||||
|
||||
private readonly Bindable<string> score = new Bindable<string>();
|
||||
|
||||
@ -228,16 +228,12 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
beatmapId.Value = Model.ID.ToString();
|
||||
beatmapId.BindValueChanged(idString =>
|
||||
beatmapId.Value = Model.ID;
|
||||
beatmapId.BindValueChanged(id =>
|
||||
{
|
||||
int parsed;
|
||||
Model.ID = id.NewValue ?? 0;
|
||||
|
||||
int.TryParse(idString.NewValue, out parsed);
|
||||
|
||||
Model.ID = parsed;
|
||||
|
||||
if (idString.NewValue != idString.OldValue)
|
||||
if (id.NewValue != id.OldValue)
|
||||
Model.BeatmapInfo = null;
|
||||
|
||||
if (Model.BeatmapInfo != null)
|
||||
|
@ -214,7 +214,7 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[Resolved]
|
||||
private TournamentGameBase game { get; set; }
|
||||
|
||||
private readonly Bindable<string> userId = new Bindable<string>();
|
||||
private readonly Bindable<int?> userId = new Bindable<int?>();
|
||||
|
||||
private readonly Container drawableContainer;
|
||||
|
||||
@ -278,14 +278,12 @@ namespace osu.Game.Tournament.Screens.Editors
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
userId.Value = user.Id.ToString();
|
||||
userId.BindValueChanged(idString =>
|
||||
userId.Value = user.Id;
|
||||
userId.BindValueChanged(id =>
|
||||
{
|
||||
int.TryParse(idString.NewValue, out var parsed);
|
||||
user.Id = id.NewValue ?? 0;
|
||||
|
||||
user.Id = parsed;
|
||||
|
||||
if (idString.NewValue != idString.OldValue)
|
||||
if (id.NewValue != id.OldValue)
|
||||
user.Username = string.Empty;
|
||||
|
||||
if (!string.IsNullOrEmpty(user.Username))
|
||||
|
@ -196,8 +196,6 @@ namespace osu.Game.Beatmaps
|
||||
{
|
||||
var beatmapIds = beatmapSet.Beatmaps.Where(b => b.OnlineBeatmapID.HasValue).Select(b => b.OnlineBeatmapID).ToList();
|
||||
|
||||
LogForModel(beatmapSet, $"Validating online IDs for {beatmapSet.Beatmaps.Count} beatmaps...");
|
||||
|
||||
// ensure all IDs are unique
|
||||
if (beatmapIds.GroupBy(b => b).Any(g => g.Count() > 1))
|
||||
{
|
||||
@ -324,6 +322,14 @@ namespace osu.Game.Beatmaps
|
||||
/// <returns>The first result for the provided query, or null if no results were found.</returns>
|
||||
public BeatmapSetInfo QueryBeatmapSet(Expression<Func<BeatmapSetInfo, bool>> query) => beatmaps.ConsumableItems.AsNoTracking().FirstOrDefault(query);
|
||||
|
||||
protected override bool CanSkipImport(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
return false;
|
||||
|
||||
return existing.Beatmaps.Any(b => b.OnlineBeatmapID != null);
|
||||
}
|
||||
|
||||
protected override bool CanReuseExisting(BeatmapSetInfo existing, BeatmapSetInfo import)
|
||||
{
|
||||
if (!base.CanReuseExisting(existing, import))
|
||||
|
@ -48,7 +48,6 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
public Task UpdateAsync(BeatmapSetInfo beatmapSet, CancellationToken cancellationToken)
|
||||
{
|
||||
LogForModel(beatmapSet, "Performing online lookups...");
|
||||
return Task.WhenAll(beatmapSet.Beatmaps.Select(b => UpdateAsync(beatmapSet, b, cancellationToken)).ToArray());
|
||||
}
|
||||
|
||||
|
@ -133,6 +133,9 @@ namespace osu.Game.Beatmaps
|
||||
|
||||
IBeatmapProcessor processor = rulesetInstance.CreateBeatmapProcessor(converted);
|
||||
|
||||
foreach (var mod in mods.OfType<IApplicableToBeatmapProcessor>())
|
||||
mod.ApplyToBeatmapProcessor(processor);
|
||||
|
||||
processor?.PreProcess();
|
||||
|
||||
// Compute default values for hitobjects, including creating nested hitobjects in-case they're needed
|
||||
|
@ -35,6 +35,7 @@ namespace osu.Game.Collections
|
||||
private const int database_version = 30000000;
|
||||
|
||||
private const string database_name = "collection.db";
|
||||
private const string database_backup_name = "collection.db.bak";
|
||||
|
||||
public readonly BindableList<BeatmapCollection> Collections = new BindableList<BeatmapCollection>();
|
||||
|
||||
@ -56,6 +57,17 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Collections.CollectionChanged += collectionsChanged;
|
||||
|
||||
if (storage.Exists(database_backup_name))
|
||||
{
|
||||
// If a backup file exists, it means the previous write operation didn't run to completion.
|
||||
// Always prefer the backup file in such a case as it's the most recent copy that is guaranteed to not be malformed.
|
||||
//
|
||||
// The database is saved 100ms after any change, and again when the game is closed, so there shouldn't be a large diff between the two files in the worst case.
|
||||
if (storage.Exists(database_name))
|
||||
storage.Delete(database_name);
|
||||
File.Copy(storage.GetFullPath(database_backup_name), storage.GetFullPath(database_name));
|
||||
}
|
||||
|
||||
if (storage.Exists(database_name))
|
||||
{
|
||||
List<BeatmapCollection> beatmapCollections;
|
||||
@ -68,7 +80,7 @@ namespace osu.Game.Collections
|
||||
}
|
||||
}
|
||||
|
||||
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e)
|
||||
private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() =>
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
@ -92,7 +104,7 @@ namespace osu.Game.Collections
|
||||
}
|
||||
|
||||
backgroundSave();
|
||||
}
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Set an endpoint for notifications to be posted to.
|
||||
@ -257,27 +269,50 @@ namespace osu.Game.Collections
|
||||
{
|
||||
Interlocked.Increment(ref lastSave);
|
||||
|
||||
// This is NOT thread-safe!!
|
||||
try
|
||||
{
|
||||
// This is NOT thread-safe!!
|
||||
var tempPath = Path.GetTempFileName();
|
||||
|
||||
using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write)))
|
||||
using (var ms = new MemoryStream())
|
||||
{
|
||||
sw.Write(database_version);
|
||||
|
||||
var collectionsCopy = Collections.ToArray();
|
||||
sw.Write(collectionsCopy.Length);
|
||||
|
||||
foreach (var c in collectionsCopy)
|
||||
using (var sw = new SerializationWriter(ms, true))
|
||||
{
|
||||
sw.Write(c.Name.Value);
|
||||
sw.Write(database_version);
|
||||
|
||||
var beatmapsCopy = c.Beatmaps.ToArray();
|
||||
sw.Write(beatmapsCopy.Length);
|
||||
var collectionsCopy = Collections.ToArray();
|
||||
sw.Write(collectionsCopy.Length);
|
||||
|
||||
foreach (var b in beatmapsCopy)
|
||||
sw.Write(b.MD5Hash);
|
||||
foreach (var c in collectionsCopy)
|
||||
{
|
||||
sw.Write(c.Name.Value);
|
||||
|
||||
var beatmapsCopy = c.Beatmaps.ToArray();
|
||||
sw.Write(beatmapsCopy.Length);
|
||||
|
||||
foreach (var b in beatmapsCopy)
|
||||
sw.Write(b.MD5Hash);
|
||||
}
|
||||
}
|
||||
|
||||
using (var fs = File.OpenWrite(tempPath))
|
||||
ms.WriteTo(fs);
|
||||
|
||||
var databasePath = storage.GetFullPath(database_name);
|
||||
var databaseBackupPath = storage.GetFullPath(database_backup_name);
|
||||
|
||||
// Back up the existing database, clearing any existing backup.
|
||||
if (File.Exists(databaseBackupPath))
|
||||
File.Delete(databaseBackupPath);
|
||||
if (File.Exists(databasePath))
|
||||
File.Move(databasePath, databaseBackupPath);
|
||||
|
||||
// Move the new database in-place of the existing one.
|
||||
File.Move(tempPath, databasePath);
|
||||
|
||||
// If everything succeeded up to this point, remove the backup file.
|
||||
if (File.Exists(databaseBackupPath))
|
||||
File.Delete(databaseBackupPath);
|
||||
}
|
||||
|
||||
if (saveFailures < 10)
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.TypeExtensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Configuration
|
||||
@ -30,7 +31,7 @@ namespace osu.Game.Configuration
|
||||
{
|
||||
public LocalisableString Label { get; }
|
||||
|
||||
public string Description { get; }
|
||||
public LocalisableString Description { get; }
|
||||
|
||||
public int? OrderPosition { get; }
|
||||
|
||||
@ -149,7 +150,7 @@ namespace osu.Game.Configuration
|
||||
break;
|
||||
|
||||
case IBindable bindable:
|
||||
var dropdownType = typeof(SettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
|
||||
var dropdownType = typeof(ModSettingsEnumDropdown<>).MakeGenericType(bindable.GetType().GetGenericArguments()[0]);
|
||||
var dropdown = (Drawable)Activator.CreateInstance(dropdownType);
|
||||
|
||||
dropdownType.GetProperty(nameof(SettingsDropdown<object>.LabelText))?.SetValue(dropdown, attr.Label);
|
||||
@ -183,5 +184,17 @@ namespace osu.Game.Configuration
|
||||
=> obj.GetSettingsSourceProperties()
|
||||
.OrderBy(attr => attr.Item1)
|
||||
.ToArray();
|
||||
|
||||
private class ModSettingsEnumDropdown<T> : SettingsEnumDropdown<T>
|
||||
where T : struct, Enum
|
||||
{
|
||||
protected override OsuDropdown<T> CreateDropdown() => new ModDropdownControl();
|
||||
|
||||
private class ModDropdownControl : DropdownControl
|
||||
{
|
||||
// Set menu's max height low enough to workaround nested scroll issues (see https://github.com/ppy/osu-framework/issues/4536).
|
||||
protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 100);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ namespace osu.Game.Database
|
||||
|
||||
private readonly Bindable<WeakReference<TModel>> itemRemoved = new Bindable<WeakReference<TModel>>();
|
||||
|
||||
public virtual IEnumerable<string> HandledExtensions => new[] { ".zip" };
|
||||
public virtual IEnumerable<string> HandledExtensions => new[] { @".zip" };
|
||||
|
||||
protected readonly FileStore Files;
|
||||
|
||||
@ -99,7 +99,7 @@ namespace osu.Game.Database
|
||||
ModelStore.ItemUpdated += item => handleEvent(() => itemUpdated.Value = new WeakReference<TModel>(item));
|
||||
ModelStore.ItemRemoved += item => handleEvent(() => itemRemoved.Value = new WeakReference<TModel>(item));
|
||||
|
||||
exportStorage = storage.GetStorageForDirectory("exports");
|
||||
exportStorage = storage.GetStorageForDirectory(@"exports");
|
||||
|
||||
Files = new FileStore(contextFactory, storage);
|
||||
|
||||
@ -282,7 +282,7 @@ namespace osu.Game.Database
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogForModel(model, $"Model creation of {archive.Name} failed.", e);
|
||||
LogForModel(model, @$"Model creation of {archive.Name} failed.", e);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -309,6 +309,12 @@ namespace osu.Game.Database
|
||||
Logger.Log($"{prefix} {message}", LoggingTarget.Database);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the implementation overrides <see cref="ComputeHash"/> with a custom implementation.
|
||||
/// Custom hash implementations must bypass the early exit in the import flow (see <see cref="computeHashFast"/> usage).
|
||||
/// </summary>
|
||||
protected virtual bool HasCustomHashFunction => false;
|
||||
|
||||
/// <summary>
|
||||
/// Create a SHA-2 hash from the provided archive based on file content of all files matching <see cref="HashableFileTypes"/>.
|
||||
/// </summary>
|
||||
@ -317,7 +323,11 @@ namespace osu.Game.Database
|
||||
/// </remarks>
|
||||
protected virtual string ComputeHash(TModel item, ArchiveReader reader = null)
|
||||
{
|
||||
// for now, concatenate all .osu files in the set to create a unique hash.
|
||||
if (reader != null)
|
||||
// fast hashing for cases where the item's files may not be populated.
|
||||
return computeHashFast(reader);
|
||||
|
||||
// for now, concatenate all hashable files in the set to create a unique hash.
|
||||
MemoryStream hashable = new MemoryStream();
|
||||
|
||||
foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename))
|
||||
@ -329,9 +339,6 @@ namespace osu.Game.Database
|
||||
if (hashable.Length > 0)
|
||||
return hashable.ComputeSHA2Hash();
|
||||
|
||||
if (reader != null)
|
||||
return reader.Name.ComputeSHA2Hash();
|
||||
|
||||
return item.Hash;
|
||||
}
|
||||
|
||||
@ -348,19 +355,48 @@ namespace osu.Game.Database
|
||||
|
||||
delayEvents();
|
||||
|
||||
bool checkedExisting = false;
|
||||
TModel existing = null;
|
||||
|
||||
if (archive != null && !HasCustomHashFunction)
|
||||
{
|
||||
// this is a fast bail condition to improve large import performance.
|
||||
item.Hash = computeHashFast(archive);
|
||||
|
||||
checkedExisting = true;
|
||||
existing = CheckForExisting(item);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
// bare minimum comparisons
|
||||
//
|
||||
// note that this should really be checking filesizes on disk (of existing files) for some degree of sanity.
|
||||
// or alternatively doing a faster hash check. either of these require database changes and reprocessing of existing files.
|
||||
if (CanSkipImport(existing, item) &&
|
||||
getFilenames(existing.Files).SequenceEqual(getShortenedFilenames(archive).Select(p => p.shortened).OrderBy(f => f)))
|
||||
{
|
||||
LogForModel(item, @$"Found existing (optimised) {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
Undelete(existing);
|
||||
return existing;
|
||||
}
|
||||
|
||||
LogForModel(item, @"Found existing (optimised) but failed pre-check.");
|
||||
}
|
||||
}
|
||||
|
||||
void rollback()
|
||||
{
|
||||
if (!Delete(item))
|
||||
{
|
||||
// We may have not yet added the model to the underlying table, but should still clean up files.
|
||||
LogForModel(item, "Dereferencing files for incomplete import.");
|
||||
LogForModel(item, @"Dereferencing files for incomplete import.");
|
||||
Files.Dereference(item.Files.Select(f => f.FileInfo).ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
LogForModel(item, "Beginning import...");
|
||||
LogForModel(item, @"Beginning import...");
|
||||
|
||||
item.Files = archive != null ? createFileInfos(archive, Files) : new List<TFileModel>();
|
||||
item.Hash = ComputeHash(item, archive);
|
||||
@ -371,22 +407,24 @@ namespace osu.Game.Database
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!write.IsTransactionLeader) throw new InvalidOperationException($"Ensure there is no parent transaction so errors can correctly be handled by {this}");
|
||||
if (!write.IsTransactionLeader) throw new InvalidOperationException(@$"Ensure there is no parent transaction so errors can correctly be handled by {this}");
|
||||
|
||||
var existing = CheckForExisting(item);
|
||||
if (!checkedExisting)
|
||||
existing = CheckForExisting(item);
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
if (CanReuseExisting(existing, item))
|
||||
{
|
||||
Undelete(existing);
|
||||
LogForModel(item, $"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
LogForModel(item, @$"Found existing {HumanisedModelName} for {item} (ID {existing.ID}) – skipping import.");
|
||||
// existing item will be used; rollback new import and exit early.
|
||||
rollback();
|
||||
flushEvents(true);
|
||||
return existing;
|
||||
}
|
||||
|
||||
LogForModel(item, @"Found existing but failed re-use check.");
|
||||
Delete(existing);
|
||||
ModelStore.PurgeDeletable(s => s.ID == existing.ID);
|
||||
}
|
||||
@ -403,12 +441,12 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
LogForModel(item, "Import successfully completed!");
|
||||
LogForModel(item, @"Import successfully completed!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (!(e is TaskCanceledException))
|
||||
LogForModel(item, "Database import or population failed and has been rolled back.", e);
|
||||
LogForModel(item, @"Database import or population failed and has been rolled back.", e);
|
||||
|
||||
rollback();
|
||||
flushEvents(false);
|
||||
@ -428,7 +466,7 @@ namespace osu.Game.Database
|
||||
var retrievedItem = ModelStore.ConsumableItems.FirstOrDefault(s => s.ID == item.ID);
|
||||
|
||||
if (retrievedItem == null)
|
||||
throw new ArgumentException("Specified model could not be found", nameof(item));
|
||||
throw new ArgumentException(@"Specified model could not be found", nameof(item));
|
||||
|
||||
using (var outputStream = exportStorage.GetStream($"{getValidFilename(item.ToString())}{HandledExtensions.First()}", FileAccess.Write, FileMode.Create))
|
||||
ExportModelTo(retrievedItem, outputStream);
|
||||
@ -637,6 +675,22 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
private string computeHashFast(ArchiveReader reader)
|
||||
{
|
||||
MemoryStream hashable = new MemoryStream();
|
||||
|
||||
foreach (var file in reader.Filenames.Where(f => HashableFileTypes.Any(ext => f.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f))
|
||||
{
|
||||
using (Stream s = reader.GetStream(file))
|
||||
s.CopyTo(hashable);
|
||||
}
|
||||
|
||||
if (hashable.Length > 0)
|
||||
return hashable.ComputeSHA2Hash();
|
||||
|
||||
return reader.Name.ComputeSHA2Hash();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create all required <see cref="IO.FileInfo"/>s for the provided archive, adding them to the global file store.
|
||||
/// </summary>
|
||||
@ -644,18 +698,14 @@ namespace osu.Game.Database
|
||||
{
|
||||
var fileInfos = new List<TFileModel>();
|
||||
|
||||
string prefix = reader.Filenames.GetCommonPrefix();
|
||||
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
|
||||
prefix = string.Empty;
|
||||
|
||||
// import files to manager
|
||||
foreach (string file in reader.Filenames)
|
||||
foreach (var filenames in getShortenedFilenames(reader))
|
||||
{
|
||||
using (Stream s = reader.GetStream(file))
|
||||
using (Stream s = reader.GetStream(filenames.original))
|
||||
{
|
||||
fileInfos.Add(new TFileModel
|
||||
{
|
||||
Filename = file.Substring(prefix.Length).ToStandardisedPath(),
|
||||
Filename = filenames.shortened,
|
||||
FileInfo = files.Add(s)
|
||||
});
|
||||
}
|
||||
@ -664,6 +714,17 @@ namespace osu.Game.Database
|
||||
return fileInfos;
|
||||
}
|
||||
|
||||
private IEnumerable<(string original, string shortened)> getShortenedFilenames(ArchiveReader reader)
|
||||
{
|
||||
string prefix = reader.Filenames.GetCommonPrefix();
|
||||
if (!(prefix.EndsWith('/') || prefix.EndsWith('\\')))
|
||||
prefix = string.Empty;
|
||||
|
||||
// import files to manager
|
||||
foreach (string file in reader.Filenames)
|
||||
yield return (file, file.Substring(prefix.Length).ToStandardisedPath());
|
||||
}
|
||||
|
||||
#region osu-stable import
|
||||
|
||||
/// <summary>
|
||||
@ -696,7 +757,7 @@ namespace osu.Game.Database
|
||||
{
|
||||
string fullPath = storage.GetFullPath(ImportFromStablePath);
|
||||
|
||||
Logger.Log($"Folder \"{fullPath}\" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
|
||||
Logger.Log(@$"Folder ""{fullPath}"" not available in the target osu!stable installation to import {HumanisedModelName}s.", LoggingTarget.Information, LogLevel.Error);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -727,7 +788,7 @@ namespace osu.Game.Database
|
||||
/// <param name="model">The model to populate.</param>
|
||||
/// <param name="archive">The archive to use as a reference for population. May be null.</param>
|
||||
/// <param name="cancellationToken">An optional cancellation token.</param>
|
||||
protected virtual Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
protected abstract Task Populate(TModel model, [CanBeNull] ArchiveReader archive, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// Perform any final actions before the import to database executes.
|
||||
@ -744,6 +805,15 @@ namespace osu.Game.Database
|
||||
/// <returns>An existing model which matches the criteria to skip importing, else null.</returns>
|
||||
protected TModel CheckForExisting(TModel model) => model.Hash == null ? null : ModelStore.ConsumableItems.FirstOrDefault(b => b.Hash == model.Hash);
|
||||
|
||||
/// <summary>
|
||||
/// Whether inport can be skipped after finding an existing import early in the process.
|
||||
/// Only valid when <see cref="ComputeHash"/> is not overridden.
|
||||
/// </summary>
|
||||
/// <param name="existing">The existing model.</param>
|
||||
/// <param name="import">The newly imported model.</param>
|
||||
/// <returns>Whether to skip this import completely.</returns>
|
||||
protected virtual bool CanSkipImport(TModel existing, TModel import) => true;
|
||||
|
||||
/// <summary>
|
||||
/// After an existing <typeparamref name="TModel"/> is found during an import process, the default behaviour is to use/restore the existing
|
||||
/// item and skip the import. This method allows changing that behaviour.
|
||||
@ -771,7 +841,7 @@ namespace osu.Game.Database
|
||||
|
||||
private DbSet<TModel> queryModel() => ContextFactory.Get().Set<TModel>();
|
||||
|
||||
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}";
|
||||
protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace(@"Info", "").ToLower()}";
|
||||
|
||||
#region Event handling / delaying
|
||||
|
||||
|
@ -26,6 +26,11 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
private readonly object writeLock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Lock object which is held during <see cref="BlockAllOperations"/> sections.
|
||||
/// </summary>
|
||||
private readonly SemaphoreSlim blockingLock = new SemaphoreSlim(1);
|
||||
|
||||
private static readonly GlobalStatistic<int> reads = GlobalStatistics.Get<int>("Realm", "Get (Read)");
|
||||
private static readonly GlobalStatistic<int> writes = GlobalStatistics.Get<int>("Realm", "Get (Write)");
|
||||
private static readonly GlobalStatistic<int> refreshes = GlobalStatistics.Get<int>("Realm", "Dirty Refreshes");
|
||||
@ -33,17 +38,12 @@ namespace osu.Game.Database
|
||||
private static readonly GlobalStatistic<int> pending_writes = GlobalStatistics.Get<int>("Realm", "Pending writes");
|
||||
private static readonly GlobalStatistic<int> active_usages = GlobalStatistics.Get<int>("Realm", "Active usages");
|
||||
|
||||
private readonly ManualResetEventSlim blockingResetEvent = new ManualResetEventSlim(true);
|
||||
|
||||
private Realm context;
|
||||
|
||||
public Realm Context
|
||||
{
|
||||
get
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new InvalidOperationException($"Attempted to access {nameof(Context)} on a disposed context factory");
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
context = createContext();
|
||||
@ -64,7 +64,7 @@ namespace osu.Game.Database
|
||||
public RealmUsage GetForRead()
|
||||
{
|
||||
reads.Value++;
|
||||
return new RealmUsage(this);
|
||||
return new RealmUsage(createContext());
|
||||
}
|
||||
|
||||
public RealmWriteUsage GetForWrite()
|
||||
@ -73,8 +73,28 @@ namespace osu.Game.Database
|
||||
pending_writes.Value++;
|
||||
|
||||
Monitor.Enter(writeLock);
|
||||
return new RealmWriteUsage(createContext(), writeComplete);
|
||||
}
|
||||
|
||||
return new RealmWriteUsage(this);
|
||||
/// <summary>
|
||||
/// Flush any active contexts and block any further writes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This should be used in places we need to ensure no ongoing reads/writes are occurring with realm.
|
||||
/// ie. to move the realm backing file to a new location.
|
||||
/// </remarks>
|
||||
/// <returns>An <see cref="IDisposable"/> which should be disposed to end the blocking section.</returns>
|
||||
public IDisposable BlockAllOperations()
|
||||
{
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||
|
||||
blockingLock.Wait();
|
||||
flushContexts();
|
||||
|
||||
return new InvokeOnDisposal<RealmContextFactory>(this, endBlockingSection);
|
||||
|
||||
static void endBlockingSection(RealmContextFactory factory) => factory.blockingLock.Release();
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
@ -87,15 +107,31 @@ namespace osu.Game.Database
|
||||
|
||||
private Realm createContext()
|
||||
{
|
||||
blockingResetEvent.Wait();
|
||||
|
||||
contexts_created.Value++;
|
||||
|
||||
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
||||
try
|
||||
{
|
||||
SchemaVersion = schema_version,
|
||||
MigrationCallback = onMigration,
|
||||
});
|
||||
if (IsDisposed)
|
||||
throw new ObjectDisposedException(nameof(RealmContextFactory));
|
||||
|
||||
blockingLock.Wait();
|
||||
|
||||
contexts_created.Value++;
|
||||
|
||||
return Realm.GetInstance(new RealmConfiguration(storage.GetFullPath($"{database_name}.realm", true))
|
||||
{
|
||||
SchemaVersion = schema_version,
|
||||
MigrationCallback = onMigration,
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
blockingLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private void writeComplete()
|
||||
{
|
||||
Monitor.Exit(writeLock);
|
||||
pending_writes.Value--;
|
||||
}
|
||||
|
||||
private void onMigration(Migration migration, ulong lastSchemaVersion)
|
||||
@ -109,26 +145,6 @@ namespace osu.Game.Database
|
||||
}
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
base.Dispose(isDisposing);
|
||||
|
||||
BlockAllOperations();
|
||||
}
|
||||
|
||||
public IDisposable BlockAllOperations()
|
||||
{
|
||||
blockingResetEvent.Reset();
|
||||
flushContexts();
|
||||
|
||||
return new InvokeOnDisposal<RealmContextFactory>(this, r => endBlockingSection());
|
||||
}
|
||||
|
||||
private void endBlockingSection()
|
||||
{
|
||||
blockingResetEvent.Set();
|
||||
}
|
||||
|
||||
private void flushContexts()
|
||||
{
|
||||
var previousContext = context;
|
||||
@ -141,6 +157,18 @@ namespace osu.Game.Database
|
||||
previousContext?.Dispose();
|
||||
}
|
||||
|
||||
protected override void Dispose(bool isDisposing)
|
||||
{
|
||||
if (!IsDisposed)
|
||||
{
|
||||
// intentionally block all operations indefinitely. this ensures that nothing can start consuming a new context after disposal.
|
||||
BlockAllOperations();
|
||||
blockingLock?.Dispose();
|
||||
}
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A usage of realm from an arbitrary thread.
|
||||
/// </summary>
|
||||
@ -148,13 +176,10 @@ namespace osu.Game.Database
|
||||
{
|
||||
public readonly Realm Realm;
|
||||
|
||||
protected readonly RealmContextFactory Factory;
|
||||
|
||||
internal RealmUsage(RealmContextFactory factory)
|
||||
internal RealmUsage(Realm context)
|
||||
{
|
||||
active_usages.Value++;
|
||||
Factory = factory;
|
||||
Realm = factory.createContext();
|
||||
Realm = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -172,11 +197,13 @@ namespace osu.Game.Database
|
||||
/// </summary>
|
||||
public class RealmWriteUsage : RealmUsage
|
||||
{
|
||||
private readonly Action onWriteComplete;
|
||||
private readonly Transaction transaction;
|
||||
|
||||
internal RealmWriteUsage(RealmContextFactory factory)
|
||||
: base(factory)
|
||||
internal RealmWriteUsage(Realm context, Action onWriteComplete)
|
||||
: base(context)
|
||||
{
|
||||
this.onWriteComplete = onWriteComplete;
|
||||
transaction = Realm.BeginWrite();
|
||||
}
|
||||
|
||||
@ -200,8 +227,7 @@ namespace osu.Game.Database
|
||||
|
||||
base.Dispose();
|
||||
|
||||
Monitor.Exit(Factory.writeLock);
|
||||
pending_writes.Value--;
|
||||
onWriteComplete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,12 +4,13 @@
|
||||
using Markdig.Syntax.Inlines;
|
||||
using osu.Framework.Graphics.Containers.Markdown;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.Containers.Markdown
|
||||
{
|
||||
public class OsuMarkdownImage : MarkdownImage, IHasTooltip
|
||||
{
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public OsuMarkdownImage(LinkInline linkInline)
|
||||
: base(linkInline.Url)
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Graphics.Containers
|
||||
@ -24,7 +25,7 @@ namespace osu.Game.Graphics.Containers
|
||||
this.sampleSet = sampleSet;
|
||||
}
|
||||
|
||||
public virtual string TooltipText { get; set; }
|
||||
public virtual LocalisableString TooltipText { get; set; }
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
|
@ -9,6 +9,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
|
||||
namespace osu.Game.Graphics.Cursor
|
||||
@ -32,7 +33,7 @@ namespace osu.Game.Graphics.Cursor
|
||||
|
||||
public override bool SetContent(object content)
|
||||
{
|
||||
if (!(content is string contentString))
|
||||
if (!(content is LocalisableString contentString))
|
||||
return false;
|
||||
|
||||
if (contentString == text.Text) return true;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
@ -58,6 +59,6 @@ namespace osu.Game.Graphics.UserInterface
|
||||
return true;
|
||||
}
|
||||
|
||||
public string TooltipText => "view in browser";
|
||||
public LocalisableString TooltipText => "view in browser";
|
||||
}
|
||||
}
|
||||
|
@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Platform;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
|
||||
private class CapsWarning : SpriteIcon, IHasTooltip
|
||||
{
|
||||
public string TooltipText => @"caps lock is active";
|
||||
public LocalisableString TooltipText => "caps lock is active";
|
||||
|
||||
public CapsWarning()
|
||||
{
|
||||
|
@ -14,6 +14,7 @@ using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Graphics.UserInterface
|
||||
{
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Graphics.UserInterface
|
||||
private readonly Box rightBox;
|
||||
private readonly Container nubContainer;
|
||||
|
||||
public virtual string TooltipText { get; private set; }
|
||||
public virtual LocalisableString TooltipText { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether to format the tooltip as a percentage or the actual value.
|
||||
|
@ -18,8 +18,8 @@ namespace osu.Game.IO.Legacy
|
||||
/// handle null strings and simplify use with ISerializable. </summary>
|
||||
public class SerializationWriter : BinaryWriter
|
||||
{
|
||||
public SerializationWriter(Stream s)
|
||||
: base(s, Encoding.UTF8)
|
||||
public SerializationWriter(Stream s, bool leaveOpen = false)
|
||||
: base(s, Encoding.UTF8, leaveOpen)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -103,8 +103,11 @@ namespace osu.Game.Localisation
|
||||
[Description(@"简体中文")]
|
||||
zh,
|
||||
|
||||
[Description(@"繁體中文(香港)")]
|
||||
zh_hk,
|
||||
// Traditional Chinese (Hong Kong) is listed in web sources but has no associated localisations,
|
||||
// and was wrongly falling back to Simplified Chinese.
|
||||
// Can be revisited if localisations ever arrive.
|
||||
// [Description(@"繁體中文(香港)")]
|
||||
// zh_hk,
|
||||
|
||||
[Description(@"繁體中文(台灣)")]
|
||||
zh_tw
|
||||
|
@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
@ -295,7 +296,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
|
||||
public override bool Contains(Vector2 screenSpacePos) => content.Contains(screenSpacePos);
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public ScoreComponentLabel(LeaderboardScoreStatistic statistic)
|
||||
{
|
||||
@ -365,7 +366,7 @@ namespace osu.Game.Online.Leaderboards
|
||||
};
|
||||
}
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
}
|
||||
|
||||
public class LeaderboardScoreStatistic
|
||||
|
@ -24,6 +24,7 @@ using osu.Framework.Graphics.Performance;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.Logging;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Database;
|
||||
using osu.Game.Input;
|
||||
@ -156,6 +157,8 @@ namespace osu.Game
|
||||
|
||||
private readonly BindableNumber<double> globalTrackVolumeAdjust = new BindableNumber<double>(GLOBAL_TRACK_VOLUME_ADJUST);
|
||||
|
||||
private IBindable<GameThreadState> updateThreadState;
|
||||
|
||||
public OsuGameBase()
|
||||
{
|
||||
UseDevelopmentServer = DebugUtils.IsDebugBuild;
|
||||
@ -182,6 +185,10 @@ namespace osu.Game
|
||||
dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage));
|
||||
|
||||
dependencies.Cache(realmFactory = new RealmContextFactory(Storage));
|
||||
|
||||
updateThreadState = Host.UpdateThread.State.GetBoundCopy();
|
||||
updateThreadState.BindValueChanged(updateThreadStateChanged);
|
||||
|
||||
AddInternal(realmFactory);
|
||||
|
||||
dependencies.CacheAs(Storage);
|
||||
@ -356,6 +363,23 @@ namespace osu.Game
|
||||
Ruleset.BindValueChanged(onRulesetChanged);
|
||||
}
|
||||
|
||||
private IDisposable blocking;
|
||||
|
||||
private void updateThreadStateChanged(ValueChangedEvent<GameThreadState> state)
|
||||
{
|
||||
switch (state.NewValue)
|
||||
{
|
||||
case GameThreadState.Running:
|
||||
blocking?.Dispose();
|
||||
blocking = null;
|
||||
break;
|
||||
|
||||
case GameThreadState.Paused:
|
||||
blocking = realmFactory.BlockAllOperations();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
@ -10,11 +10,13 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Threading;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.API.Requests;
|
||||
using osu.Game.Rulesets;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -23,9 +25,9 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
public class BeatmapListingFilterControl : CompositeDrawable
|
||||
{
|
||||
/// <summary>
|
||||
/// Fired when a search finishes. Contains only new items in the case of pagination.
|
||||
/// Fired when a search finishes.
|
||||
/// </summary>
|
||||
public Action<List<BeatmapSetInfo>> SearchFinished;
|
||||
public Action<SearchResult> SearchFinished;
|
||||
|
||||
/// <summary>
|
||||
/// Fired when search criteria change.
|
||||
@ -212,7 +214,25 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
lastResponse = response;
|
||||
getSetsRequest = null;
|
||||
|
||||
SearchFinished?.Invoke(sets);
|
||||
// check if a non-supporter used supporter-only filters
|
||||
if (!api.LocalUser.Value.IsSupporter)
|
||||
{
|
||||
List<LocalisableString> filters = new List<LocalisableString>();
|
||||
|
||||
if (searchControl.Played.Value != SearchPlayed.Any)
|
||||
filters.Add(BeatmapsStrings.ListingSearchFiltersPlayed);
|
||||
|
||||
if (searchControl.Ranks.Any())
|
||||
filters.Add(BeatmapsStrings.ListingSearchFiltersRank);
|
||||
|
||||
if (filters.Any())
|
||||
{
|
||||
SearchFinished?.Invoke(SearchResult.SupporterOnlyFilters(filters));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SearchFinished?.Invoke(SearchResult.ResultsReturned(sets));
|
||||
};
|
||||
|
||||
api.Queue(getSetsRequest);
|
||||
@ -237,5 +257,53 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
|
||||
base.Dispose(isDisposing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Indicates the type of result of a user-requested beatmap search.
|
||||
/// </summary>
|
||||
public enum SearchResultType
|
||||
{
|
||||
/// <summary>
|
||||
/// Actual results have been returned from API.
|
||||
/// </summary>
|
||||
ResultsReturned,
|
||||
|
||||
/// <summary>
|
||||
/// The user is not a supporter, but used supporter-only search filters.
|
||||
/// </summary>
|
||||
SupporterOnlyFilters
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Describes the result of a user-requested beatmap search.
|
||||
/// </summary>
|
||||
public struct SearchResult
|
||||
{
|
||||
public SearchResultType Type { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the beatmap sets returned from API.
|
||||
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.ResultsReturned"/>.
|
||||
/// </summary>
|
||||
public List<BeatmapSetInfo> Results { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Contains the names of supporter-only filters requested by the user.
|
||||
/// Valid for read if and only if <see cref="Type"/> is <see cref="SearchResultType.SupporterOnlyFilters"/>.
|
||||
/// </summary>
|
||||
public List<LocalisableString> SupporterOnlyFiltersUsed { get; private set; }
|
||||
|
||||
public static SearchResult ResultsReturned(List<BeatmapSetInfo> results) => new SearchResult
|
||||
{
|
||||
Type = SearchResultType.ResultsReturned,
|
||||
Results = results
|
||||
};
|
||||
|
||||
public static SearchResult SupporterOnlyFilters(List<LocalisableString> filters) => new SearchResult
|
||||
{
|
||||
Type = SearchResultType.SupporterOnlyFilters,
|
||||
SupporterOnlyFiltersUsed = filters
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
using osu.Game.Rulesets;
|
||||
|
||||
@ -22,14 +23,21 @@ namespace osu.Game.Overlays.BeatmapListing
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(RulesetStore rulesets)
|
||||
{
|
||||
AddItem(new RulesetInfo
|
||||
{
|
||||
Name = @"Any"
|
||||
});
|
||||
AddTabItem(new RulesetFilterTabItemAny());
|
||||
|
||||
foreach (var r in rulesets.AvailableRulesets)
|
||||
AddItem(r);
|
||||
}
|
||||
}
|
||||
|
||||
private class RulesetFilterTabItemAny : FilterTabItem<RulesetInfo>
|
||||
{
|
||||
protected override LocalisableString LabelFor(RulesetInfo info) => BeatmapsStrings.ModeAny;
|
||||
|
||||
public RulesetFilterTabItemAny()
|
||||
: base(new RulesetInfo())
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
@ -15,7 +16,9 @@ using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Audio;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.Containers;
|
||||
using osu.Game.Overlays.BeatmapListing;
|
||||
using osu.Game.Overlays.BeatmapListing.Panels;
|
||||
using osu.Game.Resources.Localisation.Web;
|
||||
@ -33,6 +36,7 @@ namespace osu.Game.Overlays
|
||||
private Container panelTarget;
|
||||
private FillFlowContainer<BeatmapPanel> foundContent;
|
||||
private NotFoundDrawable notFoundContent;
|
||||
private SupporterRequiredDrawable supporterRequiredContent;
|
||||
private BeatmapListingFilterControl filterControl;
|
||||
|
||||
public BeatmapListingOverlay()
|
||||
@ -76,6 +80,7 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
foundContent = new FillFlowContainer<BeatmapPanel>(),
|
||||
notFoundContent = new NotFoundDrawable(),
|
||||
supporterRequiredContent = new SupporterRequiredDrawable(),
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -115,9 +120,16 @@ namespace osu.Game.Overlays
|
||||
|
||||
private Task panelLoadDelegate;
|
||||
|
||||
private void onSearchFinished(List<BeatmapSetInfo> beatmaps)
|
||||
private void onSearchFinished(BeatmapListingFilterControl.SearchResult searchResult)
|
||||
{
|
||||
var newPanels = beatmaps.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
|
||||
if (searchResult.Type == BeatmapListingFilterControl.SearchResultType.SupporterOnlyFilters)
|
||||
{
|
||||
supporterRequiredContent.UpdateText(searchResult.SupporterOnlyFiltersUsed);
|
||||
addContentToPlaceholder(supporterRequiredContent);
|
||||
return;
|
||||
}
|
||||
|
||||
var newPanels = searchResult.Results.Select<BeatmapSetInfo, BeatmapPanel>(b => new GridBeatmapPanel(b)
|
||||
{
|
||||
Anchor = Anchor.TopCentre,
|
||||
Origin = Anchor.TopCentre,
|
||||
@ -128,7 +140,7 @@ namespace osu.Game.Overlays
|
||||
//No matches case
|
||||
if (!newPanels.Any())
|
||||
{
|
||||
LoadComponentAsync(notFoundContent, addContentToPlaceholder, (cancellationToken = new CancellationTokenSource()).Token);
|
||||
addContentToPlaceholder(notFoundContent);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -170,9 +182,9 @@ namespace osu.Game.Overlays
|
||||
{
|
||||
var transform = lastContent.FadeOut(100, Easing.OutQuint);
|
||||
|
||||
if (lastContent == notFoundContent)
|
||||
if (lastContent == notFoundContent || lastContent == supporterRequiredContent)
|
||||
{
|
||||
// not found display may be used multiple times, so don't expire/dispose it.
|
||||
// the placeholders may be used multiple times, so don't expire/dispose them.
|
||||
transform.Schedule(() => panelTarget.Remove(lastContent));
|
||||
}
|
||||
else
|
||||
@ -240,6 +252,67 @@ namespace osu.Game.Overlays
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: localisation requires Text/LinkFlowContainer support for localising strings with links inside
|
||||
// (https://github.com/ppy/osu-framework/issues/4530)
|
||||
public class SupporterRequiredDrawable : CompositeDrawable
|
||||
{
|
||||
private LinkFlowContainer supporterRequiredText;
|
||||
|
||||
public SupporterRequiredDrawable()
|
||||
{
|
||||
RelativeSizeAxes = Axes.X;
|
||||
Height = 225;
|
||||
Alpha = 0;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(TextureStore textures)
|
||||
{
|
||||
AddInternal(new FillFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Y,
|
||||
AutoSizeAxes = Axes.X,
|
||||
Direction = FillDirection.Horizontal,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new Sprite
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
FillMode = FillMode.Fit,
|
||||
Texture = textures.Get(@"Online/supporter-required"),
|
||||
},
|
||||
supporterRequiredText = new LinkFlowContainer
|
||||
{
|
||||
Anchor = Anchor.Centre,
|
||||
Origin = Anchor.Centre,
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Margin = new MarginPadding { Bottom = 10 },
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void UpdateText(List<LocalisableString> filters)
|
||||
{
|
||||
supporterRequiredText.Clear();
|
||||
|
||||
supporterRequiredText.AddText(
|
||||
BeatmapsStrings.ListingSearchSupporterFilterQuoteDefault(string.Join(" and ", filters), "").ToString(),
|
||||
t =>
|
||||
{
|
||||
t.Font = OsuFont.GetFont(size: 16);
|
||||
t.Colour = Colour4.White;
|
||||
}
|
||||
);
|
||||
|
||||
supporterRequiredText.AddLink(BeatmapsStrings.ListingSearchSupporterFilterQuoteLinkText.ToString(), @"/store/products/supporter-tag");
|
||||
}
|
||||
}
|
||||
|
||||
private const double time_between_fetches = 500;
|
||||
|
||||
private double lastFetchDisplayedTime;
|
||||
|
@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet
|
||||
{
|
||||
private readonly OsuSpriteText value;
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public LocalisableString Value
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Online.API;
|
||||
@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
|
||||
|
||||
private readonly IBindable<User> localUser = new Bindable<User>();
|
||||
|
||||
public string TooltipText
|
||||
public LocalisableString TooltipText
|
||||
{
|
||||
get
|
||||
{
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Beatmaps;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Containers;
|
||||
@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons
|
||||
|
||||
private readonly bool noVideo;
|
||||
|
||||
public string TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
|
||||
public LocalisableString TooltipText => button.Enabled.Value ? "download this beatmap" : "login to download";
|
||||
|
||||
private readonly IBindable<User> localUser = new Bindable<User>();
|
||||
|
||||
|
@ -240,12 +240,15 @@ namespace osu.Game.Overlays.Chat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (sender.Equals(User.SYSTEM_USER))
|
||||
return Array.Empty<MenuItem>();
|
||||
|
||||
List<MenuItem> items = new List<MenuItem>
|
||||
{
|
||||
new OsuMenuItem("View Profile", MenuItemType.Highlighted, Action)
|
||||
};
|
||||
|
||||
if (sender.Id != api.LocalUser.Value.Id)
|
||||
if (!sender.Equals(api.LocalUser.Value))
|
||||
items.Add(new OsuMenuItem("Start Chat", MenuItemType.Standard, startChatAction));
|
||||
|
||||
return items.ToArray();
|
||||
|
@ -20,6 +20,7 @@ using System;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Extensions.IEnumerableExtensions;
|
||||
using System.Collections.Specialized;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Overlays.Comments.Buttons;
|
||||
|
||||
namespace osu.Game.Overlays.Comments
|
||||
@ -395,7 +396,7 @@ namespace osu.Game.Overlays.Comments
|
||||
|
||||
private class ParentUsername : FillFlowContainer, IHasTooltip
|
||||
{
|
||||
public string TooltipText => getParentMessage();
|
||||
public LocalisableString TooltipText => getParentMessage();
|
||||
|
||||
private readonly Comment parentComment;
|
||||
|
||||
@ -427,7 +428,7 @@ namespace osu.Game.Overlays.Comments
|
||||
if (parentComment == null)
|
||||
return string.Empty;
|
||||
|
||||
return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty;
|
||||
return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? "deleted" : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ using System;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
@ -34,7 +35,7 @@ namespace osu.Game.Overlays.Mods
|
||||
/// </summary>
|
||||
public Action<Mod> SelectionChanged;
|
||||
|
||||
public string TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
|
||||
public LocalisableString TooltipText => (SelectedMod?.Description ?? Mods.FirstOrDefault()?.Description) ?? string.Empty;
|
||||
|
||||
private const Easing mod_switch_easing = Easing.InOutSine;
|
||||
private const double mod_switch_duration = 120;
|
||||
|
@ -37,9 +37,6 @@ namespace osu.Game.Overlays.Mods
|
||||
protected readonly TriangleButton CustomiseButton;
|
||||
protected readonly TriangleButton CloseButton;
|
||||
|
||||
protected readonly Drawable MultiplierSection;
|
||||
protected readonly OsuSpriteText MultiplierLabel;
|
||||
|
||||
protected readonly FillFlowContainer FooterContainer;
|
||||
|
||||
protected override bool BlockNonPositionalInput => false;
|
||||
@ -324,30 +321,6 @@ namespace osu.Game.Overlays.Mods
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
},
|
||||
MultiplierSection = new FillFlowContainer
|
||||
{
|
||||
AutoSizeAxes = Axes.Both,
|
||||
Spacing = new Vector2(footer_button_spacing / 2, 0),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Children = new Drawable[]
|
||||
{
|
||||
new OsuSpriteText
|
||||
{
|
||||
Text = @"Score Multiplier:",
|
||||
Font = OsuFont.GetFont(size: 30),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
},
|
||||
MultiplierLabel = new OsuSpriteText
|
||||
{
|
||||
Font = OsuFont.GetFont(size: 30, weight: FontWeight.Bold),
|
||||
Origin = Anchor.CentreLeft,
|
||||
Anchor = Anchor.CentreLeft,
|
||||
Width = 70, // make width fixed so reflow doesn't occur when multiplier number changes.
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -361,11 +334,8 @@ namespace osu.Game.Overlays.Mods
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader(true)]
|
||||
private void load(OsuColour colours, AudioManager audio, OsuGameBase osu)
|
||||
private void load(AudioManager audio, OsuGameBase osu)
|
||||
{
|
||||
LowMultiplierColour = colours.Red;
|
||||
HighMultiplierColour = colours.Green;
|
||||
|
||||
availableMods = osu.AvailableMods.GetBoundCopy();
|
||||
|
||||
sampleOn = audio.Samples.Get(@"UI/check-on");
|
||||
@ -495,26 +465,6 @@ namespace osu.Game.Overlays.Mods
|
||||
|
||||
foreach (var section in ModSectionsContainer.Children)
|
||||
section.UpdateSelectedButtons(selectedMods);
|
||||
|
||||
updateMultiplier();
|
||||
}
|
||||
|
||||
private void updateMultiplier()
|
||||
{
|
||||
var multiplier = 1.0;
|
||||
|
||||
foreach (var mod in SelectedMods.Value)
|
||||
{
|
||||
multiplier *= mod.ScoreMultiplier;
|
||||
}
|
||||
|
||||
MultiplierLabel.Text = $"{multiplier:N2}x";
|
||||
if (multiplier > 1.0)
|
||||
MultiplierLabel.FadeColour(HighMultiplierColour, 200);
|
||||
else if (multiplier < 1.0)
|
||||
MultiplierLabel.FadeColour(LowMultiplierColour, 200);
|
||||
else
|
||||
MultiplierLabel.FadeColour(Color4.White, 200);
|
||||
}
|
||||
|
||||
private void modButtonPressed(Mod selectedMod)
|
||||
|
@ -11,6 +11,7 @@ using osu.Game.Graphics.UserInterface;
|
||||
using osu.Framework.Allocation;
|
||||
using osuTK.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Overlays
|
||||
{
|
||||
@ -56,7 +57,7 @@ namespace osu.Game.Overlays
|
||||
[Resolved]
|
||||
private OverlayColourProvider colourProvider { get; set; }
|
||||
|
||||
public string TooltipText => $@"{Value} view";
|
||||
public LocalisableString TooltipText => $@"{Value} view";
|
||||
|
||||
private readonly SpriteIcon icon;
|
||||
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Users;
|
||||
using osuTK;
|
||||
|
||||
@ -42,6 +43,6 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
InternalChild.FadeInFromZero(200);
|
||||
}
|
||||
|
||||
public string TooltipText => badge.Description;
|
||||
public LocalisableString TooltipText => badge.Description;
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly BindableBool DetailsVisible = new BindableBool();
|
||||
|
||||
public override string TooltipText => DetailsVisible.Value ? "collapse" : "expand";
|
||||
public override LocalisableString TooltipText => DetailsVisible.Value ? "collapse" : "expand";
|
||||
|
||||
private SpriteIcon icon;
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public override string TooltipText => "followers";
|
||||
public override LocalisableString TooltipText => "followers";
|
||||
|
||||
protected override IconUsage Icon => FontAwesome.Solid.User;
|
||||
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Graphics.Textures;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Users;
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
private OsuSpriteText levelText;
|
||||
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
private Bar levelProgressBar;
|
||||
private OsuSpriteText levelProgressText;
|
||||
|
@ -4,6 +4,7 @@
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -12,7 +13,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public override string TooltipText => "mapping subscribers";
|
||||
public override LocalisableString TooltipText => "mapping subscribers";
|
||||
|
||||
protected override IconUsage Icon => FontAwesome.Solid.Bell;
|
||||
|
||||
|
@ -5,6 +5,7 @@ using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Online.API;
|
||||
using osu.Game.Online.Chat;
|
||||
using osu.Game.Users;
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public override string TooltipText => "send message";
|
||||
public override LocalisableString TooltipText => "send message";
|
||||
|
||||
[Resolved(CanBeNull = true)]
|
||||
private ChannelManager channelManager { get; set; }
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Users;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
{
|
||||
public readonly Bindable<User> User = new Bindable<User>();
|
||||
|
||||
public string TooltipText { get; set; }
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
|
||||
private OverlinedInfoContainer info;
|
||||
|
||||
|
@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Profile.Header.Components
|
||||
@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Header.Components
|
||||
private readonly FillFlowContainer iconContainer;
|
||||
private readonly CircularContainer content;
|
||||
|
||||
public string TooltipText => "osu!supporter";
|
||||
public LocalisableString TooltipText => "osu!supporter";
|
||||
|
||||
public int SupportLevel
|
||||
{
|
||||
|
@ -143,7 +143,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical
|
||||
|
||||
private class PlayCountText : CompositeDrawable, IHasTooltip
|
||||
{
|
||||
public string TooltipText => "times played";
|
||||
public LocalisableString TooltipText => "times played";
|
||||
|
||||
public PlayCountText(int playCount)
|
||||
{
|
||||
|
@ -10,6 +10,7 @@ using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Effects;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
@ -76,7 +77,7 @@ namespace osu.Game.Overlays
|
||||
UpdateState();
|
||||
}
|
||||
|
||||
public string TooltipText => "revert to default";
|
||||
public LocalisableString TooltipText => "revert to default";
|
||||
|
||||
protected override bool OnHover(HoverEvent e)
|
||||
{
|
||||
|
49
osu.Game/Overlays/Settings/OutlinedTextBox.cs
Normal file
49
osu.Game/Overlays/Settings/OutlinedTextBox.cs
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class OutlinedTextBox : OsuTextBox
|
||||
{
|
||||
private const float border_thickness = 3;
|
||||
|
||||
private Color4 borderColourFocused;
|
||||
private Color4 borderColourUnfocused;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colour)
|
||||
{
|
||||
borderColourUnfocused = colour.Gray4.Opacity(0.5f);
|
||||
borderColourFocused = BorderColour;
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
base.OnFocusLost(e);
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
private void updateBorder()
|
||||
{
|
||||
BorderThickness = border_thickness;
|
||||
BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio
|
||||
|
||||
private class OffsetSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString(@"0ms");
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"0ms");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics
|
||||
|
||||
private class UIScaleSlider : OsuSliderBar<float>
|
||||
{
|
||||
public override string TooltipText => base.TooltipText + "x";
|
||||
public override LocalisableString TooltipText => base.TooltipText + "x";
|
||||
}
|
||||
|
||||
private class ResolutionSettingsDropdown : SettingsDropdown<Size>
|
||||
|
@ -6,6 +6,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Handlers.Mouse;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Input;
|
||||
@ -116,7 +117,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input
|
||||
|
||||
private class SensitivitySlider : OsuSliderBar<double>
|
||||
{
|
||||
public override string TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
|
||||
public override LocalisableString TooltipText => Current.Disabled ? "enable high precision mouse to adjust sensitivity" : $"{base.TooltipText}x";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings.Sections
|
||||
@ -10,6 +11,6 @@ namespace osu.Game.Overlays.Settings.Sections
|
||||
/// </summary>
|
||||
internal class SizeSlider : OsuSliderBar<float>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString(@"0.##x");
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"0.##x");
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
@ -44,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
|
||||
private class TimeSlider : OsuSliderBar<float>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString("N0") + "ms";
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"N0") + "ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
@ -62,12 +63,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface
|
||||
|
||||
private class MaximumStarsSlider : StarsSlider
|
||||
{
|
||||
public override string TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
|
||||
public override LocalisableString TooltipText => Current.IsDefault ? "no limit" : base.TooltipText;
|
||||
}
|
||||
|
||||
private class StarsSlider : OsuSliderBar<double>
|
||||
{
|
||||
public override string TooltipText => Current.Value.ToString(@"0.## stars");
|
||||
public override LocalisableString TooltipText => Current.Value.ToString(@"0.## stars");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
@ -17,14 +18,15 @@ namespace osu.Game.Overlays.Settings
|
||||
Padding = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS };
|
||||
}
|
||||
|
||||
public string TooltipText { get; set; }
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
|
||||
public override IEnumerable<string> FilterTerms
|
||||
{
|
||||
get
|
||||
{
|
||||
if (TooltipText != null)
|
||||
return base.FilterTerms.Append(TooltipText);
|
||||
if (TooltipText != default)
|
||||
// TODO: this won't work as intended once the tooltip text is translated.
|
||||
return base.FilterTerms.Append(TooltipText.ToString());
|
||||
|
||||
return base.FilterTerms;
|
||||
}
|
||||
|
@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings
|
||||
|
||||
public bool ShowsDefaultIndicator = true;
|
||||
|
||||
public string TooltipText { get; set; }
|
||||
public LocalisableString TooltipText { get; set; }
|
||||
|
||||
[Resolved]
|
||||
private OsuColour colours { get; set; }
|
||||
@ -142,4 +142,4 @@ namespace osu.Game.Overlays.Settings
|
||||
labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,65 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class SettingsNumberBox : SettingsItem<string>
|
||||
public class SettingsNumberBox : SettingsItem<int?>
|
||||
{
|
||||
protected override Drawable CreateControl() => new NumberBox
|
||||
protected override Drawable CreateControl() => new NumberControl
|
||||
{
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
};
|
||||
|
||||
public class NumberBox : SettingsTextBox.TextBox
|
||||
private sealed class NumberControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||
{
|
||||
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
|
||||
|
||||
public Bindable<int?> Current
|
||||
{
|
||||
get => current.Current;
|
||||
set => current.Current = value;
|
||||
}
|
||||
|
||||
public NumberControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
OutlinedNumberBox numberBox;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
numberBox = new OutlinedNumberBox
|
||||
{
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true
|
||||
}
|
||||
};
|
||||
|
||||
numberBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
int? value = null;
|
||||
|
||||
if (int.TryParse(e.NewValue, out var intVal))
|
||||
value = intVal;
|
||||
|
||||
current.Value = value;
|
||||
});
|
||||
|
||||
Current.BindValueChanged(e =>
|
||||
{
|
||||
numberBox.Current.Value = e.NewValue?.ToString();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class OutlinedNumberBox : OutlinedTextBox
|
||||
{
|
||||
protected override bool CanAddCharacter(char character) => char.IsNumber(character);
|
||||
}
|
||||
|
@ -1,60 +1,17 @@
|
||||
// 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.Allocation;
|
||||
using osu.Framework.Extensions.Color4Extensions;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osuTK.Graphics;
|
||||
|
||||
namespace osu.Game.Overlays.Settings
|
||||
{
|
||||
public class SettingsTextBox : SettingsItem<string>
|
||||
{
|
||||
protected override Drawable CreateControl() => new TextBox
|
||||
protected override Drawable CreateControl() => new OutlinedTextBox
|
||||
{
|
||||
Margin = new MarginPadding { Top = 5 },
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true,
|
||||
CommitOnFocusLost = true
|
||||
};
|
||||
|
||||
public class TextBox : OsuTextBox
|
||||
{
|
||||
private const float border_thickness = 3;
|
||||
|
||||
private Color4 borderColourFocused;
|
||||
private Color4 borderColourUnfocused;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuColour colour)
|
||||
{
|
||||
borderColourUnfocused = colour.Gray4.Opacity(0.5f);
|
||||
borderColourFocused = BorderColour;
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
protected override void OnFocus(FocusEvent e)
|
||||
{
|
||||
base.OnFocus(e);
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
protected override void OnFocusLost(FocusLostEvent e)
|
||||
{
|
||||
base.OnFocusLost(e);
|
||||
|
||||
updateBorder();
|
||||
}
|
||||
|
||||
private void updateBorder()
|
||||
{
|
||||
BorderThickness = border_thickness;
|
||||
BorderColour = HasFocus ? borderColourFocused : borderColourUnfocused;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -159,28 +159,6 @@ namespace osu.Game.Overlays.Toolbar
|
||||
};
|
||||
}
|
||||
|
||||
private RealmKeyBinding realmKeyBinding;
|
||||
|
||||
protected override void LoadComplete()
|
||||
{
|
||||
base.LoadComplete();
|
||||
|
||||
if (Hotkey == null) return;
|
||||
|
||||
realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
realmKeyBinding.PropertyChanged += (sender, args) =>
|
||||
{
|
||||
if (args.PropertyName == nameof(realmKeyBinding.KeyCombinationString))
|
||||
updateKeyBindingTooltip();
|
||||
};
|
||||
}
|
||||
|
||||
updateKeyBindingTooltip();
|
||||
}
|
||||
|
||||
protected override bool OnMouseDown(MouseDownEvent e) => true;
|
||||
|
||||
protected override bool OnClick(ClickEvent e)
|
||||
@ -196,6 +174,7 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
HoverBackground.FadeIn(200);
|
||||
tooltipContainer.FadeIn(100);
|
||||
|
||||
return base.OnHover(e);
|
||||
}
|
||||
|
||||
@ -222,6 +201,10 @@ namespace osu.Game.Overlays.Toolbar
|
||||
|
||||
private void updateKeyBindingTooltip()
|
||||
{
|
||||
if (Hotkey == null) return;
|
||||
|
||||
var realmKeyBinding = realmFactory.Context.All<RealmKeyBinding>().FirstOrDefault(rkb => rkb.RulesetID == null && rkb.ActionInt == (int)Hotkey.Value);
|
||||
|
||||
if (realmKeyBinding != null)
|
||||
{
|
||||
var keyBindingString = realmKeyBinding.KeyCombination.ReadableString();
|
||||
|
@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Edit
|
||||
// Audio
|
||||
new CheckAudioPresence(),
|
||||
new CheckAudioQuality(),
|
||||
new CheckMutedObjects(),
|
||||
new CheckFewHitsounds(),
|
||||
|
||||
// Compose
|
||||
new CheckUnsnappedObjects(),
|
||||
|
164
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
164
osu.Game/Rulesets/Edit/Checks/CheckFewHitsounds.cs
Normal file
@ -0,0 +1,164 @@
|
||||
// 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 osu.Game.Audio;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckFewHitsounds : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// 2 measures (4/4) of 120 BPM, typically makes up a few patterns in the map.
|
||||
/// This is almost always ok, but can still be useful for the mapper to make sure hitsounding coverage is good.
|
||||
/// </summary>
|
||||
private const int negligible_threshold_time = 4000;
|
||||
|
||||
/// <summary>
|
||||
/// 4 measures (4/4) of 120 BPM, typically makes up a large portion of a section in the song.
|
||||
/// This is ok if the section is a quiet intro, for example.
|
||||
/// </summary>
|
||||
private const int warning_threshold_time = 8000;
|
||||
|
||||
/// <summary>
|
||||
/// 12 measures (4/4) of 120 BPM, typically makes up multiple sections in the song.
|
||||
/// </summary>
|
||||
private const int problem_threshold_time = 24000;
|
||||
|
||||
// Should pass at least this many objects without hitsounds to be considered an issue (should work for Easy diffs too).
|
||||
private const int warning_threshold_objects = 4;
|
||||
private const int problem_threshold_objects = 16;
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Few or no hitsounds");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateLongPeriodProblem(this),
|
||||
new IssueTemplateLongPeriodWarning(this),
|
||||
new IssueTemplateLongPeriodNegligible(this),
|
||||
new IssueTemplateNoHitsounds(this)
|
||||
};
|
||||
|
||||
private bool mapHasHitsounds;
|
||||
private int objectsWithoutHitsounds;
|
||||
private double lastHitsoundTime;
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
if (!context.Beatmap.HitObjects.Any())
|
||||
yield break;
|
||||
|
||||
mapHasHitsounds = false;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = context.Beatmap.HitObjects.First().StartTime;
|
||||
|
||||
var hitObjectsIncludingNested = new List<HitObject>();
|
||||
|
||||
foreach (var hitObject in context.Beatmap.HitObjects)
|
||||
{
|
||||
// Samples play on the end of objects. Some objects have nested objects to accomplish playing them elsewhere (e.g. slider head/repeat).
|
||||
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
||||
hitObjectsIncludingNested.Add(nestedHitObject);
|
||||
|
||||
hitObjectsIncludingNested.Add(hitObject);
|
||||
}
|
||||
|
||||
var hitObjectsByEndTime = hitObjectsIncludingNested.OrderBy(o => o.GetEndTime()).ToList();
|
||||
var hitObjectCount = hitObjectsByEndTime.Count;
|
||||
|
||||
for (int i = 0; i < hitObjectCount; ++i)
|
||||
{
|
||||
var hitObject = hitObjectsByEndTime[i];
|
||||
|
||||
// This is used to perform an update at the end so that the period after the last hitsounded object can be an issue.
|
||||
bool isLastObject = i == hitObjectCount - 1;
|
||||
|
||||
foreach (var issue in applyHitsoundUpdate(hitObject, isLastObject))
|
||||
yield return issue;
|
||||
}
|
||||
|
||||
if (!mapHasHitsounds)
|
||||
yield return new IssueTemplateNoHitsounds(this).Create();
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> applyHitsoundUpdate(HitObject hitObject, bool isLastObject = false)
|
||||
{
|
||||
var time = hitObject.GetEndTime();
|
||||
bool hasHitsound = hitObject.Samples.Any(isHitsound);
|
||||
bool couldHaveHitsound = hitObject.Samples.Any(isHitnormal);
|
||||
|
||||
// Only generating issues on hitsounded or last objects ensures we get one issue per long period.
|
||||
// If there are no hitsounds we let the "No hitsounds" template take precedence.
|
||||
if (hasHitsound || (isLastObject && mapHasHitsounds))
|
||||
{
|
||||
var timeWithoutHitsounds = time - lastHitsoundTime;
|
||||
|
||||
if (timeWithoutHitsounds > problem_threshold_time && objectsWithoutHitsounds > problem_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodProblem(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
else if (timeWithoutHitsounds > warning_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodWarning(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
else if (timeWithoutHitsounds > negligible_threshold_time && objectsWithoutHitsounds > warning_threshold_objects)
|
||||
yield return new IssueTemplateLongPeriodNegligible(this).Create(lastHitsoundTime, timeWithoutHitsounds);
|
||||
}
|
||||
|
||||
if (hasHitsound)
|
||||
{
|
||||
mapHasHitsounds = true;
|
||||
objectsWithoutHitsounds = 0;
|
||||
lastHitsoundTime = time;
|
||||
}
|
||||
else if (couldHaveHitsound)
|
||||
++objectsWithoutHitsounds;
|
||||
}
|
||||
|
||||
private bool isHitsound(HitSampleInfo sample) => HitSampleInfo.AllAdditions.Any(sample.Name.Contains);
|
||||
private bool isHitnormal(HitSampleInfo sample) => sample.Name.Contains(HitSampleInfo.HIT_NORMAL);
|
||||
|
||||
public abstract class IssueTemplateLongPeriod : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateLongPeriod(ICheck check, IssueType type)
|
||||
: base(check, type, "Long period without hitsounds ({0:F1} seconds).")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(double time, double duration) => new Issue(this, duration / 1000f) { Time = time };
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodProblem : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodProblem(ICheck check)
|
||||
: base(check, IssueType.Problem)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodWarning : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodWarning(ICheck check)
|
||||
: base(check, IssueType.Warning)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLongPeriodNegligible : IssueTemplateLongPeriod
|
||||
{
|
||||
public IssueTemplateLongPeriodNegligible(ICheck check)
|
||||
: base(check, IssueType.Negligible)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateNoHitsounds : IssueTemplate
|
||||
{
|
||||
public IssueTemplateNoHitsounds(ICheck check)
|
||||
: base(check, IssueType.Problem, "There are no hitsounds.")
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create() => new Issue(this);
|
||||
}
|
||||
}
|
||||
}
|
158
osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
Normal file
158
osu.Game/Rulesets/Edit/Checks/CheckMutedObjects.cs
Normal file
@ -0,0 +1,158 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Utils;
|
||||
using osu.Game.Rulesets.Edit.Checks.Components;
|
||||
using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
|
||||
namespace osu.Game.Rulesets.Edit.Checks
|
||||
{
|
||||
public class CheckMutedObjects : ICheck
|
||||
{
|
||||
/// <summary>
|
||||
/// Volume percentages lower than or equal to this are typically inaudible.
|
||||
/// </summary>
|
||||
private const int muted_threshold = 5;
|
||||
|
||||
/// <summary>
|
||||
/// Volume percentages lower than or equal to this can sometimes be inaudible depending on sample used and music volume.
|
||||
/// </summary>
|
||||
private const int low_volume_threshold = 20;
|
||||
|
||||
private enum EdgeType
|
||||
{
|
||||
Head,
|
||||
Repeat,
|
||||
Tail,
|
||||
None
|
||||
}
|
||||
|
||||
public CheckMetadata Metadata { get; } = new CheckMetadata(CheckCategory.Audio, "Low volume hitobjects");
|
||||
|
||||
public IEnumerable<IssueTemplate> PossibleTemplates => new IssueTemplate[]
|
||||
{
|
||||
new IssueTemplateMutedActive(this),
|
||||
new IssueTemplateLowVolumeActive(this),
|
||||
new IssueTemplateMutedPassive(this)
|
||||
};
|
||||
|
||||
public IEnumerable<Issue> Run(BeatmapVerifierContext context)
|
||||
{
|
||||
foreach (var hitObject in context.Beatmap.HitObjects)
|
||||
{
|
||||
// Worth keeping in mind: The samples of an object always play at its end time.
|
||||
// Objects like spinners have no sound at its start because of this, while hold notes have nested objects to accomplish this.
|
||||
foreach (var nestedHitObject in hitObject.NestedHitObjects)
|
||||
{
|
||||
foreach (var issue in getVolumeIssues(hitObject, nestedHitObject))
|
||||
yield return issue;
|
||||
}
|
||||
|
||||
foreach (var issue in getVolumeIssues(hitObject))
|
||||
yield return issue;
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<Issue> getVolumeIssues(HitObject hitObject, HitObject sampledHitObject = null)
|
||||
{
|
||||
sampledHitObject ??= hitObject;
|
||||
if (!sampledHitObject.Samples.Any())
|
||||
yield break;
|
||||
|
||||
// Samples that allow themselves to be overridden by control points have a volume of 0.
|
||||
int maxVolume = sampledHitObject.Samples.Max(sample => sample.Volume > 0 ? sample.Volume : sampledHitObject.SampleControlPoint.SampleVolume);
|
||||
double samplePlayTime = sampledHitObject.GetEndTime();
|
||||
|
||||
EdgeType edgeType = getEdgeAtTime(hitObject, samplePlayTime);
|
||||
// We only care about samples played on the edges of objects, not ones like spinnerspin or slidertick.
|
||||
if (edgeType == EdgeType.None)
|
||||
yield break;
|
||||
|
||||
string postfix = hitObject is IHasDuration ? edgeType.ToString().ToLower() : null;
|
||||
|
||||
if (maxVolume <= muted_threshold)
|
||||
{
|
||||
if (edgeType == EdgeType.Head)
|
||||
yield return new IssueTemplateMutedActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
else
|
||||
yield return new IssueTemplateMutedPassive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
}
|
||||
else if (maxVolume <= low_volume_threshold && edgeType == EdgeType.Head)
|
||||
{
|
||||
yield return new IssueTemplateLowVolumeActive(this).Create(hitObject, maxVolume / 100f, sampledHitObject.GetEndTime(), postfix);
|
||||
}
|
||||
}
|
||||
|
||||
private EdgeType getEdgeAtTime(HitObject hitObject, double time)
|
||||
{
|
||||
if (Precision.AlmostEquals(time, hitObject.StartTime, 1f))
|
||||
return EdgeType.Head;
|
||||
if (Precision.AlmostEquals(time, hitObject.GetEndTime(), 1f))
|
||||
return EdgeType.Tail;
|
||||
|
||||
if (hitObject is IHasRepeats hasRepeats)
|
||||
{
|
||||
double spanDuration = hasRepeats.Duration / hasRepeats.SpanCount();
|
||||
if (spanDuration <= 0)
|
||||
// Prevents undefined behaviour in cases like where zero/negative-length sliders/hold notes exist.
|
||||
return EdgeType.None;
|
||||
|
||||
double spans = (time - hitObject.StartTime) / spanDuration;
|
||||
double acceptableDifference = 1 / spanDuration; // 1 ms of acceptable difference, as with head/tail above.
|
||||
|
||||
if (Precision.AlmostEquals(spans, Math.Ceiling(spans), acceptableDifference) ||
|
||||
Precision.AlmostEquals(spans, Math.Floor(spans), acceptableDifference))
|
||||
{
|
||||
return EdgeType.Repeat;
|
||||
}
|
||||
}
|
||||
|
||||
return EdgeType.None;
|
||||
}
|
||||
|
||||
public abstract class IssueTemplateMuted : IssueTemplate
|
||||
{
|
||||
protected IssueTemplateMuted(ICheck check, IssueType type, string unformattedMessage)
|
||||
: base(check, type, unformattedMessage)
|
||||
{
|
||||
}
|
||||
|
||||
public Issue Create(HitObject hitobject, double volume, double time, string postfix = "")
|
||||
{
|
||||
string objectName = hitobject.GetType().Name;
|
||||
if (!string.IsNullOrEmpty(postfix))
|
||||
objectName += " " + postfix;
|
||||
|
||||
return new Issue(hitobject, this, objectName, volume) { Time = time };
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateMutedActive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateMutedActive(ICheck check)
|
||||
: base(check, IssueType.Problem, "{0} has a volume of {1:0%}. Clickable objects must have clearly audible feedback.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateLowVolumeActive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateLowVolumeActive(ICheck check)
|
||||
: base(check, IssueType.Warning, "{0} has a volume of {1:0%}, ensure this is audible.")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public class IssueTemplateMutedPassive : IssueTemplateMuted
|
||||
{
|
||||
public IssueTemplateMutedPassive(ICheck check)
|
||||
: base(check, IssueType.Negligible, "{0} has a volume of {1:0%}, ensure there is no distinct sound here in the song if inaudible.")
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
18
osu.Game/Rulesets/Mods/IApplicableToBeatmapProcessor.cs
Normal file
@ -0,0 +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 osu.Game.Beatmaps;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// Interface for a <see cref="Mod"/> that applies changes to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
public interface IApplicableToBeatmapProcessor : IApplicableMod
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies this <see cref="Mod"/> to a <see cref="BeatmapProcessor"/>.
|
||||
/// </summary>
|
||||
void ApplyToBeatmapProcessor(IBeatmapProcessor beatmapProcessor);
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Mods
|
||||
public override IconUsage? Icon => OsuIcon.Dice;
|
||||
public override double ScoreMultiplier => 1;
|
||||
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SeedSettingsControl))]
|
||||
[SettingSource("Seed", "Use a custom seed instead of a random one", SettingControlType = typeof(SettingsNumberBox))]
|
||||
public Bindable<int?> Seed { get; } = new Bindable<int?>
|
||||
{
|
||||
Default = null,
|
||||
|
@ -1,92 +0,0 @@
|
||||
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
||||
// See the LICENCE file in the repository root for full licence text.
|
||||
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.UserInterface;
|
||||
using osu.Game.Graphics.UserInterface;
|
||||
using osu.Game.Overlays.Settings;
|
||||
|
||||
namespace osu.Game.Rulesets.Mods
|
||||
{
|
||||
/// <summary>
|
||||
/// A settings control for use by <see cref="IHasSeed"/> mods which have a customisable seed value.
|
||||
/// </summary>
|
||||
public class SeedSettingsControl : SettingsItem<int?>
|
||||
{
|
||||
protected override Drawable CreateControl() => new SeedControl
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
Margin = new MarginPadding { Top = 5 }
|
||||
};
|
||||
|
||||
private sealed class SeedControl : CompositeDrawable, IHasCurrentValue<int?>
|
||||
{
|
||||
private readonly BindableWithCurrent<int?> current = new BindableWithCurrent<int?>();
|
||||
|
||||
public Bindable<int?> Current
|
||||
{
|
||||
get => current;
|
||||
set
|
||||
{
|
||||
current.Current = value;
|
||||
seedNumberBox.Text = value.Value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
private readonly OsuNumberBox seedNumberBox;
|
||||
|
||||
public SeedControl()
|
||||
{
|
||||
AutoSizeAxes = Axes.Y;
|
||||
|
||||
InternalChildren = new[]
|
||||
{
|
||||
new GridContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
AutoSizeAxes = Axes.Y,
|
||||
ColumnDimensions = new[]
|
||||
{
|
||||
new Dimension(),
|
||||
new Dimension(GridSizeMode.Absolute, 2),
|
||||
new Dimension(GridSizeMode.Relative, 0.25f)
|
||||
},
|
||||
RowDimensions = new[]
|
||||
{
|
||||
new Dimension(GridSizeMode.AutoSize)
|
||||
},
|
||||
Content = new[]
|
||||
{
|
||||
new Drawable[]
|
||||
{
|
||||
seedNumberBox = new OsuNumberBox
|
||||
{
|
||||
RelativeSizeAxes = Axes.X,
|
||||
CommitOnFocusLost = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
seedNumberBox.Current.BindValueChanged(e =>
|
||||
{
|
||||
int? value = null;
|
||||
|
||||
if (int.TryParse(e.NewValue, out var intVal))
|
||||
value = intVal;
|
||||
|
||||
current.Value = value;
|
||||
});
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
if (current.Value == null)
|
||||
seedNumberBox.Text = current.Current.Value.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -13,6 +13,7 @@ using osu.Game.Graphics.Sprites;
|
||||
using osu.Game.Rulesets.Mods;
|
||||
using osuTK;
|
||||
using osu.Framework.Bindables;
|
||||
using osu.Framework.Localisation;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -29,7 +30,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private const float size = 80;
|
||||
|
||||
public virtual string TooltipText => showTooltip ? mod.IconTooltip : null;
|
||||
public virtual LocalisableString TooltipText => showTooltip ? mod.IconTooltip : null;
|
||||
|
||||
private Mod mod;
|
||||
private readonly bool showTooltip;
|
||||
|
@ -46,7 +46,7 @@ namespace osu.Game.Scoring
|
||||
[JsonIgnore]
|
||||
public int Combo { get; set; } // Todo: Shouldn't exist in here
|
||||
|
||||
[JsonIgnore]
|
||||
[JsonProperty("ruleset_id")]
|
||||
public int RulesetID { get; set; }
|
||||
|
||||
[JsonProperty("passed")]
|
||||
|
@ -7,6 +7,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using osu.Framework.Bindables;
|
||||
@ -72,6 +73,9 @@ namespace osu.Game.Scoring
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task Populate(ScoreInfo model, ArchiveReader archive, CancellationToken cancellationToken = default)
|
||||
=> Task.CompletedTask;
|
||||
|
||||
protected override void ExportModelTo(ScoreInfo model, Stream outputStream)
|
||||
{
|
||||
var file = model.Files.SingleOrDefault();
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Sprites;
|
||||
using osu.Framework.Input.Events;
|
||||
using osu.Framework.Localisation;
|
||||
using osuTK;
|
||||
using osuTK.Graphics;
|
||||
|
||||
@ -58,6 +59,6 @@ namespace osu.Game.Screens.Edit.Compose.Components
|
||||
icon.FadeColour(!IsHeld && IsHovered ? Color4.White : Color4.Black, TRANSFORM_DURATION, Easing.OutQuint);
|
||||
}
|
||||
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
}
|
||||
}
|
||||
|
@ -261,7 +261,7 @@ namespace osu.Game.Screens.Menu
|
||||
switch (state)
|
||||
{
|
||||
default:
|
||||
return true;
|
||||
return false;
|
||||
|
||||
case ButtonSystemState.Initial:
|
||||
State = ButtonSystemState.TopLevel;
|
||||
|
@ -7,6 +7,7 @@ using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Cursor;
|
||||
using osu.Framework.Graphics.Shapes;
|
||||
using osu.Framework.Localisation;
|
||||
using osu.Game.Graphics;
|
||||
using osu.Game.Online.Rooms;
|
||||
|
||||
@ -16,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Components
|
||||
{
|
||||
private readonly GameType type;
|
||||
|
||||
public string TooltipText => type.Name;
|
||||
public LocalisableString TooltipText => type.Name;
|
||||
|
||||
public DrawableGameType(GameType type)
|
||||
{
|
||||
|
@ -32,7 +32,6 @@ namespace osu.Game.Screens.OnlinePlay
|
||||
{
|
||||
IsValidMod = m => true;
|
||||
|
||||
MultiplierSection.Alpha = 0;
|
||||
DeselectAllButton.Alpha = 0;
|
||||
|
||||
Drawable selectAllButton;
|
||||
|
@ -431,7 +431,7 @@ namespace osu.Game.Screens.Select
|
||||
|
||||
public class InfoLabel : Container, IHasTooltip
|
||||
{
|
||||
public string TooltipText { get; }
|
||||
public LocalisableString TooltipText { get; }
|
||||
|
||||
public InfoLabel(BeatmapStatistic statistic)
|
||||
{
|
||||
|
@ -42,6 +42,7 @@ namespace osu.Game.Screens.Select.Carousel
|
||||
match &= !criteria.ApproachRate.HasFilter || criteria.ApproachRate.IsInRange(Beatmap.BaseDifficulty.ApproachRate);
|
||||
match &= !criteria.DrainRate.HasFilter || criteria.DrainRate.IsInRange(Beatmap.BaseDifficulty.DrainRate);
|
||||
match &= !criteria.CircleSize.HasFilter || criteria.CircleSize.IsInRange(Beatmap.BaseDifficulty.CircleSize);
|
||||
match &= !criteria.OverallDifficulty.HasFilter || criteria.OverallDifficulty.IsInRange(Beatmap.BaseDifficulty.OverallDifficulty);
|
||||
match &= !criteria.Length.HasFilter || criteria.Length.IsInRange(Beatmap.Length);
|
||||
match &= !criteria.BPM.HasFilter || criteria.BPM.IsInRange(Beatmap.BPM);
|
||||
|
||||
|
@ -24,6 +24,7 @@ namespace osu.Game.Screens.Select
|
||||
public OptionalRange<float> ApproachRate;
|
||||
public OptionalRange<float> DrainRate;
|
||||
public OptionalRange<float> CircleSize;
|
||||
public OptionalRange<float> OverallDifficulty;
|
||||
public OptionalRange<double> Length;
|
||||
public OptionalRange<double> BPM;
|
||||
public OptionalRange<int> BeatDivisor;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user