1
0
mirror of https://github.com/ppy/osu.git synced 2025-01-21 19:43:22 +08:00

Merge branch 'master' into skin-editor-import-at-cursor

This commit is contained in:
Dan Balasescu 2022-04-05 17:39:11 +09:00 committed by GitHub
commit 7623f3b90b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 882 additions and 185 deletions

View File

@ -9,7 +9,7 @@ body:
Important to note that your issue may have already been reported before. Please check: Important to note that your issue may have already been reported before. Please check:
- Pinned issues, at the top of https://github.com/ppy/osu/issues. - Pinned issues, at the top of https://github.com/ppy/osu/issues.
- Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0). - Current open `priority:0` issues, filterable [here](https://github.com/ppy/osu/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3Apriority%3A0).
- And most importantly, search for your issue. If you find that it already exists, respond with a reaction or add any further information that may be helpful. - And most importantly, search for your issue both in the [issue listing](https://github.com/ppy/osu/issues) and the [Q&A discussion listing](https://github.com/ppy/osu/discussions/categories/q-a). If you find that it already exists, respond with a reaction or add any further information that may be helpful.
- type: dropdown - type: dropdown
attributes: attributes:
@ -48,20 +48,28 @@ body:
Attaching log files is required for every reported bug. See instructions below on how to find them. Attaching log files is required for every reported bug. See instructions below on how to find them.
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead.
### Desktop platforms
If the game has not yet been closed since you found the bug: If the game has not yet been closed since you found the bug:
1. Head on to game settings and click on "Open osu! folder" 1. Head on to game settings and click on "Open osu! folder"
2. Then open the `logs` folder located there 2. Then open the `logs` folder located there
**Logs are reset when you reopen the game.** If the game crashed or has been closed since you found the bug, retrieve the logs using the file explorer instead. The default places to find the logs on desktop platforms are as follows:
The default places to find the logs are as follows:
- `%AppData%/osu/logs` *on Windows* - `%AppData%/osu/logs` *on Windows*
- `~/.local/share/osu/logs` *on Linux & macOS* - `~/.local/share/osu/logs` *on Linux & macOS*
- `Android/data/sh.ppy.osulazer/files/logs` *on Android*
- *On iOS*, they can be obtained by connecting your device to your desktop and copying the `logs` directory from the app's own document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
If you have selected a custom location for the game files, you can find the `logs` folder there. If you have selected a custom location for the game files, you can find the `logs` folder there.
### Mobile platforms
The places to find the logs on mobile platforms are as follows:
- *On Android*, navigate to `Android/data/sh.ppy.osulazer/files/logs` using a file browser app.
- *On iOS*, connect your device to a PC and copy the `logs` directory from the app's document storage using iTunes. (https://support.apple.com/en-us/HT201301#copy-to-computer)
---
After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below. After locating the `logs` folder, select all log files inside and drag them into the "Logs" box below.
- type: textarea - type: textarea

View File

@ -51,8 +51,8 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2022.404.0" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Transitive Dependencies"> <ItemGroup Label="Transitive Dependencies">
<!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. --> <!-- Realm needs to be directly referenced in all Xamarin projects, as it will not pull in its transitive dependencies otherwise. -->

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; protected override string ResourceAssembly => "osu.Game.Rulesets.Catch";
[TestCase(4.0505463516206195d, "diffcalc-test")] [TestCase(4.0505463516206195d, 127, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(5.1696411260785498d, "diffcalc-test")] [TestCase(5.1696411260785498d, 127, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new CatchModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new CatchModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset().RulesetInfo, beatmap);

View File

@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Mania.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; protected override string ResourceAssembly => "osu.Game.Rulesets.Mania";
[TestCase(2.3449735700206298d, "diffcalc-test")] [TestCase(2.3449735700206298d, 151, "diffcalc-test")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(2.7879104989252959d, "diffcalc-test")] [TestCase(2.7879104989252959d, 151, "diffcalc-test")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new ManiaModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new ManiaModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset().RulesetInfo, beatmap);

View File

@ -6,18 +6,18 @@ using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods namespace osu.Game.Rulesets.Osu.Tests.Mods
{ {
public class TestSceneOsuModAimAssist : OsuModTestScene public class TestSceneOsuModMagnetised : OsuModTestScene
{ {
[TestCase(0.1f)] [TestCase(0.1f)]
[TestCase(0.5f)] [TestCase(0.5f)]
[TestCase(1)] [TestCase(1)]
public void TestAimAssist(float strength) public void TestMagnetised(float strength)
{ {
CreateModTest(new ModTestData CreateModTest(new ModTestData
{ {
Mod = new OsuModAimAssist Mod = new OsuModMagnetised
{ {
AssistStrength = { Value = strength }, AttractionStrength = { Value = strength },
}, },
PassCondition = () => true, PassCondition = () => true,
Autoplay = false, Autoplay = false,

View File

@ -15,15 +15,20 @@ namespace osu.Game.Rulesets.Osu.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; protected override string ResourceAssembly => "osu.Game.Rulesets.Osu";
[TestCase(6.6972307565739273d, "diffcalc-test")] [TestCase(6.6972307565739273d, 206, "diffcalc-test")]
[TestCase(1.4484754139145539d, "zero-length-sliders")] [TestCase(1.4484754139145539d, 45, "zero-length-sliders")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(8.9382559208689809d, "diffcalc-test")] [TestCase(8.9382559208689809d, 206, "diffcalc-test")]
[TestCase(1.7548875851757628d, "zero-length-sliders")] [TestCase(1.7548875851757628d, 45, "zero-length-sliders")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new OsuModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new OsuModDoubleTime());
[TestCase(6.6972307218715166d, 239, "diffcalc-test")]
[TestCase(1.4484754139145537d, 54, "zero-length-sliders")]
public void TestClassicMod(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expectedStarRating, expectedMaxCombo, name, new OsuModClassic());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset().RulesetInfo, beatmap);

View File

@ -61,10 +61,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty
double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate; double preempt = IBeatmapDifficultyInfo.DifficultyRange(beatmap.Difficulty.ApproachRate, 1800, 1200, 450) / clockRate;
double drainRate = beatmap.Difficulty.DrainRate; double drainRate = beatmap.Difficulty.DrainRate;
int maxCombo = beatmap.GetMaxCombo();
int maxCombo = beatmap.HitObjects.Count;
// Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above)
maxCombo += beatmap.HitObjects.OfType<Slider>().Sum(s => s.NestedHitObjects.Count - 1);
int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle);
int sliderCount = beatmap.HitObjects.Count(h => h is Slider); int sliderCount = beatmap.HitObjects.Count(h => h is Slider);

View File

@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Automation; public override ModType Type => ModType.Automation;
public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override string Description => @"Automatic cursor movement - just follow the rhythm.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay), typeof(OsuModMagnetised) };
public bool PerformFail() => false; public bool PerformFail() => false;

View File

@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModAutoplay : ModAutoplay public class OsuModAutoplay : ModAutoplay
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{ {
public class OsuModCinema : ModCinema<OsuHitObject> public class OsuModCinema : ModCinema<OsuHitObject>
{ {
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModMagnetised), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods) public override ModReplayData CreateReplayData(IBeatmap beatmap, IReadOnlyList<Mod> mods)
=> new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" }); => new ModReplayData(new OsuAutoGenerator(beatmap, mods).Generate(), new ModCreatedUser { Username = "Autoplay" });

View File

@ -16,20 +16,20 @@ using osuTK;
namespace osu.Game.Rulesets.Osu.Mods namespace osu.Game.Rulesets.Osu.Mods
{ {
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject> internal class OsuModMagnetised : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{ {
public override string Name => "Aim Assist"; public override string Name => "Magnetised";
public override string Acronym => "AA"; public override string Acronym => "MG";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer; public override IconUsage? Icon => FontAwesome.Solid.Magnet;
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!"; public override string Description => "No need to chase the circles your cursor is a magnet!";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay), typeof(OsuModRelax) };
private IFrameStableClock gameplayClock; private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)] [SettingSource("Attraction strength", "How strong the pull is.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f) public BindableFloat AttractionStrength { get; } = new BindableFloat(0.5f)
{ {
Precision = 0.05f, Precision = 0.05f,
MinValue = 0.05f, MinValue = 0.05f,
@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Mods
private void easeTo(DrawableHitObject hitObject, Vector2 destination) private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{ {
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value); double dampLength = Interpolation.Lerp(3000, 40, AttractionStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime); float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime); float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);

View File

@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer public class OsuModRelax : ModRelax, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{ {
public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things."; public override string Description => @"You don't need to click. Give your clicking/tapping fingers a break from the heat of things.";
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModAimAssist) }).ToArray(); public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAutopilot), typeof(OsuModMagnetised) }).ToArray();
/// <summary> /// <summary>
/// How early before a hitobject's start time to trigger a hit. /// How early before a hitobject's start time to trigger a hit.

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "Everything rotates. EVERYTHING."; public override string Description => "Everything rotates. EVERYTHING.";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModWiggle), typeof(OsuModMagnetised) };
private float theta; private float theta;

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods
public override ModType Type => ModType.Fun; public override ModType Type => ModType.Fun;
public override string Description => "They just won't stay still..."; public override string Description => "They just won't stay still...";
public override double ScoreMultiplier => 1; public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModAimAssist) }; public override Type[] IncompatibleMods => new[] { typeof(OsuModTransform), typeof(OsuModMagnetised) };
private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles
private const int wiggle_strength = 10; // Higher = stronger wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles

View File

@ -195,7 +195,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(), new OsuModApproachDifferent(),
new OsuModMuted(), new OsuModMuted(),
new OsuModNoScope(), new OsuModNoScope(),
new OsuModAimAssist(), new OsuModMagnetised(),
new ModAdaptiveSpeed() new ModAdaptiveSpeed()
}; };

View File

@ -14,15 +14,15 @@ namespace osu.Game.Rulesets.Taiko.Tests
{ {
protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko";
[TestCase(2.2420075288523802d, "diffcalc-test")] [TestCase(2.2420075288523802d, 200, "diffcalc-test")]
[TestCase(2.2420075288523802d, "diffcalc-test-strong")] [TestCase(2.2420075288523802d, 200, "diffcalc-test-strong")]
public void Test(double expected, string name) public void Test(double expectedStarRating, int expectedMaxCombo, string name)
=> base.Test(expected, name); => base.Test(expectedStarRating, expectedMaxCombo, name);
[TestCase(3.134084469440479d, "diffcalc-test")] [TestCase(3.134084469440479d, 200, "diffcalc-test")]
[TestCase(3.134084469440479d, "diffcalc-test-strong")] [TestCase(3.134084469440479d, 200, "diffcalc-test-strong")]
public void TestClockRateAdjusted(double expected, string name) public void TestClockRateAdjusted(double expectedStarRating, int expectedMaxCombo, string name)
=> Test(expected, name, new TaikoModDoubleTime()); => Test(expectedStarRating, expectedMaxCombo, name, new TaikoModDoubleTime());
protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap); protected override DifficultyCalculator CreateDifficultyCalculator(IWorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset().RulesetInfo, beatmap);

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary> /// <summary>
/// Default size of a drawable taiko hit object. /// Default size of a drawable taiko hit object.
/// </summary> /// </summary>
public const float DEFAULT_SIZE = 0.45f; public const float DEFAULT_SIZE = 0.475f;
public override Judgement CreateJudgement() => new TaikoJudgement(); public override Judgement CreateJudgement() => new TaikoJudgement();

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Objects
/// <summary> /// <summary>
/// Scale multiplier for a strong drawable taiko hit object. /// Scale multiplier for a strong drawable taiko hit object.
/// </summary> /// </summary>
public const float STRONG_SCALE = 1.4f; public const float STRONG_SCALE = 1 / 0.65f;
/// <summary> /// <summary>
/// Default size of a strong drawable taiko hit object. /// Default size of a strong drawable taiko hit object.

View File

@ -11,6 +11,7 @@ using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Backgrounds;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Rulesets.Taiko.Objects;
using osuTK.Graphics; using osuTK.Graphics;
namespace osu.Game.Rulesets.Taiko.Skinning.Default namespace osu.Game.Rulesets.Taiko.Skinning.Default
@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Default
/// </summary> /// </summary>
public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour public abstract class CirclePiece : BeatSyncedContainer, IHasAccentColour
{ {
public const float SYMBOL_SIZE = 0.45f; public const float SYMBOL_SIZE = TaikoHitObject.DEFAULT_SIZE;
public const float SYMBOL_BORDER = 8; public const float SYMBOL_BORDER = 8;
private const double pre_beat_transition_time = 80; private const double pre_beat_transition_time = 80;
private Color4 accentColour; private Color4 accentColour;

View File

@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("approachcircle"), Texture = skin.GetTexture("approachcircle"),
Scale = new Vector2(0.73f), Scale = new Vector2(0.83f),
Alpha = 0.47f, // eyeballed to match stable Alpha = 0.47f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy
new Sprite new Sprite
{ {
Texture = skin.GetTexture("taikobigcircle"), Texture = skin.GetTexture("taikobigcircle"),
Scale = new Vector2(0.7f), Scale = new Vector2(0.8f),
Alpha = 0.22f, // eyeballed to match stable Alpha = 0.22f, // eyeballed to match stable
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,

View File

@ -2,10 +2,13 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Game.Configuration;
using osu.Game.Overlays.Toolbar; using osu.Game.Overlays.Toolbar;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
@ -15,7 +18,10 @@ namespace osu.Game.Tests.Visual.Menus
[TestFixture] [TestFixture]
public class TestSceneToolbarClock : OsuManualInputManagerTestScene public class TestSceneToolbarClock : OsuManualInputManagerTestScene
{ {
private Bindable<ToolbarClockDisplayMode> clockDisplayMode;
private readonly Container mainContainer; private readonly Container mainContainer;
private readonly ToolbarClock toolbarClock;
public TestSceneToolbarClock() public TestSceneToolbarClock()
{ {
@ -49,7 +55,7 @@ namespace osu.Game.Tests.Visual.Menus
RelativeSizeAxes = Axes.Y, RelativeSizeAxes = Axes.Y,
Width = 2, Width = 2,
}, },
new ToolbarClock(), toolbarClock = new ToolbarClock(),
new Box new Box
{ {
Colour = Color4.DarkRed, Colour = Color4.DarkRed,
@ -65,6 +71,12 @@ namespace osu.Game.Tests.Visual.Menus
AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale)); AddSliderStep("scale", 0.5, 4, 1, scale => mainContainer.Scale = new Vector2((float)scale));
} }
[BackgroundDependencyLoader]
private void load(OsuConfigManager config)
{
clockDisplayMode = config.GetBindable<ToolbarClockDisplayMode>(OsuSetting.ToolbarClockDisplayMode);
}
[Test] [Test]
public void TestRealGameTime() public void TestRealGameTime()
{ {
@ -76,5 +88,20 @@ namespace osu.Game.Tests.Visual.Menus
{ {
AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 }); AddStep("Set game time long", () => mainContainer.Clock = new FramedOffsetClock(Clock, false) { Offset = 3600.0 * 24 * 1000 * 98 });
} }
[Test]
public void TestDisplayModeChange()
{
AddStep("Set clock display mode", () => clockDisplayMode.Value = ToolbarClockDisplayMode.Full);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital with runtime", () => clockDisplayMode.Value == ToolbarClockDisplayMode.DigitalWithRuntime);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is digital", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Digital);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is analog", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Analog);
AddStep("Trigger click", () => toolbarClock.TriggerClick());
AddAssert("State is full", () => clockDisplayMode.Value == ToolbarClockDisplayMode.Full);
}
} }
} }

View File

@ -13,6 +13,7 @@ using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms; using osu.Game.Online.Rooms;
using osu.Game.Rulesets; using osu.Game.Rulesets;
@ -183,14 +184,41 @@ namespace osu.Game.Tests.Visual.Multiplayer
assertItemInHistoryListStep(2, 0); assertItemInHistoryListStep(2, 0);
} }
[Test]
public void TestInsertedItemDoesNotRefreshAllOthers()
{
AddStep("change to round robin queue mode", () => MultiplayerClient.ChangeSettings(new MultiplayerRoomSettings { QueueMode = QueueMode.AllPlayersRoundRobin }).WaitSafely());
// Add a few items for the local user.
addItemStep();
addItemStep();
addItemStep();
addItemStep();
addItemStep();
DrawableRoomPlaylistItem[] drawableItems = null;
AddStep("get drawable items", () => drawableItems = this.ChildrenOfType<DrawableRoomPlaylistItem>().ToArray());
// Add 1 item for another user.
AddStep("join second user", () => MultiplayerClient.AddUser(new APIUser { Id = 10 }));
addItemStep(userId: 10);
// New item inserted towards the top of the list.
assertItemInQueueListStep(7, 1);
AddAssert("all previous playlist items remained", () => drawableItems.All(this.ChildrenOfType<DrawableRoomPlaylistItem>().Contains));
}
/// <summary> /// <summary>
/// Adds a step to create a new playlist item. /// Adds a step to create a new playlist item.
/// </summary> /// </summary>
private void addItemStep(bool expired = false) => AddStep("add item", () => MultiplayerClient.AddPlaylistItem(new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap) private void addItemStep(bool expired = false, int? userId = null) => AddStep("add item", () =>
{
MultiplayerClient.AddUserPlaylistItem(userId ?? API.LocalUser.Value.OnlineID, new MultiplayerPlaylistItem(new PlaylistItem(importedBeatmap)
{ {
Expired = expired, Expired = expired,
PlayedAt = DateTimeOffset.Now PlayedAt = DateTimeOffset.Now
}))); })).WaitSafely();
});
/// <summary> /// <summary>
/// Asserts the position of a given playlist item in the queue list. /// Asserts the position of a given playlist item in the queue list.

View File

@ -5,9 +5,11 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Graphics.Containers; using osu.Game.Graphics.Containers;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API; using osu.Game.Online.API;
@ -29,6 +31,14 @@ namespace osu.Game.Tests.Visual.Online
private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single(); private BeatmapListingSearchControl searchControl => overlay.ChildrenOfType<BeatmapListingSearchControl>().Single();
private OsuConfigManager localConfig;
[BackgroundDependencyLoader]
private void load()
{
Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage));
}
[SetUpSteps] [SetUpSteps]
public void SetUpSteps() public void SetUpSteps()
{ {
@ -61,6 +71,8 @@ namespace osu.Game.Tests.Visual.Online
Id = API.LocalUser.Value.Id + 1, Id = API.LocalUser.Value.Id + 1,
}; };
}); });
AddStep("reset size", () => localConfig.SetValue(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal));
} }
[Test] [Test]
@ -121,23 +133,23 @@ namespace osu.Game.Tests.Visual.Online
} }
[Test] [Test]
public void TestCardSizeSwitching() public void TestCardSizeSwitching([Values] bool viaConfig)
{ {
AddAssert("is visible", () => overlay.State.Value == Visibility.Visible); AddAssert("is visible", () => overlay.State.Value == Visibility.Visible);
AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray())); AddStep("show many results", () => fetchFor(Enumerable.Repeat(CreateAPIBeatmapSet(Ruleset.Value), 100).ToArray()));
assertAllCardsOfType<BeatmapCardNormal>(100); assertAllCardsOfType<BeatmapCardNormal>(100);
setCardSize(BeatmapCardSize.Extra); setCardSize(BeatmapCardSize.Extra, viaConfig);
assertAllCardsOfType<BeatmapCardExtra>(100); assertAllCardsOfType<BeatmapCardExtra>(100);
setCardSize(BeatmapCardSize.Normal); setCardSize(BeatmapCardSize.Normal, viaConfig);
assertAllCardsOfType<BeatmapCardNormal>(100); assertAllCardsOfType<BeatmapCardNormal>(100);
AddStep("fetch for 0 beatmaps", () => fetchFor()); AddStep("fetch for 0 beatmaps", () => fetchFor());
AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); AddUntilStep("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
setCardSize(BeatmapCardSize.Extra); setCardSize(BeatmapCardSize.Extra, viaConfig);
AddAssert("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true); AddAssert("placeholder shown", () => overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().SingleOrDefault()?.IsPresent == true);
} }
@ -361,7 +373,13 @@ namespace osu.Game.Tests.Visual.Online
AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent)); AddUntilStep("\"no maps found\" placeholder not shown", () => !overlay.ChildrenOfType<BeatmapListingOverlay.NotFoundDrawable>().Any(d => d.IsPresent));
} }
private void setCardSize(BeatmapCardSize cardSize) => AddStep($"set card size to {cardSize}", () => overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize); private void setCardSize(BeatmapCardSize cardSize, bool viaConfig) => AddStep($"set card size to {cardSize}", () =>
{
if (viaConfig)
localConfig.SetValue(OsuSetting.BeatmapListingCardSize, cardSize);
else
overlay.ChildrenOfType<BeatmapListingCardSizeTabControl>().Single().Current.Value = cardSize;
});
private void assertAllCardsOfType<T>(int expectedCount) private void assertAllCardsOfType<T>(int expectedCount)
where T : BeatmapCard => where T : BeatmapCard =>
@ -370,5 +388,11 @@ namespace osu.Game.Tests.Visual.Online
int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().Count(card => card.IsLoaded && card.GetType() == typeof(T)); int loadedCorrectCount = this.ChildrenOfType<BeatmapCard>().Count(card => card.IsLoaded && card.GetType() == typeof(T));
return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount; return loadedCorrectCount > 0 && loadedCorrectCount == expectedCount;
}); });
protected override void Dispose(bool isDisposing)
{
localConfig?.Dispose();
base.Dispose(isDisposing);
}
} }
} }

View File

@ -71,7 +71,9 @@ namespace osu.Game.Tests.Visual.Online
{ {
Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(), Fails = Enumerable.Range(1, 100).Select(_ => RNG.Next(10)).ToArray(),
Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(), Retries = Enumerable.Range(-2, 100).Select(_ => RNG.Next(10)).ToArray(),
} },
PassCount = RNG.Next(0, 999),
PlayCount = RNG.Next(1000, 1999),
}; };
} }

View File

@ -0,0 +1,122 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Chat;
using osu.Game.Overlays;
using osu.Game.Overlays.Chat;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneChatTextBox : OsuTestScene
{
[Cached]
private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink);
[Cached]
private readonly Bindable<Channel> currentChannel = new Bindable<Channel>();
private OsuSpriteText commitText;
private OsuSpriteText searchText;
private ChatTextBar bar;
[SetUp]
public void SetUp()
{
Schedule(() =>
{
Child = new GridContainer
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
RowDimensions = new[]
{
new Dimension(GridSizeMode.Absolute, 30),
new Dimension(GridSizeMode.AutoSize),
},
Content = new[]
{
new Drawable[]
{
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
commitText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Default.With(size: 20),
},
searchText = new OsuSpriteText
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Font = OsuFont.Default.With(size: 20),
},
},
},
},
},
new Drawable[]
{
bar = new ChatTextBar
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 0.99f,
},
},
},
};
bar.OnChatMessageCommitted += text =>
{
commitText.Text = $"{nameof(bar.OnChatMessageCommitted)}: {text}";
commitText.FadeOutFromOne(1000, Easing.InQuint);
};
bar.OnSearchTermsChanged += text =>
{
searchText.Text = $"{nameof(bar.OnSearchTermsChanged)}: {text}";
};
});
}
[Test]
public void TestVisual()
{
AddStep("Public Channel", () => currentChannel.Value = createPublicChannel("#osu"));
AddStep("Public Channel Long Name", () => currentChannel.Value = createPublicChannel("#public-channel-long-name"));
AddStep("Private Channel", () => currentChannel.Value = createPrivateChannel("peppy", 2));
AddStep("Private Long Name", () => currentChannel.Value = createPrivateChannel("test user long name", 3));
AddStep("Chat Mode Channel", () => bar.ShowSearch.Value = false);
AddStep("Chat Mode Search", () => bar.ShowSearch.Value = true);
}
private static Channel createPublicChannel(string name)
=> new Channel { Name = name, Type = ChannelType.Public, Id = 1234 };
private static Channel createPrivateChannel(string username, int id)
=> new Channel(new APIUser { Id = id, Username = username });
}
}

View File

@ -44,9 +44,6 @@ namespace osu.Game.Tests.Visual.UserInterface
private BeatmapInfo beatmapInfo; private BeatmapInfo beatmapInfo;
[Resolved]
private RealmAccess realm { get; set; }
[Cached] [Cached]
private readonly DialogOverlay dialogOverlay; private readonly DialogOverlay dialogOverlay;
@ -92,6 +89,12 @@ namespace osu.Game.Tests.Visual.UserInterface
dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler)); dependencies.Cache(scoreManager = new ScoreManager(dependencies.Get<RulesetStore>(), () => beatmapManager, LocalStorage, Realm, Scheduler));
Dependencies.Cache(Realm); Dependencies.Cache(Realm);
return dependencies;
}
[BackgroundDependencyLoader]
private void load() => Schedule(() =>
{
var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely(); var imported = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).GetResultSafely();
imported?.PerformRead(s => imported?.PerformRead(s =>
@ -115,26 +118,26 @@ namespace osu.Game.Tests.Visual.UserInterface
importedScores.Add(scoreManager.Import(score).Value); importedScores.Add(scoreManager.Import(score).Value);
} }
}); });
return dependencies;
}
[SetUp]
public void Setup() => Schedule(() =>
{
realm.Run(r =>
{
// Due to soft deletions, we can re-use deleted scores between test runs
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
});
leaderboard.BeatmapInfo = beatmapInfo;
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
}); });
[SetUpSteps] [SetUpSteps]
public void SetupSteps() public void SetupSteps()
{ {
AddUntilStep("ensure scores imported", () => importedScores.Count == 50);
AddStep("undelete scores", () =>
{
Realm.Run(r =>
{
// Due to soft deletions, we can re-use deleted scores between test runs
scoreManager.Undelete(r.All<ScoreInfo>().Where(s => s.DeletePending).ToList());
});
});
AddStep("set up leaderboard", () =>
{
leaderboard.BeatmapInfo = beatmapInfo;
leaderboard.RefetchScores(); // Required in the case that the beatmap hasn't changed
});
// Ensure the leaderboard items have finished showing up // Ensure the leaderboard items have finished showing up
AddStep("finish transforms", () => leaderboard.FinishTransforms(true)); AddStep("finish transforms", () => leaderboard.FinishTransforms(true));
AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any()); AddUntilStep("wait for drawables", () => leaderboard.ChildrenOfType<LeaderboardScore>().Any());
@ -169,11 +172,14 @@ namespace osu.Game.Tests.Visual.UserInterface
AddStep("click delete button", () => AddStep("click delete button", () =>
{ {
InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType<DialogButton>().First()); InputManager.MoveMouseTo(dialogOverlay.ChildrenOfType<DialogButton>().First());
InputManager.Click(MouseButton.Left); InputManager.PressButton(MouseButton.Left);
}); });
AddUntilStep("wait for fetch", () => leaderboard.Scores != null); AddUntilStep("wait for fetch", () => leaderboard.Scores != null);
AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID)); AddUntilStep("score removed from leaderboard", () => leaderboard.Scores.All(s => s.OnlineID != scoreBeingDeleted.OnlineID));
// "Clean up"
AddStep("release left mouse button", () => InputManager.ReleaseButton(MouseButton.Left));
} }
[Test] [Test]

View File

@ -7,8 +7,10 @@ using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Framework.Input.Events;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Tournament.Components; using osu.Game.Tournament.Components;
using osu.Game.Tournament.Screens; using osu.Game.Tournament.Screens;
using osu.Game.Tournament.Screens.Drawings; using osu.Game.Tournament.Screens.Drawings;
@ -23,6 +25,7 @@ using osu.Game.Tournament.Screens.TeamIntro;
using osu.Game.Tournament.Screens.TeamWin; using osu.Game.Tournament.Screens.TeamWin;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osuTK.Input;
namespace osu.Game.Tournament namespace osu.Game.Tournament
{ {
@ -123,16 +126,16 @@ namespace osu.Game.Tournament
new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen }, new ScreenButton(typeof(RoundEditorScreen)) { Text = "Rounds Editor", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderEditorScreen)) { Text = "Bracket Editor", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen }, new ScreenButton(typeof(ScheduleScreen), Key.S) { Text = "Schedule", RequestSelection = SetScreen },
new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderScreen), Key.B) { Text = "Bracket", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen }, new ScreenButton(typeof(TeamIntroScreen), Key.I) { Text = "Team Intro", RequestSelection = SetScreen },
new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen }, new ScreenButton(typeof(SeedingScreen), Key.D) { Text = "Seeding", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen }, new ScreenButton(typeof(MapPoolScreen), Key.M) { Text = "Map Pool", RequestSelection = SetScreen },
new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen }, new ScreenButton(typeof(GameplayScreen), Key.G) { Text = "Gameplay", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen }, new ScreenButton(typeof(TeamWinScreen), Key.W) { Text = "Win", RequestSelection = SetScreen },
new Separator(), new Separator(),
new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen }, new ScreenButton(typeof(DrawingsScreen)) { Text = "Drawings", RequestSelection = SetScreen },
new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen }, new ScreenButton(typeof(ShowcaseScreen)) { Text = "Showcase", RequestSelection = SetScreen },
@ -231,13 +234,60 @@ namespace osu.Game.Tournament
{ {
public readonly Type Type; public readonly Type Type;
public ScreenButton(Type type) private readonly Key? shortcutKey;
public ScreenButton(Type type, Key? shortcutKey = null)
{ {
this.shortcutKey = shortcutKey;
Type = type; Type = type;
BackgroundColour = OsuColour.Gray(0.2f); BackgroundColour = OsuColour.Gray(0.2f);
Action = () => RequestSelection?.Invoke(type); Action = () => RequestSelection?.Invoke(type);
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
if (shortcutKey != null)
{
Add(new Container
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Size = new Vector2(24),
Margin = new MarginPadding(5),
Masking = true,
CornerRadius = 4,
Alpha = 0.5f,
Blending = BlendingParameters.Additive,
Children = new Drawable[]
{
new Box
{
Colour = OsuColour.Gray(0.1f),
RelativeSizeAxes = Axes.Both,
},
new OsuSpriteText
{
Font = OsuFont.Default.With(size: 24),
Y = -2,
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Text = shortcutKey.ToString(),
}
}
});
}
}
protected override bool OnKeyDown(KeyDownEvent e)
{
if (e.Key == shortcutKey)
{
TriggerClick();
return true;
}
return base.OnKeyDown(e);
} }
private bool isSelected; private bool isSelected;

View File

@ -5,6 +5,7 @@ using System.Collections.Generic;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Beatmaps.Timing; using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Beatmaps namespace osu.Game.Beatmaps
{ {
@ -70,4 +71,27 @@ namespace osu.Game.Beatmaps
/// </summary> /// </summary>
new IReadOnlyList<T> HitObjects { get; } new IReadOnlyList<T> HitObjects { get; }
} }
public static class BeatmapExtensions
{
/// <summary>
/// Finds the maximum achievable combo by hitting all <see cref="HitObject"/>s in a beatmap.
/// </summary>
public static int GetMaxCombo(this IBeatmap beatmap)
{
int combo = 0;
foreach (var h in beatmap.HitObjects)
addCombo(h, ref combo);
return combo;
static void addCombo(HitObject hitObject, ref int combo)
{
if (hitObject.CreateJudgement().MaxResult.AffectsCombo())
combo++;
foreach (var nested in hitObject.NestedHitObjects)
addCombo(nested, ref combo);
}
}
}
} }

View File

@ -10,6 +10,7 @@ using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Platform; using osu.Framework.Platform;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Input; using osu.Game.Input;
using osu.Game.Input.Bindings; using osu.Game.Input.Bindings;
using osu.Game.Localisation; using osu.Game.Localisation;
@ -44,6 +45,8 @@ namespace osu.Game.Configuration
SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f); SetDefault(OsuSetting.ChatDisplayHeight, ChatOverlay.DEFAULT_HEIGHT, 0.2f, 1f);
SetDefault(OsuSetting.BeatmapListingCardSize, BeatmapCardSize.Normal);
SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full); SetDefault(OsuSetting.ToolbarClockDisplayMode, ToolbarClockDisplayMode.Full);
// Online settings // Online settings
@ -297,6 +300,7 @@ namespace osu.Game.Configuration
RandomSelectAlgorithm, RandomSelectAlgorithm,
ShowFpsDisplay, ShowFpsDisplay,
ChatDisplayHeight, ChatDisplayHeight,
BeatmapListingCardSize,
ToolbarClockDisplayMode, ToolbarClockDisplayMode,
Version, Version,
ShowConvertedBeatmaps, ShowConvertedBeatmaps,

View File

@ -11,6 +11,7 @@ using osu.Framework.Bindables;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Utils;
namespace osu.Game.Online.Rooms namespace osu.Game.Online.Rooms
{ {
@ -84,6 +85,19 @@ namespace osu.Game.Online.Rooms
Beatmap = beatmap; Beatmap = beatmap;
} }
public PlaylistItem(MultiplayerPlaylistItem item)
: this(new APIBeatmap { OnlineID = item.BeatmapID })
{
ID = item.ID;
OwnerID = item.OwnerID;
RulesetID = item.RulesetID;
Expired = item.Expired;
PlaylistOrder = item.PlaylistOrder;
PlayedAt = item.PlayedAt;
RequiredMods = item.RequiredMods.ToArray();
AllowedMods = item.AllowedMods.ToArray();
}
public void MarkInvalid() => valid.Value = false; public void MarkInvalid() => valid.Value = false;
#region Newtonsoft.Json implicit ShouldSerialize() methods #region Newtonsoft.Json implicit ShouldSerialize() methods
@ -101,13 +115,13 @@ namespace osu.Game.Online.Rooms
#endregion #endregion
public PlaylistItem With(IBeatmapInfo beatmap) => new PlaylistItem(beatmap) public PlaylistItem With(Optional<IBeatmapInfo> beatmap = default, Optional<ushort?> playlistOrder = default) => new PlaylistItem(beatmap.GetOr(Beatmap))
{ {
ID = ID, ID = ID,
OwnerID = OwnerID, OwnerID = OwnerID,
RulesetID = RulesetID, RulesetID = RulesetID,
Expired = Expired, Expired = Expired,
PlaylistOrder = PlaylistOrder, PlaylistOrder = playlistOrder.GetOr(PlaylistOrder),
PlayedAt = PlayedAt, PlayedAt = PlayedAt,
AllowedMods = AllowedMods, AllowedMods = AllowedMods,
RequiredMods = RequiredMods, RequiredMods = RequiredMods,
@ -119,6 +133,7 @@ namespace osu.Game.Online.Rooms
&& Beatmap.OnlineID == other.Beatmap.OnlineID && Beatmap.OnlineID == other.Beatmap.OnlineID
&& RulesetID == other.RulesetID && RulesetID == other.RulesetID
&& Expired == other.Expired && Expired == other.Expired
&& PlaylistOrder == other.PlaylistOrder
&& AllowedMods.SequenceEqual(other.AllowedMods) && AllowedMods.SequenceEqual(other.AllowedMods)
&& RequiredMods.SequenceEqual(other.RequiredMods); && RequiredMods.SequenceEqual(other.RequiredMods);
} }

View File

@ -1061,6 +1061,12 @@ namespace osu.Game
return true; return true;
case GlobalAction.RandomSkin: case GlobalAction.RandomSkin:
// Don't allow random skin selection while in the skin editor.
// This is mainly to stop many "osu! default (modified)" skins being created via the SkinManager.EnsureMutableSkin() path.
// If people want this to work we can potentially avoid selecting default skins when the editor is open, or allow a maximum of one mutable skin somehow.
if (skinEditor.State.Value == Visibility.Visible)
return false;
SkinManager.SelectRandomSkin(); SkinManager.SelectRandomSkin();
return true; return true;
} }

View File

@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Beatmaps.Drawables.Cards; using osu.Game.Beatmaps.Drawables.Cards;
using osu.Game.Configuration;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
@ -53,7 +54,9 @@ namespace osu.Game.Overlays.BeatmapListing
/// <summary> /// <summary>
/// The currently selected <see cref="BeatmapCardSize"/>. /// The currently selected <see cref="BeatmapCardSize"/>.
/// </summary> /// </summary>
public IBindable<BeatmapCardSize> CardSize { get; } = new Bindable<BeatmapCardSize>(); public IBindable<BeatmapCardSize> CardSize => cardSize;
private readonly Bindable<BeatmapCardSize> cardSize = new Bindable<BeatmapCardSize>();
private readonly BeatmapListingSearchControl searchControl; private readonly BeatmapListingSearchControl searchControl;
private readonly BeatmapListingSortTabControl sortControl; private readonly BeatmapListingSortTabControl sortControl;
@ -128,6 +131,9 @@ namespace osu.Game.Overlays.BeatmapListing
}; };
} }
[Resolved]
private OsuConfigManager config { get; set; }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider, IAPIProvider api) private void load(OverlayColourProvider colourProvider, IAPIProvider api)
{ {
@ -141,6 +147,8 @@ namespace osu.Game.Overlays.BeatmapListing
{ {
base.LoadComplete(); base.LoadComplete();
config.BindWith(OsuSetting.BeatmapListingCardSize, cardSize);
var sortCriteria = sortControl.Current; var sortCriteria = sortControl.Current;
var sortDirection = sortControl.SortDirection; var sortDirection = sortControl.SortDirection;

View File

@ -5,6 +5,8 @@ using osu.Framework.Allocation;
using osu.Framework.Extensions.LocalisationExtensions; using osu.Framework.Extensions.LocalisationExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Framework.Localisation;
using osu.Game.Graphics; using osu.Game.Graphics;
using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Sprites;
using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterface;
@ -19,7 +21,7 @@ namespace osu.Game.Overlays.BeatmapSet
protected readonly FailRetryGraph Graph; protected readonly FailRetryGraph Graph;
private readonly FillFlowContainer header; private readonly FillFlowContainer header;
private readonly OsuSpriteText successPercent; private readonly SuccessRatePercentage successPercent;
private readonly Bar successRate; private readonly Bar successRate;
private readonly Container percentContainer; private readonly Container percentContainer;
@ -45,6 +47,7 @@ namespace osu.Game.Overlays.BeatmapSet
float rate = playCount != 0 ? (float)passCount / playCount : 0; float rate = playCount != 0 ? (float)passCount / playCount : 0;
successPercent.Text = rate.ToLocalisableString(@"0.#%"); successPercent.Text = rate.ToLocalisableString(@"0.#%");
successPercent.TooltipText = $"{passCount} / {playCount}";
successRate.Length = rate; successRate.Length = rate;
percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic); percentContainer.ResizeWidthTo(successRate.Length, 250, Easing.InOutCubic);
@ -80,7 +83,7 @@ namespace osu.Game.Overlays.BeatmapSet
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Width = 0f, Width = 0f,
Child = successPercent = new OsuSpriteText Child = successPercent = new SuccessRatePercentage
{ {
Anchor = Anchor.TopRight, Anchor = Anchor.TopRight,
Origin = Anchor.TopCentre, Origin = Anchor.TopCentre,
@ -121,5 +124,10 @@ namespace osu.Game.Overlays.BeatmapSet
Graph.Padding = new MarginPadding { Top = header.DrawHeight }; Graph.Padding = new MarginPadding { Top = header.DrawHeight };
} }
private class SuccessRatePercentage : OsuSpriteText, IHasTooltip
{
public LocalisableString TooltipText { get; set; }
}
} }
} }

View File

@ -0,0 +1,163 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using System;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Shapes;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.UserInterface;
using osu.Game.Graphics.Containers;
using osu.Game.Online.Chat;
using osuTK;
namespace osu.Game.Overlays.Chat
{
public class ChatTextBar : Container
{
public readonly BindableBool ShowSearch = new BindableBool();
public event Action<string>? OnChatMessageCommitted;
public event Action<string>? OnSearchTermsChanged;
[Resolved]
private Bindable<Channel> currentChannel { get; set; } = null!;
private OsuTextFlowContainer chattingTextContainer = null!;
private Container searchIconContainer = null!;
private ChatTextBox chatTextBox = null!;
private const float chatting_text_width = 180;
private const float search_icon_width = 40;
[BackgroundDependencyLoader]
private void load(OverlayColourProvider colourProvider)
{
RelativeSizeAxes = Axes.X;
Height = 60;
Children = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Colour = colourProvider.Background5,
},
new GridContainer
{
RelativeSizeAxes = Axes.Both,
ColumnDimensions = new[]
{
new Dimension(GridSizeMode.AutoSize),
new Dimension(GridSizeMode.AutoSize),
new Dimension(),
},
Content = new[]
{
new Drawable[]
{
chattingTextContainer = new OsuTextFlowContainer(t => t.Font = t.Font.With(size: 20))
{
Masking = true,
Width = chatting_text_width,
Padding = new MarginPadding { Left = 10 },
RelativeSizeAxes = Axes.Y,
TextAnchor = Anchor.CentreRight,
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
Colour = colourProvider.Background1,
},
searchIconContainer = new Container
{
RelativeSizeAxes = Axes.Y,
Width = search_icon_width,
Child = new SpriteIcon
{
Icon = FontAwesome.Solid.Search,
Origin = Anchor.CentreRight,
Anchor = Anchor.CentreRight,
Size = new Vector2(20),
Margin = new MarginPadding { Right = 2 },
},
},
new Container
{
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Right = 5 },
Child = chatTextBox = new ChatTextBox
{
Anchor = Anchor.CentreLeft,
Origin = Anchor.CentreLeft,
RelativeSizeAxes = Axes.X,
ShowSearch = { BindTarget = ShowSearch },
HoldFocus = true,
ReleaseFocusOnCommit = false,
},
},
},
},
},
};
}
protected override void LoadComplete()
{
base.LoadComplete();
chatTextBox.Current.ValueChanged += chatTextBoxChange;
chatTextBox.OnCommit += chatTextBoxCommit;
ShowSearch.BindValueChanged(change =>
{
bool showSearch = change.NewValue;
chattingTextContainer.FadeTo(showSearch ? 0 : 1);
searchIconContainer.FadeTo(showSearch ? 1 : 0);
// Clear search terms if any exist when switching back to chat mode
if (!showSearch)
OnSearchTermsChanged?.Invoke(string.Empty);
}, true);
currentChannel.BindValueChanged(change =>
{
Channel newChannel = change.NewValue;
switch (newChannel?.Type)
{
case ChannelType.Public:
chattingTextContainer.Text = $"chatting in {newChannel.Name}";
break;
case ChannelType.PM:
chattingTextContainer.Text = $"chatting with {newChannel.Name}";
break;
default:
chattingTextContainer.Text = string.Empty;
break;
}
}, true);
}
private void chatTextBoxChange(ValueChangedEvent<string> change)
{
if (ShowSearch.Value)
OnSearchTermsChanged?.Invoke(change.NewValue);
}
private void chatTextBoxCommit(TextBox sender, bool newText)
{
if (ShowSearch.Value)
return;
OnChatMessageCommitted?.Invoke(sender.Text);
sender.Text = string.Empty;
}
}
}

View File

@ -0,0 +1,38 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable enable
using osu.Framework.Bindables;
using osu.Game.Graphics.UserInterface;
namespace osu.Game.Overlays.Chat
{
public class ChatTextBox : FocusedTextBox
{
public readonly BindableBool ShowSearch = new BindableBool();
public override bool HandleLeftRightArrows => !ShowSearch.Value;
protected override void LoadComplete()
{
base.LoadComplete();
ShowSearch.BindValueChanged(change =>
{
bool showSearch = change.NewValue;
PlaceholderText = showSearch ? "type here to search" : "type here";
Text = string.Empty;
}, true);
}
protected override void Commit()
{
if (ShowSearch.Value)
return;
base.Commit();
}
}
}

View File

@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance
HeaderText = @"Confirm deletion of"; HeaderText = @"Confirm deletion of";
Buttons = new PopupDialogButton[] Buttons = new PopupDialogButton[]
{ {
new PopupDialogOkButton new PopupDialogDangerousButton
{ {
Text = @"Yes. Go for it.", Text = @"Yes. Go for it.",
Action = deleteAction Action = deleteAction

View File

@ -190,7 +190,7 @@ namespace osu.Game.Overlays.Toolbar
public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Action == Hotkey) if (e.Action == Hotkey && !e.Repeat)
{ {
TriggerClick(); TriggerClick();
return true; return true;

View File

@ -15,6 +15,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing;
using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Skills;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Utils;
namespace osu.Game.Rulesets.Difficulty namespace osu.Game.Rulesets.Difficulty
{ {
@ -119,15 +120,23 @@ namespace osu.Game.Rulesets.Difficulty
/// <summary> /// <summary>
/// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap. /// Calculates the difficulty of the beatmap using all mod combinations applicable to the beatmap.
/// </summary> /// </summary>
/// <remarks>
/// This can only be used to compute difficulties for legacy mod combinations.
/// </remarks>
/// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns> /// <returns>A collection of structures describing the difficulty of the beatmap for each mod combination.</returns>
public IEnumerable<DifficultyAttributes> CalculateAll(CancellationToken cancellationToken = default) public IEnumerable<DifficultyAttributes> CalculateAllLegacyCombinations(CancellationToken cancellationToken = default)
{ {
var rulesetInstance = ruleset.CreateInstance();
foreach (var combination in CreateDifficultyAdjustmentModCombinations()) foreach (var combination in CreateDifficultyAdjustmentModCombinations())
{ {
if (combination is MultiMod multi) Mod classicMod = rulesetInstance.CreateAllMods().SingleOrDefault(m => m is ModClassic);
yield return Calculate(multi.Mods, cancellationToken);
else var finalCombination = ModUtils.FlattenMod(combination);
yield return Calculate(combination.Yield(), cancellationToken); if (classicMod != null)
finalCombination = finalCombination.Append(classicMod);
yield return Calculate(finalCombination.ToArray(), cancellationToken);
} }
} }

View File

@ -5,6 +5,8 @@ using System;
using System.Linq; using System.Linq;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Localisation; using osu.Framework.Localisation;
using osu.Framework.Threading; using osu.Framework.Threading;
using osu.Game.Graphics; using osu.Game.Graphics;
@ -27,6 +29,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
[CanBeNull] [CanBeNull]
private MultiplayerRoom room => multiplayerClient.Room; private MultiplayerRoom room => multiplayerClient.Room;
private Sample countdownTickSample;
[BackgroundDependencyLoader]
private void load(AudioManager audio)
{
countdownTickSample = audio.Samples.Get(@"Multiplayer/countdown-tick");
// disabled for now pending further work on sound effect
// countdownTickFinalSample = audio.Samples.Get(@"Multiplayer/countdown-tick-final");
}
protected override void LoadComplete() protected override void LoadComplete()
{ {
base.LoadComplete(); base.LoadComplete();
@ -36,7 +48,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
} }
private MultiplayerCountdown countdown; private MultiplayerCountdown countdown;
private DateTimeOffset countdownChangeTime; private double countdownChangeTime;
private ScheduledDelegate countdownUpdateDelegate; private ScheduledDelegate countdownUpdateDelegate;
private void onRoomUpdated() => Scheduler.AddOnce(() => private void onRoomUpdated() => Scheduler.AddOnce(() =>
@ -44,20 +56,55 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != room?.Countdown) if (countdown != room?.Countdown)
{ {
countdown = room?.Countdown; countdown = room?.Countdown;
countdownChangeTime = DateTimeOffset.Now; countdownChangeTime = Time.Current;
} }
scheduleNextCountdownUpdate();
updateButtonText();
updateButtonColour();
});
private void scheduleNextCountdownUpdate()
{
countdownUpdateDelegate?.Cancel();
if (countdown != null) if (countdown != null)
countdownUpdateDelegate ??= Scheduler.AddDelayed(updateButtonText, 100, true); {
// The remaining time on a countdown may be at a fractional portion between two seconds.
// We want to align certain audio/visual cues to the point at which integer seconds change.
// To do so, we schedule to the next whole second. Note that scheduler invocation isn't
// guaranteed to be accurate, so this may still occur slightly late, but even in such a case
// the next invocation will be roughly correct.
double timeToNextSecond = countdownTimeRemaining.TotalMilliseconds % 1000;
countdownUpdateDelegate = Scheduler.AddDelayed(onCountdownTick, timeToNextSecond);
}
else else
{ {
countdownUpdateDelegate?.Cancel(); countdownUpdateDelegate?.Cancel();
countdownUpdateDelegate = null; countdownUpdateDelegate = null;
} }
void onCountdownTick()
{
updateButtonText(); updateButtonText();
updateButtonColour();
}); int secondsRemaining = countdownTimeRemaining.Seconds;
playTickSound(secondsRemaining);
if (secondsRemaining > 0)
scheduleNextCountdownUpdate();
}
}
private void playTickSound(int secondsRemaining)
{
if (secondsRemaining < 10) countdownTickSample?.Play();
// disabled for now pending further work on sound effect
// if (secondsRemaining <= 3) countdownTickFinalSample?.Play();
}
private void updateButtonText() private void updateButtonText()
{ {
@ -75,15 +122,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
if (countdown != null) if (countdown != null)
{ {
TimeSpan timeElapsed = DateTimeOffset.Now - countdownChangeTime; string countdownText = $"Starting in {countdownTimeRemaining:mm\\:ss}";
TimeSpan countdownRemaining;
if (timeElapsed > countdown.TimeRemaining)
countdownRemaining = TimeSpan.Zero;
else
countdownRemaining = countdown.TimeRemaining - timeElapsed;
string countdownText = $"Starting in {countdownRemaining:mm\\:ss}";
switch (localUser?.State) switch (localUser?.State)
{ {
@ -116,6 +155,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match
} }
} }
private TimeSpan countdownTimeRemaining
{
get
{
double timeElapsed = Time.Current - countdownChangeTime;
TimeSpan remaining;
if (timeElapsed > countdown.TimeRemaining.TotalMilliseconds)
remaining = TimeSpan.Zero;
else
remaining = countdown.TimeRemaining - TimeSpan.FromMilliseconds(timeElapsed);
return remaining;
}
}
private void updateButtonColour() private void updateButtonColour()
{ {
if (room == null) if (room == null)

View File

@ -117,9 +117,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match.Playlist
{ {
base.PlaylistItemChanged(item); base.PlaylistItemChanged(item);
var newApiItem = Playlist.SingleOrDefault(i => i.ID == item.ID);
var existingApiItemInQueue = queueList.Items.SingleOrDefault(i => i.ID == item.ID);
// Test if the only change between the two playlist items is the order.
if (newApiItem != null && existingApiItemInQueue != null && existingApiItemInQueue.With(playlistOrder: newApiItem.PlaylistOrder).Equals(newApiItem))
{
// Set the new playlist order directly without refreshing the DrawablePlaylistItem.
existingApiItemInQueue.PlaylistOrder = newApiItem.PlaylistOrder;
// The following isn't really required, but is here for safety and explicitness.
// MultiplayerQueueList internally binds to changes in Playlist to invalidate its own layout, which is mutated on every playlist operation.
queueList.Invalidate();
}
else
{
removeItemFromLists(item.ID); removeItemFromLists(item.ID);
addItemToLists(item); addItemToLists(item);
} }
}
private void addItemToLists(MultiplayerPlaylistItem item) private void addItemToLists(MultiplayerPlaylistItem item)
{ {

View File

@ -9,6 +9,7 @@ using Newtonsoft.Json;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Logging;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Extensions; using osu.Game.Extensions;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -83,10 +84,18 @@ namespace osu.Game.Screens.Play.HUD
/// </summary> /// </summary>
/// <returns>The new instance.</returns> /// <returns>The new instance.</returns>
public Drawable CreateInstance() public Drawable CreateInstance()
{
try
{ {
Drawable d = (Drawable)Activator.CreateInstance(Type); Drawable d = (Drawable)Activator.CreateInstance(Type);
d.ApplySkinnableInfo(this); d.ApplySkinnableInfo(this);
return d; return d;
} }
catch (Exception e)
{
Logger.Error(e, $"Unable to create skin component {Type.Name}");
return Drawable.Empty();
}
}
} }
} }

View File

@ -26,7 +26,7 @@ namespace osu.Game.Screens.Select
HeaderText = @"Confirm deletion of"; HeaderText = @"Confirm deletion of";
Buttons = new PopupDialogButton[] Buttons = new PopupDialogButton[]
{ {
new PopupDialogOkButton new PopupDialogDangerousButton
{ {
Text = @"Yes. Totally. Delete it.", Text = @"Yes. Totally. Delete it.",
Action = () => manager?.Delete(beatmap), Action = () => manager?.Delete(beatmap),

View File

@ -174,7 +174,7 @@ namespace osu.Game.Screens.Select
public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e) public virtual bool OnPressed(KeyBindingPressEvent<GlobalAction> e)
{ {
if (e.Action == Hotkey) if (e.Action == Hotkey && !e.Repeat)
{ {
TriggerClick(); TriggerClick();
return true; return true;

View File

@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select
HeaderText = "Confirm deletion of local score"; HeaderText = "Confirm deletion of local score";
Buttons = new PopupDialogButton[] Buttons = new PopupDialogButton[]
{ {
new PopupDialogOkButton new PopupDialogDangerousButton
{ {
Text = "Yes. Please.", Text = "Yes. Please.",
Action = () => scoreManager?.Delete(score) Action = () => scoreManager?.Delete(score)

View File

@ -158,6 +158,13 @@ namespace osu.Game.Skinning
break; break;
} }
switch (component.LookupName)
{
// Temporary until default skin has a valid hit lighting.
case @"lighting":
return Drawable.Empty();
}
if (GetTexture(component.LookupName) is Texture t) if (GetTexture(component.LookupName) is Texture t)
return new Sprite { Texture = t }; return new Sprite { Texture = t };

View File

@ -197,13 +197,6 @@ namespace osu.Game.Skinning.Editor
SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true); SelectedComponents.BindCollectionChanged((_, __) => Scheduler.AddOnce(populateSettings), true);
} }
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game?.UnregisterImportHandler(this);
}
public void UpdateTargetScreen(Drawable targetScreen) public void UpdateTargetScreen(Drawable targetScreen)
{ {
this.targetScreen = targetScreen; this.targetScreen = targetScreen;
@ -340,6 +333,8 @@ namespace osu.Game.Skinning.Editor
availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item); availableTargets.FirstOrDefault(t => t.Components.Contains(item))?.Remove(item);
} }
#region Drag & drop import handling
public Task Import(params string[] paths) public Task Import(params string[] paths)
{ {
Schedule(() => Schedule(() =>
@ -377,5 +372,14 @@ namespace osu.Game.Skinning.Editor
public Task Import(params ImportTask[] tasks) => throw new NotImplementedException(); public Task Import(params ImportTask[] tasks) => throw new NotImplementedException();
public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" }; public IEnumerable<string> HandledExtensions => new[] { ".jpg", ".jpeg", ".png" };
#endregion
protected override void Dispose(bool isDisposing)
{
base.Dispose(isDisposing);
game?.UnregisterImportHandler(this);
}
} }
} }

View File

@ -23,7 +23,7 @@ namespace osu.Game.Skinning
/// The <see cref="ISkin"/> which is being transformed. /// The <see cref="ISkin"/> which is being transformed.
/// </summary> /// </summary>
[NotNull] [NotNull]
protected ISkin Skin { get; } protected internal ISkin Skin { get; }
protected LegacySkinTransformer([NotNull] ISkin skin) protected LegacySkinTransformer([NotNull] ISkin skin)
{ {

View File

@ -7,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading;
using osu.Framework.Extensions; using osu.Framework.Extensions;
using osu.Framework.IO.Stores; using osu.Framework.IO.Stores;
using osu.Game.Database; using osu.Game.Database;
@ -61,7 +60,7 @@ namespace osu.Game.Skinning
return null; return null;
} }
private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache, LazyThreadSafetyMode.ExecutionAndPublication); private void invalidateCache() => fileToStoragePathMapping = new Lazy<Dictionary<string, string>>(initialiseFileCache);
private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source => private Dictionary<string, string> initialiseFileCache() => liveSource.PerformRead(source =>
{ {

View File

@ -159,16 +159,7 @@ namespace osu.Game.Skinning
var components = new List<Drawable>(); var components = new List<Drawable>();
foreach (var i in skinnableInfo) foreach (var i in skinnableInfo)
{
try
{
components.Add(i.CreateInstance()); components.Add(i.CreateInstance());
}
catch (Exception e)
{
Logger.Error(e, $"Unable to create skin component {i.Type.Name}");
}
}
return new SkinnableTargetComponentsContainer return new SkinnableTargetComponentsContainer
{ {

View File

@ -26,6 +26,7 @@ using osu.Game.IO;
using osu.Game.IO.Archives; using osu.Game.IO.Archives;
using osu.Game.Models; using osu.Game.Models;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using osu.Game.Utils;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
@ -149,20 +150,26 @@ namespace osu.Game.Skinning
if (!s.Protected) if (!s.Protected)
return; return;
string[] existingSkinNames = realm.Run(r => r.All<SkinInfo>()
.Where(skin => !skin.DeletePending)
.AsEnumerable()
.Select(skin => skin.Name).ToArray());
// if the user is attempting to save one of the default skin implementations, create a copy first. // if the user is attempting to save one of the default skin implementations, create a copy first.
var result = skinModelManager.Import(new SkinInfo var skinInfo = new SkinInfo
{ {
Name = s.Name + @" (modified)",
Creator = s.Creator, Creator = s.Creator,
InstantiationInfo = s.InstantiationInfo, InstantiationInfo = s.InstantiationInfo,
}); Name = NamingUtils.GetNextBestName(existingSkinNames, $"{s.Name} (modified)")
};
var result = skinModelManager.Import(skinInfo);
if (result != null) if (result != null)
{ {
// save once to ensure the required json content is populated. // save once to ensure the required json content is populated.
// currently this only happens on save. // currently this only happens on save.
result.PerformRead(skin => Save(skin.CreateInstance(this))); result.PerformRead(skin => Save(skin.CreateInstance(this)));
CurrentSkinInfo.Value = result; CurrentSkinInfo.Value = result;
} }
}); });

View File

@ -2,20 +2,23 @@
// See the LICENCE file in the repository root for full licence text. // See the LICENCE file in the repository root for full licence text.
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Textures; using osu.Framework.Graphics.Textures;
using osu.Game.Configuration; using osu.Game.Configuration;
using osu.Game.Graphics.Sprites;
using osu.Game.Overlays.Settings; using osu.Game.Overlays.Settings;
using osuTK; using osuTK;
namespace osu.Game.Skinning namespace osu.Game.Skinning
{ {
/// <summary> /// <summary>
/// A skinnable element which uses a stable sprite and can therefore share implementation logic. /// A skinnable element which uses a single texture backing.
/// </summary> /// </summary>
public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable public class SkinnableSprite : SkinnableDrawable, ISkinnableDrawable
{ {
@ -55,13 +58,7 @@ namespace osu.Game.Skinning
var texture = textures.Get(component.LookupName); var texture = textures.Get(component.LookupName);
if (texture == null) if (texture == null)
{ return new SpriteNotFound(component.LookupName);
return new SpriteIcon
{
Size = new Vector2(100),
Icon = FontAwesome.Solid.QuestionCircle
};
}
return new Sprite { Texture = texture }; return new Sprite { Texture = texture };
} }
@ -87,7 +84,7 @@ namespace osu.Game.Skinning
// Round-about way of getting the user's skin to find available resources. // Round-about way of getting the user's skin to find available resources.
// In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins // In the future we'll probably want to allow access to resources from the fallbacks, or potentially other skins
// but that requires further thought. // but that requires further thought.
var highestPrioritySkin = ((SkinnableSprite)SettingSourceObject).source.AllSources.First() as Skin; var highestPrioritySkin = getHighestPriorityUserSkin(((SkinnableSprite)SettingSourceObject).source.AllSources) as Skin;
string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files string[] availableFiles = highestPrioritySkin?.SkinInfo.PerformRead(s => s.Files
.Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal) .Where(f => f.Filename.EndsWith(".png", StringComparison.Ordinal)
@ -96,6 +93,49 @@ namespace osu.Game.Skinning
if (availableFiles?.Length > 0) if (availableFiles?.Length > 0)
Items = availableFiles; Items = availableFiles;
static ISkin getHighestPriorityUserSkin(IEnumerable<ISkin> skins)
{
foreach (var skin in skins)
{
if (skin is LegacySkinTransformer transformer && isUserSkin(transformer.Skin))
return transformer.Skin;
if (isUserSkin(skin))
return skin;
}
return null;
}
// Temporarily used to exclude undesirable ISkin implementations
static bool isUserSkin(ISkin skin)
=> skin.GetType() == typeof(DefaultSkin)
|| skin.GetType() == typeof(DefaultLegacySkin)
|| skin.GetType() == typeof(LegacySkin);
}
}
public class SpriteNotFound : CompositeDrawable
{
public SpriteNotFound(string lookup)
{
AutoSizeAxes = Axes.Both;
InternalChildren = new Drawable[]
{
new SpriteIcon
{
Size = new Vector2(50),
Icon = FontAwesome.Solid.QuestionCircle
},
new OsuSpriteText
{
Position = new Vector2(25, 50),
Text = $"missing: {lookup}",
Origin = Anchor.TopCentre,
}
};
} }
} }
} }

View File

@ -22,10 +22,13 @@ namespace osu.Game.Tests.Beatmaps
protected abstract string ResourceAssembly { get; } protected abstract string ResourceAssembly { get; }
protected void Test(double expected, string name, params Mod[] mods) protected void Test(double expectedStarRating, int expectedMaxCombo, string name, params Mod[] mods)
{ {
var attributes = CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods);
// Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences. // Platform-dependent math functions (Pow, Cbrt, Exp, etc) may result in minute differences.
Assert.That(CreateDifficultyCalculator(getBeatmap(name)).Calculate(mods).StarRating, Is.EqualTo(expected).Within(0.00001)); Assert.That(attributes.StarRating, Is.EqualTo(expectedStarRating).Within(0.00001));
Assert.That(attributes.MaxCombo, Is.EqualTo(expectedMaxCombo));
} }
private IWorkingBeatmap getBeatmap(string name) private IWorkingBeatmap getBeatmap(string name)

View File

@ -31,7 +31,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
public override IBindable<bool> IsConnected => isConnected; public override IBindable<bool> IsConnected => isConnected;
private readonly Bindable<bool> isConnected = new Bindable<bool>(true); private readonly Bindable<bool> isConnected = new Bindable<bool>(true);
/// <summary>
/// The local client's <see cref="Room"/>. This is not always equivalent to the server-side room.
/// </summary>
public new Room? APIRoom => base.APIRoom; public new Room? APIRoom => base.APIRoom;
public Action<MultiplayerRoom>? RoomSetupAction; public Action<MultiplayerRoom>? RoomSetupAction;
public bool RoomJoined { get; private set; } public bool RoomJoined { get; private set; }
@ -46,6 +50,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
/// </summary> /// </summary>
private readonly List<MultiplayerPlaylistItem> serverSidePlaylist = new List<MultiplayerPlaylistItem>(); private readonly List<MultiplayerPlaylistItem> serverSidePlaylist = new List<MultiplayerPlaylistItem>();
/// <summary>
/// Guaranteed up-to-date API room.
/// </summary>
private Room? serverSideAPIRoom;
private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex]; private MultiplayerPlaylistItem? currentItem => Room?.Playlist[currentIndex];
private int currentIndex; private int currentIndex;
private long lastPlaylistItemId; private long lastPlaylistItemId;
@ -192,13 +201,13 @@ namespace osu.Game.Tests.Visual.Multiplayer
protected override async Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null) protected override async Task<MultiplayerRoom> JoinRoom(long roomId, string? password = null)
{ {
var apiRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId); serverSideAPIRoom = roomManager.ServerSideRooms.Single(r => r.RoomID.Value == roomId);
if (password != apiRoom.Password.Value) if (password != serverSideAPIRoom.Password.Value)
throw new InvalidOperationException("Invalid password."); throw new InvalidOperationException("Invalid password.");
serverSidePlaylist.Clear(); serverSidePlaylist.Clear();
serverSidePlaylist.AddRange(apiRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item))); serverSidePlaylist.AddRange(serverSideAPIRoom.Playlist.Select(item => new MultiplayerPlaylistItem(item)));
lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID); lastPlaylistItemId = serverSidePlaylist.Max(item => item.ID);
var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id) var localUser = new MultiplayerRoomUser(api.LocalUser.Value.Id)
@ -210,11 +219,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Settings = Settings =
{ {
Name = apiRoom.Name.Value, Name = serverSideAPIRoom.Name.Value,
MatchType = apiRoom.Type.Value, MatchType = serverSideAPIRoom.Type.Value,
Password = password, Password = password,
QueueMode = apiRoom.QueueMode.Value, QueueMode = serverSideAPIRoom.QueueMode.Value,
AutoStartDuration = apiRoom.AutoStartDuration.Value AutoStartDuration = serverSideAPIRoom.AutoStartDuration.Value
}, },
Playlist = serverSidePlaylist.ToList(), Playlist = serverSidePlaylist.ToList(),
Users = { localUser }, Users = { localUser },
@ -449,8 +458,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item) public async Task EditUserPlaylistItem(int userId, MultiplayerPlaylistItem item)
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
Debug.Assert(APIRoom != null);
Debug.Assert(currentItem != null); Debug.Assert(currentItem != null);
Debug.Assert(serverSideAPIRoom != null);
item.OwnerID = userId; item.OwnerID = userId;
@ -469,6 +478,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
item.PlaylistOrder = existingItem.PlaylistOrder; item.PlaylistOrder = existingItem.PlaylistOrder;
serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item; serverSidePlaylist[serverSidePlaylist.IndexOf(existingItem)] = item;
serverSideAPIRoom.Playlist[serverSideAPIRoom.Playlist.IndexOf(serverSideAPIRoom.Playlist.Single(i => i.ID == item.ID))] = new PlaylistItem(item);
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
} }
@ -479,6 +489,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
Debug.Assert(APIRoom != null); Debug.Assert(APIRoom != null);
Debug.Assert(serverSideAPIRoom != null);
var item = serverSidePlaylist.Find(i => i.ID == playlistItemId); var item = serverSidePlaylist.Find(i => i.ID == playlistItemId);
@ -495,6 +506,7 @@ namespace osu.Game.Tests.Visual.Multiplayer
throw new InvalidOperationException("Attempted to remove an item which has already been played."); throw new InvalidOperationException("Attempted to remove an item which has already been played.");
serverSidePlaylist.Remove(item); serverSidePlaylist.Remove(item);
serverSideAPIRoom.Playlist.RemoveAll(i => i.ID == item.ID);
await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false); await ((IMultiplayerClient)this).PlaylistItemRemoved(playlistItemId).ConfigureAwait(false);
await updateCurrentItem(Room).ConfigureAwait(false); await updateCurrentItem(Room).ConfigureAwait(false);
@ -576,10 +588,12 @@ namespace osu.Game.Tests.Visual.Multiplayer
private async Task addItem(MultiplayerPlaylistItem item) private async Task addItem(MultiplayerPlaylistItem item)
{ {
Debug.Assert(Room != null); Debug.Assert(Room != null);
Debug.Assert(serverSideAPIRoom != null);
item.ID = ++lastPlaylistItemId; item.ID = ++lastPlaylistItemId;
serverSidePlaylist.Add(item); serverSidePlaylist.Add(item);
serverSideAPIRoom.Playlist.Add(new PlaylistItem(item));
await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false); await ((IMultiplayerClient)this).PlaylistItemAdded(item).ConfigureAwait(false);
await updatePlaylistOrder(Room).ConfigureAwait(false); await updatePlaylistOrder(Room).ConfigureAwait(false);
@ -603,6 +617,8 @@ namespace osu.Game.Tests.Visual.Multiplayer
private async Task updatePlaylistOrder(MultiplayerRoom room) private async Task updatePlaylistOrder(MultiplayerRoom room)
{ {
Debug.Assert(serverSideAPIRoom != null);
List<MultiplayerPlaylistItem> orderedActiveItems; List<MultiplayerPlaylistItem> orderedActiveItems;
switch (room.Settings.QueueMode) switch (room.Settings.QueueMode)
@ -648,6 +664,10 @@ namespace osu.Game.Tests.Visual.Multiplayer
await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false); await ((IMultiplayerClient)this).PlaylistItemChanged(item).ConfigureAwait(false);
} }
// Also ensure that the API room's playlist is correct.
foreach (var item in serverSideAPIRoom.Playlist)
item.PlaylistOrder = serverSidePlaylist.Single(i => i.ID == item.ID).PlaylistOrder;
} }
} }
} }

View File

@ -36,8 +36,8 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Realm" Version="10.10.0" /> <PackageReference Include="Realm" Version="10.10.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.404.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
<PackageReference Include="Sentry" Version="3.14.1" /> <PackageReference Include="Sentry" Version="3.14.1" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,8 +61,8 @@
<Reference Include="System.Net.Http" /> <Reference Include="System.Net.Http" />
</ItemGroup> </ItemGroup>
<ItemGroup Label="Package References"> <ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework.iOS" Version="2022.404.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2022.405.0" />
</ItemGroup> </ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) --> <!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net6.0) -->
<PropertyGroup> <PropertyGroup>
@ -84,7 +84,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="5.0.14" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="5.0.14" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="ppy.osu.Framework" Version="2022.325.0" /> <PackageReference Include="ppy.osu.Framework" Version="2022.404.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" /> <PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" /> <PackageReference Include="NUnit" Version="3.13.2" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" /> <PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />