Merge branch 'master' into Issue#9170

This commit is contained in:
Dean Herbert 2020-07-28 13:36:42 +09:00 committed by GitHub
commit cc0d5d74d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
114 changed files with 1716 additions and 1030 deletions

View File

@ -5,6 +5,6 @@
"version": "3.1.100" "version": "3.1.100"
}, },
"msbuild-sdks": { "msbuild-sdks": {
"Microsoft.Build.Traversal": "2.0.50" "Microsoft.Build.Traversal": "2.0.52"
} }
} }

View File

@ -51,7 +51,7 @@
<Reference Include="Java.Interop" /> <Reference Include="Java.Interop" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="ppy.osu.Game.Resources" Version="2020.715.0" /> <PackageReference Include="ppy.osu.Game.Resources" Version="2020.727.1" />
<PackageReference Include="ppy.osu.Framework.Android" Version="2020.714.1" /> <PackageReference Include="ppy.osu.Framework.Android" Version="2020.723.0" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -16,6 +16,7 @@ using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Screens.Menu; using osu.Game.Screens.Menu;
using osu.Game.Updater; using osu.Game.Updater;
using osu.Desktop.Windows;
namespace osu.Desktop namespace osu.Desktop
{ {
@ -98,6 +99,9 @@ namespace osu.Desktop
LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add);
LoadComponentAsync(new DiscordRichPresence(), Add); LoadComponentAsync(new DiscordRichPresence(), Add);
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
LoadComponentAsync(new GameplayWinKeyBlocker(), Add);
} }
protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen)

View File

@ -0,0 +1,41 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Platform;
using osu.Game.Configuration;
namespace osu.Desktop.Windows
{
public class GameplayWinKeyBlocker : Component
{
private Bindable<bool> allowScreenSuspension;
private Bindable<bool> disableWinKey;
private GameHost host;
[BackgroundDependencyLoader]
private void load(GameHost host, OsuConfigManager config)
{
this.host = host;
allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy();
allowScreenSuspension.BindValueChanged(_ => updateBlocking());
disableWinKey = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey);
disableWinKey.BindValueChanged(_ => updateBlocking(), true);
}
private void updateBlocking()
{
bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value;
if (shouldDisable)
host.InputThread.Scheduler.Add(WindowsKey.Disable);
else
host.InputThread.Scheduler.Add(WindowsKey.Enable);
}
}
}

View File

@ -0,0 +1,80 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Runtime.InteropServices;
namespace osu.Desktop.Windows
{
internal class WindowsKey
{
private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam);
private static bool isBlocked;
private const int wh_keyboard_ll = 13;
private const int wm_keydown = 256;
private const int wm_syskeyup = 261;
//Resharper disable once NotAccessedField.Local
private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC
private static IntPtr keyHook;
[StructLayout(LayoutKind.Explicit)]
private readonly struct KdDllHookStruct
{
[FieldOffset(0)]
public readonly int VkCode;
[FieldOffset(8)]
public readonly int Flags;
}
private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam)
{
if (wParam >= wm_keydown && wParam <= wm_syskeyup)
{
switch (lParam.VkCode)
{
case 0x5B: // left windows key
case 0x5C: // right windows key
return 1;
}
}
return callNextHookEx(0, nCode, wParam, ref lParam);
}
internal static void Disable()
{
if (keyHook != IntPtr.Zero || isBlocked)
return;
keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0);
isBlocked = true;
}
internal static void Enable()
{
if (keyHook == IntPtr.Zero || !isBlocked)
return;
keyHook = unhookWindowsHookEx(keyHook);
keyboardHookDelegate = null;
keyHook = IntPtr.Zero;
isBlocked = false;
}
[DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")]
private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId);
[DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")]
private static extern IntPtr unhookWindowsHookEx(IntPtr hHook);
[DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")]
private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam);
}
}

View File

@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods
public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss); public void TestDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new Droplet { StartTime = 1000 }), shouldMiss);
// We only care about testing misses, hits are tested via JuiceStream // We only care about testing misses, hits are tested via JuiceStream
[TestCase(true)] [TestCase(false)]
public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss);
} }
} }

View File

@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty
if (mods.Any(m => m is ModHidden)) if (mods.Any(m => m is ModHidden))
{ {
value *= 1.05 + 0.075 * (10.0 - Math.Min(10.0, Attributes.ApproachRate)); // 7.5% for each AR below 10
// Hiddens gives almost nothing on max approach rate, and more the lower it is // Hiddens gives almost nothing on max approach rate, and more the lower it is
if (approachRate <= 10.0) if (approachRate <= 10.0)
value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10

View File

@ -1,17 +1,11 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Catch.Judgements;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Catch.Mods namespace osu.Game.Rulesets.Catch.Mods
{ {
public class CatchModPerfect : ModPerfect public class CatchModPerfect : ModPerfect
{ {
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> !(result.Judgement is CatchBananaJudgement)
&& base.FailCondition(healthProcessor, result);
} }
} }

View File

@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays
} }
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
if (!Position.HasValue) return new List<IInput>(); if (!Position.HasValue) return;
return new List<IInput> inputs.Add(new CatchReplayState
{ {
new CatchReplayState PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(),
{ CatcherX = Position.Value
PressedActions = CurrentFrame?.Actions ?? new List<CatchAction>(), });
CatcherX = Position.Value
},
};
} }
public class CatchReplayState : ReplayState<CatchAction> public class CatchReplayState : ReplayState<CatchAction>

View File

@ -2,6 +2,7 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
@ -10,6 +11,8 @@ using osu.Game.Replays;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Objects;
using osu.Game.Rulesets.Mania.Replays; using osu.Game.Rulesets.Mania.Replays;
using osu.Game.Rulesets.Mania.Scoring;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests
assertTailJudgement(HitResult.Meh); assertTailJudgement(HitResult.Meh);
} }
[Test]
public void TestMissReleaseAndHitSecondRelease()
{
var windows = new ManiaHitWindows();
windows.SetDifficulty(10);
var beatmap = new Beatmap<ManiaHitObject>
{
HitObjects =
{
new HoldNote
{
StartTime = 1000,
Duration = 500,
Column = 0,
},
new HoldNote
{
StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10,
Duration = 500,
Column = 0,
},
},
BeatmapInfo =
{
BaseDifficulty = new BeatmapDifficulty
{
SliderTickRate = 4,
OverallDifficulty = 10,
},
Ruleset = new ManiaRuleset().RulesetInfo
},
};
performTest(new List<ReplayFrame>
{
new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1),
new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()),
}, beatmap);
AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type == HitResult.Miss));
AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject))
.All(j => j.Type == HitResult.Perfect));
}
private void assertHeadJudgement(HitResult result) private void assertHeadJudgement(HitResult result)
=> AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result);
@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests
private ScoreAccessibleReplayPlayer currentPlayer; private ScoreAccessibleReplayPlayer currentPlayer;
private void performTest(List<ReplayFrame> frames) private void performTest(List<ReplayFrame> frames, Beatmap<ManiaHitObject> beatmap = null)
{ {
AddStep("load player", () => if (beatmap == null)
{ {
Beatmap.Value = CreateWorkingBeatmap(new Beatmap<ManiaHitObject> beatmap = new Beatmap<ManiaHitObject>
{ {
HitObjects = HitObjects =
{ {
@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests
BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 },
Ruleset = new ManiaRuleset().RulesetInfo Ruleset = new ManiaRuleset().RulesetInfo
}, },
}); };
Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f });
}
AddStep("load player", () =>
{
Beatmap.Value = CreateWorkingBeatmap(beatmap);
var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } });

View File

@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables
if (action != Action.Value) if (action != Action.Value)
return false; return false;
// The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed).
// But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time.
// Note: Unlike below, we use the tail's start time to determine the time offset.
if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime))
return false;
beginHoldAt(Time.Current - Head.HitObject.StartTime); beginHoldAt(Time.Current - Head.HitObject.StartTime);
Head.UpdateResult(); Head.UpdateResult();

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays
protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any();
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() } }; public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<ManiaAction> { PressedActions = CurrentFrame?.Actions ?? new List<ManiaAction>() });
}
} }
} }

View File

@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests
const double time_slider = 1500; const double time_slider = 1500;
const double time_circle = 1510; const double time_circle = 1510;
Vector2 positionCircle = Vector2.Zero; Vector2 positionCircle = Vector2.Zero;
Vector2 positionSlider = new Vector2(80); Vector2 positionSlider = new Vector2(30);
var hitObjects = new List<OsuHitObject> var hitObjects = new List<OsuHitObject>
{ {

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests
private void testSingle(float circleSize, bool auto = false) private void testSingle(float circleSize, bool auto = false)
{ {
var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 }; var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 };
spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize });

View File

@ -1,26 +1,28 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework; using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Utils; using osu.Framework.Graphics.Sprites;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Framework.Timing; using osu.Framework.Timing;
using osu.Framework.Utils;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Replays;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics.Sprites;
using osu.Game.Replays;
using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Osu.Replays;
using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays;
using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Storyboards; using osu.Game.Storyboards;
using osu.Game.Tests.Visual;
using osuTK;
using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap;
namespace osu.Game.Rulesets.Osu.Tests namespace osu.Game.Rulesets.Osu.Tests
@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Osu.Tests
protected override bool Autoplay => true; protected override bool Autoplay => true;
protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer();
protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null)
{ {
var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager);
@ -129,18 +133,44 @@ namespace osu.Game.Rulesets.Osu.Tests
.ToList() .ToList()
}; };
[Test]
public void TestSpinnerNormalBonusRewinding()
{
addSeekStep(1000);
AddAssert("player score matching expected bonus score", () =>
{
// multipled by 2 to nullify the score multiplier. (autoplay mod selected)
var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2;
return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 100;
});
addSeekStep(0);
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
}
[Test]
public void TestSpinnerCompleteBonusRewinding()
{
addSeekStep(2500);
addSeekStep(0);
AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0);
}
[Test] [Test]
public void TestSpinPerMinuteOnRewind() public void TestSpinPerMinuteOnRewind()
{ {
double estimatedSpm = 0; double estimatedSpm = 0;
addSeekStep(2500); addSeekStep(1000);
AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute);
addSeekStep(5000); addSeekStep(2000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
addSeekStep(2500); addSeekStep(1000);
AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0));
} }
@ -160,12 +190,17 @@ namespace osu.Game.Rulesets.Osu.Tests
Position = new Vector2(256, 192), Position = new Vector2(256, 192),
EndTime = 6000, EndTime = 6000,
}, },
// placeholder object to avoid hitting the results screen
new HitCircle
{
StartTime = 99999,
}
} }
}; };
private class ScoreExposedPlayer : TestPlayer
{
public new ScoreProcessor ScoreProcessor => base.ScoreProcessor;
public ScoreExposedPlayer()
: base(false, false)
{
}
}
} }
} }

View File

@ -2,6 +2,7 @@
// 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.Linq;
using osuTK; using osuTK;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
@ -11,6 +12,7 @@ using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning;
using osu.Game.Rulesets.Osu.UI;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Game.Skinning; using osu.Game.Skinning;
@ -81,6 +83,42 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
foreach (var drawableHitObject in NestedHitObjects) foreach (var drawableHitObject in NestedHitObjects)
drawableHitObject.AccentColour.Value = colour.NewValue; drawableHitObject.AccentColour.Value = colour.NewValue;
}, true); }, true);
Tracking.BindValueChanged(updateSlidingSample);
}
private SkinnableSound slidingSample;
protected override void LoadSamples()
{
base.LoadSamples();
slidingSample?.Expire();
slidingSample = null;
var firstSample = HitObject.Samples.FirstOrDefault();
if (firstSample != null)
{
var clone = HitObject.SampleControlPoint.ApplyTo(firstSample);
clone.Name = "sliderslide";
AddInternal(slidingSample = new SkinnableSound(clone)
{
Looping = true
});
}
}
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
// note that samples will not start playing if exiting a seek operation in the middle of a slider.
// may be something we want to address at a later point, but not so easy to make happen right now
// (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update).
if (tracking.NewValue && ShouldPlaySamples)
slidingSample?.Play();
else
slidingSample?.Stop();
} }
protected override void AddNestedHitObject(DrawableHitObject hitObject) protected override void AddNestedHitObject(DrawableHitObject hitObject)
@ -156,6 +194,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Tracking.Value = Ball.Tracking; Tracking.Value = Ball.Tracking;
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X);
double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1);
Ball.UpdateProgress(completionProgress); Ball.UpdateProgress(completionProgress);

View File

@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
private readonly Drawable scaleContainer; private readonly Drawable scaleContainer;
public override bool DisplayResult => false;
public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider)
: base(sliderRepeat) : base(sliderRepeat)
{ {

View File

@ -14,6 +14,7 @@ using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Sprites;
using osu.Game.Rulesets.Objects;
using osu.Framework.Utils; using osu.Framework.Utils;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Screens.Ranking; using osu.Game.Screens.Ranking;
@ -24,9 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
protected readonly Spinner Spinner; protected readonly Spinner Spinner;
private readonly Container<DrawableSpinnerTick> ticks;
public readonly SpinnerDisc Disc; public readonly SpinnerDisc Disc;
public readonly SpinnerTicks Ticks; public readonly SpinnerTicks Ticks;
public readonly SpinnerSpmCounter SpmCounter; public readonly SpinnerSpmCounter SpmCounter;
private readonly SpinnerBonusDisplay bonusDisplay;
private readonly Container mainContainer; private readonly Container mainContainer;
@ -60,6 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
ticks = new Container<DrawableSpinnerTick>(),
circleContainer = new Container circleContainer = new Container
{ {
AutoSizeAxes = Axes.Both, AutoSizeAxes = Axes.Both,
@ -93,7 +98,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
{ {
Background = new SpinnerBackground Background = new SpinnerBackground
{ {
Alpha = 0.6f, Disc =
{
Alpha = 0f,
},
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Origin = Anchor.Centre, Origin = Anchor.Centre,
}, },
@ -117,18 +125,56 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
Origin = Anchor.Centre, Origin = Anchor.Centre,
Y = 120, Y = 120,
Alpha = 0 Alpha = 0
},
bonusDisplay = new SpinnerBonusDisplay
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Y = -120,
} }
}; };
} }
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
switch (hitObject)
{
case DrawableSpinnerTick tick:
ticks.Add(tick);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
ticks.Clear();
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case SpinnerBonusTick bonusTick:
return new DrawableSpinnerBonusTick(bonusTick);
case SpinnerTick tick:
return new DrawableSpinnerTick(tick);
}
return base.CreateNestedHitObject(hitObject);
}
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(OsuColour colours) private void load(OsuColour colours)
{ {
normalColour = baseColour; normalColour = baseColour;
completeColour = colours.YellowLight;
Background.AccentColour = normalColour; Background.AccentColour = normalColour;
Ticks.AccentColour = normalColour;
completeColour = colours.YellowLight.Opacity(0.75f);
Disc.AccentColour = fillColour; Disc.AccentColour = fillColour;
circle.Colour = colours.BlueDark; circle.Colour = colours.BlueDark;
@ -147,21 +193,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
if (Progress >= 1 && !Disc.Complete) if (Progress >= 1 && !Disc.Complete)
{ {
Disc.Complete = true; Disc.Complete = true;
transformFillColour(completeColour, 200);
const float duration = 200;
Disc.FadeAccent(completeColour, duration);
Background.FadeAccent(completeColour, duration);
Background.FadeOut(duration);
circle.FadeColour(completeColour, duration);
glow.FadeColour(completeColour, duration);
} }
if (userTriggered || Time.Current < Spinner.EndTime) if (userTriggered || Time.Current < Spinner.EndTime)
return; return;
// Trigger a miss result for remaining ticks to avoid infinite gameplay.
foreach (var tick in ticks.Where(t => !t.IsHit))
tick.TriggerResult(false);
ApplyResult(r => ApplyResult(r =>
{ {
if (Progress >= 1) if (Progress >= 1)
@ -191,8 +232,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
circle.Rotation = Disc.Rotation; circle.Rotation = Disc.Rotation;
Ticks.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation;
SpmCounter.SetRotation(Disc.CumulativeRotation); SpmCounter.SetRotation(Disc.CumulativeRotation);
updateBonusScore();
float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight;
float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress;
Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); Disc.Scale = new Vector2((float)Interpolation.Lerp(Disc.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1)));
@ -200,36 +244,95 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables
symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1));
} }
private int wholeSpins;
private void updateBonusScore()
{
if (ticks.Count == 0)
return;
int spins = (int)(Disc.CumulativeRotation / 360);
if (spins < wholeSpins)
{
// rewinding, silently handle
wholeSpins = spins;
return;
}
while (wholeSpins != spins)
{
var tick = ticks.FirstOrDefault(t => !t.IsHit);
// tick may be null if we've hit the spin limit.
if (tick != null)
{
tick.TriggerResult(true);
if (tick is DrawableSpinnerBonusTick)
bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired);
}
wholeSpins++;
}
}
protected override void UpdateInitialTransforms() protected override void UpdateInitialTransforms()
{ {
base.UpdateInitialTransforms(); base.UpdateInitialTransforms();
circleContainer.ScaleTo(Spinner.Scale * 0.3f); circleContainer.ScaleTo(0);
circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint); mainContainer.ScaleTo(0);
mainContainer using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
.ScaleTo(0) {
.ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint) float phaseOneScale = Spinner.Scale * 0.7f;
.Then()
.ScaleTo(1, 500, Easing.OutQuint); circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint);
mainContainer
.ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint)
.RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration);
using (BeginDelayedSequence(HitObject.TimePreempt / 2, true))
{
circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint);
mainContainer.ScaleTo(1, 400, Easing.OutQuint);
}
}
} }
protected override void UpdateStateTransforms(ArmedState state) protected override void UpdateStateTransforms(ArmedState state)
{ {
base.UpdateStateTransforms(state); base.UpdateStateTransforms(state);
var sequence = this.Delay(Spinner.Duration).FadeOut(160); using (BeginDelayedSequence(Spinner.Duration, true))
switch (state)
{ {
case ArmedState.Hit: this.FadeOut(160);
sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out);
break;
case ArmedState.Miss: switch (state)
sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); {
break; case ArmedState.Hit:
transformFillColour(completeColour, 0);
this.ScaleTo(Scale * 1.2f, 320, Easing.Out);
mainContainer.RotateTo(mainContainer.Rotation + 180, 320);
break;
case ArmedState.Miss:
this.ScaleTo(Scale * 0.8f, 320, Easing.In);
break;
}
} }
} }
private void transformFillColour(Colour4 colour, double duration)
{
Disc.FadeAccent(colour, duration);
Background.FadeAccent(colour.Darken(1), duration);
Ticks.FadeAccent(colour, duration);
circle.FadeColour(colour, duration);
glow.FadeColour(colour, duration);
}
} }
} }

View File

@ -0,0 +1,13 @@
// 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.
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerBonusTick : DrawableSpinnerTick
{
public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick)
: base(spinnerTick)
{
}
}
}

View File

@ -0,0 +1,23 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSpinnerTick : DrawableOsuHitObject
{
public override bool DisplayResult => false;
public DrawableSpinnerTick(SpinnerTick spinnerTick)
: base(spinnerTick)
{
}
/// <summary>
/// Apply a judgement result.
/// </summary>
/// <param name="hit">Whether this tick was reached.</param>
internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss);
}
}

View File

@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
private readonly Slider slider; private readonly Slider slider;
private readonly Drawable followCircle; private readonly Drawable followCircle;
private readonly DrawableSlider drawableSlider; private readonly DrawableSlider drawableSlider;
private readonly CircularContainer ball; private readonly Drawable ball;
public SliderBall(Slider slider, DrawableSlider drawableSlider = null) public SliderBall(Slider slider, DrawableSlider drawableSlider = null)
{ {
@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Alpha = 0, Alpha = 0,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()),
}, },
ball = new CircularContainer ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall())
{ {
Masking = true,
RelativeSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre, Anchor = Anchor.Centre,
Alpha = 1, Origin = Anchor.Centre,
Child = new Container },
{
RelativeSizeAxes = Axes.Both,
Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()),
}
}
}; };
} }
@ -187,12 +179,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
return; return;
Position = newPos; Position = newPos;
Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI);
lastPosition = newPos; lastPosition = newPos;
} }
private class FollowCircleContainer : Container private class FollowCircleContainer : CircularContainer
{ {
public override bool HandlePositionalInput => true; public override bool HandlePositionalInput => true;
} }

View File

@ -1,18 +1,18 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osuTK.Graphics;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Effects;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics; using osu.Game.Graphics;
using osuTK.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerBackground : CircularContainer, IHasAccentColour public class SpinnerBackground : CircularContainer, IHasAccentColour
{ {
protected Box Disc; public readonly Box Disc;
public Color4 AccentColour public Color4 AccentColour
{ {

View File

@ -0,0 +1,44 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{
/// <summary>
/// Shows incremental bonus score achieved for a spinner.
/// </summary>
public class SpinnerBonusDisplay : CompositeDrawable
{
private readonly OsuSpriteText bonusCounter;
public SpinnerBonusDisplay()
{
AutoSizeAxes = Axes.Both;
InternalChild = bonusCounter = new OsuSpriteText
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Font = OsuFont.Numeric.With(size: 24),
Alpha = 0,
};
}
private int displayedCount;
public void SetBonusCount(int count)
{
if (displayedCount == count)
return;
displayedCount = count;
bonusCounter.Text = $"{1000 * count}";
bonusCounter.FadeOutFromOne(1500);
bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint);
}
}
}

View File

@ -2,6 +2,7 @@
// 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.Linq;
using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Containers;
@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects;
using osuTK; using osuTK;
using osuTK.Graphics; using osuTK.Graphics;
using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Shapes;
using osu.Game.Graphics;
namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
{ {
public class SpinnerTicks : Container public class SpinnerTicks : Container, IHasAccentColour
{ {
public SpinnerTicks() public SpinnerTicks()
{ {
@ -20,28 +22,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
Anchor = Anchor.Centre; Anchor = Anchor.Centre;
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
const float count = 18; const float count = 8;
for (float i = 0; i < count; i++) for (float i = 0; i < count; i++)
{ {
Add(new Container Add(new Container
{ {
Colour = Color4.Black,
Alpha = 0.4f, Alpha = 0.4f,
EdgeEffect = new EdgeEffectParameters Blending = BlendingParameters.Additive,
{
Type = EdgeEffectType.Glow,
Radius = 10,
Colour = Color4.Gray.Opacity(0.2f),
},
RelativePositionAxes = Axes.Both, RelativePositionAxes = Axes.Both,
Masking = true, Masking = true,
CornerRadius = 5, CornerRadius = 5,
Size = new Vector2(60, 10), Size = new Vector2(60, 10),
Origin = Anchor.Centre, Origin = Anchor.Centre,
Position = new Vector2( Position = new Vector2(
0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f, 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.83f,
0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.83f
), ),
Rotation = -i / count * 360 + 90, Rotation = -i / count * 360 + 90,
Children = new[] Children = new[]
@ -54,5 +50,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces
}); });
} }
} }
public Color4 AccentColour
{
get => Colour;
set
{
Colour = value;
foreach (var c in Children.OfType<Container>())
{
c.EdgeEffect =
new EdgeEffectParameters
{
Type = EdgeEffectType.Glow,
Radius = 20,
Colour = value.Opacity(0.8f),
};
}
}
}
} }
} }

View File

@ -3,9 +3,9 @@
using System; using System;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -26,14 +26,43 @@ namespace osu.Game.Rulesets.Osu.Objects
/// </summary> /// </summary>
public int SpinsRequired { get; protected set; } = 1; public int SpinsRequired { get; protected set; } = 1;
/// <summary>
/// Number of spins available to give bonus, beyond <see cref="SpinsRequired"/>.
/// </summary>
public int MaximumBonusSpins { get; protected set; } = 1;
protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty)
{ {
base.ApplyDefaultsToSelf(controlPointInfo, difficulty); base.ApplyDefaultsToSelf(controlPointInfo, difficulty);
SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5));
// spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being.
SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); const double stable_matching_fudge = 0.6;
// close to 477rpm
const double maximum_rotations_per_second = 8;
double secondsDuration = Duration / 1000;
double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5);
SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond));
MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration);
}
protected override void CreateNestedHitObjects()
{
base.CreateNestedHitObjects();
int totalSpins = MaximumBonusSpins + SpinsRequired;
for (int i = 0; i < totalSpins; i++)
{
double startTime = StartTime + (float)(i + 1) / totalSpins * Duration;
AddNested(i < SpinsRequired
? new SpinnerTick { StartTime = startTime }
: new SpinnerBonusTick { StartTime = startTime });
}
} }
public override Judgement CreateJudgement() => new OsuJudgement(); public override Judgement CreateJudgement() => new OsuJudgement();

View File

@ -0,0 +1,26 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Audio;
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerBonusTick : SpinnerTick
{
public SpinnerBonusTick()
{
Samples.Add(new HitSampleInfo { Name = "spinnerbonus" });
}
public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement();
public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement
{
protected override int NumericResultFor(HitResult result) => 1100;
protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2;
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Rulesets.Judgements;
using osu.Game.Rulesets.Osu.Judgements;
using osu.Game.Rulesets.Scoring;
namespace osu.Game.Rulesets.Osu.Objects
{
public class SpinnerTick : OsuHitObject
{
public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement();
protected override HitWindows CreateHitWindows() => HitWindows.Empty;
public class OsuSpinnerTickJudgement : OsuJudgement
{
public override bool AffectsCombo => false;
protected override int NumericResultFor(HitResult result) => 100;
protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0;
}
}
}

View File

@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays
/// </summary> /// </summary>
protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2;
protected const float SPIN_RADIUS = 50; public const float SPIN_RADIUS = 50;
/// <summary> /// <summary>
/// The time in ms between each ReplayFrame. /// The time in ms between each ReplayFrame.

View File

@ -36,19 +36,10 @@ namespace osu.Game.Rulesets.Osu.Replays
} }
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<OsuAction> { PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(Position ?? Vector2.Zero)
},
new ReplayState<OsuAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<OsuAction>()
}
};
} }
} }
} }

View File

@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning
{ {
private readonly Drawable animationContent; private readonly Drawable animationContent;
private Sprite layerNd;
private Sprite layerSpec;
public LegacySliderBall(Drawable animationContent) public LegacySliderBall(Drawable animationContent)
{ {
this.animationContent = animationContent; this.animationContent = animationContent;
@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning
InternalChildren = new[] InternalChildren = new[]
{ {
new Sprite layerNd = new Sprite
{ {
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-nd"), Texture = skin.GetTexture("sliderb-nd"),
Colour = new Color4(5, 5, 5, 255), Colour = new Color4(5, 5, 5, 255),
}, },
animationContent, animationContent.With(d =>
new Sprite
{ {
d.Anchor = Anchor.Centre;
d.Origin = Anchor.Centre;
}),
layerSpec = new Sprite
{
Anchor = Anchor.Centre,
Origin = Anchor.Centre,
Texture = skin.GetTexture("sliderb-spec"), Texture = skin.GetTexture("sliderb-spec"),
Blending = BlendingParameters.Additive, Blending = BlendingParameters.Additive,
}, },
}; };
} }
protected override void UpdateAfterChildren()
{
base.UpdateAfterChildren();
//undo rotation on layers which should not be rotated.
float appliedRotation = Parent.Rotation;
layerNd.Rotation = -appliedRotation;
layerSpec.Rotation = -appliedRotation;
}
} }
} }

View File

@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning
case OsuSkinComponents.HitCircleText: case OsuSkinComponents.HitCircleText:
var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var font = GetConfig<OsuSkinConfiguration, string>(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default";
var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0; var overlap = GetConfig<OsuSkinConfiguration, float>(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2;
return !hasFont(font) return !hasFont(font)
? null ? null

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -8,6 +8,7 @@ using NUnit.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.IEnumerableExtensions;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Graphics.Animations;
using osu.Framework.Testing; using osu.Framework.Testing;
using osu.Game.Beatmaps; using osu.Game.Beatmaps;
using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.ControlPoints;
@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private TaikoScoreProcessor scoreProcessor; private TaikoScoreProcessor scoreProcessor;
private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>(); private IEnumerable<DrawableTaikoMascot> mascots => this.ChildrenOfType<DrawableTaikoMascot>();
private IEnumerable<DrawableTaikoMascot> animatedMascots =>
mascots.Where(mascot => mascot.ChildrenOfType<TextureAnimation>().All(animation => animation.FrameCount > 0));
private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>(); private IEnumerable<TaikoPlayfield> playfields => this.ChildrenOfType<TaikoPlayfield>();
[SetUp] [SetUp]
@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddStep("set clear state", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss })); AddStep("miss", () => mascots.ForEach(mascot => mascot.LastResult.Value = new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }));
AddAssert("skins with animations remain in clear state", () => someMascotsIn(TaikoMascotAnimationState.Clear)); AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail));
AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear));
AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear));
} }
[Test] [Test]
@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail);
assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Fail); assertStateAfterResult(new JudgementResult(new DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle);
assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle);
} }
@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState)
{ {
AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", TaikoMascotAnimationState[] mascotStates = null;
() => applyNewResult(judgementResult));
AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState)); AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}",
() =>
{
applyNewResult(judgementResult);
// store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test
// due to not checking if the state changed quickly enough.
Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray());
});
AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState));
} }
private void applyNewResult(JudgementResult judgementResult) private void applyNewResult(JudgementResult judgementResult)
@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning
} }
private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state);
private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state); private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state);
} }
} }

View File

@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
}; };
[Test] [Test]
public void TestSpinnerDoesNotFail() public void TestSpinnerDoesFail()
{ {
bool judged = false; bool judged = false;
AddStep("Setup judgements", () => AddStep("Setup judgements", () =>
@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests
Player.ScoreProcessor.NewJudgement += b => judged = true; Player.ScoreProcessor.NewJudgement += b => judged = true;
}); });
AddUntilStep("swell judged", () => judged); AddUntilStep("swell judged", () => judged);
AddAssert("not failed", () => !Player.HasFailed); AddAssert("failed", () => Player.HasFailed);
} }
} }
} }

View File

@ -0,0 +1,64 @@
// 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 osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
/// <summary>
/// Stores samples for the input drum.
/// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point.
/// </summary>
public class DrumSampleContainer : LifetimeManagementContainer
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
public DrumSampleContainer(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
IReadOnlyList<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
for (int i = 0; i < samplePoints.Count; i++)
{
var samplePoint = samplePoints[i];
var centre = samplePoint.GetSampleInfo();
var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP);
var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue;
var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue;
mappings[samplePoint.Time] = new DrumSample
{
Centre = addSound(centre, lifetimeStart, lifetimeEnd),
Rim = addSound(rim, lifetimeStart, lifetimeEnd)
};
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd)
{
var drawable = new SkinnableSound(hitSampleInfo)
{
LifetimeStart = lifetimeStart,
LifetimeEnd = lifetimeEnd
};
AddInternal(drawable);
return drawable;
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
}
}
}

View File

@ -1,52 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System.Collections.Generic;
using osu.Game.Audio;
using osu.Game.Beatmaps.ControlPoints;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.Taiko.Audio
{
public class DrumSampleMapping
{
private readonly ControlPointInfo controlPoints;
private readonly Dictionary<double, DrumSample> mappings = new Dictionary<double, DrumSample>();
public readonly List<SkinnableSound> Sounds = new List<SkinnableSound>();
public DrumSampleMapping(ControlPointInfo controlPoints)
{
this.controlPoints = controlPoints;
IEnumerable<SampleControlPoint> samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints;
foreach (var s in samplePoints)
{
var centre = s.GetSampleInfo();
var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP);
mappings[s.Time] = new DrumSample
{
Centre = addSound(centre),
Rim = addSound(rim)
};
}
}
private SkinnableSound addSound(HitSampleInfo hitSampleInfo)
{
var drawable = new SkinnableSound(hitSampleInfo);
Sounds.Add(drawable);
return drawable;
}
public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time];
public class DrumSample
{
public SkinnableSound Centre;
public SkinnableSound Rim;
}
}
}

View File

@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{ {
public class TaikoDrumRollJudgement : TaikoJudgement public class TaikoDrumRollJudgement : TaikoJudgement
{ {
public override bool AffectsCombo => false;
protected override double HealthIncreaseFor(HitResult result) protected override double HealthIncreaseFor(HitResult result)
{ {
// Drum rolls can be ignored with no health penalty // Drum rolls can be ignored with no health penalty

View File

@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements
{ {
public class TaikoSwellJudgement : TaikoJudgement public class TaikoSwellJudgement : TaikoJudgement
{ {
public override bool AffectsCombo => false;
protected override double HealthIncreaseFor(HitResult result) protected override double HealthIncreaseFor(HitResult result)
{ {
switch (result) switch (result)

View File

@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays
protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any();
public override List<IInput> GetPendingInputs() => new List<IInput> { new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() } }; public override void CollectPendingInputs(List<IInput> inputs)
{
inputs.Add(new ReplayState<TaikoAction> { PressedActions = CurrentFrame?.Actions ?? new List<TaikoAction>() });
}
} }
} }

View File

@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public readonly Sprite Centre; public readonly Sprite Centre;
[Resolved] [Resolved]
private DrumSampleMapping sampleMappings { get; set; } private DrumSampleContainer sampleContainer { get; set; }
public LegacyHalfDrum(bool flipped) public LegacyHalfDrum(bool flipped)
{ {
@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
public bool OnPressed(TaikoAction action) public bool OnPressed(TaikoAction action)
{ {
Drawable target = null; Drawable target = null;
var drumSample = sampleMappings.SampleAt(Time.Current); var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {

View File

@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning
return null; return null;
case TaikoSkinComponents.Mascot: case TaikoSkinComponents.Mascot:
if (GetTexture("pippidonclear0") != null) return new DrawableTaikoMascot();
return new DrawableTaikoMascot();
return null;
} }
return Source.GetDrawableComponent(component); return Source.GetDrawableComponent(component);

View File

@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI
private const float middle_split = 0.025f; private const float middle_split = 0.025f;
[Cached] [Cached]
private DrumSampleMapping sampleMapping; private DrumSampleContainer sampleContainer;
public InputDrum(ControlPointInfo controlPoints) public InputDrum(ControlPointInfo controlPoints)
{ {
sampleMapping = new DrumSampleMapping(controlPoints); sampleContainer = new DrumSampleContainer(controlPoints);
RelativeSizeAxes = Axes.Both; RelativeSizeAxes = Axes.Both;
} }
@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load() private void load()
{ {
Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.Both, new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
Children = new Drawable[]
{ {
new TaikoHalfDrum(false) RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit,
Scale = new Vector2(0.9f),
Children = new Drawable[]
{ {
Name = "Left Half", new TaikoHalfDrum(false)
Anchor = Anchor.Centre, {
Origin = Anchor.CentreRight, Name = "Left Half",
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
RelativePositionAxes = Axes.X, Origin = Anchor.CentreRight,
X = -middle_split / 2, RelativeSizeAxes = Axes.Both,
RimAction = TaikoAction.LeftRim, RelativePositionAxes = Axes.X,
CentreAction = TaikoAction.LeftCentre X = -middle_split / 2,
}, RimAction = TaikoAction.LeftRim,
new TaikoHalfDrum(true) CentreAction = TaikoAction.LeftCentre
{ },
Name = "Right Half", new TaikoHalfDrum(true)
Anchor = Anchor.Centre, {
Origin = Anchor.CentreLeft, Name = "Right Half",
RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre,
RelativePositionAxes = Axes.X, Origin = Anchor.CentreLeft,
X = middle_split / 2, RelativeSizeAxes = Axes.Both,
RimAction = TaikoAction.RightRim, RelativePositionAxes = Axes.X,
CentreAction = TaikoAction.RightCentre X = middle_split / 2,
RimAction = TaikoAction.RightRim,
CentreAction = TaikoAction.RightCentre
}
} }
} }),
}); sampleContainer
};
AddRangeInternal(sampleMapping.Sounds);
} }
/// <summary> /// <summary>
@ -93,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.UI
private readonly Sprite centreHit; private readonly Sprite centreHit;
[Resolved] [Resolved]
private DrumSampleMapping sampleMappings { get; set; } private DrumSampleContainer sampleContainer { get; set; }
public TaikoHalfDrum(bool flipped) public TaikoHalfDrum(bool flipped)
{ {
@ -154,7 +156,7 @@ namespace osu.Game.Rulesets.Taiko.UI
Drawable target = null; Drawable target = null;
Drawable back = null; Drawable back = null;
var drumSample = sampleMappings.SampleAt(Time.Current); var drumSample = sampleContainer.SampleAt(Time.Current);
if (action == CentreAction) if (action == CentreAction)
{ {

View File

@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI
} }
private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex)
=> skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); {
var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}");
if (frameIndex == 0 && texture == null)
texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}");
return texture;
}
} }
} }

View File

@ -173,19 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
},
new ReplayState<TestAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
}
};
} }
} }

View File

@ -113,19 +113,10 @@ namespace osu.Game.Tests.Visual.Gameplay
{ {
} }
public override List<IInput> GetPendingInputs() public override void CollectPendingInputs(List<IInput> inputs)
{ {
return new List<IInput> inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) });
{ inputs.Add(new ReplayState<TestAction> { PressedActions = CurrentFrame?.Actions ?? new List<TestAction>() });
new MousePositionAbsoluteInput
{
Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero)
},
new ReplayState<TestAction>
{
PressedActions = CurrentFrame?.Actions ?? new List<TestAction>()
}
};
} }
} }

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using NUnit.Framework; using NUnit.Framework;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
@ -65,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer
private void bindHandler(double delay = 0) private void bindHandler(double delay = 0)
{ {
var roomScores = new List<RoomScore>(); var roomScores = new List<MultiplayerScore>();
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
{ {
roomScores.Add(new RoomScore roomScores.Add(new MultiplayerScore
{ {
ID = i, ID = i,
Accuracy = 0.9 - 0.01 * i, Accuracy = 0.9 - 0.01 * i,

View File

@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online
Add(selector = new SpotlightSelector()); Add(selector = new SpotlightSelector());
} }
[Test]
public void TestVisibility()
{
AddStep("Toggle Visibility", selector.ToggleVisibility);
}
[Test] [Test]
public void TestLocalSpotlights() public void TestLocalSpotlights()
{ {

View File

@ -1,84 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using NUnit.Framework;
using osu.Game.Overlays;
using osu.Game.Users;
namespace osu.Game.Tests.Visual.Online
{
[TestFixture]
public class TestSceneSocialOverlay : OsuTestScene
{
protected override bool UseOnlineAPI => true;
public TestSceneSocialOverlay()
{
SocialOverlay s = new SocialOverlay
{
Users = new[]
{
new User
{
Username = @"flyte",
Id = 3103765,
Country = new Country { FlagName = @"JP" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg",
},
new User
{
Username = @"Cookiezi",
Id = 124493,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg",
},
new User
{
Username = @"Angelsim",
Id = 1777162,
Country = new Country { FlagName = @"KR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
},
new User
{
Username = @"Rafis",
Id = 2558286,
Country = new Country { FlagName = @"PL" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg",
},
new User
{
Username = @"hvick225",
Id = 50265,
Country = new Country { FlagName = @"TW" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg",
},
new User
{
Username = @"peppy",
Id = 2,
Country = new Country { FlagName = @"AU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
},
new User
{
Username = @"filsdelama",
Id = 2831793,
Country = new Country { FlagName = @"FR" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg"
},
new User
{
Username = @"_index",
Id = 652457,
Country = new Country { FlagName = @"RU" },
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg"
},
},
};
Add(s);
AddStep(@"toggle", s.ToggleVisibility);
}
}
}

View File

@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online
Spacing = new Vector2(10f), Spacing = new Vector2(10f),
Children = new Drawable[] Children = new Drawable[]
{ {
new UserBrickPanel(new User
{
Username = @"flyte",
Id = 3103765,
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg"
}),
new UserBrickPanel(new User
{
Username = @"peppy",
Id = 2,
Colour = "99EB47",
CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
}),
flyte = new UserGridPanel(new User flyte = new UserGridPanel(new User
{ {
Username = @"flyte", Username = @"flyte",

View File

@ -11,14 +11,14 @@ using osu.Game.Graphics.Sprites;
namespace osu.Game.Tests.Visual.UserInterface namespace osu.Game.Tests.Visual.UserInterface
{ {
[TestFixture] [TestFixture]
public class TestSceneHueAnimation : OsuTestScene public class TestSceneLogoAnimation : OsuTestScene
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(LargeTextureStore textures) private void load(LargeTextureStore textures)
{ {
HueAnimation anim2; LogoAnimation anim2;
Add(anim2 = new HueAnimation Add(anim2 = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,
@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.UserInterface
Colour = Colour4.White, Colour = Colour4.White,
}); });
HueAnimation anim; LogoAnimation anim;
Add(anim = new HueAnimation Add(anim = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
FillMode = FillMode.Fit, FillMode = FillMode.Fit,

View File

@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface
} }
}); });
addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange);
addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue);
addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green);
addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink);
addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red);
} }
private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme) private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme)
@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestNoBackgroundHeader : OverlayHeader private class TestNoBackgroundHeader : OverlayHeader
{ {
protected override OverlayTitle CreateTitle() => new TestTitle(); protected override OverlayTitle CreateTitle() => new TestTitle();
public TestNoBackgroundHeader()
{
ContentSidePadding = 100;
}
} }
private class TestNoControlHeader : OverlayHeader private class TestNoControlHeader : OverlayHeader
@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface
private class TestEnumTabControlHeader : TabControlOverlayHeader<TestEnum> private class TestEnumTabControlHeader : TabControlOverlayHeader<TestEnum>
{ {
public TestEnumTabControlHeader()
{
ContentSidePadding = 30;
}
protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings");
protected override OverlayTitle CreateTitle() => new TestTitle(); protected override OverlayTitle CreateTitle() => new TestTitle();
@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface
public TestBreadcrumbControlHeader() public TestBreadcrumbControlHeader()
{ {
ContentSidePadding = 10;
TabControl.AddItem("tab1"); TabControl.AddItem("tab1");
TabControl.AddItem("tab2"); TabControl.AddItem("tab2");
TabControl.Current.Value = "tab2"; TabControl.Current.Value = "tab2";

View File

@ -0,0 +1,278 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics.Containers;
using osu.Framework.Lists;
using osu.Framework.Threading;
using osu.Game.Rulesets;
using osu.Game.Rulesets.Mods;
namespace osu.Game.Beatmaps
{
public class BeatmapDifficultyManager : CompositeDrawable
{
// Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes.
private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager));
// A permanent cache to prevent re-computations.
private readonly ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty> difficultyCache = new ConcurrentDictionary<DifficultyCacheLookup, StarDifficulty>();
// All bindables that should be updated along with the current ruleset + mods.
private readonly LockedWeakList<BindableStarDifficulty> trackedBindables = new LockedWeakList<BindableStarDifficulty>();
[Resolved]
private BeatmapManager beatmapManager { get; set; }
[Resolved]
private Bindable<RulesetInfo> currentRuleset { get; set; }
[Resolved]
private Bindable<IReadOnlyList<Mod>> currentMods { get; set; }
protected override void LoadComplete()
{
base.LoadComplete();
currentRuleset.BindValueChanged(_ => updateTrackedBindables());
currentMods.BindValueChanged(_ => updateTrackedBindables(), true);
}
/// <summary>
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> that follows the currently-selected ruleset and mods.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default)
{
var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken);
trackedBindables.Add(bindable);
return bindable;
}
/// <summary>
/// Retrieves a bindable containing the star difficulty of a <see cref="BeatmapInfo"/> with a given <see cref="RulesetInfo"/> and <see cref="Mod"/> combination.
/// </summary>
/// <remarks>
/// The bindable will not update to follow the currently-selected ruleset and mods.
/// </remarks>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with. If <c>null</c>, the <paramref name="beatmapInfo"/>'s ruleset is used.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with. If <c>null</c>, no mods will be assumed.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>A bindable that is updated to contain the star difficulty when it becomes available.</returns>
public IBindable<StarDifficulty> GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods,
CancellationToken cancellationToken = default)
=> createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken);
/// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops computing the star difficulty.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
public async Task<StarDifficulty> GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null,
CancellationToken cancellationToken = default)
{
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing;
return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken,
TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler);
}
/// <summary>
/// Retrieves the difficulty of a <see cref="BeatmapInfo"/>.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to get the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="mods">The <see cref="Mod"/>s to get the difficulty with.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable<Mod> mods = null)
{
if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key))
return existing;
return computeDifficulty(key, beatmapInfo, rulesetInfo);
}
private CancellationTokenSource trackedUpdateCancellationSource;
/// <summary>
/// Updates all tracked <see cref="BindableStarDifficulty"/> using the current ruleset and mods.
/// </summary>
private void updateTrackedBindables()
{
trackedUpdateCancellationSource?.Cancel();
trackedUpdateCancellationSource = new CancellationTokenSource();
foreach (var b in trackedBindables)
{
if (trackedUpdateCancellationSource.IsCancellationRequested)
break;
using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken))
updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token);
}
}
/// <summary>
/// Updates the value of a <see cref="BindableStarDifficulty"/> with a given ruleset + mods.
/// </summary>
/// <param name="bindable">The <see cref="BindableStarDifficulty"/> to update.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to update with.</param>
/// <param name="mods">The <see cref="Mod"/>s to update with.</param>
/// <param name="cancellationToken">A token that may be used to cancel this update.</param>
private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
{
GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t =>
{
// We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events.
Schedule(() =>
{
if (!cancellationToken.IsCancellationRequested)
bindable.Value = t.Result;
});
}, cancellationToken);
}
/// <summary>
/// Creates a new <see cref="BindableStarDifficulty"/> and triggers an initial value update.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> that star difficulty should correspond to.</param>
/// <param name="initialRulesetInfo">The initial <see cref="RulesetInfo"/> to get the difficulty with.</param>
/// <param name="initialMods">The initial <see cref="Mod"/>s to get the difficulty with.</param>
/// <param name="cancellationToken">An optional <see cref="CancellationToken"/> which stops updating the star difficulty for the given <see cref="BeatmapInfo"/>.</param>
/// <returns>The <see cref="BindableStarDifficulty"/>.</returns>
private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable<Mod> initialMods,
CancellationToken cancellationToken)
{
var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken);
updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken);
return bindable;
}
/// <summary>
/// Computes the difficulty defined by a <see cref="DifficultyCacheLookup"/> key, and stores it to the timed cache.
/// </summary>
/// <param name="key">The <see cref="DifficultyCacheLookup"/> that defines the computation parameters.</param>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/> to compute the difficulty of.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/> to compute the difficulty with.</param>
/// <returns>The <see cref="StarDifficulty"/>.</returns>
private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo)
{
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ??= beatmapInfo.Ruleset;
try
{
var ruleset = rulesetInfo.CreateInstance();
Debug.Assert(ruleset != null);
var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo));
var attributes = calculator.Calculate(key.Mods);
return difficultyCache[key] = new StarDifficulty(attributes.StarRating);
}
catch
{
return difficultyCache[key] = new StarDifficulty(0);
}
}
/// <summary>
/// Attempts to retrieve an existing difficulty for the combination.
/// </summary>
/// <param name="beatmapInfo">The <see cref="BeatmapInfo"/>.</param>
/// <param name="rulesetInfo">The <see cref="RulesetInfo"/>.</param>
/// <param name="mods">The <see cref="Mod"/>s.</param>
/// <param name="existingDifficulty">The existing difficulty value, if present.</param>
/// <param name="key">The <see cref="DifficultyCacheLookup"/> key that was used to perform this lookup. This can be further used to query <see cref="computeDifficulty"/>.</param>
/// <returns>Whether an existing difficulty was found.</returns>
private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable<Mod> mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key)
{
// In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset.
rulesetInfo ??= beatmapInfo.Ruleset;
// Difficulty can only be computed if the beatmap and ruleset are locally available.
if (beatmapInfo.ID == 0 || rulesetInfo.ID == null)
{
// If not, fall back to the existing star difficulty (e.g. from an online source).
existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty);
key = default;
return true;
}
key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods);
return difficultyCache.TryGetValue(key, out existingDifficulty);
}
private readonly struct DifficultyCacheLookup : IEquatable<DifficultyCacheLookup>
{
public readonly int BeatmapId;
public readonly int RulesetId;
public readonly Mod[] Mods;
public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable<Mod> mods)
{
BeatmapId = beatmapId;
RulesetId = rulesetId;
Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty<Mod>();
}
public bool Equals(DifficultyCacheLookup other)
=> BeatmapId == other.BeatmapId
&& RulesetId == other.RulesetId
&& Mods.SequenceEqual(other.Mods);
public override int GetHashCode()
{
var hashCode = new HashCode();
hashCode.Add(BeatmapId);
hashCode.Add(RulesetId);
foreach (var mod in Mods)
hashCode.Add(mod.Acronym);
return hashCode.ToHashCode();
}
}
private class BindableStarDifficulty : Bindable<StarDifficulty>
{
public readonly BeatmapInfo Beatmap;
public readonly CancellationToken CancellationToken;
public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken)
{
Beatmap = beatmap;
CancellationToken = cancellationToken;
}
}
}
public readonly struct StarDifficulty
{
public readonly double Stars;
public StarDifficulty(double stars)
{
Stars = stars;
// Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...)
}
}
}

View File

@ -64,49 +64,49 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="time">The time to find the difficulty control point at.</param> /// <param name="time">The time to find the difficulty control point at.</param>
/// <returns>The difficulty control point.</returns> /// <returns>The difficulty control point.</returns>
public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time); public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT);
/// <summary> /// <summary>
/// Finds the effect control point that is active at <paramref name="time"/>. /// Finds the effect control point that is active at <paramref name="time"/>.
/// </summary> /// </summary>
/// <param name="time">The time to find the effect control point at.</param> /// <param name="time">The time to find the effect control point at.</param>
/// <returns>The effect control point.</returns> /// <returns>The effect control point.</returns>
public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time); public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT);
/// <summary> /// <summary>
/// Finds the sound control point that is active at <paramref name="time"/>. /// Finds the sound control point that is active at <paramref name="time"/>.
/// </summary> /// </summary>
/// <param name="time">The time to find the sound control point at.</param> /// <param name="time">The time to find the sound control point at.</param>
/// <returns>The sound control point.</returns> /// <returns>The sound control point.</returns>
public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null); public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT);
/// <summary> /// <summary>
/// Finds the timing control point that is active at <paramref name="time"/>. /// Finds the timing control point that is active at <paramref name="time"/>.
/// </summary> /// </summary>
/// <param name="time">The time to find the timing control point at.</param> /// <param name="time">The time to find the timing control point at.</param>
/// <returns>The timing control point.</returns> /// <returns>The timing control point.</returns>
public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null); public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT);
/// <summary> /// <summary>
/// Finds the maximum BPM represented by any timing control point. /// Finds the maximum BPM represented by any timing control point.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public double BPMMaximum => public double BPMMaximum =>
60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
/// <summary> /// <summary>
/// Finds the minimum BPM represented by any timing control point. /// Finds the minimum BPM represented by any timing control point.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public double BPMMinimum => public double BPMMinimum =>
60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
/// <summary> /// <summary>
/// Finds the mode BPM (most common BPM) represented by the control points. /// Finds the mode BPM (most common BPM) represented by the control points.
/// </summary> /// </summary>
[JsonIgnore] [JsonIgnore]
public double BPMMode => public double BPMMode =>
60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength; 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength;
/// <summary> /// <summary>
/// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state. /// Remove all <see cref="ControlPointGroup"/>s and return to a pristine state.
@ -170,12 +170,12 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
/// <param name="list">The list to search.</param> /// <param name="list">The list to search.</param>
/// <param name="time">The time to find the control point at.</param> /// <param name="time">The time to find the control point at.</param>
/// <param name="prePoint">The control point to use when <paramref name="time"/> is before any control points. If null, a new control point will be constructed.</param> /// <param name="fallback">The control point to use when <paramref name="time"/> is before any control points.</param>
/// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns> /// <returns>The active control point at <paramref name="time"/>, or a fallback <see cref="ControlPoint"/> if none found.</returns>
private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T prePoint = null) private T binarySearchWithFallback<T>(IReadOnlyList<T> list, double time, T fallback)
where T : ControlPoint, new() where T : ControlPoint
{ {
return binarySearch(list, time) ?? prePoint ?? new T(); return binarySearch(list, time) ?? fallback;
} }
/// <summary> /// <summary>

View File

@ -7,6 +7,11 @@ namespace osu.Game.Beatmaps.ControlPoints
{ {
public class DifficultyControlPoint : ControlPoint public class DifficultyControlPoint : ControlPoint
{ {
public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint
{
SpeedMultiplierBindable = { Disabled = true },
};
/// <summary> /// <summary>
/// The speed multiplier at this control point. /// The speed multiplier at this control point.
/// </summary> /// </summary>

View File

@ -7,6 +7,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{ {
public class EffectControlPoint : ControlPoint public class EffectControlPoint : ControlPoint
{ {
public static readonly EffectControlPoint DEFAULT = new EffectControlPoint
{
KiaiModeBindable = { Disabled = true },
OmitFirstBarLineBindable = { Disabled = true }
};
/// <summary> /// <summary>
/// Whether the first bar line of this control point is ignored. /// Whether the first bar line of this control point is ignored.
/// </summary> /// </summary>

View File

@ -10,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints
{ {
public const string DEFAULT_BANK = "normal"; public const string DEFAULT_BANK = "normal";
public static readonly SampleControlPoint DEFAULT = new SampleControlPoint
{
SampleBankBindable = { Disabled = true },
SampleVolumeBindable = { Disabled = true }
};
/// <summary> /// <summary>
/// The default sample bank at this control point. /// The default sample bank at this control point.
/// </summary> /// </summary>

View File

@ -13,6 +13,21 @@ namespace osu.Game.Beatmaps.ControlPoints
/// </summary> /// </summary>
public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple }; public readonly Bindable<TimeSignatures> TimeSignatureBindable = new Bindable<TimeSignatures>(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple };
/// <summary>
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
/// </summary>
private const double default_beat_length = 60000.0 / 60.0;
public static readonly TimingControlPoint DEFAULT = new TimingControlPoint
{
BeatLengthBindable =
{
Value = default_beat_length,
Disabled = true
},
TimeSignatureBindable = { Disabled = true }
};
/// <summary> /// <summary>
/// The time signature at this control point. /// The time signature at this control point.
/// </summary> /// </summary>

View File

@ -99,6 +99,7 @@ namespace osu.Game.Configuration
Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised);
Set(OsuSetting.IncreaseFirstObjectVisibility, true); Set(OsuSetting.IncreaseFirstObjectVisibility, true);
Set(OsuSetting.GameplayDisableWinKey, true);
// Update // Update
Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer);
@ -229,6 +230,7 @@ namespace osu.Game.Configuration
IntroSequence, IntroSequence,
UIHoldActivationDelay, UIHoldActivationDelay,
HitLighting, HitLighting,
MenuBackgroundSource MenuBackgroundSource,
GameplayDisableWinKey
} }
} }

View File

@ -43,14 +43,6 @@ namespace osu.Game.Graphics.Containers
/// </summary> /// </summary>
public double MinimumBeatLength { get; set; } public double MinimumBeatLength { get; set; }
/// <summary>
/// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing.
/// </summary>
private const double default_beat_length = 60000.0 / 60.0;
private TimingControlPoint defaultTiming;
private EffectControlPoint defaultEffect;
protected bool IsBeatSyncedWithTrack { get; private set; } protected bool IsBeatSyncedWithTrack { get; private set; }
protected override void Update() protected override void Update()
@ -81,8 +73,8 @@ namespace osu.Game.Graphics.Containers
if (timingPoint == null || !IsBeatSyncedWithTrack) if (timingPoint == null || !IsBeatSyncedWithTrack)
{ {
currentTrackTime = Clock.CurrentTime; currentTrackTime = Clock.CurrentTime;
timingPoint = defaultTiming; timingPoint = TimingControlPoint.DEFAULT;
effectPoint = defaultEffect; effectPoint = EffectControlPoint.DEFAULT;
} }
double beatLength = timingPoint.BeatLength / Divisor; double beatLength = timingPoint.BeatLength / Divisor;
@ -116,17 +108,6 @@ namespace osu.Game.Graphics.Containers
private void load(IBindable<WorkingBeatmap> beatmap) private void load(IBindable<WorkingBeatmap> beatmap)
{ {
Beatmap.BindTo(beatmap); Beatmap.BindTo(beatmap);
defaultTiming = new TimingControlPoint
{
BeatLength = default_beat_length,
};
defaultEffect = new EffectControlPoint
{
KiaiMode = false,
OmitFirstBarLine = false
};
} }
protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes)

View File

@ -1,7 +1,6 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using System.Linq;
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.Graphics.Cursor;
@ -55,7 +54,16 @@ namespace osu.Game.Graphics.Cursor
return; return;
} }
var newTarget = inputManager.HoveredDrawables.OfType<IProvideCursor>().FirstOrDefault(t => t.ProvidingUserCursor) ?? this; IProvideCursor newTarget = this;
foreach (var d in inputManager.HoveredDrawables)
{
if (d is IProvideCursor p && p.ProvidingUserCursor)
{
newTarget = p;
break;
}
}
if (currentTarget == newTarget) if (currentTarget == newTarget)
return; return;

View File

@ -19,6 +19,7 @@ using osu.Game.Input.Bindings;
using osu.Game.Overlays; using osu.Game.Overlays;
using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Notifications;
using SixLabors.ImageSharp; using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Jpeg;
namespace osu.Game.Graphics namespace osu.Game.Graphics
{ {
@ -119,7 +120,9 @@ namespace osu.Game.Graphics
break; break;
case ScreenshotFormat.Jpg: case ScreenshotFormat.Jpg:
image.SaveAsJpeg(stream); const int jpeg_quality = 92;
image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality });
break; break;
default: default:

View File

@ -11,13 +11,13 @@ using osu.Framework.Graphics.Textures;
namespace osu.Game.Graphics.Sprites namespace osu.Game.Graphics.Sprites
{ {
public class HueAnimation : Sprite public class LogoAnimation : Sprite
{ {
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
private void load(ShaderManager shaders, TextureStore textures) private void load(ShaderManager shaders, TextureStore textures)
{ {
TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation");
RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); // Masking isn't supported for now RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now
} }
private float animationProgress; private float animationProgress;
@ -36,15 +36,15 @@ namespace osu.Game.Graphics.Sprites
public override bool IsPresent => true; public override bool IsPresent => true;
protected override DrawNode CreateDrawNode() => new HueAnimationDrawNode(this); protected override DrawNode CreateDrawNode() => new LogoAnimationDrawNode(this);
private class HueAnimationDrawNode : SpriteDrawNode private class LogoAnimationDrawNode : SpriteDrawNode
{ {
private HueAnimation source => (HueAnimation)Source; private LogoAnimation source => (LogoAnimation)Source;
private float progress; private float progress;
public HueAnimationDrawNode(HueAnimation source) public LogoAnimationDrawNode(LogoAnimation source)
: base(source) : base(source)
{ {
} }

View File

@ -67,6 +67,8 @@ namespace osu.Game.Graphics.UserInterface
public bool OnPressed(GlobalAction action) public bool OnPressed(GlobalAction action)
{ {
if (!HasFocus) return false;
if (action == GlobalAction.Back) if (action == GlobalAction.Back)
{ {
if (Text.Length > 0) if (Text.Length > 0)

View File

@ -2,9 +2,8 @@
// 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 Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.Multiplayer
{ {
public class APICreatedRoom : Room public class APICreatedRoom : Room
{ {

View File

@ -6,7 +6,7 @@ using osu.Game.Beatmaps;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Rulesets; using osu.Game.Rulesets;
namespace osu.Game.Online.API namespace osu.Game.Online.Multiplayer
{ {
public class APIPlaylistBeatmap : APIBeatmap public class APIPlaylistBeatmap : APIBeatmap
{ {

View File

@ -3,7 +3,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
namespace osu.Game.Online.API.Requests.Responses namespace osu.Game.Online.Multiplayer
{ {
public class APIScoreToken public class APIScoreToken
{ {

View File

@ -4,10 +4,9 @@
using System.Net.Http; using System.Net.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API;
using osu.Game.Online.Multiplayer;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class CreateRoomRequest : APIRequest<APICreatedRoom> public class CreateRoomRequest : APIRequest<APICreatedRoom>
{ {

View File

@ -3,9 +3,9 @@
using System.Net.Http; using System.Net.Http;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class CreateRoomScoreRequest : APIRequest<APIScoreToken> public class CreateRoomScoreRequest : APIRequest<APIScoreToken>
{ {

View File

@ -3,8 +3,9 @@
using System.Collections.Generic; using System.Collections.Generic;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Game.Online.API;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores> public class GetRoomPlaylistScoresRequest : APIRequest<RoomPlaylistScores>
{ {
@ -23,6 +24,6 @@ namespace osu.Game.Online.API.Requests
public class RoomPlaylistScores public class RoomPlaylistScores
{ {
[JsonProperty("scores")] [JsonProperty("scores")]
public List<RoomScore> Scores { get; set; } public List<MultiplayerScore> Scores { get; set; }
} }
} }

View File

@ -1,9 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Game.Online.Multiplayer; using osu.Game.Online.API;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class GetRoomRequest : APIRequest<Room> public class GetRoomRequest : APIRequest<Room>
{ {

View File

@ -2,9 +2,10 @@
// 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.Collections.Generic; using System.Collections.Generic;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class GetRoomScoresRequest : APIRequest<List<APIUserScoreAggregate>> public class GetRoomScoresRequest : APIRequest<List<APIUserScoreAggregate>>
{ {

View File

@ -4,10 +4,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using Humanizer; using Humanizer;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.Multiplayer; using osu.Game.Online.API;
using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Lounge.Components;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class GetRoomsRequest : APIRequest<List<Room>> public class GetRoomsRequest : APIRequest<List<Room>>
{ {

View File

@ -3,9 +3,9 @@
using System.Net.Http; using System.Net.Http;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.Multiplayer; using osu.Game.Online.API;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class JoinRoomRequest : APIRequest public class JoinRoomRequest : APIRequest
{ {

View File

@ -6,15 +6,15 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Converters; using Newtonsoft.Json.Converters;
using osu.Game.Online.Multiplayer; using osu.Game.Online.API;
using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Scoring;
using osu.Game.Scoring; using osu.Game.Scoring;
using osu.Game.Users; using osu.Game.Users;
namespace osu.Game.Online.API namespace osu.Game.Online.Multiplayer
{ {
public class RoomScore public class MultiplayerScore
{ {
[JsonProperty("id")] [JsonProperty("id")]
public int ID { get; set; } public int ID { get; set; }

View File

@ -3,9 +3,9 @@
using System.Net.Http; using System.Net.Http;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.Multiplayer; using osu.Game.Online.API;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class PartRoomRequest : APIRequest public class PartRoomRequest : APIRequest
{ {

View File

@ -4,11 +4,12 @@
using System.Net.Http; using System.Net.Http;
using Newtonsoft.Json; using Newtonsoft.Json;
using osu.Framework.IO.Network; using osu.Framework.IO.Network;
using osu.Game.Online.API;
using osu.Game.Scoring; using osu.Game.Scoring;
namespace osu.Game.Online.API.Requests namespace osu.Game.Online.Multiplayer
{ {
public class SubmitRoomScoreRequest : APIRequest<RoomScore> public class SubmitRoomScoreRequest : APIRequest<MultiplayerScore>
{ {
private readonly int scoreId; private readonly int scoreId;
private readonly int roomId; private readonly int roomId;

View File

@ -18,6 +18,7 @@ using osu.Game.Screens.Menu;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Humanizer;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Audio; using osu.Framework.Audio;
using osu.Framework.Bindables; using osu.Framework.Bindables;
@ -63,7 +64,8 @@ namespace osu.Game
private ChannelManager channelManager; private ChannelManager channelManager;
private NotificationOverlay notifications; [NotNull]
private readonly NotificationOverlay notifications = new NotificationOverlay();
private NowPlayingOverlay nowPlaying; private NowPlayingOverlay nowPlaying;
@ -82,7 +84,7 @@ namespace osu.Game
public virtual Storage GetStorageForStableInstall() => null; public virtual Storage GetStorageForStableInstall() => null;
public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight; public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0);
private IdleTracker idleTracker; private IdleTracker idleTracker;
@ -250,7 +252,7 @@ namespace osu.Game
case LinkAction.OpenEditorTimestamp: case LinkAction.OpenEditorTimestamp:
case LinkAction.JoinMultiplayerMatch: case LinkAction.JoinMultiplayerMatch:
case LinkAction.Spectate: case LinkAction.Spectate:
waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification
{ {
Text = @"This link type is not yet supported!", Text = @"This link type is not yet supported!",
Icon = FontAwesome.Solid.LifeRing, Icon = FontAwesome.Solid.LifeRing,
@ -536,14 +538,14 @@ namespace osu.Game
MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false; MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false;
// todo: all archive managers should be able to be looped here. // todo: all archive managers should be able to be looped here.
SkinManager.PostNotification = n => notifications?.Post(n); SkinManager.PostNotification = n => notifications.Post(n);
SkinManager.GetStableStorage = GetStorageForStableInstall; SkinManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PostNotification = n => notifications?.Post(n); BeatmapManager.PostNotification = n => notifications.Post(n);
BeatmapManager.GetStableStorage = GetStorageForStableInstall; BeatmapManager.GetStableStorage = GetStorageForStableInstall;
BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); BeatmapManager.PresentImport = items => PresentBeatmap(items.First());
ScoreManager.PostNotification = n => notifications?.Post(n); ScoreManager.PostNotification = n => notifications.Post(n);
ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.GetStableStorage = GetStorageForStableInstall;
ScoreManager.PresentImport = items => PresentScore(items.First()); ScoreManager.PresentImport = items => PresentScore(items.First());
@ -615,12 +617,12 @@ namespace osu.Game
loadComponentSingleFile(MusicController = new MusicController(), Add, true); loadComponentSingleFile(MusicController = new MusicController(), Add, true);
loadComponentSingleFile(notifications = new NotificationOverlay loadComponentSingleFile(notifications.With(d =>
{ {
GetToolbarHeight = () => ToolbarOffset, d.GetToolbarHeight = () => ToolbarOffset;
Anchor = Anchor.TopRight, d.Anchor = Anchor.TopRight;
Origin = Anchor.TopRight, d.Origin = Anchor.TopRight;
}, rightFloatingOverlayContent.Add, true); }), rightFloatingOverlayContent.Add, true);
loadComponentSingleFile(screenshotManager, Add); loadComponentSingleFile(screenshotManager, Add);
@ -758,7 +760,7 @@ namespace osu.Game
Schedule(() => notifications.Post(new SimpleNotification Schedule(() => notifications.Post(new SimpleNotification
{ {
Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb,
Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty),
})); }));
} }
else if (recentLogCount == short_term_display_limit) else if (recentLogCount == short_term_display_limit)

View File

@ -199,6 +199,10 @@ namespace osu.Game
ScoreManager.Undelete(getBeatmapScores(item), true); ScoreManager.Undelete(getBeatmapScores(item), true);
}); });
var difficultyManager = new BeatmapDifficultyManager();
dependencies.Cache(difficultyManager);
AddInternal(difficultyManager);
dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore));
dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory));
dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore));

View File

@ -225,6 +225,9 @@ namespace osu.Game.Overlays.Dashboard.Friends
case OverlayPanelDisplayStyle.List: case OverlayPanelDisplayStyle.List:
return new UserListPanel(user); return new UserListPanel(user);
case OverlayPanelDisplayStyle.Brick:
return new UserBrickPanel(user);
} }
} }

View File

@ -12,9 +12,26 @@ namespace osu.Game.Overlays
{ {
public abstract class OverlayHeader : Container public abstract class OverlayHeader : Container
{ {
public const int CONTENT_X_MARGIN = 50; private float contentSidePadding;
/// <summary>
/// Horizontal padding of the header content.
/// </summary>
protected float ContentSidePadding
{
get => contentSidePadding;
set
{
contentSidePadding = value;
content.Padding = new MarginPadding
{
Horizontal = value
};
}
}
private readonly Box titleBackground; private readonly Box titleBackground;
private readonly Container content;
protected readonly FillFlowContainer HeaderInfo; protected readonly FillFlowContainer HeaderInfo;
@ -50,14 +67,10 @@ namespace osu.Game.Overlays
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Colour = Color4.Gray, Colour = Color4.Gray,
}, },
new Container content = new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding
{
Horizontal = CONTENT_X_MARGIN,
},
Children = new[] Children = new[]
{ {
CreateTitle().With(title => CreateTitle().With(title =>
@ -79,6 +92,8 @@ namespace osu.Game.Overlays
CreateContent() CreateContent()
} }
}); });
ContentSidePadding = 50;
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]

View File

@ -34,6 +34,10 @@ namespace osu.Game.Overlays
{ {
Icon = FontAwesome.Solid.Bars Icon = FontAwesome.Solid.Bars
}); });
AddTabItem(new PanelDisplayTabItem(OverlayPanelDisplayStyle.Brick)
{
Icon = FontAwesome.Solid.Th
});
} }
protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer
@ -96,6 +100,7 @@ namespace osu.Game.Overlays
public enum OverlayPanelDisplayStyle public enum OverlayPanelDisplayStyle
{ {
Card, Card,
List List,
Brick
} }
} }

View File

@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Profile
public ProfileHeader() public ProfileHeader()
{ {
ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN;
User.ValueChanged += e => updateDisplay(e.NewValue); User.ValueChanged += e => updateDisplay(e.NewValue);
TabControl.AddItem("info"); TabControl.AddItem("info");

View File

@ -18,10 +18,8 @@ using osu.Game.Online.API.Requests;
namespace osu.Game.Overlays.Rankings namespace osu.Game.Overlays.Rankings
{ {
public class SpotlightSelector : VisibilityContainer, IHasCurrentValue<APISpotlight> public class SpotlightSelector : CompositeDrawable, IHasCurrentValue<APISpotlight>
{ {
private const int duration = 300;
private readonly BindableWithCurrent<APISpotlight> current = new BindableWithCurrent<APISpotlight>(); private readonly BindableWithCurrent<APISpotlight> current = new BindableWithCurrent<APISpotlight>();
public readonly Bindable<RankingsSortCriteria> Sort = new Bindable<RankingsSortCriteria>(); public readonly Bindable<RankingsSortCriteria> Sort = new Bindable<RankingsSortCriteria>();
@ -37,10 +35,7 @@ namespace osu.Game.Overlays.Rankings
set => dropdown.Items = value; set => dropdown.Items = value;
} }
protected override bool StartHidden => true;
private readonly Box background; private readonly Box background;
private readonly Container content;
private readonly SpotlightsDropdown dropdown; private readonly SpotlightsDropdown dropdown;
private readonly InfoColumn startDateColumn; private readonly InfoColumn startDateColumn;
private readonly InfoColumn endDateColumn; private readonly InfoColumn endDateColumn;
@ -51,73 +46,68 @@ namespace osu.Game.Overlays.Rankings
{ {
RelativeSizeAxes = Axes.X; RelativeSizeAxes = Axes.X;
AutoSizeAxes = Axes.Y; AutoSizeAxes = Axes.Y;
Add(content = new Container InternalChildren = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, background = new Box
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{ {
background = new Box RelativeSizeAxes = Axes.Both,
{ },
RelativeSizeAxes = Axes.Both, new Container
}, {
new Container RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN },
Child = new FillFlowContainer
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, AutoSizeAxes = Axes.Y,
Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN }, Direction = FillDirection.Vertical,
Child = new FillFlowContainer Children = new Drawable[]
{ {
RelativeSizeAxes = Axes.X, new Container
AutoSizeAxes = Axes.Y,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{ {
new Container Margin = new MarginPadding { Vertical = 20 },
{ RelativeSizeAxes = Axes.X,
Margin = new MarginPadding { Vertical = 20 }, Height = 40,
RelativeSizeAxes = Axes.X, Depth = -float.MaxValue,
Height = 40, Child = dropdown = new SpotlightsDropdown
Depth = -float.MaxValue,
Child = dropdown = new SpotlightsDropdown
{
RelativeSizeAxes = Axes.X,
Current = Current
}
},
new Container
{ {
RelativeSizeAxes = Axes.X, RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y, Current = Current
Children = new Drawable[] }
},
new Container
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Children = new Drawable[]
{
new FillFlowContainer
{ {
new FillFlowContainer AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Spacing = new Vector2(10, 0),
Margin = new MarginPadding { Bottom = 5 },
Children = new Drawable[]
{ {
AutoSizeAxes = Axes.Both, startDateColumn = new InfoColumn(@"Start Date"),
Direction = FillDirection.Horizontal, endDateColumn = new InfoColumn(@"End Date"),
Spacing = new Vector2(10, 0), mapCountColumn = new InfoColumn(@"Map Count"),
Margin = new MarginPadding { Bottom = 5 }, participantsColumn = new InfoColumn(@"Participants")
Children = new Drawable[]
{
startDateColumn = new InfoColumn(@"Start Date"),
endDateColumn = new InfoColumn(@"End Date"),
mapCountColumn = new InfoColumn(@"Map Count"),
participantsColumn = new InfoColumn(@"Participants")
}
},
new RankingsSortTabControl
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Current = Sort
} }
},
new RankingsSortTabControl
{
Anchor = Anchor.CentreRight,
Origin = Anchor.CentreRight,
Current = Sort
} }
} }
} }
} }
} }
} }
}); };
} }
[BackgroundDependencyLoader] [BackgroundDependencyLoader]
@ -134,10 +124,6 @@ namespace osu.Game.Overlays.Rankings
participantsColumn.Value = response.Spotlight.Participants?.ToString("N0"); participantsColumn.Value = response.Spotlight.Participants?.ToString("N0");
} }
protected override void PopIn() => content.FadeIn(duration, Easing.OutQuint);
protected override void PopOut() => content.FadeOut(duration, Easing.OutQuint);
private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd"); private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd");
private class InfoColumn : FillFlowContainer private class InfoColumn : FillFlowContainer

View File

@ -81,8 +81,6 @@ namespace osu.Game.Overlays.Rankings
{ {
base.LoadComplete(); base.LoadComplete();
selector.Show();
selectedSpotlight.BindValueChanged(_ => onSpotlightChanged()); selectedSpotlight.BindValueChanged(_ => onSpotlightChanged());
sort.BindValueChanged(_ => onSpotlightChanged()); sort.BindValueChanged(_ => onSpotlightChanged());
Ruleset.BindValueChanged(onRulesetChanged); Ruleset.BindValueChanged(onRulesetChanged);

View File

@ -1,6 +1,7 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence. // 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. // See the LICENCE file in the repository root for full licence text.
using osu.Framework;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Game.Configuration; using osu.Game.Configuration;
@ -78,6 +79,15 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay
Bindable = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode) Bindable = config.GetBindable<ScoringMode>(OsuSetting.ScoreDisplayMode)
} }
}; };
if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows)
{
Add(new SettingsCheckbox
{
LabelText = "Disable Windows key during gameplay",
Bindable = config.GetBindable<bool>(OsuSetting.GameplayDisableWinKey)
});
}
} }
} }
} }

View File

@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.General
[Resolved] [Resolved]
private OsuColour colours { get; set; } private OsuColour colours { get; set; }
private UserPanel panel; private UserGridPanel panel;
private UserDropdown dropdown; private UserDropdown dropdown;
/// <summary> /// <summary>

View File

@ -1,33 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Framework.Extensions.Color4Extensions;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Game.Overlays.SearchableList;
namespace osu.Game.Overlays.Social
{
public class FilterControl : SearchableListFilterControl<SocialSortCriteria, SortDirection>
{
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"47253a");
protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank;
protected override SortDirection DefaultCategory => SortDirection.Ascending;
public FilterControl()
{
Tabs.Margin = new MarginPadding { Top = 10 };
}
}
public enum SocialSortCriteria
{
Rank,
Name,
Location,
//[Description("Time Zone")]
//TimeZone,
//[Description("World Map")]
//WorldMap,
}
}

View File

@ -1,67 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using osu.Game.Overlays.SearchableList;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics;
using osu.Framework.Allocation;
using System.ComponentModel;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Graphics.Sprites;
namespace osu.Game.Overlays.Social
{
public class Header : SearchableListHeader<SocialTab>
{
private OsuSpriteText browser;
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"38202e");
protected override SocialTab DefaultTab => SocialTab.AllPlayers;
protected override IconUsage Icon => FontAwesome.Solid.Users;
protected override Drawable CreateHeaderText()
{
return new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new[]
{
new OsuSpriteText
{
Text = "social ",
Font = OsuFont.GetFont(size: 25),
},
browser = new OsuSpriteText
{
Text = "browser",
Font = OsuFont.GetFont(size: 25, weight: FontWeight.Light),
},
},
};
}
[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
browser.Colour = colours.Pink;
}
}
public enum SocialTab
{
[Description("All Players")]
AllPlayers,
[Description("Friends")]
Friends,
//[Description("Team Members")]
//TeamMembers,
//[Description("Chat Channels")]
//ChatChannels,
}
}

View File

@ -1,242 +0,0 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osuTK;
using osuTK.Graphics;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Graphics.UserInterface;
using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Overlays.SearchableList;
using osu.Game.Overlays.Social;
using osu.Game.Users;
using System.Threading;
using osu.Framework.Allocation;
using osu.Framework.Extensions.Color4Extensions;
using osu.Framework.Threading;
namespace osu.Game.Overlays
{
public class SocialOverlay : SearchableListOverlay<SocialTab, SocialSortCriteria, SortDirection>
{
private readonly LoadingSpinner loading;
private FillFlowContainer<UserPanel> panels;
protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"60284b");
protected override Color4 TrianglesColourLight => Color4Extensions.FromHex(@"672b51");
protected override Color4 TrianglesColourDark => Color4Extensions.FromHex(@"5c2648");
protected override SearchableListHeader<SocialTab> CreateHeader() => new Header();
protected override SearchableListFilterControl<SocialSortCriteria, SortDirection> CreateFilterControl() => new FilterControl();
private User[] users = Array.Empty<User>();
public User[] Users
{
get => users;
set
{
if (users == value)
return;
users = value ?? Array.Empty<User>();
if (LoadState >= LoadState.Ready)
recreatePanels();
}
}
public SocialOverlay()
: base(OverlayColourScheme.Pink)
{
Add(loading = new LoadingSpinner());
Filter.Search.Current.ValueChanged += text =>
{
if (!string.IsNullOrEmpty(text.NewValue))
{
// force searching in players until searching for friends is supported
Header.Tabs.Current.Value = SocialTab.AllPlayers;
if (Filter.Tabs.Current.Value != SocialSortCriteria.Rank)
Filter.Tabs.Current.Value = SocialSortCriteria.Rank;
}
};
Header.Tabs.Current.ValueChanged += _ => queueUpdate();
Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate();
Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels();
Filter.Dropdown.Current.ValueChanged += _ => recreatePanels();
currentQuery.BindTo(Filter.Search.Current);
currentQuery.ValueChanged += query =>
{
queryChangedDebounce?.Cancel();
if (string.IsNullOrEmpty(query.NewValue))
queueUpdate();
else
queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500);
};
}
[BackgroundDependencyLoader]
private void load()
{
recreatePanels();
}
private APIRequest getUsersRequest;
private readonly Bindable<string> currentQuery = new Bindable<string>();
private ScheduledDelegate queryChangedDebounce;
private void queueUpdate() => Scheduler.AddOnce(updateSearch);
private CancellationTokenSource loadCancellation;
private void updateSearch()
{
queryChangedDebounce?.Cancel();
if (!IsLoaded)
return;
Users = null;
clearPanels();
getUsersRequest?.Cancel();
if (API?.IsLoggedIn != true)
return;
switch (Header.Tabs.Current.Value)
{
case SocialTab.Friends:
var friendRequest = new GetFriendsRequest(); // TODO filter arguments?
friendRequest.Success += users => Users = users.ToArray();
API.Queue(getUsersRequest = friendRequest);
break;
default:
var userRequest = new GetUsersRequest(); // TODO filter arguments!
userRequest.Success += res => Users = res.Users.Select(r => r.User).ToArray();
API.Queue(getUsersRequest = userRequest);
break;
}
}
private void recreatePanels()
{
clearPanels();
if (Users == null)
{
loading.Hide();
return;
}
IEnumerable<User> sortedUsers = Users;
switch (Filter.Tabs.Current.Value)
{
case SocialSortCriteria.Location:
sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName);
break;
case SocialSortCriteria.Name:
sortedUsers = sortedUsers.OrderBy(u => u.Username);
break;
}
if (Filter.Dropdown.Current.Value == SortDirection.Descending)
sortedUsers = sortedUsers.Reverse();
var newPanels = new FillFlowContainer<UserPanel>
{
RelativeSizeAxes = Axes.X,
AutoSizeAxes = Axes.Y,
Spacing = new Vector2(10f),
Margin = new MarginPadding { Top = 10 },
ChildrenEnumerable = sortedUsers.Select(u =>
{
UserPanel panel;
switch (Filter.DisplayStyleControl.DisplayStyle.Value)
{
case PanelDisplayStyle.Grid:
panel = new UserGridPanel(u)
{
Anchor = Anchor.TopCentre,
Origin = Anchor.TopCentre,
Width = 290,
};
break;
default:
panel = new UserListPanel(u);
break;
}
panel.Status.BindTo(u.Status);
panel.Activity.BindTo(u.Activity);
return panel;
})
};
LoadComponentAsync(newPanels, f =>
{
if (panels != null)
ScrollFlow.Remove(panels);
loading.Hide();
ScrollFlow.Add(panels = newPanels);
}, (loadCancellation = new CancellationTokenSource()).Token);
}
private void onFilterUpdate()
{
if (Filter.Tabs.Current.Value == SocialSortCriteria.Rank)
{
queueUpdate();
return;
}
recreatePanels();
}
private void clearPanels()
{
loading.Show();
loadCancellation?.Cancel();
if (panels != null)
{
panels.Expire();
panels = null;
}
}
public override void APIStateChanged(IAPIProvider api, APIState state)
{
switch (state)
{
case APIState.Online:
queueUpdate();
break;
default:
Users = null;
clearPanels();
break;
}
}
}
}

View File

@ -22,6 +22,7 @@ namespace osu.Game.Overlays
protected OsuTabControl<T> TabControl; protected OsuTabControl<T> TabControl;
private readonly Box controlBackground; private readonly Box controlBackground;
private readonly Container tabControlContainer;
private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>(); private readonly BindableWithCurrent<T> current = new BindableWithCurrent<T>();
public Bindable<T> Current public Bindable<T> Current
@ -30,6 +31,16 @@ namespace osu.Game.Overlays
set => current.Current = value; set => current.Current = value;
} }
protected new float ContentSidePadding
{
get => base.ContentSidePadding;
set
{
base.ContentSidePadding = value;
tabControlContainer.Padding = new MarginPadding { Horizontal = value };
}
}
protected TabControlOverlayHeader() protected TabControlOverlayHeader()
{ {
HeaderInfo.Add(new Container HeaderInfo.Add(new Container
@ -42,11 +53,16 @@ namespace osu.Game.Overlays
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
}, },
TabControl = CreateTabControl().With(control => tabControlContainer = new Container
{ {
control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN }; RelativeSizeAxes = Axes.X,
control.Current = Current; AutoSizeAxes = Axes.Y,
}) Padding = new MarginPadding { Horizontal = ContentSidePadding },
Child = TabControl = CreateTabControl().With(control =>
{
control.Current = Current;
})
}
} }
}); });
} }

View File

@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods
protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result)
=> !(result.Judgement is IgnoreJudgement) => !(result.Judgement is IgnoreJudgement)
&& result.Judgement.AffectsCombo
&& result.Type != result.Judgement.MaxResult; && result.Type != result.Judgement.MaxResult;
} }
} }

View File

@ -126,12 +126,12 @@ namespace osu.Game.Rulesets.Objects.Drawables
if (Result == null) if (Result == null)
throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}.");
loadSamples(); LoadSamples();
} }
protected override void LoadComplete() protected override void LoadAsyncComplete()
{ {
base.LoadComplete(); base.LoadAsyncComplete();
HitObject.DefaultsApplied += onDefaultsApplied; HitObject.DefaultsApplied += onDefaultsApplied;
@ -145,14 +145,19 @@ namespace osu.Game.Rulesets.Objects.Drawables
} }
samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); samplesBindable = HitObject.SamplesBindable.GetBoundCopy();
samplesBindable.CollectionChanged += (_, __) => loadSamples(); samplesBindable.CollectionChanged += (_, __) => LoadSamples();
apply(HitObject); apply(HitObject);
}
protected override void LoadComplete()
{
base.LoadComplete();
updateState(ArmedState.Idle, true); updateState(ArmedState.Idle, true);
} }
private void loadSamples() protected virtual void LoadSamples()
{ {
if (Samples != null) if (Samples != null)
{ {
@ -353,17 +358,32 @@ namespace osu.Game.Rulesets.Objects.Drawables
[Resolved(canBeNull: true)] [Resolved(canBeNull: true)]
private GameplayClock gameplayClock { get; set; } private GameplayClock gameplayClock { get; set; }
/// <summary>
/// Calculate the position to be used for sample playback at a specified X position (0..1).
/// </summary>
/// <param name="position">The lookup X position. Generally should be <see cref="SamplePlaybackPosition"/>.</param>
/// <returns></returns>
protected double CalculateSamplePlaybackBalance(double position)
{
const float balance_adjust_amount = 0.4f;
return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0);
}
/// <summary>
/// Whether samples should currently be playing. Will be false during seek operations.
/// </summary>
protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true;
/// <summary> /// <summary>
/// Plays all the hit sounds for this <see cref="DrawableHitObject"/>. /// Plays all the hit sounds for this <see cref="DrawableHitObject"/>.
/// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit. /// This is invoked automatically when this <see cref="DrawableHitObject"/> is hit.
/// </summary> /// </summary>
public virtual void PlaySamples() public virtual void PlaySamples()
{ {
const float balance_adjust_amount = 0.4f; if (Samples != null && ShouldPlaySamples)
if (Samples != null && gameplayClock?.IsSeeking != true)
{ {
Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition);
Samples.Play(); Samples.Play();
} }
} }

View File

@ -4,7 +4,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations; using JetBrains.Annotations;
using osu.Framework.Input.StateChanges;
using osu.Game.Input.Handlers; using osu.Game.Input.Handlers;
using osu.Game.Replays; using osu.Game.Replays;
@ -69,8 +68,6 @@ namespace osu.Game.Rulesets.Replays
return true; return true;
} }
public override List<IInput> GetPendingInputs() => new List<IInput>();
private const double sixty_frame_time = 1000.0 / 60; private const double sixty_frame_time = 1000.0 / 60;
protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2; protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2;

View File

@ -2,11 +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 System.Collections.Generic; using System.Collections.Generic;
using JetBrains.Annotations;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Framework.Caching; using osu.Framework.Caching;
using osu.Framework.Graphics; using osu.Framework.Graphics;
using osu.Framework.Layout; using osu.Framework.Layout;
using osu.Framework.Threading;
using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects.Types;
using osuTK; using osuTK;
@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
private readonly IBindable<double> timeRange = new BindableDouble(); private readonly IBindable<double> timeRange = new BindableDouble();
private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>(); private readonly IBindable<ScrollingDirection> direction = new Bindable<ScrollingDirection>();
private readonly Dictionary<DrawableHitObject, Cached> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, Cached>(); private readonly Dictionary<DrawableHitObject, InitialState> hitObjectInitialStateCache = new Dictionary<DrawableHitObject, InitialState>();
[Resolved] [Resolved]
private IScrollingInfo scrollingInfo { get; set; } private IScrollingInfo scrollingInfo { get; set; }
@ -175,10 +177,10 @@ namespace osu.Game.Rulesets.UI.Scrolling
{ {
// The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame). // The cache may not exist if the hitobject state hasn't been computed yet (e.g. if the hitobject was added + defaults applied in the same frame).
// In such a case, combinedObjCache will take care of updating the hitobject. // In such a case, combinedObjCache will take care of updating the hitobject.
if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var objCache)) if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state))
{ {
combinedObjCache.Invalidate(); combinedObjCache.Invalidate();
objCache.Invalidate(); state.Cache.Invalidate();
} }
} }
@ -190,8 +192,8 @@ namespace osu.Game.Rulesets.UI.Scrolling
if (!layoutCache.IsValid) if (!layoutCache.IsValid)
{ {
foreach (var cached in hitObjectInitialStateCache.Values) foreach (var state in hitObjectInitialStateCache.Values)
cached.Invalidate(); state.Cache.Invalidate();
combinedObjCache.Invalidate(); combinedObjCache.Invalidate();
scrollingInfo.Algorithm.Reset(); scrollingInfo.Algorithm.Reset();
@ -215,16 +217,18 @@ namespace osu.Game.Rulesets.UI.Scrolling
foreach (var obj in Objects) foreach (var obj in Objects)
{ {
if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache)) if (!hitObjectInitialStateCache.TryGetValue(obj, out var state))
objCache = hitObjectInitialStateCache[obj] = new Cached(); state = hitObjectInitialStateCache[obj] = new InitialState(new Cached());
if (objCache.IsValid) if (state.Cache.IsValid)
continue; continue;
computeLifetimeStartRecursive(obj); state.ScheduledComputation?.Cancel();
computeInitialStateRecursive(obj); state.ScheduledComputation = computeInitialStateRecursive(obj);
objCache.Validate(); computeLifetimeStartRecursive(obj);
state.Cache.Validate();
} }
combinedObjCache.Validate(); combinedObjCache.Validate();
@ -267,8 +271,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength);
} }
// Cant use AddOnce() since the delegate is re-constructed every invocation private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() =>
{ {
if (hitObject.HitObject is IHasDuration e) if (hitObject.HitObject is IHasDuration e)
{ {
@ -325,5 +328,19 @@ namespace osu.Game.Rulesets.UI.Scrolling
break; break;
} }
} }
private class InitialState
{
[NotNull]
public readonly Cached Cache;
[CanBeNull]
public ScheduledDelegate ScheduledComputation;
public InitialState(Cached cache)
{
Cache = cache;
}
}
} }
} }

View File

@ -260,7 +260,7 @@ namespace osu.Game.Screens.Menu
private class LazerLogo : CompositeDrawable private class LazerLogo : CompositeDrawable
{ {
private HueAnimation highlight, background; private LogoAnimation highlight, background;
public float Progress public float Progress
{ {
@ -282,13 +282,13 @@ namespace osu.Game.Screens.Menu
{ {
InternalChildren = new Drawable[] InternalChildren = new Drawable[]
{ {
highlight = new HueAnimation highlight = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-highlight"), Texture = textures.Get(@"Intro/Triangles/logo-highlight"),
Colour = Color4.White, Colour = Color4.White,
}, },
background = new HueAnimation background = new LogoAnimation
{ {
RelativeSizeAxes = Axes.Both, RelativeSizeAxes = Axes.Both,
Texture = textures.Get(@"Intro/Triangles/logo-background"), Texture = textures.Get(@"Intro/Triangles/logo-background"),

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using osu.Framework.Allocation; using osu.Framework.Allocation;
using osu.Framework.Bindables; using osu.Framework.Bindables;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API.Requests.Responses;
using osu.Game.Online.Leaderboards; using osu.Game.Online.Leaderboards;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;

View File

@ -10,7 +10,6 @@ using osu.Framework.Bindables;
using osu.Framework.Logging; using osu.Framework.Logging;
using osu.Framework.Screens; using osu.Framework.Screens;
using osu.Game.Online.API; using osu.Game.Online.API;
using osu.Game.Online.API.Requests;
using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer;
using osu.Game.Rulesets; using osu.Game.Rulesets;
using osu.Game.Scoring; using osu.Game.Scoring;

Some files were not shown because too many files have changed in this diff Show More