1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-21 12:57:20 +08:00

Merge branch 'master' into extended-statistics-without-replay

This commit is contained in:
Dean Herbert 2022-02-03 18:59:48 +09:00
commit df9d99f5aa
34 changed files with 784 additions and 55 deletions

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2022.128.0" />
</ItemGroup>
<ItemGroup Label="Transitive Dependencies">

View File

@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Catch.Mods
public override double ScoreMultiplier => 1.12;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableNumber<float> SizeMultiplier { get; } = new BindableNumber<float>
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods
public override Type[] IncompatibleMods => new[] { typeof(ModHidden) };
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableNumber<float> SizeMultiplier { get; } = new BindableNumber<float>
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 3f,

View File

@ -0,0 +1,27 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Rulesets.Osu.Mods;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAimAssist : OsuModTestScene
{
[TestCase(0.1f)]
[TestCase(0.5f)]
[TestCase(1)]
public void TestAimAssist(float strength)
{
CreateModTest(new ModTestData
{
Mod = new OsuModAimAssist
{
AssistStrength = { Value = strength },
},
PassCondition = () => true,
Autoplay = false,
});
}
}
}

View File

@ -0,0 +1,154 @@
// 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 NUnit.Framework;
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Timing;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Replays;
using osuTK;
namespace osu.Game.Rulesets.Osu.Tests.Mods
{
public class TestSceneOsuModAlternate : OsuModTestScene
{
[Test]
public void TestInputAtIntro() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 1000,
Position = new Vector2(100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(200), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(200)),
new OsuReplayFrame(1000, new Vector2(100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputAlternating() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 4,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
new HitCircle
{
StartTime = 1500,
Position = new Vector2(300, 100),
},
new HitCircle
{
StartTime = 2000,
Position = new Vector2(400, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.RightButton),
new OsuReplayFrame(1001, new Vector2(200, 100)),
new OsuReplayFrame(1500, new Vector2(300, 100), OsuAction.LeftButton),
new OsuReplayFrame(1501, new Vector2(300, 100)),
new OsuReplayFrame(2000, new Vector2(400, 100), OsuAction.RightButton),
new OsuReplayFrame(2001, new Vector2(400, 100)),
}
});
[Test]
public void TestInputSingular() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 0 && Player.ScoreProcessor.HighestCombo.Value == 1,
Autoplay = false,
Beatmap = new Beatmap
{
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 1000,
Position = new Vector2(200, 100),
},
},
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(1000, new Vector2(200, 100), OsuAction.LeftButton),
}
});
[Test]
public void TestInputSingularWithBreak() => CreateModTest(new ModTestData
{
Mod = new OsuModAlternate(),
PassCondition = () => Player.ScoreProcessor.Combo.Value == 2,
Autoplay = false,
Beatmap = new Beatmap
{
Breaks = new List<BreakPeriod>
{
new BreakPeriod(500, 2250),
},
HitObjects = new List<HitObject>
{
new HitCircle
{
StartTime = 500,
Position = new Vector2(100),
},
new HitCircle
{
StartTime = 2500,
Position = new Vector2(100),
}
}
},
ReplayFrames = new List<ReplayFrame>
{
new OsuReplayFrame(500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(501, new Vector2(100)),
new OsuReplayFrame(2500, new Vector2(100), OsuAction.LeftButton),
new OsuReplayFrame(2501, new Vector2(100)),
}
});
}
}

View File

@ -0,0 +1,83 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Utils;
using osu.Game.Configuration;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.UI;
using osuTK;
namespace osu.Game.Rulesets.Osu.Mods
{
internal class OsuModAimAssist : Mod, IUpdatableByPlayfield, IApplicableToDrawableRuleset<OsuHitObject>
{
public override string Name => "Aim Assist";
public override string Acronym => "AA";
public override IconUsage? Icon => FontAwesome.Solid.MousePointer;
public override ModType Type => ModType.Fun;
public override string Description => "No need to chase the circle the circle chases you!";
public override double ScoreMultiplier => 1;
public override Type[] IncompatibleMods => new[] { typeof(OsuModAutopilot), typeof(OsuModWiggle), typeof(OsuModTransform), typeof(ModAutoplay) };
private IFrameStableClock gameplayClock;
[SettingSource("Assist strength", "How much this mod will assist you.", 0)]
public BindableFloat AssistStrength { get; } = new BindableFloat(0.5f)
{
Precision = 0.05f,
MinValue = 0.05f,
MaxValue = 1.0f,
};
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
gameplayClock = drawableRuleset.FrameStableClock;
// Hide judgment displays and follow points as they won't make any sense.
// Judgements can potentially be turned on in a future where they display at a position relative to their drawable counterpart.
drawableRuleset.Playfield.DisplayJudgements.Value = false;
(drawableRuleset.Playfield as OsuPlayfield)?.FollowPoints.Hide();
}
public void Update(Playfield playfield)
{
var cursorPos = playfield.Cursor.ActiveCursor.DrawPosition;
foreach (var drawable in playfield.HitObjectContainer.AliveObjects)
{
switch (drawable)
{
case DrawableHitCircle circle:
easeTo(circle, cursorPos);
break;
case DrawableSlider slider:
if (!slider.HeadCircle.Result.HasResult)
easeTo(slider, cursorPos);
else
easeTo(slider, cursorPos - slider.Ball.DrawPosition);
break;
}
}
}
private void easeTo(DrawableHitObject hitObject, Vector2 destination)
{
double dampLength = Interpolation.Lerp(3000, 40, AssistStrength.Value);
float x = (float)Interpolation.DampContinuously(hitObject.X, destination.X, dampLength, gameplayClock.ElapsedFrameTime);
float y = (float)Interpolation.DampContinuously(hitObject.Y, destination.Y, dampLength, gameplayClock.ElapsedFrameTime);
hitObject.Position = new Vector2(x, y);
}
}
}

View File

@ -0,0 +1,106 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Input.Bindings;
using osu.Framework.Input.Events;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play;
namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAlternate : Mod, IApplicableToDrawableRuleset<OsuHitObject>, IApplicableToPlayer
{
public override string Name => @"Alternate";
public override string Acronym => @"AL";
public override string Description => @"Don't use the same key twice in a row!";
public override double ScoreMultiplier => 1.0;
public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay) };
public override ModType Type => ModType.Conversion;
public override IconUsage? Icon => FontAwesome.Solid.Keyboard;
private double firstObjectValidJudgementTime;
private IBindable<bool> isBreakTime;
private const double flash_duration = 1000;
private OsuAction? lastActionPressed;
private DrawableRuleset<OsuHitObject> ruleset;
private IFrameStableClock gameplayClock;
public void ApplyToDrawableRuleset(DrawableRuleset<OsuHitObject> drawableRuleset)
{
ruleset = drawableRuleset;
drawableRuleset.KeyBindingInputManager.Add(new InputInterceptor(this));
var firstHitObject = ruleset.Objects.FirstOrDefault();
firstObjectValidJudgementTime = (firstHitObject?.StartTime ?? 0) - (firstHitObject?.HitWindows.WindowFor(HitResult.Meh) ?? 0);
gameplayClock = drawableRuleset.FrameStableClock;
}
public void ApplyToPlayer(Player player)
{
isBreakTime = player.IsBreakTime.GetBoundCopy();
isBreakTime.ValueChanged += e =>
{
if (e.NewValue)
lastActionPressed = null;
};
}
private bool checkCorrectAction(OsuAction action)
{
if (isBreakTime.Value)
return true;
if (gameplayClock.CurrentTime < firstObjectValidJudgementTime)
return true;
switch (action)
{
case OsuAction.LeftButton:
case OsuAction.RightButton:
break;
// Any action which is not left or right button should be ignored.
default:
return true;
}
if (lastActionPressed != action)
{
// User alternated correctly.
lastActionPressed = action;
return true;
}
ruleset.Cursor.FlashColour(Colour4.Red, flash_duration, Easing.OutQuint);
return false;
}
private class InputInterceptor : Component, IKeyBindingHandler<OsuAction>
{
private readonly OsuModAlternate mod;
public InputInterceptor(OsuModAlternate mod)
{
this.mod = mod;
}
public bool OnPressed(KeyBindingPressEvent<OsuAction> e)
// if the pressed action is incorrect, block it from reaching gameplay.
=> !mod.checkCorrectAction(e.Action);
public void OnReleased(KeyBindingReleaseEvent<OsuAction> e)
{
}
}
}
}

View File

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

View File

@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModAutoplay : ModAutoplay
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods
{
public class OsuModCinema : ModCinema<OsuHitObject>
{
public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray();
public override Type[] IncompatibleMods => base.IncompatibleMods.Concat(new[] { typeof(OsuModAimAssist), typeof(OsuModAutopilot), typeof(OsuModSpunOut) }).ToArray();
public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList<Mod> mods) => new Score
{

View File

@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods
};
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableNumber<float> SizeMultiplier { get; } = new BindableNumber<float>
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 2f,

View File

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

View File

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

View File

@ -169,6 +169,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModClassic(),
new OsuModRandom(),
new OsuModMirror(),
new OsuModAlternate(),
};
case ModType.Automation:
@ -193,6 +194,7 @@ namespace osu.Game.Rulesets.Osu
new OsuModApproachDifferent(),
new OsuModMuted(),
new OsuModNoScope(),
new OsuModAimAssist(),
};
case ModType.System:

View File

@ -31,7 +31,8 @@ namespace osu.Game.Rulesets.Osu.UI
private readonly ProxyContainer approachCircles;
private readonly ProxyContainer spinnerProxies;
private readonly JudgementContainer<DrawableOsuJudgement> judgementLayer;
private readonly FollowPointRenderer followPoints;
public FollowPointRenderer FollowPoints { get; }
public static readonly Vector2 BASE_SIZE = new Vector2(512, 384);
@ -50,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI
{
playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both },
spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both },
followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
FollowPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both },
judgementLayer = new JudgementContainer<DrawableOsuJudgement> { RelativeSizeAxes = Axes.Both },
HitObjectContainer,
judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both },
@ -131,13 +132,13 @@ namespace osu.Game.Rulesets.Osu.UI
protected override void OnHitObjectAdded(HitObject hitObject)
{
base.OnHitObjectAdded(hitObject);
followPoints.AddFollowPoints((OsuHitObject)hitObject);
FollowPoints.AddFollowPoints((OsuHitObject)hitObject);
}
protected override void OnHitObjectRemoved(HitObject hitObject)
{
base.OnHitObjectRemoved(hitObject);
followPoints.RemoveFollowPoints((OsuHitObject)hitObject);
FollowPoints.RemoveFollowPoints((OsuHitObject)hitObject);
}
private void onNewResult(DrawableHitObject judgedObject, JudgementResult result)

View File

@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Mods
public override double ScoreMultiplier => 1.12;
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public override BindableNumber<float> SizeMultiplier { get; } = new BindableNumber<float>
public override BindableFloat SizeMultiplier { get; } = new BindableFloat
{
MinValue = 0.5f,
MaxValue = 1.5f,

View File

@ -15,8 +15,12 @@ using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Taiko;
using osu.Game.Rulesets.Taiko.Mods;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.OnlinePlay.Multiplayer;
using osu.Game.Screens.OnlinePlay.Multiplayer.Match;
using osu.Game.Screens.OnlinePlay.Multiplayer.Participants;
using osu.Game.Tests.Beatmaps;
using osu.Game.Tests.Resources;
@ -77,6 +81,27 @@ namespace osu.Game.Tests.Visual.Multiplayer
AddUntilStep("wait for join", () => RoomJoined);
}
[Test]
public void TestTaikoOnlyMod()
{
AddStep("add playlist item", () =>
{
SelectedRoom.Value.Playlist.Add(new PlaylistItem
{
Beatmap = { Value = new TestBeatmap(new TaikoRuleset().RulesetInfo).BeatmapInfo },
Ruleset = { Value = new TaikoRuleset().RulesetInfo },
AllowedMods = { new TaikoModSwap() }
});
});
ClickButtonWhenEnabled<MultiplayerMatchSettingsOverlay.CreateOrUpdateButton>();
AddUntilStep("wait for join", () => RoomJoined);
AddStep("select swap mod", () => Client.ChangeUserMods(API.LocalUser.Value.OnlineID, new[] { new TaikoModSwap() }));
AddUntilStep("participant panel has mod", () => this.ChildrenOfType<ParticipantPanel>().Any(p => p.ChildrenOfType<ModIcon>().Any(m => m.Mod is TaikoModSwap)));
}
[Test]
public void TestSettingValidity()
{

View File

@ -128,6 +128,7 @@ namespace osu.Game.Tests.Visual.Navigation
AddStep("choose clear all scores", () => InputManager.Key(Key.Number4));
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);
@ -172,6 +173,7 @@ namespace osu.Game.Tests.Visual.Navigation
InputManager.Click(MouseButton.Left);
});
AddUntilStep("wait for dialog display", () => Game.Dependencies.Get<DialogOverlay>().IsLoaded);
AddUntilStep("wait for dialog", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog != null);
AddStep("confirm deletion", () => InputManager.Key(Key.Number1));
AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get<DialogOverlay>().CurrentDialog == null);

View File

@ -601,7 +601,7 @@ namespace osu.Game.Tests.Visual.SongSelect
{
BeatmapSetInfo testMixed = null;
createCarousel();
createCarousel(new List<BeatmapSetInfo>());
AddStep("add mixed ruleset beatmapset", () =>
{
@ -765,22 +765,22 @@ namespace osu.Game.Tests.Visual.SongSelect
{
bool changed = false;
createCarousel(c =>
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 1; i <= (count ?? set_count); i++)
{
beatmapSets.Add(randomDifficulties
? TestResources.CreateTestBeatmapSetInfo()
: TestResources.CreateTestBeatmapSetInfo(3));
}
}
createCarousel(beatmapSets, c =>
{
carouselAdjust?.Invoke(c);
if (beatmapSets == null)
{
beatmapSets = new List<BeatmapSetInfo>();
for (int i = 1; i <= (count ?? set_count); i++)
{
beatmapSets.Add(randomDifficulties
? TestResources.CreateTestBeatmapSetInfo()
: TestResources.CreateTestBeatmapSetInfo(3));
}
}
carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria());
carousel.BeatmapSetsChanged = () => changed = true;
carousel.BeatmapSets = beatmapSets;
@ -789,7 +789,7 @@ namespace osu.Game.Tests.Visual.SongSelect
AddUntilStep("Wait for load", () => changed);
}
private void createCarousel(Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
private void createCarousel(List<BeatmapSetInfo> beatmapSets, Action<BeatmapCarousel> carouselAdjust = null, Container target = null)
{
AddStep("Create carousel", () =>
{
@ -803,6 +803,8 @@ namespace osu.Game.Tests.Visual.SongSelect
carouselAdjust?.Invoke(carousel);
carousel.BeatmapSets = beatmapSets;
(target ?? this).Child = carousel;
});
}

View File

@ -197,7 +197,24 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[]
{
new OsuModHidden(),
new OsuModHardRock(),
new OsuModFlashlight
{
FollowDelay = { Value = 200 },
SizeMultiplier = { Value = 5 },
},
new OsuModDifficultyAdjust
{
CircleSize = { Value = 11 },
ApproachRate = { Value = 10 },
OverallDifficulty = { Value = 10 },
DrainRate = { Value = 10 },
ExtendedLimits = { Value = true }
}
},
Ruleset = new OsuRuleset().RulesetInfo,
BeatmapInfo = beatmapInfo,
User = new APIUser
@ -217,7 +234,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
User = new APIUser
@ -237,7 +254,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -258,7 +275,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -279,7 +296,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 1,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -300,7 +317,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9826,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -321,7 +338,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.9654,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -342,7 +359,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.6025,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -363,7 +380,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.5140,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,
@ -384,7 +401,7 @@ namespace osu.Game.Tests.Visual.SongSelect
Accuracy = 0.4222,
MaxCombo = 244,
TotalScore = 1707827,
//Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), },
BeatmapInfo = beatmapInfo,
Ruleset = new OsuRuleset().RulesetInfo,

View File

@ -30,6 +30,7 @@ namespace osu.Game.Beatmaps.Drawables
{
background = new Box
{
Alpha = 0.9f,
RelativeSizeAxes = Axes.Both
},
new FillFlowContainer

View File

@ -10,6 +10,8 @@ using osu.Framework.Graphics.Sprites;
using osu.Game.Configuration;
using System;
using JetBrains.Annotations;
using osu.Framework.Audio;
using osu.Framework.Audio.Sample;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Textures;
using osu.Framework.Input.Events;
@ -30,13 +32,17 @@ namespace osu.Game.Graphics.Cursor
private DragRotationState dragRotationState;
private Vector2 positionMouseDown;
private Sample tapSample;
[BackgroundDependencyLoader(true)]
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager)
private void load([NotNull] OsuConfigManager config, [CanBeNull] ScreenshotManager screenshotManager, AudioManager audio)
{
cursorRotate = config.GetBindable<bool>(OsuSetting.CursorRotation);
if (screenshotManager != null)
screenshotCursorVisibility.BindTo(screenshotManager.CursorVisibility);
tapSample = audio.Samples.Get(@"UI/cursor-tap");
}
protected override bool OnMouseMove(MouseMoveEvent e)
@ -87,6 +93,8 @@ namespace osu.Game.Graphics.Cursor
dragRotationState = DragRotationState.DragStarted;
positionMouseDown = e.MousePosition;
}
playTapSample();
}
return base.OnMouseDown(e);
@ -104,6 +112,9 @@ namespace osu.Game.Graphics.Cursor
activeCursor.RotateTo(0, 600 * (1 + Math.Abs(activeCursor.Rotation / 720)), Easing.OutElasticHalf);
dragRotationState = DragRotationState.NotDragging;
}
if (State.Value == Visibility.Visible)
playTapSample(0.8);
}
base.OnMouseUp(e);
@ -121,6 +132,18 @@ namespace osu.Game.Graphics.Cursor
activeCursor.ScaleTo(0.6f, 250, Easing.In);
}
private void playTapSample(double baseFrequency = 1f)
{
const float random_range = 0.02f;
SampleChannel channel = tapSample.GetChannel();
// Scale to [-0.75, 0.75] so that the sample isn't fully panned left or right (sounds weird)
channel.Balance.Value = ((activeCursor.X / DrawWidth) * 2 - 1) * 0.75;
channel.Frequency.Value = baseFrequency - (random_range / 2f) + RNG.NextDouble(random_range);
channel.Play();
}
public class Cursor : Container
{
private Container cursorContainer;

View File

@ -399,7 +399,10 @@ namespace osu.Game.Online.API
lock (queue)
{
if (state.Value == APIState.Offline)
{
request.Fail(new WebException(@"User not logged in"));
return;
}
queue.Enqueue(request);
}
@ -416,7 +419,7 @@ namespace osu.Game.Online.API
if (failOldRequests)
{
foreach (var req in oldQueueRequests)
req.Fail(new WebException(@"Disconnected from server"));
req.Fail(new WebException($@"Request failed from flush operation (state {state.Value})"));
}
}
}

View File

@ -32,7 +32,7 @@ using osu.Game.Utils;
namespace osu.Game.Online.Leaderboards
{
public class LeaderboardScore : OsuClickableContainer, IHasContextMenu
public class LeaderboardScore : OsuClickableContainer, IHasContextMenu, IHasCustomTooltip<ScoreInfo>
{
public const float HEIGHT = 60;
@ -70,6 +70,9 @@ namespace osu.Game.Online.Leaderboards
[Resolved]
private Storage storage { get; set; }
public ITooltip<ScoreInfo> GetCustomTooltip() => new LeaderboardScoreTooltip();
public virtual ScoreInfo TooltipContent => Score;
public LeaderboardScore(ScoreInfo score, int? rank, bool isOnlineScope = true)
{
Score = score;
@ -183,7 +186,6 @@ namespace osu.Game.Online.Leaderboards
Anchor = Anchor.BottomLeft,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10f, 0f),
Margin = new MarginPadding { Left = edge_margin },
Children = statisticsLabels
},
@ -228,7 +230,6 @@ namespace osu.Game.Online.Leaderboards
Origin = Anchor.BottomRight,
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(1),
ChildrenEnumerable = Score.Mods.Select(mod => new ModIcon(mod) { Scale = new Vector2(0.375f) })
},
},
@ -313,6 +314,7 @@ namespace osu.Game.Online.Leaderboards
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Padding = new MarginPadding { Right = 10 },
Children = new Drawable[]
{
new Container

View File

@ -0,0 +1,219 @@
// 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.Shapes;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Cursor;
using osu.Game.Scoring;
using osuTK;
using osu.Game.Graphics.Sprites;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.UI;
#nullable enable
namespace osu.Game.Online.Leaderboards
{
public class LeaderboardScoreTooltip : VisibilityContainer, ITooltip<ScoreInfo>
{
private OsuSpriteText timestampLabel = null!;
private FillFlowContainer<HitResultCell> topScoreStatistics = null!;
private FillFlowContainer<HitResultCell> bottomScoreStatistics = null!;
private FillFlowContainer<ModCell> modStatistics = null!;
public LeaderboardScoreTooltip()
{
AutoSizeAxes = Axes.Both;
AutoSizeDuration = 200;
AutoSizeEasing = Easing.OutQuint;
Masking = true;
CornerRadius = 5;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChildren = new Drawable[]
{
new Box
{
RelativeSizeAxes = Axes.Both,
Alpha = 0.9f,
Colour = colours.Gray3,
},
new FillFlowContainer
{
Margin = new MarginPadding(5),
Spacing = new Vector2(10),
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// Info row
timestampLabel = new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
},
// Mods row
modStatistics = new FillFlowContainer<ModCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Spacing = new Vector2(5, 0),
},
new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
// Actual stats rows
topScoreStatistics = new FillFlowContainer<HitResultCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
},
bottomScoreStatistics = new FillFlowContainer<HitResultCell>
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
},
}
},
}
}
};
}
private ScoreInfo? displayedScore;
public void SetContent(ScoreInfo score)
{
if (displayedScore?.Equals(score) == true)
return;
displayedScore = score;
timestampLabel.Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}";
modStatistics.Clear();
topScoreStatistics.Clear();
bottomScoreStatistics.Clear();
foreach (var mod in score.Mods)
{
modStatistics.Add(new ModCell(mod));
}
foreach (var result in score.GetStatisticsForDisplay())
{
if (result.Result > HitResult.Perfect)
bottomScoreStatistics.Add(new HitResultCell(result));
else
topScoreStatistics.Add(new HitResultCell(result));
}
}
protected override void PopIn() => this.FadeIn(20, Easing.OutQuint);
protected override void PopOut() => this.FadeOut(80, Easing.OutQuint);
public void Move(Vector2 pos) => Position = pos;
private class HitResultCell : CompositeDrawable
{
private readonly string displayName;
private readonly HitResult result;
private readonly int count;
public HitResultCell(HitResultDisplayStatistic stat)
{
AutoSizeAxes = Axes.Both;
displayName = stat.DisplayName;
result = stat.Result;
count = stat.Count;
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
InternalChild = new FillFlowContainer
{
Height = 12,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(5f, 0f),
Children = new Drawable[]
{
new OsuSpriteText
{
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold),
Text = displayName.ToUpperInvariant(),
Colour = colours.ForHitResult(result),
},
new OsuSpriteText
{
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = count.ToString(),
},
}
};
}
}
private class ModCell : CompositeDrawable
{
private readonly Mod mod;
public ModCell(Mod mod)
{
AutoSizeAxes = Axes.Both;
this.mod = mod;
}
[BackgroundDependencyLoader]
private void load()
{
FillFlowContainer container;
InternalChild = container = new FillFlowContainer
{
Height = 15,
AutoSizeAxes = Axes.X,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(2f, 0f),
Children = new Drawable[]
{
new ModIcon(mod, showTooltip: false).With(icon =>
{
icon.Origin = Anchor.CentreLeft;
icon.Anchor = Anchor.CentreLeft;
icon.Scale = new Vector2(15f / icon.Height);
}),
}
};
string description = mod.SettingDescription;
if (!string.IsNullOrEmpty(description))
{
container.Add(new OsuSpriteText
{
RelativeSizeAxes = Axes.Y,
Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold),
Text = mod.SettingDescription,
Origin = Anchor.CentreLeft,
Anchor = Anchor.CentreLeft,
Margin = new MarginPadding { Top = 1 },
});
}
}
}
}
}

View File

@ -105,6 +105,7 @@ namespace osu.Game.Rulesets.Mods
{
ShowsDefaultIndicator = false,
Current = currentNumber,
KeyboardStep = 0.1f,
}
};

View File

@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Mods
public override string Description => "Restricted view area.";
[SettingSource("Flashlight size", "Multiplier applied to the default flashlight size.")]
public abstract BindableNumber<float> SizeMultiplier { get; }
public abstract BindableFloat SizeMultiplier { get; }
[SettingSource("Change size based on combo", "Decrease the flashlight size as combo increases.")]
public abstract BindableBool ComboBasedSize { get; }

View File

@ -149,6 +149,10 @@ namespace osu.Game.Rulesets
var instanceInfo = (Activator.CreateInstance(resolvedType) as Ruleset)?.RulesetInfo
?? throw new RulesetLoadException(@"Instantiation failure");
// If a ruleset isn't up-to-date with the API, it could cause a crash at an arbitrary point of execution.
// To eagerly handle cases of missing implementations, enumerate all types here and mark as non-available on throw.
resolvedType.Assembly.GetTypes();
r.Name = instanceInfo.Name;
r.ShortName = instanceInfo.ShortName;
r.InstantiationInfo = instanceInfo.InstantiationInfo;

View File

@ -14,6 +14,8 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components
{
private readonly APIUserScoreAggregate score;
public override ScoreInfo TooltipContent => null; // match aggregate scores can't show statistics that the custom tooltip displays.
public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool isOnlineScope = true)
: base(score.CreateScoreInfo(), rank, isOnlineScope)
{

View File

@ -2,6 +2,7 @@
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface;
using osu.Game.Online;
using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
using osu.Game.Online.Rooms;
using osu.Game.Rulesets;
using osu.Game.Screens.Play.HUD;
using osu.Game.Users;
@ -184,8 +186,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants
const double fade_time = 50;
// Todo: Should use the room's selected item to determine ruleset.
var ruleset = rulesets.GetRuleset(0)?.CreateInstance();
var currentItem = Playlist.GetCurrentItem();
Debug.Assert(currentItem != null);
var ruleset = rulesets.GetRuleset(currentItem.RulesetID)?.CreateInstance();
int? currentModeRank = ruleset != null ? User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank : null;
userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty;

View File

@ -17,6 +17,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays;
using osu.Game.Overlays.Notifications;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
using osu.Game.Rulesets.UI;
using osu.Game.Screens.Play.HUD;
using osu.Game.Skinning;
@ -83,10 +84,7 @@ namespace osu.Game.Screens.Play
Children = new Drawable[]
{
CreateFailingLayer(),
mainComponents = new SkinnableTargetContainer(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both,
},
mainComponents = new MainComponentsContainer(),
topRightElements = new FillFlowContainer
{
Anchor = Anchor.TopRight,
@ -325,5 +323,29 @@ namespace osu.Game.Screens.Play
break;
}
}
private class MainComponentsContainer : SkinnableTargetContainer
{
private Bindable<ScoringMode> scoringMode;
[Resolved]
private OsuConfigManager config { get; set; }
public MainComponentsContainer()
: base(SkinnableTarget.MainHUDComponents)
{
RelativeSizeAxes = Axes.Both;
}
protected override void LoadComplete()
{
base.LoadComplete();
// When the scoring mode changes, relative positions of elements may change (see DefaultSkin.GetDrawableComponent).
// This is a best effort implementation for cases where users haven't customised layouts.
scoringMode = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode);
scoringMode.BindValueChanged(val => Reload());
}
}
}
}

View File

@ -5,8 +5,12 @@ using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses;
using osu.Game.Replays;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Replays;
using osu.Game.Scoring;
namespace osu.Game.Tests.Visual
{
@ -50,18 +54,37 @@ namespace osu.Game.Tests.Visual
return CreateModPlayer(ruleset);
}
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(AllowFail);
protected virtual TestPlayer CreateModPlayer(Ruleset ruleset) => new ModTestPlayer(currentTestData, AllowFail);
protected class ModTestPlayer : TestPlayer
{
private readonly bool allowFail;
private readonly ModTestData currentTestData;
protected override bool CheckModsAllowFailure() => allowFail;
public ModTestPlayer(bool allowFail)
public ModTestPlayer(ModTestData data, bool allowFail)
: base(false, false)
{
this.allowFail = allowFail;
currentTestData = data;
}
protected override void PrepareReplay()
{
if (currentTestData.Autoplay && currentTestData.ReplayFrames?.Count > 0)
throw new InvalidOperationException(@$"{nameof(ModTestData.Autoplay)} must be false when {nameof(ModTestData.ReplayFrames)} is specified.");
if (currentTestData.ReplayFrames != null)
{
DrawableRuleset?.SetReplayScore(new Score
{
Replay = new Replay { Frames = currentTestData.ReplayFrames },
ScoreInfo = new ScoreInfo { User = new APIUser { Username = @"Test" } },
});
}
base.PrepareReplay();
}
}
@ -72,6 +95,12 @@ namespace osu.Game.Tests.Visual
/// </summary>
public bool Autoplay = true;
/// <summary>
/// The frames to use for replay. <see cref="Autoplay"/> must be set to false.
/// </summary>
[CanBeNull]
public List<ReplayFrame> ReplayFrames;
/// <summary>
/// The beatmap for this test case.
/// </summary>

View File

@ -37,7 +37,7 @@
</PackageReference>
<PackageReference Include="Realm" Version="10.8.0" />
<PackageReference Include="ppy.osu.Framework" Version="2022.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
<PackageReference Include="Sentry" Version="3.13.0" />
<PackageReference Include="SharpCompress" Version="0.30.1" />
<PackageReference Include="NUnit" Version="3.13.2" />

View File

@ -61,7 +61,7 @@
</ItemGroup>
<ItemGroup Label="Package References">
<PackageReference Include="ppy.osu.Framework.iOS" Version="2022.128.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.115.0" />
<PackageReference Include="ppy.osu.Game.Resources" Version="2022.202.0" />
</ItemGroup>
<!-- See https://github.com/dotnet/runtime/issues/35988 (can be removed after Xamarin uses net5.0 / net6.0) -->
<PropertyGroup>