diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs new file mode 100644 index 0000000000..b53ca6161b --- /dev/null +++ b/osu.Desktop/DiscordRichPresence.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using DiscordRPC; +using DiscordRPC.Message; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Users; +using LogLevel = osu.Framework.Logging.LogLevel; +using User = osu.Game.Users.User; + +namespace osu.Desktop +{ + internal class DiscordRichPresence : Component + { + private const string client_id = "367827983903490050"; + + private DiscordRpcClient client; + + [Resolved] + private IBindable ruleset { get; set; } + + private Bindable user; + + private readonly IBindable status = new Bindable(); + private readonly IBindable activity = new Bindable(); + + private readonly RichPresence presence = new RichPresence + { + Assets = new Assets { LargeImageKey = "osu_logo_lazer", } + }; + + [BackgroundDependencyLoader] + private void load(IAPIProvider provider) + { + client = new DiscordRpcClient(client_id) + { + SkipIdenticalPresence = false // handles better on discord IPC loss, see updateStatus call in onReady. + }; + + client.OnReady += onReady; + client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network, LogLevel.Error); + client.OnConnectionFailed += (_, e) => Logger.Log($"An connection occurred with Discord RPC Client: {e.Type}", LoggingTarget.Network, LogLevel.Error); + + (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => + { + status.UnbindBindings(); + status.BindTo(u.NewValue.Status); + + activity.UnbindBindings(); + activity.BindTo(u.NewValue.Activity); + }, true); + + ruleset.BindValueChanged(_ => updateStatus()); + status.BindValueChanged(_ => updateStatus()); + activity.BindValueChanged(_ => updateStatus()); + + client.Initialize(); + } + + private void onReady(object _, ReadyMessage __) + { + Logger.Log("Discord RPC Client ready.", LoggingTarget.Network, LogLevel.Debug); + updateStatus(); + } + + private void updateStatus() + { + if (status.Value is UserStatusOffline) + { + client.ClearPresence(); + return; + } + + if (status.Value is UserStatusOnline && activity.Value != null) + { + presence.State = activity.Value.Status; + presence.Details = getDetails(activity.Value); + } + else + { + presence.State = "Idle"; + presence.Details = string.Empty; + } + + // update user information + presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); + + // update ruleset + presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom"; + presence.Assets.SmallImageText = ruleset.Value.Name; + + client.SetPresence(presence); + } + + private string getDetails(UserActivity activity) + { + switch (activity) + { + case UserActivity.SoloGame solo: + return solo.Beatmap.ToString(); + + case UserActivity.Editing edit: + return edit.Beatmap.ToString(); + } + + return string.Empty; + } + + protected override void Dispose(bool isDisposing) + { + client.Dispose(); + base.Dispose(isDisposing); + } + } +} diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 66e7bb381c..f70cc24159 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -60,6 +60,8 @@ namespace osu.Desktop else Add(new SimpleUpdateManager()); } + + LoadComponentAsync(new DiscordRichPresence(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 60cada3ae7..61299cc23f 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -29,6 +29,7 @@ + diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 065771bc4a..b8a844cb86 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -137,10 +137,5 @@ namespace osu.Game.Rulesets.Catch public override int? LegacyID => 2; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new CatchReplayFrame(); - - public CatchRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs index da2edcee44..c07087efaf 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModNightcore.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModNightcore : ModNightcore + public class CatchModNightcore : ModNightcore { public override double ScoreMultiplier => 1.06; } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8b53ce01f6..bf630cf892 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -186,11 +186,6 @@ namespace osu.Game.Rulesets.Mania public override RulesetSettingsSubsection CreateSettings() => new ManiaSettingsSubsection(this); - public ManiaRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - public override IEnumerable AvailableVariants { get diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs index 2d94fb6af5..4cc712060c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModNightcore.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModNightcore : ModNightcore + public class ManiaModNightcore : ModNightcore { public override double ScoreMultiplier => 1; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index d0ce0c33c2..02ce77e707 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -70,6 +70,21 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100)); } + [Test] + public void TestSpinPerMinuteOnRewind() + { + double estimatedSpm = 0; + + addSeekStep(2500); + AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); + + addSeekStep(5000); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + + addSeekStep(2500); + AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); + } + private void addSeekStep(double time) { AddStep($"seek to {time}", () => track.Seek(time)); @@ -84,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Spinner { Position = new Vector2(256, 192), - EndTime = 5000, + EndTime = 6000, }, // placeholder object to avoid hitting the results screen new HitObject diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs index 5668c17792..7780e23a26 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModNightcore.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModNightcore : ModNightcore + public class OsuModNightcore : ModNightcore { public override double ScoreMultiplier => 1.12; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1261d3d19a..de11ab6419 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; - private readonly SpinnerSpmCounter spmCounter; + public readonly SpinnerSpmCounter SpmCounter; private readonly Container mainContainer; @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, } }, - spmCounter = new SpinnerSpmCounter + SpmCounter = new SpinnerSpmCounter { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -177,8 +177,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; - if (!spmCounter.IsPresent && Disc.Tracking) - spmCounter.FadeIn(HitObject.TimeFadeIn); + if (!SpmCounter.IsPresent && Disc.Tracking) + SpmCounter.FadeIn(HitObject.TimeFadeIn); base.Update(); } @@ -189,7 +189,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; - spmCounter.SetRotation(Disc.RotationAbsolute); + SpmCounter.SetRotation(Disc.RotationAbsolute); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs index aa9caf193e..cedf2f6e09 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs @@ -24,16 +24,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private OsuRulesetConfigManager config { get; set; } private Slider slider; - private float defaultPathRadius; [BackgroundDependencyLoader] private void load(ISkinSource skin) { slider = (Slider)drawableObject.HitObject; - defaultPathRadius = skin.GetConfig(OsuSkinConfiguration.SliderPathRadius)?.Value ?? OsuHitObject.OBJECT_RADIUS; scaleBindable = slider.ScaleBindable.GetBoundCopy(); - scaleBindable.BindValueChanged(_ => updatePathRadius(), true); + scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); pathVersion = slider.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => Refresh()); @@ -48,9 +46,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; } - private void updatePathRadius() - => PathRadius = defaultPathRadius * scaleBindable.Value; - private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) => AccentColour = skin.GetConfig(OsuSkinColour.SliderTrackOverride)?.Value ?? defaultAccentColour; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs index b1d90c49f6..97a7b98c5b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.MathUtils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -62,6 +63,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void SetRotation(float currentRotation) { + // Never calculate SPM by same time of record to avoid 0 / 0 = NaN or X / 0 = Infinity result. + if (Precision.AlmostEquals(0, Time.Elapsed)) + return; + // If we've gone back in time, it's fine to work with a fresh set of records for now if (records.Count > 0 && Time.Current < records.Last().Time) records.Clear(); @@ -71,6 +76,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var record = records.Peek(); while (records.Count > 0 && Time.Current - records.Peek().Time > spm_count_duration) record = records.Dequeue(); + SpinsPerMinute = (currentRotation - record.Rotation) / (Time.Current - record.Time) * 1000 * 60 / 360; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 835ae2564c..27af615935 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -183,10 +183,5 @@ namespace osu.Game.Rulesets.Osu public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new OsuReplayFrame(); public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - - public OsuRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index dea08f843e..d41135ca69 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.MathUtils; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osuTK.Graphics; @@ -15,19 +16,27 @@ namespace osu.Game.Rulesets.Osu.Skinning private class LegacyDrawableSliderPath : DrawableSliderPath { + private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) { - if (CalculatedBorderPortion != 0f && position <= CalculatedBorderPortion) + float realBorderPortion = shadow_portion + CalculatedBorderPortion; + float realGradientPortion = 1 - realBorderPortion; + + if (position <= shadow_portion) + return new Color4(0f, 0f, 0f, 0.25f * position / shadow_portion); + + if (position <= realBorderPortion) return BorderColour; - position -= BORDER_PORTION; + position -= realBorderPortion; Color4 outerColour = AccentColour.Darken(0.1f); Color4 innerColour = lighten(AccentColour, 0.5f); - return Interpolation.ValueAt(position / GRADIENT_PORTION, outerColour, innerColour, 0, 1); + return Interpolation.ValueAt(position / realGradientPortion, outerColour, innerColour, 0, 1); } /// diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 71770dedce..266b619334 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Skinning /// Their hittable area is 128px, but the actual circle portion is 118px. /// We must account for some gameplay elements such as slider bodies, where this padding is not present. /// - private const float legacy_circle_radius = 64 - 5; + public const float LEGACY_CIRCLE_RADIUS = 64 - 5; public OsuLegacySkinTransformer(ISkinSource source) { @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { case OsuSkinConfiguration.SliderPathRadius: if (hasHitCircle.Value) - return SkinUtils.As(new BindableFloat(legacy_circle_radius)); + return SkinUtils.As(new BindableFloat(LEGACY_CIRCLE_RADIUS)); break; } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs index e45081b6d6..5377eb1072 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModNightcore.cs @@ -2,10 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModNightcore : ModNightcore + public class TaikoModNightcore : ModNightcore { public override double ScoreMultiplier => 1.12; } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 4d6c5fa1c0..ca7ab30867 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -136,10 +136,5 @@ namespace osu.Game.Rulesets.Taiko public override int? LegacyID => 1; public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - - public TaikoRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } } } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs new file mode 100644 index 0000000000..f2b3a16f68 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -0,0 +1,54 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; +using osu.Game.IO; +using osu.Game.IO.Serialization; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyBeatmapEncoderTest + { + private const string normal = "Soleily - Renatus (Gamu) [Insane].osu"; + + private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); + + [TestCaseSource(nameof(allBeatmaps))] + public void TestDecodeEncodedBeatmap(string name) + { + var decoded = decode(normal, out var encoded); + + Assert.That(decoded.HitObjects.Count, Is.EqualTo(encoded.HitObjects.Count)); + Assert.That(encoded.Serialize(), Is.EqualTo(decoded.Serialize())); + } + + private Beatmap decode(string filename, out Beatmap encoded) + { + using (var stream = TestResources.OpenResource(filename)) + using (var sr = new LineBufferedReader(stream)) + { + var legacyDecoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr); + + using (var ms = new MemoryStream()) + using (var sw = new StreamWriter(ms)) + using (var sr2 = new LineBufferedReader(ms)) + { + new LegacyBeatmapEncoder(legacyDecoded).Encode(sw); + sw.Flush(); + + ms.Position = 0; + + encoded = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(sr2); + return legacyDecoded; + } + } + } + } +} diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 66084a3204..a57405628a 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -9,7 +9,9 @@ namespace osu.Game.Tests.Resources { public static class TestResources { - public static Stream OpenResource(string name) => new DllResourceStore("osu.Game.Tests.dll").GetStream($"Resources/{name}"); + public static DllResourceStore GetStore() => new DllResourceStore("osu.Game.Tests.dll"); + + public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); public static Stream GetTestBeatmapStream(bool virtualTrack = false) => new DllResourceStore("osu.Game.Resources.dll").GetStream($"Beatmaps/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index c958932730..ae20bbc86d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -194,11 +194,6 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestScrollingRuleset : Ruleset { - public TestScrollingRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawableScrollingRuleset(this, beatmap, mods); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs new file mode 100644 index 0000000000..3473b03eaf --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . 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.Extensions.IEnumerableExtensions; +using osu.Game.Beatmaps.Timing; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Visual.UserInterface; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneNightcoreBeatContainer : TestSceneBeatSyncedContainer + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(ModNightcore<>) + }; + + protected override void LoadComplete() + { + base.LoadComplete(); + + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + Beatmap.Value.Track.Start(); + Beatmap.Value.Track.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); + + Add(new ModNightcore.NightcoreBeatContainer()); + + AddStep("change signature to quadruple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleQuadruple)); + AddStep("change signature to triple", () => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.ForEach(p => p.TimeSignature = TimeSignatures.SimpleTriple)); + } + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 546f6ac182..d47c972564 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -1,23 +1,66 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Graphics; using osu.Game.Overlays; +using osu.Game.Overlays.News; namespace osu.Game.Tests.Visual.Online { public class TestSceneNewsOverlay : OsuTestScene { - private NewsOverlay news; + private TestNewsOverlay news; protected override void LoadComplete() { base.LoadComplete(); - Add(news = new NewsOverlay()); + Add(news = new TestNewsOverlay()); AddStep(@"Show", news.Show); AddStep(@"Hide", news.Hide); AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); + + AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest())); + } + + private class TestNewsOverlay : NewsOverlay + { + public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content); + } + + private class NewsCoverTest : NewsContent + { + public NewsCoverTest() + { + Spacing = new osuTK.Vector2(0, 10); + + var article = new NewsArticleCover.ArticleInfo + { + Author = "Ephemeral", + CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg", + Time = new DateTime(2019, 12, 4), + Title = "New Featured Artist: Kurokotei" + }; + + Children = new Drawable[] + { + new NewsArticleCover(article) + { + Height = 200 + }, + new NewsArticleCover(article) + { + Height = 120 + }, + new NewsArticleCover(article) + { + RelativeSizeAxes = Axes.None, + Size = new osuTK.Vector2(400, 200), + } + }; + } } } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs index ed9e01a67e..66144cbfe4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapDetailArea.cs @@ -8,6 +8,9 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps; using osuTK; @@ -20,8 +23,10 @@ namespace osu.Game.Tests.Visual.SongSelect { public override IReadOnlyList RequiredTypes => new[] { typeof(BeatmapDetails) }; + private ModDisplay modDisplay; + [BackgroundDependencyLoader] - private void load(OsuGameBase game) + private void load(OsuGameBase game, RulesetStore rulesets) { BeatmapDetailArea detailsArea; Add(detailsArea = new BeatmapDetailArea @@ -31,6 +36,16 @@ namespace osu.Game.Tests.Visual.SongSelect Size = new Vector2(550f, 450f), }); + Add(modDisplay = new ModDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Position = new Vector2(0, 25), + }); + + modDisplay.Current.BindTo(SelectedMods); + AddStep("all metrics", () => detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap { BeatmapInfo = @@ -163,6 +178,60 @@ namespace osu.Game.Tests.Visual.SongSelect })); AddStep("null beatmap", () => detailsArea.Beatmap = null); + + Ruleset ruleset = rulesets.AvailableRulesets.First().CreateInstance(); + + AddStep("with EZ mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Easy Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the easy mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModEasy) }; + }); + + AddStep("with HR mod", () => + { + detailsArea.Beatmap = new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = + { + Version = "Has Hard Rock Mod", + Metadata = new BeatmapMetadata + { + Source = "osu!lazer", + Tags = "this beatmap has the hard rock mod enabled", + }, + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 3, + DrainRate = 3, + OverallDifficulty = 3, + ApproachRate = 3, + }, + StarDifficulty = 1f, + } + }); + + SelectedMods.Value = new[] { ruleset.GetAllMods().First(m => m is ModHardRock) }; + }); } } } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 9ea254b23f..46efe38d37 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Beatmaps private class DummyRulesetInfo : RulesetInfo { - public override Ruleset CreateInstance() => new DummyRuleset(this); + public override Ruleset CreateInstance() => new DummyRuleset(); private class DummyRuleset : Ruleset { @@ -70,11 +70,6 @@ namespace osu.Game.Beatmaps public override string ShortName => "dummy"; - public DummyRuleset(RulesetInfo rulesetInfo = null) - : base(rulesetInfo) - { - } - private class DummyBeatmapConverter : IBeatmapConverter { public event Action> ObjectConverted; diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index f8275ec4f6..447d52d980 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Beatmaps.ControlPoints; using osu.Game.IO; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Beatmaps.Formats { @@ -293,22 +294,22 @@ namespace osu.Game.Beatmaps.Formats { string[] split = line.Split(','); - if (!Enum.TryParse(split[0], out EventType type)) + if (!Enum.TryParse(split[0], out LegacyEventType type)) throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { - case EventType.Background: + case LegacyEventType.Background: string bgFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.BackgroundFile = bgFilename.ToStandardisedPath(); break; - case EventType.Video: + case LegacyEventType.Video: string videoFilename = split[2].Trim('"'); beatmap.BeatmapInfo.Metadata.VideoFile = videoFilename.ToStandardisedPath(); break; - case EventType.Break: + case LegacyEventType.Break: double start = getOffsetTime(Parsing.ParseDouble(split[1])); var breakEvent = new BreakPeriod @@ -358,9 +359,9 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { - EffectFlags effectFlags = (EffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlag(EffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlag(EffectFlags.OmitFirstBarLine); + LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); + kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); @@ -448,13 +449,5 @@ namespace osu.Game.Beatmaps.Formats private double getOffsetTime(double time) => time + (ApplyOffsets ? offset : 0); protected virtual TimingControlPoint CreateTimingControlPoint() => new TimingControlPoint(); - - [Flags] - internal enum EffectFlags - { - None = 0, - Kiai = 1, - OmitFirstBarLine = 8 - } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs new file mode 100644 index 0000000000..433becd8cc --- /dev/null +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -0,0 +1,410 @@ +// Copyright (c) ppy Pty Ltd . 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.IO; +using System.Linq; +using System.Text; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Legacy; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; + +namespace osu.Game.Beatmaps.Formats +{ + public class LegacyBeatmapEncoder + { + public const int LATEST_VERSION = 128; + + private readonly IBeatmap beatmap; + + public LegacyBeatmapEncoder(IBeatmap beatmap) + { + this.beatmap = beatmap; + + if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) + throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); + } + + public void Encode(TextWriter writer) + { + writer.WriteLine($"osu file format v{LATEST_VERSION}"); + + writer.WriteLine(); + handleGeneral(writer); + + writer.WriteLine(); + handleEditor(writer); + + writer.WriteLine(); + handleMetadata(writer); + + writer.WriteLine(); + handleDifficulty(writer); + + writer.WriteLine(); + handleEvents(writer); + + writer.WriteLine(); + handleTimingPoints(writer); + + writer.WriteLine(); + handleHitObjects(writer); + } + + private void handleGeneral(TextWriter writer) + { + writer.WriteLine("[General]"); + + writer.WriteLine(FormattableString.Invariant($"AudioFilename: {Path.GetFileName(beatmap.Metadata.AudioFile)}")); + writer.WriteLine(FormattableString.Invariant($"AudioLeadIn: {beatmap.BeatmapInfo.AudioLeadIn}")); + writer.WriteLine(FormattableString.Invariant($"PreviewTime: {beatmap.Metadata.PreviewTime}")); + // Todo: Not all countdown types are supported by lazer yet + writer.WriteLine(FormattableString.Invariant($"Countdown: {(beatmap.BeatmapInfo.Countdown ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"SampleSet: {toLegacySampleBank(beatmap.ControlPointInfo.SamplePoints[0].SampleBank)}")); + writer.WriteLine(FormattableString.Invariant($"StackLeniency: {beatmap.BeatmapInfo.StackLeniency}")); + writer.WriteLine(FormattableString.Invariant($"Mode: {beatmap.BeatmapInfo.RulesetID}")); + writer.WriteLine(FormattableString.Invariant($"LetterboxInBreaks: {(beatmap.BeatmapInfo.LetterboxInBreaks ? '1' : '0')}")); + // if (beatmap.BeatmapInfo.UseSkinSprites) + // writer.WriteLine(@"UseSkinSprites: 1"); + // if (b.AlwaysShowPlayfield) + // writer.WriteLine(@"AlwaysShowPlayfield: 1"); + // if (b.OverlayPosition != OverlayPosition.NoChange) + // writer.WriteLine(@"OverlayPosition: " + b.OverlayPosition); + // if (!string.IsNullOrEmpty(b.SkinPreference)) + // writer.WriteLine(@"SkinPreference:" + b.SkinPreference); + // if (b.EpilepsyWarning) + // writer.WriteLine(@"EpilepsyWarning: 1"); + // if (b.CountdownOffset > 0) + // writer.WriteLine(@"CountdownOffset: " + b.CountdownOffset.ToString()); + if (beatmap.BeatmapInfo.RulesetID == 3) + writer.WriteLine(FormattableString.Invariant($"SpecialStyle: {(beatmap.BeatmapInfo.SpecialStyle ? '1' : '0')}")); + writer.WriteLine(FormattableString.Invariant($"WidescreenStoryboard: {(beatmap.BeatmapInfo.WidescreenStoryboard ? '1' : '0')}")); + // if (b.SamplesMatchPlaybackRate) + // writer.WriteLine(@"SamplesMatchPlaybackRate: 1"); + } + + private void handleEditor(TextWriter writer) + { + writer.WriteLine("[Editor]"); + + if (beatmap.BeatmapInfo.Bookmarks.Length > 0) + writer.WriteLine(FormattableString.Invariant($"Bookmarks: {string.Join(',', beatmap.BeatmapInfo.Bookmarks)}")); + writer.WriteLine(FormattableString.Invariant($"DistanceSpacing: {beatmap.BeatmapInfo.DistanceSpacing}")); + writer.WriteLine(FormattableString.Invariant($"BeatDivisor: {beatmap.BeatmapInfo.BeatDivisor}")); + writer.WriteLine(FormattableString.Invariant($"GridSize: {beatmap.BeatmapInfo.GridSize}")); + writer.WriteLine(FormattableString.Invariant($"TimelineZoom: {beatmap.BeatmapInfo.TimelineZoom}")); + } + + private void handleMetadata(TextWriter writer) + { + writer.WriteLine("[Metadata]"); + + writer.WriteLine(FormattableString.Invariant($"Title: {beatmap.Metadata.Title}")); + writer.WriteLine(FormattableString.Invariant($"TitleUnicode: {beatmap.Metadata.TitleUnicode}")); + writer.WriteLine(FormattableString.Invariant($"Artist: {beatmap.Metadata.Artist}")); + writer.WriteLine(FormattableString.Invariant($"ArtistUnicode: {beatmap.Metadata.ArtistUnicode}")); + writer.WriteLine(FormattableString.Invariant($"Creator: {beatmap.Metadata.AuthorString}")); + writer.WriteLine(FormattableString.Invariant($"Version: {beatmap.BeatmapInfo.Version}")); + writer.WriteLine(FormattableString.Invariant($"Source: {beatmap.Metadata.Source}")); + writer.WriteLine(FormattableString.Invariant($"Tags: {beatmap.Metadata.Tags}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapID: {beatmap.BeatmapInfo.OnlineBeatmapID ?? 0}")); + writer.WriteLine(FormattableString.Invariant($"BeatmapSetID: {beatmap.BeatmapInfo.BeatmapSet.OnlineBeatmapSetID ?? -1}")); + } + + private void handleDifficulty(TextWriter writer) + { + writer.WriteLine("[Difficulty]"); + + writer.WriteLine(FormattableString.Invariant($"HPDrainRate: {beatmap.BeatmapInfo.BaseDifficulty.DrainRate}")); + writer.WriteLine(FormattableString.Invariant($"CircleSize: {beatmap.BeatmapInfo.BaseDifficulty.CircleSize}")); + writer.WriteLine(FormattableString.Invariant($"OverallDifficulty: {beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty}")); + writer.WriteLine(FormattableString.Invariant($"ApproachRate: {beatmap.BeatmapInfo.BaseDifficulty.ApproachRate}")); + writer.WriteLine(FormattableString.Invariant($"SliderMultiplier: {beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier}")); + writer.WriteLine(FormattableString.Invariant($"SliderTickRate: {beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate}")); + } + + private void handleEvents(TextWriter writer) + { + writer.WriteLine("[Events]"); + + if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.BackgroundFile)) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Background},0,\"{beatmap.BeatmapInfo.Metadata.BackgroundFile}\",0,0")); + + if (!string.IsNullOrEmpty(beatmap.BeatmapInfo.Metadata.VideoFile)) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Video},0,\"{beatmap.BeatmapInfo.Metadata.VideoFile}\",0,0")); + + foreach (var b in beatmap.Breaks) + writer.WriteLine(FormattableString.Invariant($"{(int)LegacyEventType.Break},{b.StartTime},{b.EndTime}")); + } + + private void handleTimingPoints(TextWriter writer) + { + if (beatmap.ControlPointInfo.Groups.Count == 0) + return; + + writer.WriteLine("[TimingPoints]"); + + foreach (var group in beatmap.ControlPointInfo.Groups) + { + var timingPoint = group.ControlPoints.OfType().FirstOrDefault(); + var difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(group.Time); + var samplePoint = beatmap.ControlPointInfo.SamplePointAt(group.Time); + var effectPoint = beatmap.ControlPointInfo.EffectPointAt(group.Time); + + // Convert beat length the legacy format + double beatLength; + if (timingPoint != null) + beatLength = timingPoint.BeatLength; + else + beatLength = -100 / difficultyPoint.SpeedMultiplier; + + // Apply the control point to a hit sample to uncover legacy properties (e.g. suffix) + HitSampleInfo tempHitSample = samplePoint.ApplyTo(new HitSampleInfo()); + + // Convert effect flags to the legacy format + LegacyEffectFlags effectFlags = LegacyEffectFlags.None; + if (effectPoint.KiaiMode) + effectFlags |= LegacyEffectFlags.Kiai; + if (effectPoint.OmitFirstBarLine) + effectFlags |= LegacyEffectFlags.OmitFirstBarLine; + + writer.Write(FormattableString.Invariant($"{group.Time},")); + writer.Write(FormattableString.Invariant($"{beatLength},")); + writer.Write(FormattableString.Invariant($"{(int)beatmap.ControlPointInfo.TimingPointAt(group.Time).TimeSignature},")); + writer.Write(FormattableString.Invariant($"{(int)toLegacySampleBank(tempHitSample.Bank)},")); + writer.Write(FormattableString.Invariant($"{toLegacyCustomSampleBank(tempHitSample.Suffix)},")); + writer.Write(FormattableString.Invariant($"{tempHitSample.Volume},")); + writer.Write(FormattableString.Invariant($"{(timingPoint != null ? '1' : '0')},")); + writer.Write(FormattableString.Invariant($"{(int)effectFlags}")); + writer.WriteLine(); + } + } + + private void handleHitObjects(TextWriter writer) + { + if (beatmap.HitObjects.Count == 0) + return; + + writer.WriteLine("[HitObjects]"); + + switch (beatmap.BeatmapInfo.RulesetID) + { + case 0: + foreach (var h in beatmap.HitObjects) + handleOsuHitObject(writer, h); + break; + + case 1: + foreach (var h in beatmap.HitObjects) + handleTaikoHitObject(writer, h); + break; + + case 2: + foreach (var h in beatmap.HitObjects) + handleCatchHitObject(writer, h); + break; + + case 3: + foreach (var h in beatmap.HitObjects) + handleManiaHitObject(writer, h); + break; + } + } + + private void handleOsuHitObject(TextWriter writer, HitObject hitObject) + { + var positionData = (IHasPosition)hitObject; + + writer.Write(FormattableString.Invariant($"{positionData.X},")); + writer.Write(FormattableString.Invariant($"{positionData.Y},")); + writer.Write(FormattableString.Invariant($"{hitObject.StartTime},")); + writer.Write(FormattableString.Invariant($"{(int)getObjectType(hitObject)},")); + + writer.Write(hitObject is IHasCurve + ? FormattableString.Invariant($"0,") + : FormattableString.Invariant($"{(int)toLegacyHitSoundType(hitObject.Samples)},")); + + if (hitObject is IHasCurve curveData) + { + addCurveData(writer, curveData, positionData); + writer.Write(getSampleBank(hitObject.Samples, zeroBanks: true)); + } + else + { + if (hitObject is IHasEndTime endTimeData) + writer.Write(FormattableString.Invariant($"{endTimeData.EndTime},")); + writer.Write(getSampleBank(hitObject.Samples)); + } + + writer.WriteLine(); + } + + private static LegacyHitObjectType getObjectType(HitObject hitObject) + { + var comboData = (IHasCombo)hitObject; + + var type = (LegacyHitObjectType)(comboData.ComboOffset << 4); + + if (comboData.NewCombo) type |= LegacyHitObjectType.NewCombo; + + switch (hitObject) + { + case IHasCurve _: + type |= LegacyHitObjectType.Slider; + break; + + case IHasEndTime _: + type |= LegacyHitObjectType.Spinner | LegacyHitObjectType.NewCombo; + break; + + default: + type |= LegacyHitObjectType.Circle; + break; + } + + return type; + } + + private void addCurveData(TextWriter writer, IHasCurve curveData, IHasPosition positionData) + { + PathType? lastType = null; + + for (int i = 0; i < curveData.Path.ControlPoints.Count; i++) + { + PathControlPoint point = curveData.Path.ControlPoints[i]; + + if (point.Type.Value != null) + { + if (point.Type.Value != lastType) + { + switch (point.Type.Value) + { + case PathType.Bezier: + writer.Write("B|"); + break; + + case PathType.Catmull: + writer.Write("C|"); + break; + + case PathType.PerfectCurve: + writer.Write("P|"); + break; + + case PathType.Linear: + writer.Write("L|"); + break; + } + + lastType = point.Type.Value; + } + else + { + // New segment with the same type - duplicate the control point + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}|")); + } + } + + if (i != 0) + { + writer.Write(FormattableString.Invariant($"{positionData.X + point.Position.Value.X}:{positionData.Y + point.Position.Value.Y}")); + writer.Write(i != curveData.Path.ControlPoints.Count - 1 ? "|" : ","); + } + } + + writer.Write(FormattableString.Invariant($"{curveData.RepeatCount + 1},")); + writer.Write(FormattableString.Invariant($"{curveData.Path.Distance},")); + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(FormattableString.Invariant($"{(int)toLegacyHitSoundType(curveData.NodeSamples[i])}")); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + + for (int i = 0; i < curveData.NodeSamples.Count; i++) + { + writer.Write(getSampleBank(curveData.NodeSamples[i], true)); + writer.Write(i != curveData.NodeSamples.Count - 1 ? "|" : ","); + } + } + + private void handleTaikoHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); + + private void handleCatchHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); + + private void handleManiaHitObject(TextWriter writer, HitObject hitObject) => throw new NotImplementedException(); + + private string getSampleBank(IList samples, bool banksOnly = false, bool zeroBanks = false) + { + LegacySampleBank normalBank = toLegacySampleBank(samples.SingleOrDefault(s => s.Name == HitSampleInfo.HIT_NORMAL)?.Bank); + LegacySampleBank addBank = toLegacySampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name) && s.Name != HitSampleInfo.HIT_NORMAL)?.Bank); + + StringBuilder sb = new StringBuilder(); + + sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)normalBank)}:")); + sb.Append(FormattableString.Invariant($"{(zeroBanks ? 0 : (int)addBank)}")); + + if (!banksOnly) + { + string customSampleBank = toLegacyCustomSampleBank(samples.FirstOrDefault(s => !string.IsNullOrEmpty(s.Name))?.Suffix); + string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; + int volume = samples.FirstOrDefault()?.Volume ?? 100; + + sb.Append(":"); + sb.Append(FormattableString.Invariant($"{customSampleBank}:")); + sb.Append(FormattableString.Invariant($"{volume}:")); + sb.Append(FormattableString.Invariant($"{sampleFilename}")); + } + + return sb.ToString(); + } + + private LegacyHitSoundType toLegacyHitSoundType(IList samples) + { + LegacyHitSoundType type = LegacyHitSoundType.None; + + foreach (var sample in samples) + { + switch (sample.Name) + { + case HitSampleInfo.HIT_WHISTLE: + type |= LegacyHitSoundType.Whistle; + break; + + case HitSampleInfo.HIT_FINISH: + type |= LegacyHitSoundType.Finish; + break; + + case HitSampleInfo.HIT_CLAP: + type |= LegacyHitSoundType.Clap; + break; + } + } + + return type; + } + + private LegacySampleBank toLegacySampleBank(string sampleBank) + { + switch (sampleBank?.ToLowerInvariant()) + { + case "normal": + return LegacySampleBank.Normal; + + case "soft": + return LegacySampleBank.Soft; + + case "drum": + return LegacySampleBank.Drum; + + default: + return LegacySampleBank.None; + } + } + + private string toLegacyCustomSampleBank(string sampleSuffix) => string.IsNullOrEmpty(sampleSuffix) ? "0" : sampleSuffix; + } +} diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index e401e3fb97..b1585d04c5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -148,47 +148,6 @@ namespace osu.Game.Beatmaps.Formats Fonts } - internal enum LegacySampleBank - { - None = 0, - Normal = 1, - Soft = 2, - Drum = 3 - } - - internal enum EventType - { - Background = 0, - Video = 1, - Break = 2, - Colour = 3, - Sprite = 4, - Sample = 5, - Animation = 6 - } - - internal enum LegacyOrigins - { - TopLeft, - Centre, - CentreLeft, - TopRight, - BottomCentre, - TopCentre, - Custom, - CentreRight, - BottomLeft, - BottomRight - } - - internal enum StoryLayer - { - Background = 0, - Fail = 1, - Pass = 2, - Foreground = 3 - } - internal class LegacyDifficultyControlPoint : DifficultyControlPoint { public LegacyDifficultyControlPoint() diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index ccd46ab559..67c4105e6d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -12,6 +12,7 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Game.IO; using osu.Game.Storyboards; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Beatmaps.Formats { @@ -83,12 +84,12 @@ namespace osu.Game.Beatmaps.Formats { storyboardSprite = null; - if (!Enum.TryParse(split[0], out EventType type)) + if (!Enum.TryParse(split[0], out LegacyEventType type)) throw new InvalidDataException($@"Unknown event type: {split[0]}"); switch (type) { - case EventType.Sprite: + case LegacyEventType.Sprite: { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); @@ -100,7 +101,7 @@ namespace osu.Game.Beatmaps.Formats break; } - case EventType.Animation: + case LegacyEventType.Animation: { var layer = parseLayer(split[1]); var origin = parseOrigin(split[2]); @@ -115,7 +116,7 @@ namespace osu.Game.Beatmaps.Formats break; } - case EventType.Sample: + case LegacyEventType.Sample: { var time = double.Parse(split[1], CultureInfo.InvariantCulture); var layer = parseLayer(split[2]); @@ -176,7 +177,7 @@ namespace osu.Game.Beatmaps.Formats { var startValue = float.Parse(split[4], CultureInfo.InvariantCulture); var endValue = split.Length > 5 ? float.Parse(split[5], CultureInfo.InvariantCulture) : startValue; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startValue), new Vector2(endValue)); + timelineGroup?.Scale.Add(easing, startTime, endTime, startValue, endValue); break; } @@ -186,7 +187,7 @@ namespace osu.Game.Beatmaps.Formats var startY = float.Parse(split[5], CultureInfo.InvariantCulture); var endX = split.Length > 6 ? float.Parse(split[6], CultureInfo.InvariantCulture) : startX; var endY = split.Length > 7 ? float.Parse(split[7], CultureInfo.InvariantCulture) : startY; - timelineGroup?.Scale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); + timelineGroup?.VectorScale.Add(easing, startTime, endTime, new Vector2(startX, startY), new Vector2(endX, endY)); break; } @@ -271,7 +272,7 @@ namespace osu.Game.Beatmaps.Formats } } - private string parseLayer(string value) => Enum.Parse(typeof(StoryLayer), value).ToString(); + private string parseLayer(string value) => Enum.Parse(typeof(LegacyStoryLayer), value).ToString(); private Anchor parseOrigin(string value) { diff --git a/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs new file mode 100644 index 0000000000..5bf80c34d7 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyEffectFlags.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Beatmaps.Legacy +{ + [Flags] + internal enum LegacyEffectFlags + { + None = 0, + Kiai = 1, + OmitFirstBarLine = 8 + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacyEventType.cs b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs new file mode 100644 index 0000000000..32a7122978 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyEventType.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyEventType + { + Background = 0, + Video = 1, + Break = 2, + Colour = 3, + Sprite = 4, + Sample = 5, + Animation = 6 + } +} diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs similarity index 63% rename from osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs rename to osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs index eab37b682c..ec9839b893 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectType.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyHitObjectType.cs @@ -1,12 +1,12 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; -namespace osu.Game.Rulesets.Objects.Legacy +namespace osu.Game.Beatmaps.Legacy { [Flags] - internal enum ConvertHitObjectType + internal enum LegacyHitObjectType { Circle = 1, Slider = 1 << 1, diff --git a/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs new file mode 100644 index 0000000000..d7743565f8 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyHitSoundType.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Beatmaps.Legacy +{ + [Flags] + internal enum LegacyHitSoundType + { + None = 0, + Normal = 1, + Whistle = 2, + Finish = 4, + Clap = 8 + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs new file mode 100644 index 0000000000..31f67d6dfd --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyOrigins.cs @@ -0,0 +1,19 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyOrigins + { + TopLeft, + Centre, + CentreLeft, + TopRight, + BottomCentre, + TopCentre, + Custom, + CentreRight, + BottomLeft, + BottomRight + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs new file mode 100644 index 0000000000..8cac29cb87 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacySampleBank.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacySampleBank + { + None = 0, + Normal = 1, + Soft = 2, + Drum = 3 + } +} diff --git a/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs new file mode 100644 index 0000000000..5237445640 --- /dev/null +++ b/osu.Game/Beatmaps/Legacy/LegacyStoryLayer.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Beatmaps.Legacy +{ + internal enum LegacyStoryLayer + { + Background = 0, + Fail = 1, + Pass = 2, + Foreground = 3 + } +} diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 2e76ab964f..b9ef279f5c 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -33,6 +33,11 @@ namespace osu.Game.Graphics.Containers /// public double TimeSinceLastBeat { get; private set; } + /// + /// How many beats per beatlength to trigger. Defaults to 1. + /// + public int Divisor { get; set; } = 1; + /// /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. /// @@ -42,6 +47,8 @@ namespace osu.Game.Graphics.Containers private EffectControlPoint defaultEffect; private TrackAmplitudes defaultAmplitudes; + protected bool IsBeatSyncedWithTrack { get; private set; } + protected override void Update() { Track track = null; @@ -65,26 +72,34 @@ namespace osu.Game.Graphics.Containers effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); if (timingPoint.BeatLength == 0) + { + IsBeatSyncedWithTrack = false; return; + } + + IsBeatSyncedWithTrack = true; } else { + IsBeatSyncedWithTrack = false; currentTrackTime = Clock.CurrentTime; timingPoint = defaultTiming; effectPoint = defaultEffect; } - int beatIndex = (int)((currentTrackTime - timingPoint.Time) / timingPoint.BeatLength); + double beatLength = timingPoint.BeatLength / Divisor; + + int beatIndex = (int)((currentTrackTime - timingPoint.Time) / beatLength) - (effectPoint.OmitFirstBarLine ? 1 : 0); // The beats before the start of the first control point are off by 1, this should do the trick if (currentTrackTime < timingPoint.Time) beatIndex--; - TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % timingPoint.BeatLength; + TimeUntilNextBeat = (timingPoint.Time - currentTrackTime) % beatLength; if (TimeUntilNextBeat < 0) - TimeUntilNextBeat += timingPoint.BeatLength; + TimeUntilNextBeat += beatLength; - TimeSinceLastBeat = timingPoint.BeatLength - TimeUntilNextBeat; + TimeSinceLastBeat = beatLength - TimeUntilNextBeat; if (timingPoint.Equals(lastTimingPoint) && beatIndex == lastBeat) return; diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8bfc28e774..23c931d161 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -153,6 +153,10 @@ namespace osu.Game.Online.API userReq.Success += u => { LocalUser.Value = u; + + // todo: save/pull from settings + LocalUser.Value.Status.Value = new UserStatusOnline(); + failureCount = 0; //we're connected! diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs new file mode 100644 index 0000000000..e484309a18 --- /dev/null +++ b/osu.Game/Overlays/News/NewsArticleCover.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Overlays.News +{ + public class NewsArticleCover : Container + { + public NewsArticleCover(ArticleInfo info) + { + RelativeSizeAxes = Axes.X; + Masking = true; + CornerRadius = 4; + + NewsBackground bg; + + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f)) + }, + new DelayedLoadWrapper(bg = new NewsBackground(info.CoverUrl) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Alpha = 0 + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.6f)), + Alpha = 1f, + }, + new DateContainer(info.Time) + { + Margin = new MarginPadding + { + Right = 20, + Top = 20, + } + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding + { + Left = 25, + Bottom = 50, + }, + Font = OsuFont.GetFont(Typeface.Exo, 24, FontWeight.Bold), + Text = info.Title, + }, + new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding + { + Left = 25, + Bottom = 30, + }, + Font = OsuFont.GetFont(Typeface.Exo, 16, FontWeight.Bold), + Text = "by " + info.Author + } + }; + + bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); + } + + [LongRunningLoad] + private class NewsBackground : Sprite + { + private readonly string url; + + public NewsBackground(string coverUrl) + { + url = coverUrl ?? "Headers/news"; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore store) + { + Texture = store.Get(url); + } + } + + private class DateContainer : Container, IHasTooltip + { + private readonly DateTime date; + + public DateContainer(DateTime date) + { + this.date = date; + + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + Masking = true; + CornerRadius = 4; + AutoSizeAxes = Axes.Both; + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(Typeface.Exo, 12, FontWeight.Black, false, false), + Text = date.ToString("d MMM yyy").ToUpper(), + Margin = new MarginPadding + { + Vertical = 4, + Horizontal = 8, + } + } + }; + } + + public string TooltipText => date.ToString("dddd dd MMMM yyyy hh:mm:ss UTCz").ToUpper(); + } + + //fake API data struct to use for now as a skeleton for data, as there is no API struct for news article info for now + public class ArticleInfo + { + public string Title { get; set; } + public string CoverUrl { get; set; } + public DateTime Time { get; set; } + public string Author { get; set; } + } + } +} diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index aadca8883e..e7471cb21d 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -16,7 +17,6 @@ namespace osu.Game.Overlays { private NewsHeader header; - //ReSharper disable NotAccessedField.Local private Container content; public readonly Bindable Current = new Bindable(null); @@ -59,6 +59,21 @@ namespace osu.Game.Overlays Current.TriggerChange(); } + private CancellationTokenSource loadContentCancellation; + + protected void LoadAndShowContent(NewsContent newContent) + { + content.FadeTo(0.2f, 300, Easing.OutQuint); + + loadContentCancellation?.Cancel(); + + LoadComponentAsync(newContent, c => + { + content.Child = c; + content.FadeIn(300, Easing.OutQuint); + }, (loadContentCancellation = new CancellationTokenSource()).Token); + } + public void ShowFrontPage() { Current.Value = null; diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index c14e02e64d..a8c79bb896 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -1,15 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.UI; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime + public abstract class ModNightcore : ModDoubleTime, IApplicableToDrawableRuleset + where TObject : HitObject { public override string Name => "Nightcore"; public override string Acronym => "NC"; @@ -34,5 +44,105 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + drawableRuleset.Overlays.Add(new NightcoreBeatContainer()); + } + + public class NightcoreBeatContainer : BeatSyncedContainer + { + private SkinnableSound hatSample; + private SkinnableSound clapSample; + private SkinnableSound kickSample; + private SkinnableSound finishSample; + + private int? firstBeat; + + public NightcoreBeatContainer() + { + Divisor = 2; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")), + clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")), + kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")), + finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")), + }; + } + + private const int bars_per_segment = 4; + + protected override void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, TrackAmplitudes amplitudes) + { + base.OnNewBeat(beatIndex, timingPoint, effectPoint, amplitudes); + + int beatsPerBar = (int)timingPoint.TimeSignature; + int segmentLength = beatsPerBar * Divisor * bars_per_segment; + + if (!IsBeatSyncedWithTrack) + { + firstBeat = null; + return; + } + + if (!firstBeat.HasValue || beatIndex < firstBeat) + // decide on a good starting beat index if once has not yet been decided. + firstBeat = beatIndex < 0 ? 0 : (beatIndex / segmentLength + 1) * segmentLength; + + if (beatIndex >= firstBeat) + playBeatFor(beatIndex % segmentLength, timingPoint.TimeSignature); + } + + private void playBeatFor(int beatIndex, TimeSignatures signature) + { + if (beatIndex == 0) + finishSample?.Play(); + + switch (signature) + { + case TimeSignatures.SimpleTriple: + switch (beatIndex % 6) + { + case 0: + kickSample?.Play(); + break; + + case 3: + clapSample?.Play(); + break; + + default: + hatSample?.Play(); + break; + } + + break; + + case TimeSignatures.SimpleQuadruple: + switch (beatIndex % 4) + { + case 0: + kickSample?.Play(); + break; + + case 2: + clapSample?.Play(); + break; + + default: + hatSample?.Play(); + break; + } + + break; + } + } + } } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index bdd019719b..3eab4555d1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -11,6 +11,7 @@ using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; using osu.Framework.MathUtils; +using osu.Game.Beatmaps.Legacy; namespace osu.Game.Rulesets.Objects.Legacy { @@ -46,27 +47,27 @@ namespace osu.Game.Rulesets.Objects.Legacy double startTime = Parsing.ParseDouble(split[2]) + Offset; - ConvertHitObjectType type = (ConvertHitObjectType)Parsing.ParseInt(split[3]); + LegacyHitObjectType type = (LegacyHitObjectType)Parsing.ParseInt(split[3]); - int comboOffset = (int)(type & ConvertHitObjectType.ComboOffset) >> 4; - type &= ~ConvertHitObjectType.ComboOffset; + int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; + type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlag(ConvertHitObjectType.NewCombo); - type &= ~ConvertHitObjectType.NewCombo; + bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); + type &= ~LegacyHitObjectType.NewCombo; - var soundType = (LegacySoundType)Parsing.ParseInt(split[4]); + var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); var bankInfo = new SampleBankInfo(); HitObject result = null; - if (type.HasFlag(ConvertHitObjectType.Circle)) + if (type.HasFlag(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlag(ConvertHitObjectType.Slider)) + else if (type.HasFlag(LegacyHitObjectType.Slider)) { PathType pathType = PathType.Catmull; double? length = null; @@ -157,7 +158,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } // Populate node sound types with the default hit object sound type - var nodeSoundTypes = new List(); + var nodeSoundTypes = new List(); for (int i = 0; i < nodes; i++) nodeSoundTypes.Add(soundType); @@ -172,7 +173,7 @@ namespace osu.Game.Rulesets.Objects.Legacy break; int.TryParse(adds[i], out var sound); - nodeSoundTypes[i] = (LegacySoundType)sound; + nodeSoundTypes[i] = (LegacyHitSoundType)sound; } } @@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Objects.Legacy // The samples are played when the slider ends, which is the last node result.Samples = nodeSamples[^1]; } - else if (type.HasFlag(ConvertHitObjectType.Spinner)) + else if (type.HasFlag(LegacyHitObjectType.Spinner)) { double endTime = Math.Max(startTime, Parsing.ParseDouble(split[5]) + Offset); @@ -195,7 +196,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlag(ConvertHitObjectType.Hold)) + else if (type.HasFlag(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts @@ -231,8 +232,8 @@ namespace osu.Game.Rulesets.Objects.Legacy string[] split = str.Split(':'); - var bank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[0]); - var addbank = (LegacyBeatmapDecoder.LegacySampleBank)Parsing.ParseInt(split[1]); + var bank = (LegacySampleBank)Parsing.ParseInt(split[0]); + var addbank = (LegacySampleBank)Parsing.ParseInt(split[1]); string stringBank = bank.ToString().ToLowerInvariant(); if (stringBank == @"none") @@ -333,7 +334,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// The hold end time. protected abstract HitObject CreateHold(Vector2 position, bool newCombo, int comboOffset, double endTime); - private List convertSoundType(LegacySoundType type, SampleBankInfo bankInfo) + private List convertSoundType(LegacyHitSoundType type, SampleBankInfo bankInfo) { // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario if (!string.IsNullOrEmpty(bankInfo.Filename)) @@ -359,7 +360,7 @@ namespace osu.Game.Rulesets.Objects.Legacy } }; - if (type.HasFlag(LegacySoundType.Finish)) + if (type.HasFlag(LegacyHitSoundType.Finish)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -370,7 +371,7 @@ namespace osu.Game.Rulesets.Objects.Legacy }); } - if (type.HasFlag(LegacySoundType.Whistle)) + if (type.HasFlag(LegacyHitSoundType.Whistle)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -381,7 +382,7 @@ namespace osu.Game.Rulesets.Objects.Legacy }); } - if (type.HasFlag(LegacySoundType.Clap)) + if (type.HasFlag(LegacyHitSoundType.Clap)) { soundTypes.Add(new LegacyHitSampleInfo { @@ -430,15 +431,5 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; } - - [Flags] - private enum LegacySoundType - { - None = 0, - Normal = 1, - Whistle = 2, - Finish = 4, - Clap = 8 - } } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 6f5fe066aa..7ad93379f0 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -49,9 +49,9 @@ namespace osu.Game.Rulesets public virtual ISkin CreateLegacySkinProvider(ISkinSource source) => null; - protected Ruleset(RulesetInfo rulesetInfo = null) + protected Ruleset() { - RulesetInfo = rulesetInfo ?? createRulesetInfo(); + RulesetInfo = createRulesetInfo(); } /// diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 6a69fd8dd0..d695e0b56d 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets { if (!Available) return null; - return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo), this); + return (Ruleset)Activator.CreateInstance(Type.GetType(InstantiationInfo)); } public bool Equals(RulesetInfo other) => other != null && ID == other.ID && Available == other.Available && Name == other.Name && InstantiationInfo == other.InstantiationInfo; diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 7d13afe9e5..5d0c5c7ccf 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets { var context = usage.Context; - var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r, (RulesetInfo)null)).ToList(); + var instances = loadedAssemblies.Values.Select(r => (Ruleset)Activator.CreateInstance(r)).ToList(); //add all legacy modes in correct order foreach (var r in instances.Where(r => r.LegacyID != null).OrderBy(r => r.LegacyID)) @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets // this allows for debug builds to successfully load rulesets (even though debug rulesets have a 0.0.0 version). asm.Version = null; return Assembly.Load(asm); - }, null), (RulesetInfo)null)).RulesetInfo; + }, null))).RulesetInfo; r.Name = instanceInfo.Name; r.ShortName = instanceInfo.ShortName; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 6984716a2c..5d9757778d 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -32,6 +32,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => new TimelineHitObjectDisplay(composer.EditorBeatmap); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineHitObjectDisplay(composer.EditorBeatmap); } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index c5bdc230d0..9c9c33274f 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -12,11 +12,18 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using System; using osu.Game.Beatmaps; +using osu.Framework.Bindables; +using System.Collections.Generic; +using osu.Game.Rulesets.Mods; +using System.Linq; namespace osu.Game.Screens.Select.Details { public class AdvancedStats : Container { + [Resolved] + private IBindable> mods { get; set; } + private readonly StatisticRow firstValue, hpDrain, accuracy, approachRate, starDifficulty; private BeatmapInfo beatmap; @@ -30,22 +37,7 @@ namespace osu.Game.Screens.Select.Details beatmap = value; - //mania specific - if ((Beatmap?.Ruleset?.ID ?? 0) == 3) - { - firstValue.Title = "Key Amount"; - firstValue.Value = (int)MathF.Round(Beatmap?.BaseDifficulty?.CircleSize ?? 0); - } - else - { - firstValue.Title = "Circle Size"; - firstValue.Value = Beatmap?.BaseDifficulty?.CircleSize ?? 0; - } - - hpDrain.Value = Beatmap?.BaseDifficulty?.DrainRate ?? 0; - accuracy.Value = Beatmap?.BaseDifficulty?.OverallDifficulty ?? 0; - approachRate.Value = Beatmap?.BaseDifficulty?.ApproachRate ?? 0; - starDifficulty.Value = (float)(Beatmap?.StarDifficulty ?? 0); + updateStatistics(); } } @@ -73,6 +65,45 @@ namespace osu.Game.Screens.Select.Details starDifficulty.AccentColour = colours.Yellow; } + protected override void LoadComplete() + { + base.LoadComplete(); + + mods.BindValueChanged(_ => updateStatistics(), true); + } + + private void updateStatistics() + { + BeatmapDifficulty baseDifficulty = Beatmap?.BaseDifficulty; + BeatmapDifficulty adjustedDifficulty = null; + + if (baseDifficulty != null && mods.Value.Any(m => m is IApplicableToDifficulty)) + { + adjustedDifficulty = baseDifficulty.Clone(); + + foreach (var mod in mods.Value.OfType()) + mod.ApplyToDifficulty(adjustedDifficulty); + } + + //mania specific + if ((Beatmap?.Ruleset?.ID ?? 0) == 3) + { + firstValue.Title = "Key Amount"; + firstValue.Value = ((int)MathF.Round(baseDifficulty?.CircleSize ?? 0), (int)MathF.Round(adjustedDifficulty?.CircleSize ?? 0)); + } + else + { + firstValue.Title = "Circle Size"; + firstValue.Value = (baseDifficulty?.CircleSize ?? 0, adjustedDifficulty?.CircleSize); + } + + starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null); + + hpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate); + accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty); + approachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate); + } + private class StatisticRow : Container, IHasAccentColour { private const float value_width = 25; @@ -80,8 +111,11 @@ namespace osu.Game.Screens.Select.Details private readonly float maxValue; private readonly bool forceDecimalPlaces; - private readonly OsuSpriteText name, value; - private readonly Bar bar; + private readonly OsuSpriteText name, valueText; + private readonly Bar bar, modBar; + + [Resolved] + private OsuColour colours { get; set; } public string Title { @@ -89,16 +123,29 @@ namespace osu.Game.Screens.Select.Details set => name.Text = value; } - private float difficultyValue; + private (float baseValue, float? adjustedValue) value; - public float Value + public (float baseValue, float? adjustedValue) Value { - get => difficultyValue; + get => value; set { - difficultyValue = value; - bar.Length = value / maxValue; - this.value.Text = value.ToString(forceDecimalPlaces ? "0.00" : "0.##"); + if (value == this.value) + return; + + this.value = value; + + bar.Length = value.baseValue / maxValue; + + valueText.Text = (value.adjustedValue ?? value.baseValue).ToString(forceDecimalPlaces ? "0.00" : "0.##"); + modBar.Length = (value.adjustedValue ?? 0) / maxValue; + + if (value.adjustedValue > value.baseValue) + modBar.AccentColour = valueText.Colour = colours.Red; + else if (value.adjustedValue < value.baseValue) + modBar.AccentColour = valueText.Colour = colours.BlueDark; + else + modBar.AccentColour = valueText.Colour = Color4.White; } } @@ -135,13 +182,22 @@ namespace osu.Game.Screens.Select.Details BackgroundColour = Color4.White.Opacity(0.5f), Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, }, + modBar = new Bar + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + Alpha = 0.5f, + Height = 5, + Padding = new MarginPadding { Left = name_width + 10, Right = value_width + 10 }, + }, new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Width = value_width, RelativeSizeAxes = Axes.Y, - Child = value = new OsuSpriteText + Child = valueText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 364c971874..7b6e667d4f 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -16,7 +16,8 @@ namespace osu.Game.Storyboards { public CommandTimeline X = new CommandTimeline(); public CommandTimeline Y = new CommandTimeline(); - public CommandTimeline Scale = new CommandTimeline(); + public CommandTimeline Scale = new CommandTimeline(); + public CommandTimeline VectorScale = new CommandTimeline(); public CommandTimeline Rotation = new CommandTimeline(); public CommandTimeline Colour = new CommandTimeline(); public CommandTimeline Alpha = new CommandTimeline(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 4f8e39fa1b..a076bb54df 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -8,21 +8,71 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardAnimation : TextureAnimation, IFlippable + public class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; private set; } - public bool FlipH { get; set; } - public bool FlipV { get; set; } + private bool flipH; + + public bool FlipH + { + get => flipH; + set + { + if (flipH == value) + return; + + flipH = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private bool flipV; + + public bool FlipV + { + get => flipV; + set + { + if (flipV == value) + return; + + flipV = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private Vector2 vectorScale = Vector2.One; + + public Vector2 VectorScale + { + get => vectorScale; + set + { + if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) + value.X = Precision.FLOAT_EPSILON; + if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) + value.Y = Precision.FLOAT_EPSILON; + + if (vectorScale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}."); + + vectorScale = value; + Invalidate(Invalidation.MiscGeometry); + } + } public override bool RemoveWhenNotAlive => false; protected override Vector2 DrawScale - => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; public override Anchor Origin { diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index ff48dab7e5..ac795b3349 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -8,21 +8,71 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Framework.MathUtils; using osu.Game.Beatmaps; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSprite : Sprite, IFlippable + public class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; private set; } - public bool FlipH { get; set; } - public bool FlipV { get; set; } + private bool flipH; + + public bool FlipH + { + get => flipH; + set + { + if (flipH == value) + return; + + flipH = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private bool flipV; + + public bool FlipV + { + get => flipV; + set + { + if (flipV == value) + return; + + flipV = value; + Invalidate(Invalidation.MiscGeometry); + } + } + + private Vector2 vectorScale = Vector2.One; + + public Vector2 VectorScale + { + get => vectorScale; + set + { + if (Math.Abs(value.X) < Precision.FLOAT_EPSILON) + value.X = Precision.FLOAT_EPSILON; + if (Math.Abs(value.Y) < Precision.FLOAT_EPSILON) + value.Y = Precision.FLOAT_EPSILON; + + if (vectorScale == value) + return; + + if (!Validation.IsFinite(value)) throw new ArgumentException($@"{nameof(VectorScale)} must be finite, but is {value}."); + + vectorScale = value; + Invalidate(Invalidation.MiscGeometry); + } + } public override bool RemoveWhenNotAlive => false; protected override Vector2 DrawScale - => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y); + => new Vector2(FlipH ? -base.DrawScale.X : base.DrawScale.X, FlipV ? -base.DrawScale.Y : base.DrawScale.Y) * VectorScale; public override Anchor Origin { diff --git a/osu.Game/Storyboards/Drawables/IFlippable.cs b/osu.Game/Storyboards/Drawables/IFlippable.cs index 1c4cdde22d..165b3d97cc 100644 --- a/osu.Game/Storyboards/Drawables/IFlippable.cs +++ b/osu.Game/Storyboards/Drawables/IFlippable.cs @@ -6,13 +6,13 @@ using osu.Framework.Graphics.Transforms; namespace osu.Game.Storyboards.Drawables { - public interface IFlippable : ITransformable + internal interface IFlippable : ITransformable { bool FlipH { get; set; } bool FlipV { get; set; } } - public class TransformFlipH : Transform + internal class TransformFlipH : Transform { private bool valueAt(double time) => time < EndTime ? StartValue : EndValue; @@ -23,7 +23,7 @@ namespace osu.Game.Storyboards.Drawables protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipH; } - public class TransformFlipV : Transform + internal class TransformFlipV : Transform { private bool valueAt(double time) => time < EndTime ? StartValue : EndValue; @@ -34,7 +34,7 @@ namespace osu.Game.Storyboards.Drawables protected override void ReadIntoStartValue(IFlippable d) => StartValue = d.FlipV; } - public static class FlippableExtensions + internal static class FlippableExtensions { /// /// Adjusts after a delay. diff --git a/osu.Game/Storyboards/Drawables/IVectorScalable.cs b/osu.Game/Storyboards/Drawables/IVectorScalable.cs new file mode 100644 index 0000000000..fcc407d460 --- /dev/null +++ b/osu.Game/Storyboards/Drawables/IVectorScalable.cs @@ -0,0 +1,21 @@ +// Copyright (c) ppy Pty Ltd . 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.Transforms; +using osuTK; + +namespace osu.Game.Storyboards.Drawables +{ + internal interface IVectorScalable : ITransformable + { + Vector2 VectorScale { get; set; } + } + + internal static class VectorScalableExtensions + { + public static TransformSequence VectorScaleTo(this T target, Vector2 newVectorScale, double duration = 0, Easing easing = Easing.None) + where T : class, IVectorScalable + => target.TransformTo(nameof(IVectorScalable.VectorScale), newVectorScale, duration, easing); + } +} diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index d5e69fd103..abf9f58804 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -65,20 +65,30 @@ namespace osu.Game.Storyboards { applyCommands(drawable, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = value, (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); + applyCommands(drawable, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing)); applyCommands(drawable, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing)); - applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), false); + applyCommands(drawable, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, easing) => d.TransformBlendingMode(value, duration), + false); + + if (drawable is IVectorScalable vectorScalable) + { + applyCommands(drawable, getCommands(g => g.VectorScale, triggeredGroups), (d, value) => vectorScalable.VectorScale = value, + (d, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing)); + } if (drawable is IFlippable flippable) { - applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), false); - applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), false); + applyCommands(drawable, getCommands(g => g.FlipH, triggeredGroups), (d, value) => flippable.FlipH = value, (d, value, duration, easing) => flippable.TransformFlipH(value, duration), + false); + applyCommands(drawable, getCommands(g => g.FlipV, triggeredGroups), (d, value) => flippable.FlipV = value, (d, value, duration, easing) => flippable.TransformFlipV(value, duration), + false); } } - private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, bool alwaysInitialize = true) + private void applyCommands(Drawable drawable, IEnumerable.TypedCommand> commands, DrawablePropertyInitializer initializeProperty, DrawableTransformer transform, + bool alwaysInitialize = true) where T : struct { var initialized = false; diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index ebd9dbecd1..5d0ffd5a67 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -26,9 +26,9 @@ namespace osu.Game.Users [JsonProperty(@"country")] public Country Country; - public Bindable Status = new Bindable(); + public readonly Bindable Status = new Bindable(); - public IBindable Activity = new Bindable(); + public readonly Bindable Activity = new Bindable(); //public Team Team; diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 918c547978..8030fc55a2 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -62,7 +62,7 @@ namespace osu.Game.Users public class InLobby : UserActivity { - public override string Status => @"In a Multiplayer Lobby"; + public override string Status => @"In a multiplayer lobby"; } } }